Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ID-3491: Admin API for statusoppdatering på pnr #155

Merged
merged 4 commits into from
Nov 8, 2023
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
2 changes: 1 addition & 1 deletion .github/workflows/call-buildimage.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ jobs:
uses: felleslosninger/eid-github-workflows/.github/workflows/spring-boot-build-publish-image.yml@main
with:
image-name: idporten-user-service
java-version: 17
java-version: 21
secrets:
maven-user: ${{ secrets.MAVEN_USER }}
maven-password: ${{ secrets.MAVEN_PASSWORD }}
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/call-maventests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,15 +11,15 @@ jobs:
call-workflow-maven-build:
uses: felleslosninger/eid-github-workflows/.github/workflows/maven-build.yml@main
with:
java-version: 17
java-version: 21
secrets:
maven-user: ${{ secrets.MAVEN_USER }}
maven-password: ${{ secrets.MAVEN_PASSWORD }}
call-container-scan:
uses: felleslosninger/eid-github-workflows/.github/workflows/spring-boot-container-scan.yml@main
with:
image-name: idporten-user-service
java-version: 17
java-version: 21
secrets:
eid-build-token: ${{ secrets.EID_BUILD_PAT }}
maven-user: ${{ secrets.MAVEN_USER }}
Expand Down
4 changes: 2 additions & 2 deletions docker/dev.Dockerfile
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
FROM maven:3.8-eclipse-temurin-17 as builder
FROM maven:3.9-eclipse-temurin-21 as builder

ARG GIT_PACKAGE_TOKEN
ARG GIT_PACKAGE_USERNAME
Expand All @@ -13,7 +13,7 @@ COPY src /home/app/src
RUN --mount=type=cache,target=/root/.m2/repository mvn -f /home/app/pom.xml clean package -Dmaven.test.skip=true -Dmaven.gitcommitid.skip=true


FROM eclipse-temurin:17-jre-jammy
FROM eclipse-temurin:21-jre-jammy

ARG APPLICATION=idporten-user-service
RUN mkdir /var/log/${APPLICATION}
Expand Down
4 changes: 2 additions & 2 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.1.2</version>
<version>3.1.5</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>no.idporten</groupId>
Expand All @@ -14,7 +14,7 @@
<name>idporten-user-service</name>
<description>ID-porten user service</description>
<properties>
<java.version>17</java.version>
<java.version>21</java.version>
<idporten-validators.version>1.0.2</idporten-validators.version>
<idporten-fnr-generate.version>1.0.1</idporten-fnr-generate.version>
<springdoc.version>2.2.0</springdoc.version>
Expand Down
31 changes: 30 additions & 1 deletion src/main/java/no/idporten/userservice/api/ApiUserService.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import no.idporten.userservice.api.admin.UpdateAttributesRequest;
import no.idporten.userservice.api.admin.UpdatePidStatusRequest;
import no.idporten.userservice.api.admin.UpdateStatusRequest;
import no.idporten.userservice.api.login.CreateUserRequest;
import no.idporten.userservice.api.login.UpdateUserLoginRequest;
Expand All @@ -13,6 +14,7 @@
import no.idporten.validators.identifier.PersonIdentifierValidator;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.CollectionUtils;
import org.springframework.util.StringUtils;

