Closes #2187 : Add additional user configuration in wildfly

This commit is contained in:
Elena Mokeeva 2023-03-02 12:01:53 +01:00 committed by Elena Mokeeva
parent 43e02dcb78
commit 8226f8d8ba
9 changed files with 289 additions and 11 deletions

View File

@ -0,0 +1,24 @@
package pro.taskana.example.wildfly;
import java.util.List;
public class AdditionalUserProperties {
private Boolean enableUserIdHeader;
private List<String> authorizedUsers;
public Boolean getEnableUserIdHeader() {
return enableUserIdHeader;
}
public void setEnableUserIdHeader(Boolean enableUserIdHeader) {
this.enableUserIdHeader = enableUserIdHeader;
}
public List<String> getAuthorizedUsers() {
return authorizedUsers;
}
public void setAuthorizedUsers(List<String> authorizedUsers) {
this.authorizedUsers = authorizedUsers;
}
}

View File

@ -1,6 +1,7 @@
package pro.taskana.example.wildfly; package pro.taskana.example.wildfly;
import java.io.InputStream; import java.io.InputStream;
import java.util.List;
import java.util.Properties; import java.util.Properties;
import javax.naming.Context; import javax.naming.Context;
import javax.naming.InitialContext; import javax.naming.InitialContext;
@ -11,6 +12,7 @@ import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.DependsOn; import org.springframework.context.annotation.DependsOn;
import org.springframework.core.env.Environment;
import org.springframework.jdbc.datasource.DataSourceTransactionManager; import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.transaction.PlatformTransactionManager; import org.springframework.transaction.PlatformTransactionManager;
import pro.taskana.sampledata.SampleDataGenerator; import pro.taskana.sampledata.SampleDataGenerator;
@ -61,4 +63,12 @@ public class TaskanaWildflyConfiguration {
throw e; throw e;
} }
} }
@Bean
public AdditionalUserProperties getAdditionalUserProperties(Environment env) {
AdditionalUserProperties properties = new AdditionalUserProperties();
properties.setAuthorizedUsers(List.of(env.getProperty("authorizedUsers", "").split("\\|")));
properties.setEnableUserIdHeader(env.getProperty("enableUserIdHeader", Boolean.TYPE, false));
return properties;
}
} }

View File

