diff --git a/Jenkinsfile b/Jenkinsfile index 5c20846045d5bbd76178db777912239d71a6c638..c3da1a0c0878ed560dfd5702e47ec1313a29dd96 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -95,7 +95,7 @@ pipeline { container("quarkus-22"){ withCredentials([usernamePassword(credentialsId: 'jenkins-docker-login', usernameVariable: 'USER', passwordVariable: 'PASSWORD')]) { configFileProvider([configFile(fileId: 'maven-settings', variable: 'MAVEN_SETTINGS')]) { - sh './mvnw -pl user-manager-server -s $MAVEN_SETTINGS clean package -DskipTests -Pnative -Dquarkus.container-image.registry=docker.ozg-sh.de -Dquarkus.container-image.username=${USER} -Dquarkus.container-image.password=${PASSWORD} -Dquarkus.container-image.push=true -Dquarkus.container-image.build=true -Dmaven.wagon.http.retryHandler.count=3' + sh './mvnw -pl user-manager-server -s $MAVEN_SETTINGS clean verify -Pnative -Dquarkus.container-image.registry=docker.ozg-sh.de -Dquarkus.container-image.username=${USER} -Dquarkus.container-image.password=${PASSWORD} -Dquarkus.container-image.push=true -Dquarkus.container-image.build=true -Dmaven.wagon.http.retryHandler.count=3' } } } diff --git a/user-manager-server/pom.xml b/user-manager-server/pom.xml index cc6499439a4d8d9bfbb65222a94e49662932f067..3439e27d92f0347ef4733a213a79bcf78b7db01f 100644 --- a/user-manager-server/pom.xml +++ b/user-manager-server/pom.xml @@ -18,11 +18,10 @@ <kop-common.version>1.1.4-SNAPSHOT</kop-common.version> <jacoco.plugin.version>0.8.8</jacoco.plugin.version> - <quarkus.platform.version>2.10.3.Final</quarkus.platform.version> + <quarkus.platform.version>2.12.2.Final</quarkus.platform.version> <surefire-plugin.version>3.0.0-M7</surefire-plugin.version> <maven-failsafe-plugin.version>3.0.0-M7</maven-failsafe-plugin.version> <git-commit-id-plugin.version>4.9.10</git-commit-id-plugin.version> - <quarkus-logging-json.version>1.1.1</quarkus-logging-json.version> <!-- Versions already declared in kop-common-dependencies bom -> use bom as parent!? --> <mapstruct.version>1.5.1.Final</mapstruct.version> @@ -82,15 +81,15 @@ </dependency> <dependency> <groupId>io.quarkus</groupId> - <artifactId>quarkus-arc</artifactId> + <artifactId>quarkus-resteasy-reactive-links</artifactId> </dependency> <dependency> <groupId>io.quarkus</groupId> - <artifactId>quarkus-hal</artifactId> + <artifactId>quarkus-arc</artifactId> </dependency> <dependency> <groupId>io.quarkus</groupId> - <artifactId>quarkus-resteasy-reactive-links</artifactId> + <artifactId>quarkus-hal</artifactId> </dependency> <dependency> <groupId>io.quarkus</groupId> @@ -116,10 +115,6 @@ <groupId>io.quarkus</groupId> <artifactId>quarkus-mongodb-panache</artifactId> </dependency> - <dependency> - <groupId>io.quarkus</groupId> - <artifactId>quarkus-resteasy-reactive</artifactId> - </dependency> <dependency> <groupId>io.quarkus</groupId> <artifactId>quarkus-scheduler</artifactId> @@ -143,13 +138,12 @@ <!-- Logging --> <dependency> - <groupId>io.quarkiverse.loggingjson</groupId> - <artifactId>quarkus-logging-json</artifactId> - <version>${quarkus-logging-json.version}</version> + <groupId>io.quarkus</groupId> + <artifactId>quarkus-logging-json</artifactId> </dependency> <dependency> - <groupId>org.jboss.logmanager</groupId> - <artifactId>log4j2-jboss-logmanager</artifactId> + <groupId>org.jboss.logmanager</groupId> + <artifactId>log4j2-jboss-logmanager</artifactId> </dependency> <!-- Tools --> diff --git a/user-manager-server/src/main/java/de/itvsh/kop/user/NativeConfig.java b/user-manager-server/src/main/java/de/itvsh/kop/user/NativeConfig.java new file mode 100644 index 0000000000000000000000000000000000000000..b2edd0a2acfa1fddc3e0429a13d2b49087b3880b --- /dev/null +++ b/user-manager-server/src/main/java/de/itvsh/kop/user/NativeConfig.java @@ -0,0 +1,25 @@ +package de.itvsh.kop.user; + +import io.quarkus.runtime.annotations.RegisterForReflection; + +@RegisterForReflection(targets = { + org.apache.logging.log4j.message.ReusableMessageFactory.class, + org.apache.logging.log4j.message.DefaultFlowMessageFactory.class, + org.jboss.resteasy.plugins.providers.multipart.MultipartReader.class, + org.jboss.resteasy.plugins.providers.multipart.ListMultipartReader.class, + org.jboss.resteasy.plugins.providers.multipart.MultipartFormDataReader.class, + org.jboss.resteasy.plugins.providers.multipart.MultipartRelatedReader.class, + org.jboss.resteasy.plugins.providers.multipart.MapMultipartFormDataReader.class, + org.jboss.resteasy.plugins.providers.multipart.MultipartWriter.class, + org.jboss.resteasy.plugins.providers.multipart.MultipartFormDataWriter.class, + org.jboss.resteasy.plugins.providers.multipart.MultipartRelatedWriter.class, + org.jboss.resteasy.plugins.providers.multipart.ListMultipartWriter.class, + org.jboss.resteasy.plugins.providers.multipart.MapMultipartFormDataWriter.class, + org.jboss.resteasy.plugins.providers.multipart.MultipartFormAnnotationReader.class, + org.jboss.resteasy.plugins.providers.multipart.MultipartFormAnnotationWriter.class, + org.jboss.resteasy.plugins.providers.multipart.MimeMultipartProvider.class, + org.jboss.resteasy.plugins.providers.multipart.XopWithMultipartRelatedReader.class, + org.jboss.resteasy.plugins.providers.multipart.XopWithMultipartRelatedWriter.class +}) +public class NativeConfig { +} diff --git a/user-manager-server/src/main/java/de/itvsh/kop/user/UserProfileResource.java b/user-manager-server/src/main/java/de/itvsh/kop/user/UserProfileResource.java index 0c33bd5835b83c5d53c173d41a9e542da4bdffc1..772094c67aa2d3ea5df9927857abfa7362ce3cee 100644 --- a/user-manager-server/src/main/java/de/itvsh/kop/user/UserProfileResource.java +++ b/user-manager-server/src/main/java/de/itvsh/kop/user/UserProfileResource.java @@ -5,26 +5,31 @@ import javax.ws.rs.GET; import javax.ws.rs.Path; import javax.ws.rs.PathParam; import javax.ws.rs.Produces; +import javax.ws.rs.core.Context; import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.UriInfo; -import io.quarkus.hal.HalEntityWrapper; import org.jboss.resteasy.reactive.common.util.RestMediaType; +import io.quarkus.hal.HalEntityWrapper; + @Path("/api/userProfiles") public class UserProfileResource { - public static final String USER_PROFILE_RESOURCE_PATH_PREFIX = "/api/userProfiles/"; + static final String USER_PROFILE_RESOURCE_PATH_TEMPLATE = "api/userProfiles/{0}"; // NOSONAR @Inject UserService userService; @Inject UserProfileResourceAssembler userProfileResourceAssembler; + @Context + UriInfo uriInfo; @GET @Path("/{id}") @Produces({ MediaType.APPLICATION_JSON, RestMediaType.APPLICATION_HAL_JSON }) public HalEntityWrapper findById(@PathParam(value = "id") String id) { User user = userService.findById(id); - return userProfileResourceAssembler.toUserProfileResource(user); + return userProfileResourceAssembler.toUserProfileResource(user, uriInfo); } } diff --git a/user-manager-server/src/main/java/de/itvsh/kop/user/UserProfileResourceAssembler.java b/user-manager-server/src/main/java/de/itvsh/kop/user/UserProfileResourceAssembler.java index c93f500ce0462162051e754300164c5621b6e4db..d65ec03d12c8108be4fc5b9c81b9e53526064de6 100644 --- a/user-manager-server/src/main/java/de/itvsh/kop/user/UserProfileResourceAssembler.java +++ b/user-manager-server/src/main/java/de/itvsh/kop/user/UserProfileResourceAssembler.java @@ -1,18 +1,25 @@ package de.itvsh.kop.user; -import de.itvsh.kop.user.settings.UserSettingsResource; -import io.quarkus.hal.HalEntityWrapper; - import javax.enterprise.context.ApplicationScoped; import javax.ws.rs.core.Link; +import javax.ws.rs.core.UriInfo; + +import de.itvsh.kop.user.settings.UserSettingsResource; +import io.quarkus.hal.HalEntityWrapper; @ApplicationScoped public class UserProfileResourceAssembler { + static final String REL_SETTINGS = "settings"; + static final String REL_SELF = "self"; - HalEntityWrapper toUserProfileResource(User user) { + HalEntityWrapper toUserProfileResource(User user, UriInfo uriInfo) { var result = new HalEntityWrapper(user); - result.addLinks(Link.fromPath(UserProfileResource.USER_PROFILE_RESOURCE_PATH_PREFIX + user.getId().toHexString()).rel("self").build()); - result.addLinks(Link.fromPath(UserSettingsResource.SETTINGS_LINK_PATTERN.formatted(user.getId().toHexString())).rel("settings").build()); + + result.addLinks( + Link.fromUri(uriInfo.getBaseUri().toString() + UserProfileResource.USER_PROFILE_RESOURCE_PATH_TEMPLATE).rel(REL_SELF) + .build(user.getId().toHexString()), + Link.fromUri(uriInfo.getBaseUri().toString() + UserSettingsResource.SETTINGS_LINK_PATTERN).rel(REL_SETTINGS) + .build(user.getId().toHexString())); return result; } diff --git a/user-manager-server/src/main/java/de/itvsh/kop/user/common/callcontext/CallContextUser.java b/user-manager-server/src/main/java/de/itvsh/kop/user/common/callcontext/CallContextUser.java new file mode 100644 index 0000000000000000000000000000000000000000..c13b5a8ce70b2b4b231203f2f7c95561db7752f0 --- /dev/null +++ b/user-manager-server/src/main/java/de/itvsh/kop/user/common/callcontext/CallContextUser.java @@ -0,0 +1,36 @@ +package de.itvsh.kop.user.common.callcontext; + +import java.io.Serializable; +import java.security.Principal; +import java.util.Collection; +import java.util.Optional; + +import lombok.Builder; +import lombok.Getter; +import lombok.Singular; + +@Builder +@Getter +public class CallContextUser implements Serializable, Principal { + + private static final long serialVersionUID = 1L; + + private final String clientName; + + @Builder.Default + private final transient Optional<String> userId = Optional.empty(); + @Builder.Default + private final transient Optional<String> userName = Optional.empty(); + @Singular + private final Collection<String> organisatorischeEinheitenIds; + @Builder.Default + private final transient boolean organisationEinheitenIdCheckNecessary = false; + + @Builder.Default + private final transient boolean authenticated = false; + + @Override + public String getName() { + return clientName; + } +} diff --git a/user-manager-server/src/main/java/de/itvsh/kop/user/common/callcontext/CurrentCallContextUserService.java b/user-manager-server/src/main/java/de/itvsh/kop/user/common/callcontext/CurrentCallContextUserService.java new file mode 100644 index 0000000000000000000000000000000000000000..c3293e92809dd95bc690e4371cb53b56f0458a54 --- /dev/null +++ b/user-manager-server/src/main/java/de/itvsh/kop/user/common/callcontext/CurrentCallContextUserService.java @@ -0,0 +1,19 @@ +package de.itvsh.kop.user.common.callcontext; + +import java.util.Optional; + +import javax.enterprise.context.RequestScoped; + +@RequestScoped +public class CurrentCallContextUserService { + private CallContextUser user; + + void setCallContextUser(CallContextUser callContextUser) { + this.user = callContextUser; + } + + public Optional<CallContextUser> getCurrentCallContextUser() { + return Optional.ofNullable(user); + } + +} diff --git a/user-manager-server/src/main/java/de/itvsh/kop/user/common/callcontext/GrpcCallContextInterceptor.java b/user-manager-server/src/main/java/de/itvsh/kop/user/common/callcontext/GrpcCallContextInterceptor.java new file mode 100644 index 0000000000000000000000000000000000000000..ef9cf393a22a38fb7acdb37eb622e01b84e10afe --- /dev/null +++ b/user-manager-server/src/main/java/de/itvsh/kop/user/common/callcontext/GrpcCallContextInterceptor.java @@ -0,0 +1,138 @@ +package de.itvsh.kop.user.common.callcontext; + +import java.nio.charset.StandardCharsets; +import java.util.Collection; +import java.util.Optional; +import java.util.UUID; +import java.util.stream.Stream; +import java.util.stream.StreamSupport; + +import javax.enterprise.context.ApplicationScoped; +import javax.inject.Inject; + +import org.apache.logging.log4j.CloseableThreadContext; + +import io.grpc.ForwardingServerCallListener; +import io.grpc.Metadata; +import io.grpc.Metadata.Key; +import io.grpc.ServerCall; +import io.grpc.ServerCall.Listener; +import io.grpc.ServerCallHandler; +import io.grpc.ServerInterceptor; +import io.quarkus.grpc.GlobalInterceptor; +import lombok.extern.log4j.Log4j2; + +@ApplicationScoped +@GlobalInterceptor +@Log4j2 +public class GrpcCallContextInterceptor implements ServerInterceptor { + + private static final String REQUEST_ID_KEY = "requestId"; + + static final String KEY_USER_ID = "USER_ID-bin"; + static final String KEY_USER_NAME = "USER_NAME-bin"; + static final String KEY_CLIENT_NAME = "CLIENT_NAME-bin"; + static final String KEY_REQUEST_ID = "REQUEST_ID-bin"; + static final String KEY_ACCESS_LIMITED_ORGAID = "ACCESS_LIMITED_TO_ORGANISATORISCHEEINHEITENID-bin"; + static final String KEY_ACCESS_LIMITED = "ACCESS_LIMITED-bin"; + + static final String REQUEST_ID_PLACEHOLDER = "no-requestId-given"; + + @Inject + CurrentCallContextUserService userService; + + @Override + public <Q, S> Listener<Q> interceptCall(ServerCall<Q, S> call, Metadata headers, ServerCallHandler<Q, S> next) { + return new LogContextSettingListener<>(next.startCall(call, headers), headers); + } + + class LogContextSettingListener<A> extends ForwardingServerCallListener.SimpleForwardingServerCallListener<A> { + private final String requestId; + private final Metadata headers; + + public LogContextSettingListener(ServerCall.Listener<A> delegate, Metadata headers) { + super(delegate); + this.headers = headers; + this.requestId = getRequestId(); + } + + @Override + public void onMessage(A message) { + doSurroundOn(() -> super.onMessage(message)); + } + + @Override + public void onHalfClose() { + doSurroundOn(super::onHalfClose); + } + + @Override + public void onCancel() { + doSurroundOn(super::onCancel); + } + + @Override + public void onComplete() { + doSurroundOn(super::onComplete); + } + + @Override + public void onReady() { + doSurroundOn(super::onReady); + } + + void doSurroundOn(Runnable runnable) { + try (CloseableThreadContext.Instance ctc = CloseableThreadContext.put(REQUEST_ID_KEY, requestId)) { + startSecurityContext(); + runnable.run(); + } + } + + String getRequestId() { + return getFromHeaders(KEY_REQUEST_ID, headers).orElseGet(() -> UUID.randomUUID().toString()); + } + + void startSecurityContext() { + userService.setCallContextUser(createCallContextUser()); + + } + + CallContextUser createCallContextUser() { + var builder = CallContextUser.builder() + .userId(getFromHeaders(GrpcCallContextInterceptor.KEY_USER_ID, headers)) + .userName(getFromHeaders(KEY_USER_NAME, headers)) + .authenticated(true) + .organisatorischeEinheitenIds(getCollection(KEY_ACCESS_LIMITED_ORGAID, headers)); + + getFromHeaders(KEY_ACCESS_LIMITED, headers).map(Boolean::parseBoolean).ifPresentOrElse( + builder::organisationEinheitenIdCheckNecessary, + () -> builder.organisationEinheitenIdCheckNecessary(false)); + + // TODO throw exception if missing required data as soon all clients are fine + // with using headers for auth data. + getFromHeaders(KEY_CLIENT_NAME, headers).ifPresentOrElse(builder::clientName, () -> LOG.warn("Missing client name in grpc header.")); + + return builder.build(); + } + } + + // TODO move to a grpcUtil class in common + static Optional<String> getFromHeaders(String key, Metadata headers) { + return Optional.ofNullable(headers.get(createKeyOf(key))).map(val -> new String(val, StandardCharsets.UTF_8)); + } + + // TODO move to a grpcUtil class in common + static Collection<String> getCollection(String key, Metadata headers) { + return Optional.ofNullable(headers.getAll(createKeyOf(key))) + .map(vals -> StreamSupport.stream(vals.spliterator(), false)) + .orElseGet(Stream::empty) + .map(bytes -> new String(bytes, StandardCharsets.UTF_8)) + .toList(); + } + + // TODO move to a grpcUtil class in common + static Key<byte[]> createKeyOf(String key) { + return Key.of(key, Metadata.BINARY_BYTE_MARSHALLER); + } + +} diff --git a/user-manager-server/src/main/java/de/itvsh/kop/user/common/HttpRequestInterceptor.java b/user-manager-server/src/main/java/de/itvsh/kop/user/common/callcontext/HttpRequestInterceptor.java similarity index 65% rename from user-manager-server/src/main/java/de/itvsh/kop/user/common/HttpRequestInterceptor.java rename to user-manager-server/src/main/java/de/itvsh/kop/user/common/callcontext/HttpRequestInterceptor.java index 90d148c87317430848c5aee922289426dec670bb..d9151b813c0ed1e57bdec1152cf82ba8e211ca59 100644 --- a/user-manager-server/src/main/java/de/itvsh/kop/user/common/HttpRequestInterceptor.java +++ b/user-manager-server/src/main/java/de/itvsh/kop/user/common/callcontext/HttpRequestInterceptor.java @@ -1,4 +1,4 @@ -package de.itvsh.kop.user.common; +package de.itvsh.kop.user.common.callcontext; import java.io.IOException; @@ -12,19 +12,23 @@ import org.apache.logging.log4j.CloseableThreadContext; @Provider public class HttpRequestInterceptor implements ReaderInterceptor { - private static final String REQUEST_ID_KEY = "requestId"; - private static final String REQUEST_ID_PLACEHOLDER = "no-requestId-given"; + static final String REQUEST_ID_KEY = "requestId"; + static final String REQUEST_ID_PLACEHOLDER = "no-requestId-given"; @Override public Object aroundReadFrom(ReaderInterceptorContext context) throws IOException, WebApplicationException { - var requestId = context.getHeaders().getFirst(REQUEST_ID_KEY); + Object obj = null; - try (final CloseableThreadContext.Instance ctc = CloseableThreadContext.put(REQUEST_ID_KEY, - StringUtils.isNotEmpty(requestId) ? requestId : REQUEST_ID_PLACEHOLDER)) { + try (final CloseableThreadContext.Instance ctc = CloseableThreadContext.put(REQUEST_ID_KEY, getRequestId(context))) { obj = context.proceed(); } return obj; } + + String getRequestId(ReaderInterceptorContext context) { + var requestId = context.getHeaders().getFirst(REQUEST_ID_KEY); + return StringUtils.isNotEmpty(requestId) ? requestId : REQUEST_ID_PLACEHOLDER; + } } diff --git a/user-manager-server/src/main/java/de/itvsh/kop/user/common/callcontext/HttpSecurityFilter.java b/user-manager-server/src/main/java/de/itvsh/kop/user/common/callcontext/HttpSecurityFilter.java new file mode 100644 index 0000000000000000000000000000000000000000..0386d543bd51507a6a45cee4c55c76f9db2484f9 --- /dev/null +++ b/user-manager-server/src/main/java/de/itvsh/kop/user/common/callcontext/HttpSecurityFilter.java @@ -0,0 +1,65 @@ +package de.itvsh.kop.user.common.callcontext; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.Objects; +import java.util.Optional; + +import javax.inject.Inject; +import javax.ws.rs.container.ContainerRequestContext; +import javax.ws.rs.container.ContainerRequestFilter; +import javax.ws.rs.container.PreMatching; +import javax.ws.rs.ext.Provider; + +import org.eclipse.microprofile.jwt.JsonWebToken; + +@Provider +@PreMatching +public class HttpSecurityFilter implements ContainerRequestFilter { + public static final String ORGANSIATIONSEINHEIT_IDS_CLAIM = "organisationseinheitIds"; + public static final String USERID_CLAIM = "userId"; + + @Inject + CurrentCallContextUserService userService; + + @Inject + JsonWebToken jwt; + + @Override + public void filter(ContainerRequestContext requestContext) throws IOException { + userService.setCallContextUser(createCallContextUser()); + } + + CallContextUser createCallContextUser() { + var builder = CallContextUser.builder() + .authenticated(true) + .userName(Optional.ofNullable(jwt.getSubject())) + .organisatorischeEinheitenIds(getOrganisationseinheitIds()) + .userId(getUserId()); + + return builder.build(); + } + + @SuppressWarnings("unchecked") + List<String> getOrganisationseinheitIds() { + List<String> organisationseinheitIds = new ArrayList<>(); + + var claimValue = jwt.getClaim(ORGANSIATIONSEINHEIT_IDS_CLAIM); + if (Objects.nonNull(claimValue)) { + organisationseinheitIds.addAll((Collection<String>) claimValue); + } + + return organisationseinheitIds; + } + + private Optional<String> getUserId() { + var claimValue = jwt.getClaim(USERID_CLAIM); + if (Objects.nonNull(claimValue)) { + return Optional.of(claimValue.toString()); + } + + return Optional.empty(); + } +} diff --git a/user-manager-server/src/main/java/de/itvsh/kop/user/keycloak/KeycloakApiService.java b/user-manager-server/src/main/java/de/itvsh/kop/user/keycloak/KeycloakApiService.java index e98bc4b43da8940e5a12c6bee1324e18642cb4f6..57c9c658d003d9e0e9899edf3e9c6c95646aea74 100644 --- a/user-manager-server/src/main/java/de/itvsh/kop/user/keycloak/KeycloakApiService.java +++ b/user-manager-server/src/main/java/de/itvsh/kop/user/keycloak/KeycloakApiService.java @@ -17,9 +17,7 @@ import org.keycloak.representations.idm.UserRepresentation; import de.itvsh.kop.user.RemoteUserIterator; import de.itvsh.kop.user.User; import de.itvsh.kop.user.UserResourceMapper; -import de.itvsh.kop.user.common.errorhandling.KeycloakClientException; import de.itvsh.kop.user.common.errorhandling.KeycloakUnavailableException; -import io.quarkus.logging.Log; @ApplicationScoped class KeycloakApiService { @@ -54,14 +52,8 @@ class KeycloakApiService { private <T> T handlingKeycloakException(Supplier<T> runnable) { try { return runnable.get(); - } catch (ClientErrorException e) { - var exception = new KeycloakClientException(properties.user(), properties.realm(), keycloakUrl, e); - Log.error(exception.getMessage()); - throw exception; - } catch (ProcessingException | IllegalStateException e) { - var exception = new KeycloakUnavailableException(properties.user(), properties.realm(), keycloakUrl, e); - Log.error(exception.getMessage()); - throw exception; + } catch (ClientErrorException | ProcessingException | IllegalStateException e) { + throw new KeycloakUnavailableException(properties.user(), properties.realm(), keycloakUrl, e); } } } \ No newline at end of file diff --git a/user-manager-server/src/main/java/de/itvsh/kop/user/settings/UserSettingsResource.java b/user-manager-server/src/main/java/de/itvsh/kop/user/settings/UserSettingsResource.java index ece380b283a92bf7a9ae754c67e8ffbdaa2bd38d..6714e440a255e37009ef04aad1d473c58403234b 100644 --- a/user-manager-server/src/main/java/de/itvsh/kop/user/settings/UserSettingsResource.java +++ b/user-manager-server/src/main/java/de/itvsh/kop/user/settings/UserSettingsResource.java @@ -8,14 +8,16 @@ import javax.ws.rs.PATCH; import javax.ws.rs.Path; import javax.ws.rs.PathParam; import javax.ws.rs.Produces; +import javax.ws.rs.core.Context; import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.UriInfo; -import de.itvsh.kop.user.User; -import de.itvsh.kop.user.UserService; import org.eclipse.microprofile.jwt.JsonWebToken; import org.jboss.resteasy.reactive.ResponseStatus; import org.jboss.resteasy.reactive.common.util.RestMediaType; +import de.itvsh.kop.user.User; +import de.itvsh.kop.user.UserService; import de.itvsh.kop.user.common.errorhandling.AccessForbiddenException; import de.itvsh.kop.user.common.errorhandling.FunctionalException; import io.quarkus.hal.HalEntityWrapper; @@ -23,7 +25,7 @@ import io.quarkus.hal.HalEntityWrapper; @Path("/api/user") public class UserSettingsResource { - public static final String SETTINGS_LINK_PATTERN = "/api/user/%s/settings"; + public static final String SETTINGS_LINK_PATTERN = "api/user/{0}/settings"; @Inject UserSettingsService userSettingsService; @@ -36,13 +38,16 @@ public class UserSettingsResource { @Inject UserSettingsResourceAssembler resourceAssembler; + @Context + UriInfo uriInfo; + @GET @Path("/{id}/settings") @Produces({ MediaType.APPLICATION_JSON, RestMediaType.APPLICATION_HAL_JSON }) public HalEntityWrapper getUserSettings(@PathParam("id") String userId) { checkUserAccess(userId); - return resourceAssembler.toUserSettingResource(userSettingsService.findUserSettings(userId), userId); + return resourceAssembler.toUserSettingResource(userSettingsService.findUserSettings(userId), userId, uriInfo); } @PATCH @@ -56,8 +61,8 @@ public class UserSettingsResource { throw new FunctionalException(() -> "Request Body missing."); } var updatedSettings = userSettingsService.updateUserSettingsByUserId(userSettings, userId); - - return resourceAssembler.toUserSettingResource(updatedSettings, userId); + + return resourceAssembler.toUserSettingResource(updatedSettings, userId, uriInfo); } void checkUserAccess(String userId) { diff --git a/user-manager-server/src/main/java/de/itvsh/kop/user/settings/UserSettingsResourceAssembler.java b/user-manager-server/src/main/java/de/itvsh/kop/user/settings/UserSettingsResourceAssembler.java index 579c85fb64cd9635168236dd297274e5295411fd..3b847b0010314e67e10de3a9d007accb9073e672 100644 --- a/user-manager-server/src/main/java/de/itvsh/kop/user/settings/UserSettingsResourceAssembler.java +++ b/user-manager-server/src/main/java/de/itvsh/kop/user/settings/UserSettingsResourceAssembler.java @@ -2,6 +2,7 @@ package de.itvsh.kop.user.settings; import javax.enterprise.context.ApplicationScoped; import javax.ws.rs.core.Link; +import javax.ws.rs.core.UriInfo; import io.quarkus.hal.HalEntityWrapper; @@ -10,11 +11,12 @@ class UserSettingsResourceAssembler { static final String REL_SELF = "self"; static final String REL_EDIT = "edit"; - HalEntityWrapper toUserSettingResource(UserSettings setting, String userId) { + HalEntityWrapper toUserSettingResource(UserSettings setting, String userId, UriInfo uriInfo) { var result = new HalEntityWrapper(setting); - result.addLinks(Link.fromPath(UserSettingsResource.SETTINGS_LINK_PATTERN.formatted(userId)).rel(REL_EDIT).build()); - result.addLinks(Link.fromPath(UserSettingsResource.SETTINGS_LINK_PATTERN.formatted(userId)).rel(REL_SELF).build()); + result.addLinks( + Link.fromUri(uriInfo.getBaseUri().toString() + UserSettingsResource.SETTINGS_LINK_PATTERN).rel(REL_EDIT).build(userId), + Link.fromUri(uriInfo.getBaseUri().toString() + UserSettingsResource.SETTINGS_LINK_PATTERN).rel(REL_SELF).build(userId)); return result; } diff --git a/user-manager-server/src/main/resources/application-remotekc.yaml b/user-manager-server/src/main/resources/application-remotekc.yaml index 4384aa9b58610c4fe9cc823e81ecfb815865dbdf..e7b0d18daad528f4061e8c58c8d74a6f4983ad8d 100644 --- a/user-manager-server/src/main/resources/application-remotekc.yaml +++ b/user-manager-server/src/main/resources/application-remotekc.yaml @@ -8,7 +8,6 @@ quarkus: origins: http://localhost:4300 kop: keycloak: - url: https://sso.dev.ozg-sh.de/auth sync: cron: "* */10 * * * ?" api: diff --git a/user-manager-server/src/test/java/de/itvsh/kop/user/UserProfileResourceAssemblerTest.java b/user-manager-server/src/test/java/de/itvsh/kop/user/UserProfileResourceAssemblerTest.java new file mode 100644 index 0000000000000000000000000000000000000000..db47ad1cdcaef0f83bec931d5820e992e5b7f13b --- /dev/null +++ b/user-manager-server/src/test/java/de/itvsh/kop/user/UserProfileResourceAssemblerTest.java @@ -0,0 +1,54 @@ +package de.itvsh.kop.user; + +import static org.assertj.core.api.Assertions.*; +import static org.mockito.Mockito.*; + +import java.net.URI; +import java.net.URISyntaxException; + +import javax.ws.rs.core.UriInfo; + +import org.bson.types.ObjectId; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.mockito.Mock; +import org.mockito.Spy; + +class UserProfileResourceAssemblerTest { + + @Nested + class TestAssembler { + + @Spy + private UserProfileResourceAssembler assembler; + + @Mock + private UriInfo uriInfo; + + private static final ObjectId ID = new ObjectId(); + + @BeforeEach + void init() throws URISyntaxException { + when(uriInfo.getBaseUri()).thenReturn(new URI("http://test:8080/")); + + } + + @Test + void shouldAddSelfLink() { + var res = assembler.toUserProfileResource(UserTestFactory.createBuilder().id(ID).build(), uriInfo); + + assertThat(res.getLinks().get(UserProfileResourceAssembler.REL_SELF)).isNotNull().hasFieldOrPropertyWithValue("href", + "http://test:8080/api/userProfiles/" + ID.toHexString()); + } + + @Test + void shouldAddSettingsLink() { + var res = assembler.toUserProfileResource(UserTestFactory.createBuilder().id(ID).build(), uriInfo); + + assertThat(res.getLinks().get(UserProfileResourceAssembler.REL_SETTINGS)).isNotNull().hasFieldOrPropertyWithValue("href", + "http://test:8080/api/user/" + ID.toHexString() + "/settings"); + } + } + +} diff --git a/user-manager-server/src/test/java/de/itvsh/kop/user/UserProfileResourceITCase.java b/user-manager-server/src/test/java/de/itvsh/kop/user/UserProfileResourceITCase.java index add9fe736b4e08bf662de4a04ae8c6064fb5c236..626ba347b5153baecdc03b5cff8fc336e7369aeb 100644 --- a/user-manager-server/src/test/java/de/itvsh/kop/user/UserProfileResourceITCase.java +++ b/user-manager-server/src/test/java/de/itvsh/kop/user/UserProfileResourceITCase.java @@ -1,22 +1,27 @@ package de.itvsh.kop.user; -import de.itvsh.kop.user.settings.UserSettingsResource; -import io.quarkus.test.junit.QuarkusTest; -import io.quarkus.test.junit.TestProfile; +import static io.restassured.RestAssured.*; + +import java.text.MessageFormat; + +import javax.inject.Inject; +import javax.ws.rs.core.MediaType; + import org.apache.http.HttpStatus; import org.hamcrest.CoreMatchers; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -import javax.inject.Inject; -import javax.ws.rs.core.MediaType; - -import static io.restassured.RestAssured.*; +import de.itvsh.kop.user.settings.UserSettingsResourceTestProfile; +import io.quarkus.test.junit.QuarkusTest; +import io.quarkus.test.junit.TestProfile; @QuarkusTest @TestProfile(UserSettingsResourceTestProfile.class) class UserProfileResourceITCase { + private static final String HTTP_LOCALHOST = "http://localhost:8081/"; + @Inject private UserRepository userRepository; @@ -30,14 +35,15 @@ class UserProfileResourceITCase { @Test void shouldGetUserWithHyperLinks() { - var userId = user.getId(); + var userId = user.getId(); + String url = MessageFormat.format(UserProfileResource.USER_PROFILE_RESOURCE_PATH_TEMPLATE, userId); given() .when() .accept(MediaType.APPLICATION_JSON) - .get(UserProfileResource.USER_PROFILE_RESOURCE_PATH_PREFIX + userId) + .get(url) .then() .statusCode(HttpStatus.SC_OK) - .body("_links.settings.href", CoreMatchers.is(UserSettingsResource.SETTINGS_LINK_PATTERN.formatted(userId))) - .body("_links.self.href", CoreMatchers.is(UserProfileResource.USER_PROFILE_RESOURCE_PATH_PREFIX + userId)); + .body("_links.settings.href", CoreMatchers.is(HTTP_LOCALHOST + "api/user/" + userId + "/settings")) + .body("_links.self.href", CoreMatchers.is(HTTP_LOCALHOST + url)); } } \ No newline at end of file diff --git a/user-manager-server/src/test/java/de/itvsh/kop/user/UserTestFactory.java b/user-manager-server/src/test/java/de/itvsh/kop/user/UserTestFactory.java index f6f3d9b9027508d59af36a8e1f7386fc755dafeb..d497b0140f3a760a95bdf6401a468e5e9a9ffb27 100644 --- a/user-manager-server/src/test/java/de/itvsh/kop/user/UserTestFactory.java +++ b/user-manager-server/src/test/java/de/itvsh/kop/user/UserTestFactory.java @@ -7,6 +7,8 @@ import org.bson.types.ObjectId; import com.thedeanda.lorem.LoremIpsum; +import de.itvsh.kop.user.User; +import de.itvsh.kop.user.User.UserBuilder; import de.itvsh.kop.user.settings.NotificationsSendFor; import de.itvsh.kop.user.settings.UserSettings; diff --git a/user-manager-server/src/test/java/de/itvsh/kop/user/common/callcontext/CallContextMetadataTestFactory.java b/user-manager-server/src/test/java/de/itvsh/kop/user/common/callcontext/CallContextMetadataTestFactory.java new file mode 100644 index 0000000000000000000000000000000000000000..09d92cea80d86e729ec0fa8a731419c6d1dea45f --- /dev/null +++ b/user-manager-server/src/test/java/de/itvsh/kop/user/common/callcontext/CallContextMetadataTestFactory.java @@ -0,0 +1,35 @@ +package de.itvsh.kop.user.common.callcontext; + +import java.util.UUID; + +import org.bson.types.ObjectId; + +import com.thedeanda.lorem.LoremIpsum; + +import io.grpc.Metadata; + +public class CallContextMetadataTestFactory { + + public static final String CLIENT = "testclient-0817"; + public static final String ID = new ObjectId().toHexString(); + public static final String NAME = LoremIpsum.getInstance().getName(); + public static final String ORGANISATORISCHE_EINHEITEN_ID = "0815"; + + public static final String REQUEST_ID = UUID.randomUUID().toString(); + public static final Boolean DO_ORGA_ID_ACCESS_CHECK = Boolean.TRUE; + + public static Metadata createMetadata() { + var result = new Metadata(); + result.put(GrpcCallContextInterceptor.createKeyOf(GrpcCallContextInterceptor.KEY_USER_ID), ID.getBytes()); + result.put(GrpcCallContextInterceptor.createKeyOf(GrpcCallContextInterceptor.KEY_USER_NAME), NAME.getBytes()); + result.put(GrpcCallContextInterceptor.createKeyOf(GrpcCallContextInterceptor.KEY_CLIENT_NAME), CLIENT.getBytes()); + result.put(GrpcCallContextInterceptor.createKeyOf(GrpcCallContextInterceptor.KEY_ACCESS_LIMITED_ORGAID), + ORGANISATORISCHE_EINHEITEN_ID.getBytes()); + result.put(GrpcCallContextInterceptor.createKeyOf(GrpcCallContextInterceptor.KEY_REQUEST_ID), REQUEST_ID.getBytes()); + result.put(GrpcCallContextInterceptor.createKeyOf(GrpcCallContextInterceptor.KEY_ACCESS_LIMITED), + DO_ORGA_ID_ACCESS_CHECK.toString().getBytes()); + + return result; + } + +} diff --git a/user-manager-server/src/test/java/de/itvsh/kop/user/common/callcontext/CallContextUserTestFactory.java b/user-manager-server/src/test/java/de/itvsh/kop/user/common/callcontext/CallContextUserTestFactory.java new file mode 100644 index 0000000000000000000000000000000000000000..696e5a497bb20b5598dbe3dfe7ec16c89b5bd59f --- /dev/null +++ b/user-manager-server/src/test/java/de/itvsh/kop/user/common/callcontext/CallContextUserTestFactory.java @@ -0,0 +1,24 @@ +package de.itvsh.kop.user.common.callcontext; + +import java.util.List; +import java.util.Optional; + +public class CallContextUserTestFactory { + static final String NAME = CallContextMetadataTestFactory.NAME; + static final String ID = CallContextMetadataTestFactory.ID; + static final List<String> ORGANISATORISCHE_EINHEITEN_ID = List.of(CallContextMetadataTestFactory.ORGANISATORISCHE_EINHEITEN_ID); + + public static CallContextUser create() { + return createBuilder().build(); + } + + public static CallContextUser.CallContextUserBuilder createBuilder() { + return CallContextUser.builder() + .clientName(CallContextMetadataTestFactory.CLIENT) + .userId(Optional.of(CallContextMetadataTestFactory.ID)) + .userName(Optional.of(CallContextMetadataTestFactory.NAME)) + .organisatorischeEinheitenId(CallContextMetadataTestFactory.ORGANISATORISCHE_EINHEITEN_ID) + .authenticated(true) + .organisationEinheitenIdCheckNecessary(CallContextMetadataTestFactory.DO_ORGA_ID_ACCESS_CHECK); + } +} diff --git a/user-manager-server/src/test/java/de/itvsh/kop/user/common/callcontext/CurrentCallContextUserServiceTest.java b/user-manager-server/src/test/java/de/itvsh/kop/user/common/callcontext/CurrentCallContextUserServiceTest.java new file mode 100644 index 0000000000000000000000000000000000000000..601a68b5fbf07ec5f22f8777d925cc79104c0175 --- /dev/null +++ b/user-manager-server/src/test/java/de/itvsh/kop/user/common/callcontext/CurrentCallContextUserServiceTest.java @@ -0,0 +1,28 @@ +package de.itvsh.kop.user.common.callcontext; + +import static org.assertj.core.api.Assertions.*; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.mockito.Spy; + +class CurrentCallContextUserServiceTest { + @Spy + private CurrentCallContextUserService userService; + + @Nested + class TestService { + CallContextUser user = CallContextUserTestFactory.create(); + + @BeforeEach + void init() { + userService.setCallContextUser(user); + } + + @Test + void shouldGetUser() { + assertThat(userService.getCurrentCallContextUser()).isPresent().get().isEqualTo(user); + } + } +} diff --git a/user-manager-server/src/test/java/de/itvsh/kop/user/common/callcontext/GrpcCallContextInterceptorTest.java b/user-manager-server/src/test/java/de/itvsh/kop/user/common/callcontext/GrpcCallContextInterceptorTest.java new file mode 100644 index 0000000000000000000000000000000000000000..2be53e0cfa83f2c9fbe7050581ada500cfb1c584 --- /dev/null +++ b/user-manager-server/src/test/java/de/itvsh/kop/user/common/callcontext/GrpcCallContextInterceptorTest.java @@ -0,0 +1,231 @@ +package de.itvsh.kop.user.common.callcontext; + +import static de.itvsh.kop.user.common.callcontext.GrpcCallContextInterceptor.*; +import static org.assertj.core.api.Assertions.*; +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.Mockito.*; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.Spy; + +import de.itvsh.kop.user.common.callcontext.GrpcCallContextInterceptor.LogContextSettingListener; +import io.grpc.Metadata; +import io.grpc.ServerCall; + +class CallContextHandleInterceptorTest { + + @InjectMocks + private GrpcCallContextInterceptor interceptor; + + @Mock + private ServerCall.Listener<Object> delegate; + + @Spy + private CurrentCallContextUserService userService; + + @Nested + class TestCallListener { + private LogContextSettingListener<?> listener; + private Metadata headers = CallContextMetadataTestFactory.createMetadata(); + + @BeforeEach + void createListener() { + listener = spy(interceptor.new LogContextSettingListener<>(delegate, headers)); + } + + @Nested + class TestGetRequestId { + @Test + void shouldReturnIdFromHeader() { + var reqId = listener.getRequestId(); + + assertThat(reqId).isEqualTo(CallContextMetadataTestFactory.REQUEST_ID); + } + + @Test + void shouldReturnNewRequestIdIfNoGiven() { + headers.removeAll(createKeyOf(KEY_REQUEST_ID)); + + var reqId = listener.getRequestId(); + + assertThat(reqId).isNotBlank().isNotEqualTo(CallContextMetadataTestFactory.REQUEST_ID); + } + + } + + @DisplayName("Map headers to user") + @Nested + class TestHeadersToUser { + @Test + void shouldFilledUser() { + var user = buildListener().createCallContextUser(); + + assertThat(user).usingRecursiveComparison().isEqualTo(CallContextUserTestFactory.create()); + } + + @Test + void shouldFillOrgaIdCollection() { + Metadata metadata = CallContextMetadataTestFactory.createMetadata(); + metadata.put(createKeyOf(KEY_ACCESS_LIMITED_ORGAID), "orgaid_2".getBytes()); + + var user = buildListener(metadata).createCallContextUser(); + + assertThat(user.getOrganisatorischeEinheitenIds()).hasSize(2).contains( + CallContextMetadataTestFactory.ORGANISATORISCHE_EINHEITEN_ID, + "orgaid_2"); + } + + @Test + void shouldFillEmptyListIfNoOrgaIds() { + Metadata metadata = CallContextMetadataTestFactory.createMetadata(); + metadata.removeAll(createKeyOf(KEY_ACCESS_LIMITED_ORGAID)); + + var user = buildListener(metadata).createCallContextUser(); + + assertThat(user.getOrganisatorischeEinheitenIds()).isEmpty(); + } + + @DisplayName("for key isOrganisationEinheitenIdCheckNecessary") + @Nested + class TestIsOrganisationEinheitenIdCheckNecessary { + + @Test + void shouldMapValueIfKeyIsPresent() { + var metadata = CallContextMetadataTestFactory.createMetadata(); + + var user = buildListener(metadata).createCallContextUser(); + + assertThat(user.isOrganisationEinheitenIdCheckNecessary()).isTrue(); + } + + @Test + void shouldMapFalseIfKeyIsNotPresent() { + var metadata = CallContextMetadataTestFactory.createMetadata(); + metadata.removeAll(createKeyOf(KEY_ACCESS_LIMITED)); + + var user = buildListener(metadata).createCallContextUser(); + + assertThat(user.isOrganisationEinheitenIdCheckNecessary()).isFalse(); + } + } + + } + + @Nested + class TestStartSecurityContext { + @Test + void shouldHaveAuthenticatedAuthorization() { + + listener.startSecurityContext(); + + var userOptional = userService.getCurrentCallContextUser(); + + assertThat(userOptional).isPresent(); + assertThat(userOptional.get().isAuthenticated()).isTrue(); + } + + } + + @Nested + class TestDoSurround { + + private Runnable mockRunnable = mock(Runnable.class); + + @Test + void shouldRunRunnable() { + listener.doSurroundOn(mockRunnable); + + verify(mockRunnable).run(); + } + + @Test + void shouldStartSecurityContext() { + listener.doSurroundOn(mockRunnable); + + verify(listener).startSecurityContext(); + } + + @Test + void shouldClearSecurityContext() { + listener.doSurroundOn(mockRunnable); + + // verify(listener).clearSecurityContext(); + } + + @Test + void shouldClearSecurityContextOnException() { + var testException = new RuntimeException() { + }; + + assertThatThrownBy(() -> listener.doSurroundOn(() -> { + throw testException; + })).isSameAs(testException); + + // verify(listener).clearSecurityContext(); + } + } + + @Nested + class TestOnFunctions { + + private LogContextSettingListener<Object> listener; + + @BeforeEach + void initListener() { + listener = buildListener(); + } + + @Test + void onMessageShouldCallSurround() { + Object message = mock(Object.class); + + listener.onMessage(message); + + verify(listener).doSurroundOn(any()); + } + + @Test + void onHalfCloseShouldCallSurround() { + listener.onHalfClose(); + + verify(listener).doSurroundOn(any()); + } + + @Test + void onCancelShouldCallSurround() { + listener.onCancel(); + + verify(listener).doSurroundOn(any()); + } + + @Test + void onCompleteShouldCallSurround() { + listener.onComplete(); + + verify(listener).doSurroundOn(any()); + } + + @Test + void onReadyShouldCallSurround() { + listener.onReady(); + + verify(listener).doSurroundOn(any()); + } + } + + private LogContextSettingListener<Object> buildListener() { + return buildListener(headers); + } + + private LogContextSettingListener<Object> buildListener(Metadata headers) { + return spy(interceptor.new LogContextSettingListener<Object>(delegate, headers)); + } + + } + +} diff --git a/user-manager-server/src/test/java/de/itvsh/kop/user/common/callcontext/HttpRequestInterceptorTest.java b/user-manager-server/src/test/java/de/itvsh/kop/user/common/callcontext/HttpRequestInterceptorTest.java new file mode 100644 index 0000000000000000000000000000000000000000..2c89ff8cbb028401ea295edb52e79f1f0623ccb1 --- /dev/null +++ b/user-manager-server/src/test/java/de/itvsh/kop/user/common/callcontext/HttpRequestInterceptorTest.java @@ -0,0 +1,69 @@ +package de.itvsh.kop.user.common.callcontext; + +import static org.assertj.core.api.Assertions.*; +import static org.mockito.Mockito.*; + +import java.util.List; +import java.util.UUID; + +import javax.ws.rs.core.MultivaluedHashMap; +import javax.ws.rs.ext.ReaderInterceptorContext; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.mockito.Mock; +import org.mockito.Spy; + +import de.itvsh.kop.user.common.callcontext.HttpRequestInterceptor; + +class HttpRequestInterceptorTest { + @Spy + private HttpRequestInterceptor interceptor; + + @Nested + class TestInterceptor { + + @Mock + private ReaderInterceptorContext context; + + @Nested + class WithRequestId { + String id = UUID.randomUUID().toString(); + + @BeforeEach + void initHeader() { + var headers = new MultivaluedHashMap<String, String>(); + + headers.put(HttpRequestInterceptor.REQUEST_ID_KEY, List.of(id)); + when(context.getHeaders()).thenReturn(headers); + } + + @Test + void shouldSetRequestId() { + var requestId = interceptor.getRequestId(context); + + assertThat(requestId).isEqualTo(id); + } + } + + @Nested + class WithoutRequestId { + + @BeforeEach + void initHeader() { + var headers = new MultivaluedHashMap<String, String>(); + headers.put("xxx", List.of("y")); + when(context.getHeaders()).thenReturn(headers); + } + + @Test + void shouldSetDummyRequestId() { + var requestId = interceptor.getRequestId(context); + + assertThat(requestId).isEqualTo(HttpRequestInterceptor.REQUEST_ID_PLACEHOLDER); + } + } + + } +} diff --git a/user-manager-server/src/test/java/de/itvsh/kop/user/common/callcontext/HttpSecurityFilterTest.java b/user-manager-server/src/test/java/de/itvsh/kop/user/common/callcontext/HttpSecurityFilterTest.java new file mode 100644 index 0000000000000000000000000000000000000000..e3bb0723aa53e612793c44577859bab5d3753c6f --- /dev/null +++ b/user-manager-server/src/test/java/de/itvsh/kop/user/common/callcontext/HttpSecurityFilterTest.java @@ -0,0 +1,94 @@ +package de.itvsh.kop.user.common.callcontext; + +import static org.assertj.core.api.Assertions.*; +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.Mockito.*; + +import java.io.IOException; + +import javax.ws.rs.container.ContainerRequestContext; + +import org.eclipse.microprofile.jwt.JsonWebToken; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.Spy; + +class HttpSecurityFilterTest { + @Spy + @InjectMocks + private HttpSecurityFilter securityFilter; + + @Mock + private JsonWebToken token; + + @Mock + private CurrentCallContextUserService contextUserService; + + @Mock + private ContainerRequestContext requestContext; + + @Nested + class TestSettingUser { + @Test + void shouldSetUser() throws IOException { + when(token.getClaim(HttpSecurityFilter.USERID_CLAIM)).thenReturn(CallContextUserTestFactory.ID); + when(token.getClaim(HttpSecurityFilter.ORGANSIATIONSEINHEIT_IDS_CLAIM)) + .thenReturn(CallContextUserTestFactory.ORGANISATORISCHE_EINHEITEN_ID); + + securityFilter.filter(requestContext); + + verify(contextUserService).setCallContextUser(any()); + } + } + + @Nested + class TestCreateCallContextUser { + @BeforeEach + void init() { + when(token.getClaim(HttpSecurityFilter.USERID_CLAIM)).thenReturn(CallContextUserTestFactory.ID); + when(token.getSubject()).thenReturn(CallContextUserTestFactory.NAME); + when(token.getClaim(HttpSecurityFilter.ORGANSIATIONSEINHEIT_IDS_CLAIM)) + .thenReturn(CallContextUserTestFactory.ORGANISATORISCHE_EINHEITEN_ID); + } + + @Test + void shouldBeAuthenticated() { + var user = securityFilter.createCallContextUser(); + + assertThat(user.isAuthenticated()).isTrue(); + } + + @Test + void shouldHaveUserId() { + var user = securityFilter.createCallContextUser(); + + assertThat(user.getUserId()).isPresent().get().isEqualTo(CallContextUserTestFactory.ID); + } + + @Test + void shouldHaveUserName() { + var user = securityFilter.createCallContextUser(); + + assertThat(user.getUserName()).isPresent().get().isEqualTo(CallContextUserTestFactory.NAME); + } + + @Test + void shouldHaveOrganistaionsEinheitenIds() { + var user = securityFilter.createCallContextUser(); + + assertThat(user.getOrganisatorischeEinheitenIds()).isNotEmpty(); + } + + @Test + void shouldNotHaveUserName() { + when(token.getSubject()).thenReturn(null); + + var user = securityFilter.createCallContextUser(); + + assertThat(user.getUserName()).isNotPresent(); + } + } +} diff --git a/user-manager-server/src/test/java/de/itvsh/kop/user/recipient/RecipientMapperTest.java b/user-manager-server/src/test/java/de/itvsh/kop/user/recipient/RecipientMapperTest.java index 8ee4f8edca5b1745cbf8eec8f015112a2e5d8209..fa367a57aafbf9b29f49357e14b9d533d400e34b 100644 --- a/user-manager-server/src/test/java/de/itvsh/kop/user/recipient/RecipientMapperTest.java +++ b/user-manager-server/src/test/java/de/itvsh/kop/user/recipient/RecipientMapperTest.java @@ -1,9 +1,10 @@ package de.itvsh.kop.user.recipient; -import de.itvsh.kop.user.UserTestFactory; import org.junit.jupiter.api.Test; import org.mapstruct.factory.Mappers; +import de.itvsh.kop.user.UserTestFactory; + import static org.assertj.core.api.Assertions.*; class RecipientMapperTest { diff --git a/user-manager-server/src/test/java/de/itvsh/kop/user/recipient/RecipientTestFactory.java b/user-manager-server/src/test/java/de/itvsh/kop/user/recipient/RecipientTestFactory.java index 83ac2a5ceda4342bdd01da0a536ee5a0ed7eb2b7..01a03d7027c438c8f979055d83fcaffe6761a926 100644 --- a/user-manager-server/src/test/java/de/itvsh/kop/user/recipient/RecipientTestFactory.java +++ b/user-manager-server/src/test/java/de/itvsh/kop/user/recipient/RecipientTestFactory.java @@ -1,11 +1,11 @@ package de.itvsh.kop.user.recipient; +import static de.itvsh.kop.user.UserTestFactory.*; + import de.itvsh.kop.user.UserTestFactory; import de.itvsh.kop.user.grpc.Recipient; import de.itvsh.kop.user.grpc.RecipientSearchRequest; -import static de.itvsh.kop.user.UserTestFactory.*; - public class RecipientTestFactory { public static final String ORGANISTATIONSEINHEITEN_ID = UserTestFactory.ORGANISTATIONSEINHEITEN_IDS.get(0); diff --git a/user-manager-server/src/test/java/de/itvsh/kop/user/settings/UserSettingResourceAssemblerTest.java b/user-manager-server/src/test/java/de/itvsh/kop/user/settings/UserSettingResourceAssemblerTest.java index 298c5a019540a8ae0e978fecc5f2e2e8e3dba291..fd42b488728ac41a1ab824b84f833d6ac8e24271 100644 --- a/user-manager-server/src/test/java/de/itvsh/kop/user/settings/UserSettingResourceAssemblerTest.java +++ b/user-manager-server/src/test/java/de/itvsh/kop/user/settings/UserSettingResourceAssemblerTest.java @@ -1,13 +1,19 @@ package de.itvsh.kop.user.settings; import static org.assertj.core.api.Assertions.*; +import static org.mockito.Mockito.*; +import java.net.URI; +import java.net.URISyntaxException; + +import javax.ws.rs.core.UriInfo; + +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; +import org.mockito.Mock; import org.mockito.Spy; -import de.itvsh.kop.user.UserSettingsTestFactory; - class UserSettingResourceAssemblerTest { @Nested @@ -16,20 +22,30 @@ class UserSettingResourceAssemblerTest { @Spy private UserSettingsResourceAssembler assembler; + @Mock + private UriInfo uriInfo; + private static final String ID = "id"; + @BeforeEach + void init() throws URISyntaxException { + when(uriInfo.getBaseUri()).thenReturn(new URI("http://test:8080/")); + } + @Test void shouldAddSelfLink() { - var res = assembler.toUserSettingResource(UserSettingsTestFactory.create(), ID); + var res = assembler.toUserSettingResource(UserSettingsTestFactory.create(), ID, uriInfo); - assertThat(res.getLinks().get(UserSettingsResourceAssembler.REL_SELF)).isNotNull(); + assertThat(res.getLinks().get(UserSettingsResourceAssembler.REL_SELF)).isNotNull().hasFieldOrPropertyWithValue("href", + "http://test:8080/api/user/" + ID + "/settings"); } @Test void shouldAddEditLink() { - var res = assembler.toUserSettingResource(UserSettingsTestFactory.create(), ID); + var res = assembler.toUserSettingResource(UserSettingsTestFactory.create(), ID, uriInfo); - assertThat(res.getLinks().get(UserSettingsResourceAssembler.REL_EDIT)).isNotNull(); + assertThat(res.getLinks().get(UserSettingsResourceAssembler.REL_EDIT)).isNotNull().hasFieldOrPropertyWithValue("href", + "http://test:8080/api/user/" + ID + "/settings"); } } diff --git a/user-manager-server/src/test/java/de/itvsh/kop/user/UserSettingsResourceITCase.java b/user-manager-server/src/test/java/de/itvsh/kop/user/settings/UserSettingsResourceITCase.java similarity index 80% rename from user-manager-server/src/test/java/de/itvsh/kop/user/UserSettingsResourceITCase.java rename to user-manager-server/src/test/java/de/itvsh/kop/user/settings/UserSettingsResourceITCase.java index b914f98246305b90f2bef421b0a04f3a35c7f5a4..d25783fccb642a610c688c597839c1a41039e6fe 100644 --- a/user-manager-server/src/test/java/de/itvsh/kop/user/UserSettingsResourceITCase.java +++ b/user-manager-server/src/test/java/de/itvsh/kop/user/settings/UserSettingsResourceITCase.java @@ -1,4 +1,4 @@ -package de.itvsh.kop.user; +package de.itvsh.kop.user.settings; import static io.restassured.RestAssured.*; import static org.hamcrest.CoreMatchers.*; @@ -11,8 +11,8 @@ import org.eclipse.microprofile.jwt.JsonWebToken; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -import de.itvsh.kop.user.settings.NotificationsSendFor; -import de.itvsh.kop.user.settings.UserSettings; +import de.itvsh.kop.user.UserRepository; +import de.itvsh.kop.user.UserTestFactory; import io.quarkus.test.junit.QuarkusTest; import io.quarkus.test.junit.TestProfile; @@ -21,6 +21,8 @@ import io.quarkus.test.junit.TestProfile; class UserSettingsResourceITCase { private static final String API_PATTERN = "/api/user/{id}/settings"; + private static final String HTTP_LOCALHOST = "http://localhost:8081/"; + @Inject UserRepository userRepository; @@ -48,7 +50,7 @@ class UserSettingsResourceITCase { .then() .statusCode(HttpStatus.SC_OK) .body("notificationsSendFor", is("ALL")) - .body("_links.edit.href", is("/api/user/" + userId + "/settings")); + .body("_links.edit.href", is(HTTP_LOCALHOST + "api/user/" + userId + "/settings")); } @Test @@ -62,6 +64,6 @@ class UserSettingsResourceITCase { .then() .statusCode(HttpStatus.SC_OK) .body("notificationsSendFor", is("NONE")) - .body("_links.self.href", is("/api/user/" + userId + "/settings")); + .body("_links.self.href", is(HTTP_LOCALHOST + "api/user/" + userId + "/settings")); } } \ No newline at end of file diff --git a/user-manager-server/src/test/java/de/itvsh/kop/user/settings/UserSettingsResourceTest.java b/user-manager-server/src/test/java/de/itvsh/kop/user/settings/UserSettingsResourceTest.java index ca0a519a93f6c6bdff6e5e9675a01b8694619adf..e763f9bebbf9e0ed7b7c917a4597929fce431e49 100644 --- a/user-manager-server/src/test/java/de/itvsh/kop/user/settings/UserSettingsResourceTest.java +++ b/user-manager-server/src/test/java/de/itvsh/kop/user/settings/UserSettingsResourceTest.java @@ -4,11 +4,12 @@ import static org.assertj.core.api.Assertions.*; import static org.mockito.ArgumentMatchers.*; import static org.mockito.Mockito.*; +import java.net.URI; +import java.net.URISyntaxException; import java.util.UUID; -import de.itvsh.kop.user.UserService; -import de.itvsh.kop.user.UserSettingsTestFactory; -import de.itvsh.kop.user.UserTestFactory; +import javax.ws.rs.core.UriInfo; + import org.bson.types.ObjectId; import org.eclipse.microprofile.jwt.JsonWebToken; import org.junit.jupiter.api.BeforeEach; @@ -18,6 +19,8 @@ import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.Spy; +import de.itvsh.kop.user.UserService; +import de.itvsh.kop.user.UserTestFactory; import de.itvsh.kop.user.common.errorhandling.AccessForbiddenException; class UserSettingsResourceTest { @@ -35,6 +38,9 @@ class UserSettingsResourceTest { @Mock private UserSettingsService userSettingsService; + @Mock + private UriInfo uriInfo; + @Spy private UserSettingsResourceAssembler resourceAssembler; @@ -42,13 +48,13 @@ class UserSettingsResourceTest { @Nested class TestAccess { - @BeforeEach - void init() { + void init() throws URISyntaxException { when(token.getSubject()).thenReturn(UserTestFactory.EXTERNAL_ID); var user = UserTestFactory.createBuilder().id(new ObjectId()).build(); userId = user.getId().toHexString(); when(userService.findUserByExternalId(UserTestFactory.EXTERNAL_ID)).thenReturn(user); + when(uriInfo.getBaseUri()).thenReturn(new URI("http://test:8080")); } @Test diff --git a/user-manager-server/src/test/java/de/itvsh/kop/user/UserSettingsResourceTestProfile.java b/user-manager-server/src/test/java/de/itvsh/kop/user/settings/UserSettingsResourceTestProfile.java similarity index 86% rename from user-manager-server/src/test/java/de/itvsh/kop/user/UserSettingsResourceTestProfile.java rename to user-manager-server/src/test/java/de/itvsh/kop/user/settings/UserSettingsResourceTestProfile.java index 2985730a2b70d52079302f15ac00492191aef1e6..35774d07c2f153a6b2a5844c3ecd410d40b5ece0 100644 --- a/user-manager-server/src/test/java/de/itvsh/kop/user/UserSettingsResourceTestProfile.java +++ b/user-manager-server/src/test/java/de/itvsh/kop/user/settings/UserSettingsResourceTestProfile.java @@ -1,13 +1,13 @@ -package de.itvsh.kop.user; - -import io.quarkus.test.junit.QuarkusTestProfile; +package de.itvsh.kop.user.settings; import java.util.Map; +import io.quarkus.test.junit.QuarkusTestProfile; + public class UserSettingsResourceTestProfile implements QuarkusTestProfile { @Override - public Map<String,String> getConfigOverrides() { + public Map<String, String> getConfigOverrides() { return Map.of("quarkus.http.auth.permission.deny-api.enabled", "false", "quarkus.http.auth.permission.deny-api.policy", "permit", "quarkus.http.auth.permission.permit-migration.enabled", "false", diff --git a/user-manager-server/src/test/java/de/itvsh/kop/user/UserSettingsServiceTest.java b/user-manager-server/src/test/java/de/itvsh/kop/user/settings/UserSettingsServiceTest.java similarity index 88% rename from user-manager-server/src/test/java/de/itvsh/kop/user/UserSettingsServiceTest.java rename to user-manager-server/src/test/java/de/itvsh/kop/user/settings/UserSettingsServiceTest.java index c2c4ffa086e7a5a74f7dd2364f3cc4b3b859aeef..709e59481fbe2001d7a3a5b9bd76327fafd0e71e 100644 --- a/user-manager-server/src/test/java/de/itvsh/kop/user/UserSettingsServiceTest.java +++ b/user-manager-server/src/test/java/de/itvsh/kop/user/settings/UserSettingsServiceTest.java @@ -1,4 +1,4 @@ -package de.itvsh.kop.user; +package de.itvsh.kop.user.settings; import static org.assertj.core.api.Assertions.*; import static org.junit.jupiter.api.Assertions.*; @@ -6,15 +6,15 @@ import static org.mockito.Mockito.*; import java.util.Optional; -import de.itvsh.kop.user.settings.UserSettingsService; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import org.mockito.InjectMocks; import org.mockito.Mock; +import de.itvsh.kop.user.User; +import de.itvsh.kop.user.UserRepository; +import de.itvsh.kop.user.UserTestFactory; import de.itvsh.kop.user.common.errorhandling.ResourceNotFoundException; -import de.itvsh.kop.user.settings.NotificationsSendFor; -import de.itvsh.kop.user.settings.UserSettings; public class UserSettingsServiceTest { @InjectMocks @@ -50,6 +50,5 @@ public class UserSettingsServiceTest { assertThat(updatedUserSettings.getNotificationsSendFor()).isEqualTo(NotificationsSendFor.NONE); } - } } diff --git a/user-manager-server/src/test/java/de/itvsh/kop/user/UserSettingsTestFactory.java b/user-manager-server/src/test/java/de/itvsh/kop/user/settings/UserSettingsTestFactory.java similarity index 90% rename from user-manager-server/src/test/java/de/itvsh/kop/user/UserSettingsTestFactory.java rename to user-manager-server/src/test/java/de/itvsh/kop/user/settings/UserSettingsTestFactory.java index 40ddd295d5dd1d6e2e44c3067fbcbb53d2bce241..499be6404d8793a5b8bd118760bcacd96c1ed10c 100644 --- a/user-manager-server/src/test/java/de/itvsh/kop/user/UserSettingsTestFactory.java +++ b/user-manager-server/src/test/java/de/itvsh/kop/user/settings/UserSettingsTestFactory.java @@ -1,4 +1,4 @@ -package de.itvsh.kop.user; +package de.itvsh.kop.user.settings; import de.itvsh.kop.user.settings.NotificationsSendFor; import de.itvsh.kop.user.settings.UserSettings;