Expand All @@ -28,6 +30,7 @@
*/
@RequiredArgsConstructor
@Service
@Transactional(readOnly = true)
public class ApiUserService {

private final UserService userService;
Expand All @@ -47,16 +50,19 @@ public List<UserResource> searchForUser(@Valid SearchRequest searchRequest) {
return userService.searchForUser(searchRequest.getPersonIdentifier()).stream().map(this::convert).collect(Collectors.toList());
}

@Transactional
public UserResource createUser(CreateUserRequest createUserRequest) {
validatePersonIdentifier(createUserRequest.getPersonIdentifier());
IDPortenUser idPortenUser = IDPortenUser.builder().pid(createUserRequest.getPersonIdentifier()).active(true).build();
return convert(userService.createUser(idPortenUser));
}

@Transactional
public UserResource updateUserLogins(String id, UpdateUserLoginRequest updateUserLoginRequest) {
return convert(userService.updateUserWithEid(UUID.fromString(id), Login.builder().eidName(updateUserLoginRequest.getEidName()).build()));
}

@Transactional
public UserResource updateUserAttributes(String id, UpdateAttributesRequest updateAttributesRequest) {
IDPortenUser idPortenUser = userService.findUser(UUID.fromString(id));
validateUserExists(idPortenUser);
Expand All @@ -69,10 +75,33 @@ public UserResource updateUserAttributes(String id, UpdateAttributesRequest upda
return convert(idPortenUser);
}

@Transactional
public UserResource updateUserStatus(String userId, UpdateStatusRequest updateUserStatusRequest) {
IDPortenUser idPortenUser = userService.findUser(UUID.fromString(userId));
validateUserExists(idPortenUser);
String closedCode = StringUtils.hasText(updateUserStatusRequest.getClosedCode()) ? updateUserStatusRequest.getClosedCode() : null;
idPortenUser = setStatus(idPortenUser, closedCode);
return convert(userService.updateUser(idPortenUser));
}

@Transactional
public UserResource updateUserPidStatus(UpdatePidStatusRequest updateUserStatusRequest) {
String closedCode = StringUtils.hasText(updateUserStatusRequest.getClosedCode()) ? updateUserStatusRequest.getClosedCode() : null;
IDPortenUser idPortenUser = userService.findFirstUser(updateUserStatusRequest.getPersonIdentifier());
if(idPortenUser == null){
// create user
IDPortenUser newUser = IDPortenUser.builder().pid(updateUserStatusRequest.getPersonIdentifier()).active(true).build();
newUser = setStatus(newUser, closedCode);
idPortenUser = userService.createUser(newUser);
shoyheim-dd marked this conversation as resolved.
Show resolved Hide resolved
}else{
// update user
idPortenUser = setStatus(idPortenUser, closedCode);
idPortenUser = userService.updateUser(idPortenUser);
}
return convert(idPortenUser);
}

private IDPortenUser setStatus(IDPortenUser idPortenUser,String closedCode) {
if (closedCode == null) {
idPortenUser.setActive(true);
idPortenUser.setClosedCode(null);
Expand All @@ -82,7 +111,7 @@ public UserResource updateUserStatus(String userId, UpdateStatusRequest updateUs
idPortenUser.setClosedCode(closedCode);
idPortenUser.setClosedCodeLastUpdated(Clock.systemUTC().instant());
}
return convert(userService.updateUser(idPortenUser));
return idPortenUser;
}

protected void validatePersonIdentifier(String personIdentifier) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -142,4 +142,23 @@ public ResponseEntity<UserResource> updateUserStatus(@UUID(message = "Invalid us
return ResponseEntity.ok(apiUserService.updateUserStatus(id, request));
}

@Operation(
summary = "Update status for user",
description = "Update user status based on external id. Note that if the user does not exist, it will be created.",
tags = {"admin-api"},
security = @SecurityRequirement(name = "access_token"),
parameters = {
@Parameter(in = ParameterIn.PATH, name = "pid", required = true, description = "User external id")
})
@ApiResponses(value = {
@ApiResponse(responseCode = "200", description = "User status is updated"),
@ApiResponse(responseCode = "404", description = "User is not found")
})
@PreAuthorize("hasAuthority('SCOPE_idporteninternal:user.write')")
@AuditMessage(AuditID.ADMIN_USER_STATUS_UPDATED)
@PutMapping(path = "/admin/v1/users/status", consumes = MediaType.APPLICATION_JSON_VALUE, produces = MediaType.APPLICATION_JSON_VALUE)
public ResponseEntity<UserResource> updateUserStatus(@Valid @RequestBody UpdatePidStatusRequest request) {
return ResponseEntity.ok(apiUserService.updateUserPidStatus(request));
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package no.idporten.userservice.api.admin;

import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Data;
import lombok.EqualsAndHashCode;
import no.idporten.validators.identifier.PersonIdentifier;

@Data
@EqualsAndHashCode(callSuper = true)
@JsonIgnoreProperties(ignoreUnknown = true)
public class UpdatePidStatusRequest extends UpdateStatusRequest {

@PersonIdentifier(message = "Invalid person identifier")
@JsonProperty("person_identifier")
private String personIdentifier;

}
32 changes: 32 additions & 0 deletions src/main/java/no/idporten/userservice/config/ThreadConfig.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package no.idporten.userservice.config;

import java.util.concurrent.Executors;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.autoconfigure.task.TaskExecutionAutoConfiguration;
import org.springframework.boot.web.embedded.tomcat.TomcatProtocolHandlerCustomizer;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.task.AsyncTaskExecutor;
import org.springframework.core.task.support.TaskExecutorAdapter;
import org.springframework.scheduling.annotation.EnableAsync;

@EnableAsync
@Configuration
@ConditionalOnProperty(
value = "spring.thread-executor",
havingValue = "virtual"
)
public class ThreadConfig {

@Bean(TaskExecutionAutoConfiguration.APPLICATION_TASK_EXECUTOR_BEAN_NAME)
public AsyncTaskExecutor asyncTaskExecutor() {
return new TaskExecutorAdapter(Executors.newVirtualThreadPerTaskExecutor());
}

@Bean
public TomcatProtocolHandlerCustomizer<?> protocolHandlerVirtualThreadExecutorCustomizer() {
return protocolHandler -> {
protocolHandler.setExecutor(Executors.newVirtualThreadPerTaskExecutor());
};
}
}
17 changes: 16 additions & 1 deletion src/main/java/no/idporten/userservice/data/UserService.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,15 @@

import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.CollectionUtils;

import java.time.Instant;
import java.util.*;

@RequiredArgsConstructor
@Service
@Transactional(readOnly = true)
public class UserService {

private final UserRepository userRepository;
Expand All @@ -29,11 +31,20 @@ public List<IDPortenUser> searchForUser(String personIdentifier) {
return users.stream().map(IDPortenUser::new).toList();
}

public IDPortenUser findFirstUser(String personIdentifier) {
Optional<UserEntity> user = userRepository.findByPersonIdentifier(personIdentifier);
if (user.isEmpty()) {
return null;
}
return new IDPortenUser(user.get());
}

@Transactional
public IDPortenUser createUser(IDPortenUser idPortenUser) {
if (idPortenUser.getId() != null) {
throw UserServiceException.invalidUserData("User id must be assigned by server.");
}
if (! searchForUser(idPortenUser.getPid()).isEmpty()) {
if (findFirstUser(idPortenUser.getPid()) != null) {
throw UserServiceException.duplicateUser();
}
idPortenUser.setActive(Boolean.TRUE);
Expand All @@ -56,6 +67,7 @@ public UserEntity toEntity(IDPortenUser user) {
return builder.build();
}

@Transactional
public IDPortenUser updateUser(IDPortenUser idPortenUser) {
if (idPortenUser.getId() == null) {
throw UserServiceException.invalidUserData("User id is mandatory.");
Expand Down Expand Up @@ -84,6 +96,7 @@ public IDPortenUser updateUser(IDPortenUser idPortenUser) {
return new IDPortenUser(savedUser);
}

@Transactional
public IDPortenUser updateUserWithEid(UUID userUuid, Login eid) {
Optional<UserEntity> byUuid = userRepository.findByUuid(userUuid);
if (byUuid.isEmpty()) {
Expand Down Expand Up @@ -112,6 +125,7 @@ private LoginEntity findExistingEid(Login eid, List<LoginEntity> existingeIDs) {
return null;
}

@Transactional
public IDPortenUser deleteUser(UUID userUuid) {
Optional<UserEntity> userExists = userRepository.findByUuid(userUuid);
if (userExists.isEmpty()) {
Expand All @@ -122,6 +136,7 @@ public IDPortenUser deleteUser(UUID userUuid) {
return new IDPortenUser(userExists.get());
}

@Transactional
public IDPortenUser changePid(String currentPid, String newPid) {
Optional<UserEntity> userExists = userRepository.findByPersonIdentifier(currentPid);
if (userExists.isEmpty()) {
Expand Down
21 changes: 21 additions & 0 deletions src/main/resources/application-docker.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,28 @@ server:
management:
server:
port:
logging:
level:
org:
hibernate:
stat: DEBUG
SQL: DEBUG
type:
descriptor:
sql:
BasicBinder: TRACE
orm:
jdbc:
bind: TRACE
spring:
jpa:
properties:
hibernate:
generate_statistics: true
dialect: org.hibernate.dialect.MariaDBDialect
format_sql: true
show_sql: true
show-sql: true
datasource:
url: "jdbc:mariadb://db-us:3306/idporten_user"
driver-class-name: org.mariadb.jdbc.Driver
Expand Down
2 changes: 2 additions & 0 deletions src/main/resources/application.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,14 @@ server:
port: 8080

spring:
thread-executor: virtual
jpa:
hibernate:
ddl-auto: none
generate-ddl: false
show-sql: false
database-platform: org.hibernate.dialect.MariaDBDialect
open-in-view: false
flyway:
table: flyway_schema_history
security:
Expand Down
16 changes: 15 additions & 1 deletion src/test/java/no/idporten/userservice/data/UserServiceTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,7 @@ public void testfindUserId() {

@Test
@DisplayName("by person-identifier then one user is returned")
public void testfindUserByPid() {
public void testSearchUsersByPid() {
String personIdentifier = "1263";

UserEntity userEntity = UserEntity.builder().personIdentifier(personIdentifier).uuid(UUID.randomUUID()).build();
Expand All @@ -135,6 +135,20 @@ public void testfindUserByPid() {
verify(userRepository).findByPersonIdentifier(personIdentifier);

}

@Test
@DisplayName("by person-identifier then one user is returned")
public void testfindFirstUserByPid() {
String personIdentifier = "1263";

UserEntity userEntity = UserEntity.builder().personIdentifier(personIdentifier).uuid(UUID.randomUUID()).build();
when(userRepository.findByPersonIdentifier(personIdentifier)).thenReturn(Optional.of(userEntity));
IDPortenUser userFound = userService.findFirstUser(personIdentifier);
assertNotNull(userFound);
assertEquals(personIdentifier, userFound.getPid());
verify(userRepository).findByPersonIdentifier(personIdentifier);

}
}

@Nested
Expand Down
7 changes: 3 additions & 4 deletions src/test/resources/application.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ spring:
generate-ddl: false
show-sql: false
database-platform: org.hibernate.dialect.H2Dialect
open-in-view: false
flyway:
table: flyway_schema_history
security:
Expand Down Expand Up @@ -50,7 +51,8 @@ management:
enabled: false
metrics:
tags:
application: "idporten-user-service"
application: ${spring.application.name}
environment: ${spring.application.environment}

# Swagger with springdoc
springdoc:
Expand All @@ -69,9 +71,6 @@ springdoc:

# App-specific config
idporten-user-service:
audit:
application-name: idporten-user-service
audit-log-dir:
features:
allow-real-pid: false
allow-synthetic-pid: true
Expand Down