@ -2,24 +2,37 @@ package pro.taskana.example.wildfly.security;
import java.io.IOException; import java.io.IOException;
import java.security.AccessController; import java.security.AccessController;
import java.util.List;
import javax.security.auth.Subject; import javax.security.auth.Subject;
import javax.servlet.FilterChain; import javax.servlet.FilterChain;
import javax.servlet.ServletException; import javax.servlet.ServletException;
import javax.servlet.ServletRequest; import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse; import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.GenericFilterBean; import org.springframework.web.filter.GenericFilterBean;
import org.wildfly.security.auth.server.SecurityDomain; import org.wildfly.security.auth.server.SecurityDomain;
import org.wildfly.security.auth.server.SecurityIdentity; import org.wildfly.security.auth.server.SecurityIdentity;
import org.wildfly.security.authz.Roles; import org.wildfly.security.authz.Roles;
import pro.taskana.common.api.security.GroupPrincipal; import pro.taskana.common.api.security.GroupPrincipal;
import pro.taskana.example.wildfly.AdditionalUserProperties;
/** Simple Filter to map all Elytron Roles to JAAS-Principals. */ /** Simple Filter to map all Elytron Roles to JAAS-Principals. */
@Component
public class ElytronToJaasFilter extends GenericFilterBean { public class ElytronToJaasFilter extends GenericFilterBean {
private static AdditionalUserProperties additionalUserProperties;
@Autowired
public void setAdditionalUserProperties(AdditionalUserProperties prop) {
additionalUserProperties = prop;
}
@Override @Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException { throws IOException, ServletException {
SecurityIdentity securityIdentity = getSecurityIdentity(); SecurityIdentity securityIdentity = getSecurityIdentity(request);
if (securityIdentity != null) { if (securityIdentity != null) {
applySecurityIdentityToSubject(securityIdentity); applySecurityIdentityToSubject(securityIdentity);
} }
@ -50,11 +63,24 @@ public class ElytronToJaasFilter extends GenericFilterBean {
return subject; return subject;
} }
private SecurityIdentity getSecurityIdentity() { private SecurityIdentity getSecurityIdentity(ServletRequest request) {
SecurityDomain current = SecurityDomain.getCurrent(); SecurityDomain current = SecurityDomain.getCurrent();
SecurityIdentity identity = null; SecurityIdentity identity = null;
if (current != null) { if (current != null) {
identity = current.getCurrentSecurityIdentity(); if (request instanceof HttpServletRequest) {
HttpServletRequest httpRequest = (HttpServletRequest) request;
String userId = httpRequest.getHeader("userid");
Boolean enableUserIdHeader = additionalUserProperties.getEnableUserIdHeader();
List<String> authorizedUsers = additionalUserProperties.getAuthorizedUsers();
if (userId != null
&& enableUserIdHeader
&& authorizedUsers.contains(
current.getCurrentSecurityIdentity().getPrincipal().getName())) {
identity = current.getCurrentSecurityIdentity().createRunAsIdentity(userId, false);
} else {
identity = current.getCurrentSecurityIdentity();
}
}
} }
if (logger.isDebugEnabled()) { if (logger.isDebugEnabled()) {
logger.debug("Current Elytron SecurityIdentity: " + identity); logger.debug("Current Elytron SecurityIdentity: " + identity);

View File

@ -28,8 +28,8 @@ import pro.taskana.common.test.rest.RestHelper;
import pro.taskana.task.rest.models.TaskRepresentationModel; import pro.taskana.task.rest.models.TaskRepresentationModel;
/** /**
* This test class is configured to run with postgres DB if you want to run it with h2 it is needed. * This test class is configured to run with postgres DB. In order to run them on db2, change the
* to change data source configuration at project-defaults.yml. * datasource.jndi to java:jboss/datasources/ExampleDS and set taskana.schemaName to TASKANA
*/ */
@RunWith(Arquillian.class) @RunWith(Arquillian.class)
public class TaskanaWildflyTest extends AbstractAccTest { public class TaskanaWildflyTest extends AbstractAccTest {

View File

@ -22,8 +22,8 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
/** /**
* This test class is configured to run with postgres DB if you want to run it with h2 it is needed. * This test class is configured to run with postgres DB. In order to run them on db2, change the
* to change data source configuration at project-defaults.yml. * datasource.jndi to java:jboss/datasources/ExampleDS and set taskana.schemaName to TASKANA
*/ */
@RunWith(Arquillian.class) @RunWith(Arquillian.class)
public class TaskanaWildflyWithHistoryLoggerEnabledTest extends AbstractAccTest { public class TaskanaWildflyWithHistoryLoggerEnabledTest extends AbstractAccTest {

View File

@ -27,8 +27,8 @@ import pro.taskana.simplehistory.rest.models.TaskHistoryEventPagedRepresentation
import pro.taskana.task.rest.models.TaskRepresentationModel; import pro.taskana.task.rest.models.TaskRepresentationModel;
/** /**
* This test class is configured to run with postgres DB if you want to run it with h2 it is needed. * This test class is configured to run with postgres DB. In order to run them on db2, change the
* to change data source configuration at project-defaults.yml. * datasource.jndi to java:jboss/datasources/ExampleDS and set taskana.schemaName to TASKANA
*/ */
@RunWith(Arquillian.class) @RunWith(Arquillian.class)
public class TaskanaWildflyWithSimpleHistoryAndHistoryLoggerEnabledTest extends AbstractAccTest { public class TaskanaWildflyWithSimpleHistoryAndHistoryLoggerEnabledTest extends AbstractAccTest {

View File

@ -27,8 +27,8 @@ import pro.taskana.simplehistory.rest.models.TaskHistoryEventPagedRepresentation
import pro.taskana.task.rest.models.TaskRepresentationModel; import pro.taskana.task.rest.models.TaskRepresentationModel;
/** /**
* This test class is configured to run with postgres DB if you want to run it with h2 it is needed. * This test class is configured to run with postgres DB. In order to run them on db2, change the
* to change data source configuration at project-defaults.yml. * datasource.jndi to java:jboss/datasources/ExampleDS and set taskana.schemaName to TASKANA
*/ */
@RunWith(Arquillian.class) @RunWith(Arquillian.class)
public class TaskanaWildflyWithSimpleHistoryEnabledTest extends AbstractAccTest { public class TaskanaWildflyWithSimpleHistoryEnabledTest extends AbstractAccTest {

View File

@ -0,0 +1,170 @@
package pro.taskana.example.wildfly;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static pro.taskana.common.test.rest.RestHelper.TEMPLATE;
import java.io.File;
import org.assertj.core.api.ThrowableAssert.ThrowingCallable;
import org.jboss.arquillian.container.test.api.Deployment;
import org.jboss.arquillian.container.test.api.RunAsClient;
import org.jboss.arquillian.junit.Arquillian;
import org.jboss.shrinkwrap.api.Archive;
import org.jboss.shrinkwrap.api.ShrinkWrap;
import org.jboss.shrinkwrap.api.spec.WebArchive;
import org.jboss.shrinkwrap.resolver.api.maven.Maven;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.core.ParameterizedTypeReference;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.client.HttpStatusCodeException;
import pro.taskana.common.rest.RestEndpoints;
import pro.taskana.common.rest.models.TaskanaUserInfoRepresentationModel;
import pro.taskana.common.test.rest.RestHelper;
import pro.taskana.task.rest.models.TaskRepresentationModel;
/**
* This test class is configured to run with postgres DB. In order to run them on db2, change the
* datasource.jndi to java:jboss/datasources/ExampleDS and set taskana.schemaName to TASKANA
*/
@RunWith(Arquillian.class)
public class TaskanaWildflyWithUserConfigTest extends AbstractAccTest {
private static final Logger LOGGER =
LoggerFactory.getLogger(TaskanaWildflyWithUserConfigTest.class);
@Deployment(testable = false)
public static Archive<?> createTestArchive() {
String applicationPropertyFile = "application-with-additional-user-config.properties";
String dbType = System.getProperty("db.type");
if (dbType != null && !dbType.isEmpty()) {
applicationPropertyFile = "application-" + dbType + ".properties";
}
LOGGER.info(
"Running with db.type '{}' and using property file '{}'", dbType, applicationPropertyFile);
File[] files =
Maven.resolver()
.loadPomFromFile("pom.xml")
.importRuntimeDependencies()
.resolve()
.withTransitivity()
.asFile();
return ShrinkWrap.create(WebArchive.class, "taskana.war")
.addPackages(true, "pro.taskana")
.addAsResource("taskana.properties")
.addAsResource(applicationPropertyFile, "application.properties")
.addAsResource("taskana-test.ldif")
.addAsWebInfResource("int-test-web.xml", "web.xml")
.addAsWebInfResource("int-test-jboss-web.xml", "jboss-web.xml")
.addAsLibraries(files);
}
@Test
@RunAsClient
public void should_ReturnUserInformation_WhenRelevantUserInUseridHeader() {
HttpHeaders headers = RestHelper.generateHeadersForUser("user-1-1");
headers.add("userid", "teamlead-1");
ResponseEntity<TaskanaUserInfoRepresentationModel> response =
TEMPLATE.exchange(
restHelper.toUrl("/taskana" + RestEndpoints.URL_CURRENT_USER),
HttpMethod.GET,
new HttpEntity<>(headers),
ParameterizedTypeReference.forType(TaskanaUserInfoRepresentationModel.class));
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
TaskanaUserInfoRepresentationModel currentUser = response.getBody();
assertThat(currentUser).isNotNull();
assertThat(currentUser.getUserId()).isEqualTo("teamlead-1");
assertThat(currentUser.getGroupIds()).hasSize(4);
assertThat(currentUser.getRoles()).hasSize(3);
}
@Test
@RunAsClient
public void should_ReturnUserInformation_WhenCurrentUserNotAuthorizedToUseUserFromHeader() {
HttpHeaders headers = RestHelper.generateHeadersForUser("user-2-1");
headers.add("userid", "user-1-1");
ResponseEntity<TaskanaUserInfoRepresentationModel> response =
TEMPLATE.exchange(
restHelper.toUrl("/taskana" + RestEndpoints.URL_CURRENT_USER),
HttpMethod.GET,
new HttpEntity<>(headers),
ParameterizedTypeReference.forType(TaskanaUserInfoRepresentationModel.class));
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
TaskanaUserInfoRepresentationModel currentUser = response.getBody();
assertThat(currentUser).isNotNull();
assertThat(currentUser.getUserId()).isEqualTo("user-2-1");
assertThat(currentUser.getGroupIds()).hasSize(2);
assertThat(currentUser.getRoles()).hasSize(1);
}
@Test
@RunAsClient
public void should_ReturnTask_When_OnlyTheUserInTheUseridHeaderIsAuthorized() {
HttpHeaders headers = RestHelper.generateHeadersForUser("user-1-1");
headers.add("userid", "teamlead-1");
ResponseEntity<TaskRepresentationModel> response =
TEMPLATE.exchange(
restHelper.toUrl(
"/taskana" + RestEndpoints.URL_TASKS_ID,
"TKI:000000000000000000000000000000000005"),
HttpMethod.GET,
new HttpEntity<>(headers),
ParameterizedTypeReference.forType(TaskRepresentationModel.class));
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
assertThat(response.getBody()).isNotNull();
}
@Test
@RunAsClient
public void should_ThrowException_When_RequestingTaskFromUnauthorizedUserInTheAdditionalHeader() {
HttpHeaders headers = RestHelper.generateHeadersForUser("teamlead-1");
headers.add("userid", "user-1-1");
ThrowingCallable call =
() ->
TEMPLATE.exchange(
restHelper.toUrl(
"/taskana" + RestEndpoints.URL_TASKS_ID,
"TKI:000000000000000000000000000000000005"),
HttpMethod.GET,
new HttpEntity<>(headers),
ParameterizedTypeReference.forType(TaskRepresentationModel.class));
assertThatThrownBy(call)
.isInstanceOf(HttpStatusCodeException.class)
.extracting(HttpStatusCodeException.class::cast)
.extracting(HttpStatusCodeException::getStatusCode)
.isEqualTo(HttpStatus.FORBIDDEN);
}
@Test
@RunAsClient
public void should_ThrowException_When_NotUsingAdditionalHeaderBecauseCurrentUserNotAuthorized() {
HttpHeaders headers = RestHelper.generateHeadersForUser("user-2-1");
headers.add("userid", "teamlead-1");
ThrowingCallable call =
() ->
TEMPLATE.exchange(
restHelper.toUrl(
"/taskana" + RestEndpoints.URL_TASKS_ID,
"TKI:000000000000000000000000000000000005"),
HttpMethod.GET,
new HttpEntity<>(headers),
ParameterizedTypeReference.forType(TaskRepresentationModel.class));
assertThatThrownBy(call)
.isInstanceOf(HttpStatusCodeException.class)
.extracting(HttpStatusCodeException.class::cast)
.extracting(HttpStatusCodeException::getStatusCode)
.isEqualTo(HttpStatus.FORBIDDEN);
}
}

View File

@ -0,0 +1,48 @@
enableUserIdHeader=true
authorizedUsers=user-1-1|teamlead-1
######## Taskana DB #######
datasource.jndi=java:jboss/datasources/TaskanaDS
taskana.schemaName=taskana
####### properties to connect to LDAP
taskana.ldap.serverUrl=ldap://localhost:10389
taskana.ldap.bindDn=uid=admin
taskana.ldap.bindPassword=secret
taskana.ldap.baseDn=ou=Test,O=TASKANA
taskana.ldap.userSearchBase=cn=users
taskana.ldap.userSearchFilterName=objectclass
taskana.ldap.userSearchFilterValue=person
taskana.ldap.userFirstnameAttribute=givenName
taskana.ldap.userLastnameAttribute=sn
taskana.ldap.userFullnameAttribute=cn
taskana.ldap.userPhoneAttribute=phoneNumber
taskana.ldap.userMobilePhoneAttribute=mobileNumber
taskana.ldap.userEmailAttribute=email
taskana.ldap.userOrglevel1Attribute=orgLevel1
taskana.ldap.userOrglevel2Attribute=orgLevel2
taskana.ldap.userOrglevel3Attribute=someDepartement
taskana.ldap.userOrglevel4Attribute=orgLevel4
taskana.ldap.userIdAttribute=uid
taskana.ldap.userMemberOfGroupAttribute=memberOf
taskana.ldap.groupSearchBase=
taskana.ldap.groupSearchFilterName=objectclass
taskana.ldap.groupSearchFilterValue=groupOfUniqueNames
taskana.ldap.groupNameAttribute=cn
taskana.ldap.minSearchForLength=3
taskana.ldap.maxNumberOfReturnedAccessIds=50
taskana.ldap.groupsOfUser=uniquemember
####### JobScheduler cron expression that specifies when the JobSchedler runs
taskana.jobscheduler.async.cron=0 * * * * *
####### cache static resources propertiesgit add --
spring.resources.cache.cachecontrol.cache-private=true
spring.main.allow-bean-definition-overriding=true
####### tomcat is not detecting the x-forward headers from bluemix as a trustworthy proxy
server.tomcat.remoteip.internal-proxies=.*
server.forward-headers-strategy=native
# Embedded Spring LDAP server
spring.ldap.embedded.base-dn=OU=Test,O=TASKANA
spring.ldap.embedded.credential.username=uid=admin
spring.ldap.embedded.credential.password=secret
spring.ldap.embedded.ldif=classpath:taskana-test.ldif
spring.ldap.embedded.port=10389
spring.ldap.embedded.validation.enabled=false