From: Ray Lee Date: Thu, 21 Sep 2023 03:53:27 +0000 (-0400) Subject: Upgrade spring and spring security. (#365) X-Git-Url: https://git.aero2k.de/?a=commitdiff_plain;h=b3b50aad719ac2821617cedc282948f5a5883755;p=tmp%2Fjakarta-migration.git Upgrade spring and spring security. (#365) --- diff --git a/.gitignore b/.gitignore index 479e62cfc..c2a6a58d8 100644 --- a/.gitignore +++ b/.gitignore @@ -17,5 +17,6 @@ target .factorypath m2-settings.xml *.log +*.log.* cspace-app-perflog.csv .flattened-pom.xml diff --git a/3rdparty/nuxeo/nuxeo-server/9.10-HF30/lib/jackson-core-2.7.9.jar b/3rdparty/nuxeo/nuxeo-server/9.10-HF30/lib/jackson-core-2.7.9.jar deleted file mode 100644 index e87c08afa..000000000 Binary files a/3rdparty/nuxeo/nuxeo-server/9.10-HF30/lib/jackson-core-2.7.9.jar and /dev/null differ diff --git a/build.properties b/build.properties index a973faef5..5d1206ce2 100644 --- a/build.properties +++ b/build.properties @@ -22,9 +22,10 @@ domain.nuxeo=nuxeo-server # UI settings cspace.ui.package.name=cspace-ui cspace.ui.library.name=cspaceUI -cspace.ui.version=8.0.0 +cspace.ui.version=9.0.0-dev.1 cspace.ui.build.branch=master cspace.ui.build.node.ver=14 +service.ui.library.name=${cspace.ui.library.name}-service #nuxeo nuxeo.release=9.10-HF30 diff --git a/build.xml b/build.xml index a2cd3e2b3..aa2440701 100644 --- a/build.xml +++ b/build.xml @@ -346,7 +346,9 @@ - + + + diff --git a/cspace-ui/build.xml b/cspace-ui/build.xml index 4ae1cc20d..3fceeabe6 100644 --- a/cspace-ui/build.xml +++ b/cspace-ui/build.xml @@ -47,7 +47,7 @@ - + @@ -61,7 +61,7 @@ - + @@ -71,6 +71,12 @@ + + + + + + @@ -83,6 +89,18 @@ + + + + + + + + + + + + @@ -194,6 +212,16 @@ + + + + + + + + + + @@ -216,7 +244,7 @@ - + - + diff --git a/cspace-ui/build_js.sh b/cspace-ui/build_js.sh index 2bc2b4cd4..147d6ea98 100755 --- a/cspace-ui/build_js.sh +++ b/cspace-ui/build_js.sh @@ -5,6 +5,7 @@ BRANCH_NAME=$2 CHECKOUT_DIR=$3 OUTPUT_DIR=$4 LIBRARY_NAME=$5 +SERVICE_LIBRARY_NAME=$6 CODE_DIR=$CHECKOUT_DIR/$PACKAGE_NAME.js @@ -18,4 +19,9 @@ COMMIT_HASH=`git rev-parse --short HEAD` npm install popd -cp $CODE_DIR/dist/*.min.js "$OUTPUT_DIR/$LIBRARY_NAME@$COMMIT_HASH.min.js" +cp $CODE_DIR/dist/$LIBRARY_NAME.min.js "$OUTPUT_DIR/$LIBRARY_NAME@$COMMIT_HASH.min.js" + +if [ ! -z "$SERVICE_LIBRARY_NAME" ] +then + cp $CODE_DIR/dist/$SERVICE_LIBRARY_NAME.min.js "$OUTPUT_DIR/$SERVICE_LIBRARY_NAME@$COMMIT_HASH.min.js" +fi diff --git a/cspace-ui/service-ui.ftlh b/cspace-ui/service-ui.ftlh new file mode 100644 index 000000000..f563ffb5f --- /dev/null +++ b/cspace-ui/service-ui.ftlh @@ -0,0 +1,20 @@ +<#-- + This FreeMarker template is used to generate response bodies of service layer endpoints that + return HTML, e.g. login, logout, requestpasswordreset, processpasswordreset. +--> + + + + + +
+ + + + diff --git a/pom.xml b/pom.xml index 194be9de2..5a7f39845 100644 --- a/pom.xml +++ b/pom.xml @@ -14,14 +14,15 @@ UTF-8 ${revision} ${revision} + 2.14.3 9.10-HF30 ${nuxeo.general.release} ${nuxeo.general.release} ${nuxeo.general.release} 0.12.0-NX2 - 4.3.16.RELEASE - 4.1.1.RELEASE - 2.0.10.RELEASE + 5.3.28 + 5.8.4 + 0.4.3 1.7.4 2.17.1 @@ -752,6 +753,32 @@ provided + + + + com.fasterxml.jackson.core + jackson-core + ${jackson.version} + provided + + + com.fasterxml.jackson.core + jackson-databind + ${jackson.version} + provided + + + com.fasterxml.jackson.core + jackson-annotations + ${jackson.version} + provided + + + com.fasterxml.jackson.datatype + jackson-datatype-jsr310 + ${jackson.version} + provided + diff --git a/services/IntegrationTests/src/test/java/org/collectionspace/services/IntegrationTests/test/JsonIntegrationTest.java b/services/IntegrationTests/src/test/java/org/collectionspace/services/IntegrationTests/test/JsonIntegrationTest.java index fb14d6308..6b56485c1 100644 --- a/services/IntegrationTests/src/test/java/org/collectionspace/services/IntegrationTests/test/JsonIntegrationTest.java +++ b/services/IntegrationTests/src/test/java/org/collectionspace/services/IntegrationTests/test/JsonIntegrationTest.java @@ -3,22 +3,31 @@ package org.collectionspace.services.IntegrationTests.test; import java.io.File; import java.io.IOException; import java.nio.charset.StandardCharsets; - -import javax.xml.bind.DatatypeConverter; +import java.util.Base64; +import java.util.regex.Matcher; +import java.util.regex.Pattern; import static org.testng.Assert.*; +import org.apache.commons.io.IOUtils; import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.tuple.Pair; import org.apache.http.HttpEntity; import org.apache.http.HttpHost; +import org.apache.http.HttpRequest; import org.apache.http.HttpResponse; +import org.apache.http.ProtocolException; import org.apache.http.StatusLine; import org.apache.http.client.ClientProtocolException; import org.apache.http.client.HttpResponseException; +import org.apache.http.client.RedirectStrategy; import org.apache.http.client.ResponseHandler; import org.apache.http.client.fluent.Executor; import org.apache.http.client.fluent.Request; +import org.apache.http.client.methods.HttpUriRequest; import org.apache.http.entity.ContentType; +import org.apache.http.impl.client.HttpClientBuilder; +import org.apache.http.protocol.HttpContext; import org.testng.annotations.Test; import com.fasterxml.jackson.core.JsonFactory; @@ -32,29 +41,49 @@ public class JsonIntegrationTest { public static final String HOST = "localhost"; public static final int PORT = 8180; public static final String CLIENT_ID = "cspace-ui"; - public static final String CLIENT_SECRET = ""; public static final String USERNAME = "admin@core.collectionspace.org"; public static final String PASSWORD = "Administrator"; + public static final String BASIC_AUTH_CREDS = Base64.getEncoder().encodeToString((USERNAME + ":" + PASSWORD).getBytes()); public static final String BASE_URL = "http://" + HOST + ":" + PORT + "/cspace-services/"; public static final String FILE_PATH = "test-data/json/"; - + + private HttpHost host = new HttpHost(HOST, PORT); + private Executor restExecutor = Executor.newInstance() - .auth(new HttpHost(HOST, PORT), USERNAME, PASSWORD); + .auth(host, USERNAME, PASSWORD) + .authPreemptive(host); + + private Executor authExecutor = Executor.newInstance( + // Don't follow redirects. + + HttpClientBuilder.create() + .setRedirectStrategy(new RedirectStrategy() { + @Override + public boolean isRedirected(HttpRequest request, HttpResponse response, HttpContext context) + throws ProtocolException { + return false; + } + + @Override + public HttpUriRequest getRedirect(HttpRequest request, HttpResponse response, HttpContext context) + throws ProtocolException { + return null; + } + }) + .build() + ); - private Executor authExecutor = Executor.newInstance() - .auth(new HttpHost(HOST, PORT), CLIENT_ID, CLIENT_SECRET); - private ObjectMapper mapper = new ObjectMapper(); private JsonFactory jsonFactory = mapper.getFactory(); @Test public void testRecord() throws ClientProtocolException, IOException { JsonNode jsonNode; - + String csid = postJson("collectionobjects", "collectionobject1"); - + jsonNode = getJson("collectionobjects/" + csid); - + assertEquals(jsonNode.at("/document/ns2:collectionspace_core/createdBy").asText(), USERNAME); assertEquals(jsonNode.at("/document/ns2:collectionobjects_common/objectNumber").asText(), "TEST2000.4.5"); assertEquals(jsonNode.at("/document/ns2:collectionobjects_common/objectNameList/objectNameGroup/objectName").asText(), "Test Object"); @@ -77,33 +106,49 @@ public class JsonIntegrationTest { delete("collectionobjects/" + csid); } - + @Test public void testAuth() throws ClientProtocolException, IOException { - String base64EncodedPassword = DatatypeConverter.printBase64Binary(PASSWORD.getBytes(StandardCharsets.UTF_8)); - - JsonNode jsonNode; - - jsonNode = postAuthForm("oauth/token", "grant_type=password&username=" + USERNAME + "&password=" + base64EncodedPassword); + Pair loginFormResult = getLoginForm("login"); + + String sessionCookie = loginFormResult.getLeft(); + String csrfToken = loginFormResult.getRight(); + + String loggedInSessionCookie = postLoginForm("login", "username=" + USERNAME + "&password=" + PASSWORD + "&_csrf=" + csrfToken, sessionCookie); + String authCode = getAuthCode("oauth2/authorize", loggedInSessionCookie); + JsonNode jsonNode = postTokenGrant("oauth2/token", authCode); - assertEquals(jsonNode.at("/token_type").asText(), "bearer"); + assertEquals(jsonNode.at("/token_type").asText(), "Bearer"); assertTrue(StringUtils.isNotEmpty(jsonNode.at("/access_token").asText())); } - + private String postJson(String path, String filename) throws ClientProtocolException, IOException { return restExecutor.execute(Request.Post(getUrl(path)) .addHeader("Accept", ContentType.APPLICATION_JSON.getMimeType()) .addHeader("Content-type", ContentType.APPLICATION_JSON.getMimeType()) + .addHeader("Authorization", "Basic " + BASIC_AUTH_CREDS) .bodyFile(getFile(filename), ContentType.APPLICATION_JSON)) - .handleResponse(new CsidFromLocationResponseHandler()); + .handleResponse(new ResponseHandler() { + @Override + public String handleResponse(HttpResponse response) throws ClientProtocolException, IOException { + StatusLine status = response.getStatusLine(); + int statusCode = status.getStatusCode(); + + if (statusCode < 200 || statusCode > 299) { + throw new HttpResponseException(statusCode, status.getReasonPhrase()); + } + + return csidFromLocation(response); + } + }); } - + private JsonNode getJson(String path) throws ClientProtocolException, IOException { return restExecutor.execute(Request.Get(getUrl(path)) .addHeader("Accept", ContentType.APPLICATION_JSON.getMimeType())) .handleResponse(new JsonBodyResponseHandler()); } - + private JsonNode putJson(String path, String filename) throws ClientProtocolException, IOException { return restExecutor.execute(Request.Put(getUrl(path)) .addHeader("Accept", ContentType.APPLICATION_JSON.getMimeType()) @@ -111,55 +156,124 @@ public class JsonIntegrationTest { .bodyFile(getFile(filename), ContentType.APPLICATION_JSON)) .handleResponse(new JsonBodyResponseHandler()); } - + private void delete(String path) throws ClientProtocolException, IOException { restExecutor.execute(Request.Delete(getUrl(path))) - .handleResponse(new CheckStatusResponseHandler()); + .handleResponse(new ResponseHandler() { + @Override + public Integer handleResponse(HttpResponse response) throws ClientProtocolException, IOException { + StatusLine status = response.getStatusLine(); + int statusCode = status.getStatusCode(); + + if (statusCode < 200 || statusCode > 299) { + throw new HttpResponseException(statusCode, status.getReasonPhrase()); + } + + return statusCode; + } + }); + } + + private Pair getLoginForm(String path) throws ClientProtocolException, IOException { + return authExecutor.execute(Request.Get(getUrl(path))) + .handleResponse(new ResponseHandler>() { + @Override + public Pair handleResponse(HttpResponse response) throws ClientProtocolException, IOException { + StatusLine status = response.getStatusLine(); + int statusCode = status.getStatusCode(); + + if (statusCode < 200 || statusCode > 299) { + throw new HttpResponseException(statusCode, status.getReasonPhrase()); + } + + HttpEntity entity = response.getEntity(); + + if (entity == null) { + throw new ClientProtocolException("response contains no content"); + } + + ContentType contentType = ContentType.getOrDefault(entity); + String mimeType = contentType.getMimeType(); + + if (!mimeType.equals(ContentType.TEXT_HTML.getMimeType())) { + throw new ClientProtocolException("unexpected content type: " + contentType); + } + + return Pair.of( + sessionCookie(response), + csrfFromLoginForm(IOUtils.toString(entity.getContent(), StandardCharsets.UTF_8.name())) + ); + } + }); } - private JsonNode postAuthForm(String path, String values) throws ClientProtocolException, IOException { + private String postLoginForm(String path, String values, String sessionCookie) throws ClientProtocolException, IOException { return authExecutor.execute(Request.Post(getUrl(path)) - .addHeader("Accept", ContentType.APPLICATION_JSON.getMimeType()) - .addHeader("Content-type", ContentType.APPLICATION_FORM_URLENCODED.getMimeType()) - .bodyString(values, ContentType.APPLICATION_FORM_URLENCODED)) - .handleResponse(new JsonBodyResponseHandler()); + .addHeader("Cookie", "JSESSIONID=" + sessionCookie) + .addHeader("Content-type", ContentType.APPLICATION_FORM_URLENCODED.getMimeType()) + .bodyString(values, ContentType.APPLICATION_FORM_URLENCODED)) + .handleResponse(new ResponseHandler() { + @Override + public String handleResponse(HttpResponse response) throws ClientProtocolException, IOException { + StatusLine status = response.getStatusLine(); + int statusCode = status.getStatusCode(); + + if (statusCode != 302) { + throw new HttpResponseException(statusCode, status.getReasonPhrase()); + } + + return sessionCookie(response); + } + }); } - public class CsidFromLocationResponseHandler implements ResponseHandler { + private String getAuthCode(String path, String sessionCookie) throws ClientProtocolException, IOException { + String queryString = "response_type=code&client_id=" + CLIENT_ID + "&scope=cspace.full&redirect_uri=/../cspace/core/authorized&code_challenge=Ngi8oeROpsTSaOttsCJgJpiSwLQrhrvx53pvoWw8koI&code_challenge_method=S256"; - @Override - public String handleResponse(HttpResponse response) throws ClientProtocolException, IOException { - StatusLine status = response.getStatusLine(); - int statusCode = status.getStatusCode(); - - if (statusCode< 200 || statusCode > 299) { - throw new HttpResponseException(statusCode, status.getReasonPhrase()); - } - - return csidFromLocation(response.getFirstHeader("Location").getValue()); - } + return authExecutor.execute(Request.Get(getUrl(path) + "?" + queryString) + .addHeader("Cookie", "JSESSIONID=" + sessionCookie)) + .handleResponse(new ResponseHandler() { + @Override + public String handleResponse(HttpResponse response) throws ClientProtocolException, IOException { + StatusLine status = response.getStatusLine(); + int statusCode = status.getStatusCode(); + + if (statusCode != 302) { + throw new HttpResponseException(statusCode, status.getReasonPhrase()); + } + + return authCodeFromLocation(response); + } + }); } - + + private JsonNode postTokenGrant(String path, String authCode) throws ClientProtocolException, IOException { + return authExecutor.execute(Request.Post(getUrl(path)) + .addHeader("Content-type", ContentType.APPLICATION_FORM_URLENCODED.getMimeType()) + .bodyString("grant_type=authorization_code&redirect_uri=/../cspace/core/authorized&client_id=" + CLIENT_ID + "&code_verifier=xyz&code=" + authCode, ContentType.APPLICATION_FORM_URLENCODED)) + .handleResponse(new JsonBodyResponseHandler()); + } + public class JsonBodyResponseHandler implements ResponseHandler { @Override public JsonNode handleResponse(HttpResponse response) throws ClientProtocolException, IOException { StatusLine status = response.getStatusLine(); int statusCode = status.getStatusCode(); - - if (statusCode< 200 || statusCode > 299) { + + if (statusCode < 200 || statusCode > 299) { throw new HttpResponseException(statusCode, status.getReasonPhrase()); } - + HttpEntity entity = response.getEntity(); - + if (entity == null) { throw new ClientProtocolException("response contains no content"); } ContentType contentType = ContentType.getOrDefault(entity); String mimeType = contentType.getMimeType(); - + if (!mimeType.equals(ContentType.APPLICATION_JSON.getMimeType())) { throw new ClientProtocolException("unexpected content type: " + contentType); } @@ -168,34 +282,50 @@ public class JsonIntegrationTest { } } - public class CheckStatusResponseHandler implements ResponseHandler { + private String csrfFromLoginForm(String formHtml) { + Pattern pattern = Pattern.compile("\"token\":\"(.*?)\""); + Matcher matcher = pattern.matcher(formHtml); - @Override - public Integer handleResponse(HttpResponse response) throws ClientProtocolException, IOException { - StatusLine status = response.getStatusLine(); - int statusCode = status.getStatusCode(); - - if (statusCode< 200 || statusCode > 299) { - throw new HttpResponseException(statusCode, status.getReasonPhrase()); - } - - return statusCode; + if (matcher.find()) { + return matcher.group(1); + } + + return null; + } + + private String sessionCookie(HttpResponse response) { + String value = response.getFirstHeader("Set-Cookie").getValue(); + Pattern pattern = Pattern.compile("JSESSIONID=(.*?);"); + Matcher matcher = pattern.matcher(value); + + if (matcher.find()) { + return matcher.group(1); } + + return null; } - private String csidFromLocation(String location) { + private String csidFromLocation(HttpResponse response) { + String location = response.getFirstHeader("Location").getValue(); int index = location.lastIndexOf("/"); - + return location.substring(index + 1); } - + + private String authCodeFromLocation(HttpResponse response) { + String location = response.getFirstHeader("Location").getValue(); + int index = location.lastIndexOf("code="); + + return location.substring(index + 5); + } + private String getUrl(String path) { return BASE_URL + path; } - + private File getFile(String fileName) { ClassLoader classLoader = getClass().getClassLoader(); - + return new File(classLoader.getResource(FILE_PATH + fileName + ".json").getFile()); } } diff --git a/services/IntegrationTests/src/test/resources/test-data/xmlreplay/xml-replay-master.xml b/services/IntegrationTests/src/test/resources/test-data/xmlreplay/xml-replay-master.xml index 83d598153..81a6bdc03 100644 --- a/services/IntegrationTests/src/test/resources/test-data/xmlreplay/xml-replay-master.xml +++ b/services/IntegrationTests/src/test/resources/test-data/xmlreplay/xml-replay-master.xml @@ -16,7 +16,8 @@ YWRtaW5AY29yZS5jb2xsZWN0aW9uc3BhY2Uub3JnOkFkbWluaXN0cmF0b3I= - + + diff --git a/services/JaxRsServiceProvider/pom.xml b/services/JaxRsServiceProvider/pom.xml index 2f45fcb8a..cd82ee78c 100644 --- a/services/JaxRsServiceProvider/pom.xml +++ b/services/JaxRsServiceProvider/pom.xml @@ -53,17 +53,19 @@ 6.6.1 - - - org.collectionspace.services - org.collectionspace.services.authorization.service - ${project.version} - - + + + org.collectionspace.services - org.collectionspace.services.authentication.service - ${project.version} - + org.collectionspace.services.authorization.service + ${project.version} + + + org.collectionspace.services + org.collectionspace.services.authentication.service + ${project.version} + provided + servlet-api-2.5 org.mortbay.jetty @@ -156,7 +158,7 @@ org.collectionspace.services org.collectionspace.services.account.service ${project.version} - + org.collectionspace.services org.collectionspace.services.authorization-mgt.service @@ -208,6 +210,16 @@ org.collectionspace.services.loanout.service ${project.version} + + org.collectionspace.services + org.collectionspace.services.login.service + ${project.version} + + + org.collectionspace.services + org.collectionspace.services.logout.service + ${project.version} + org.collectionspace.services org.collectionspace.services.transport.service @@ -489,19 +501,19 @@ - org.springframework.security.oauth - spring-security-oauth2 - ${spring.security.oauth2.version} + org.springframework.security + spring-security-oauth2-authorization-server + ${spring.security.authorization.server.version} provided - - spring-core - org.springframework - - - spring-beans - org.springframework - + + spring-core + org.springframework + + + spring-beans + org.springframework + diff --git a/services/JaxRsServiceProvider/src/main/java/org/collectionspace/services/jaxrs/CollectionSpaceJaxRsApplication.java b/services/JaxRsServiceProvider/src/main/java/org/collectionspace/services/jaxrs/CollectionSpaceJaxRsApplication.java index 506f56393..0ef40bd9b 100644 --- a/services/JaxRsServiceProvider/src/main/java/org/collectionspace/services/jaxrs/CollectionSpaceJaxRsApplication.java +++ b/services/JaxRsServiceProvider/src/main/java/org/collectionspace/services/jaxrs/CollectionSpaceJaxRsApplication.java @@ -69,6 +69,8 @@ import org.collectionspace.services.osteology.OsteologyResource; import org.collectionspace.services.conditioncheck.ConditioncheckResource; import org.collectionspace.services.conservation.ConservationResource; import org.collectionspace.services.authorization.PermissionResource; +import org.collectionspace.services.login.LoginResource; +import org.collectionspace.services.logout.LogoutResource; import javax.servlet.ServletContext; import javax.ws.rs.core.Application; @@ -120,6 +122,8 @@ public class CollectionSpaceJaxRsApplication extends Application singletons.add(new StructuredDateResource()); singletons.add(new SystemInfoResource()); singletons.add(new IndexResource()); + singletons.add(new LoginResource()); + singletons.add(new LogoutResource()); addResourceToMapAndSingletons(new VocabularyResource()); addResourceToMapAndSingletons(new PersonAuthorityResource()); diff --git a/services/JaxRsServiceProvider/src/main/webapp/META-INF/context.xml b/services/JaxRsServiceProvider/src/main/webapp/META-INF/context.xml index e0026b701..92518bba5 100644 --- a/services/JaxRsServiceProvider/src/main/webapp/META-INF/context.xml +++ b/services/JaxRsServiceProvider/src/main/webapp/META-INF/context.xml @@ -13,7 +13,7 @@ - + + + - - - - - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Authorization - Content-Type - - - - - POST - GET - PUT - DELETE - - - - - - Location - Content-Disposition - - - - - - - - - - - - - - - + + diff --git a/services/JaxRsServiceProvider/src/main/webapp/WEB-INF/oauth-servlet.xml b/services/JaxRsServiceProvider/src/main/webapp/WEB-INF/oauth-servlet.xml deleted file mode 100644 index 2f3796353..000000000 --- a/services/JaxRsServiceProvider/src/main/webapp/WEB-INF/oauth-servlet.xml +++ /dev/null @@ -1,35 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/services/JaxRsServiceProvider/src/main/webapp/WEB-INF/web.xml b/services/JaxRsServiceProvider/src/main/webapp/WEB-INF/web.xml index 5e785a495..f1749217a 100644 --- a/services/JaxRsServiceProvider/src/main/webapp/WEB-INF/web.xml +++ b/services/JaxRsServiceProvider/src/main/webapp/WEB-INF/web.xml @@ -7,11 +7,11 @@ Description: service layer web application --> - + CollectionSpace Services - + - Sets the logging context for the Tiger web-app + Sets the logging context for the web-app cspace-logging-context java.lang.String CSpaceLoggingContext @@ -58,7 +58,7 @@ - delayBetweenAttemptsMillis - How long to wait between retries. - --> - - + @@ -85,7 +85,7 @@ CSpaceFilter org.collectionspace.services.common.profile.CSpaceFilter - + CSpaceFilter /* @@ -98,12 +98,12 @@ JsonToXmlFilter org.collectionspace.services.common.xmljson.JsonToXmlFilter - + JsonToXmlFilter /* - + @@ -111,7 +111,7 @@ XmlToJsonFilter org.collectionspace.services.common.xmljson.XmlToJsonFilter - + XmlToJsonFilter /* @@ -150,29 +150,19 @@ org.collectionspace.services.common.CollectionSpaceServiceContextListener - + org.collectionspace.services.jaxrs.CSpaceResteasyBootstrap - - - oauth - org.springframework.web.servlet.DispatcherServlet - 1 - - - oauth - /oauth/token/* - - + - Resteasy - - org.jboss.resteasy.plugins.server.servlet.HttpServletDispatcher - + Resteasy + + org.jboss.resteasy.plugins.server.servlet.HttpServletDispatcher + Resteasy diff --git a/services/account/client/src/main/java/org/collectionspace/services/client/AccountClient.java b/services/account/client/src/main/java/org/collectionspace/services/client/AccountClient.java index 6f9f6d9c0..950d301ef 100644 --- a/services/account/client/src/main/java/org/collectionspace/services/client/AccountClient.java +++ b/services/account/client/src/main/java/org/collectionspace/services/client/AccountClient.java @@ -44,11 +44,15 @@ public class AccountClient extends AbstractServiceClientImpl - - + @@ -188,7 +188,7 @@ - + @@ -201,7 +201,7 @@ - + @@ -257,7 +257,7 @@ - + @@ -298,7 +298,7 @@ - + @@ -339,4 +339,3 @@ - diff --git a/services/account/jaxb/src/main/resources/accounts_common_list.xsd b/services/account/jaxb/src/main/resources/accounts_common_list.xsd index 30fdbbef4..6c88ecdc7 100644 --- a/services/account/jaxb/src/main/resources/accounts_common_list.xsd +++ b/services/account/jaxb/src/main/resources/accounts_common_list.xsd @@ -1,5 +1,5 @@ - - + @@ -42,41 +42,41 @@ - - - - - - - - - - - - - - - - - tenant association is usually not required to be provided by the - service consumer. only in cases where a user in CollectionSpace - has access to the spaces of multiple tenants, this is used - to associate that user with more than one tenants - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + tenant association is usually not required to be provided by the + service consumer. only in cases where a user in CollectionSpace + has access to the spaces of multiple tenants, this is used + to associate that user with more than one tenants + + + + + + + + + + + + + - + @@ -93,7 +93,7 @@ - + @@ -111,8 +111,7 @@ - + - diff --git a/services/account/service/pom.xml b/services/account/service/pom.xml index 15076db32..9d1465b1a 100644 --- a/services/account/service/pom.xml +++ b/services/account/service/pom.xml @@ -56,6 +56,18 @@ org.testng testng + + org.springframework.security + spring-security-web + ${spring.security.version} + provided + + + org.apache.tomcat + tomcat-servlet-api + ${tomcat.version} + provided + diff --git a/services/account/service/src/main/java/org/collectionspace/services/account/AccountResource.java b/services/account/service/src/main/java/org/collectionspace/services/account/AccountResource.java index 46a6c7f9a..cc21837f7 100644 --- a/services/account/service/src/main/java/org/collectionspace/services/account/AccountResource.java +++ b/services/account/service/src/main/java/org/collectionspace/services/account/AccountResource.java @@ -23,6 +23,8 @@ */ package org.collectionspace.services.account; +import org.apache.chemistry.opencmis.commons.impl.UrlBuilder; +import org.apache.commons.lang3.StringUtils; import org.collectionspace.authentication.AuthN; import org.collectionspace.services.account.storage.AccountStorageClient; import org.collectionspace.services.account.storage.csidp.TokenStorageClient; @@ -43,6 +45,8 @@ import org.collectionspace.services.common.ServiceMain; import org.collectionspace.services.common.ServiceMessages; import org.collectionspace.services.common.UriInfoWrapper; import org.collectionspace.services.common.authorization_mgt.AuthorizationCommon; +import org.collectionspace.services.common.config.ConfigUtils; +import org.collectionspace.services.common.config.TenantBindingConfigReaderImpl; import org.collectionspace.services.common.context.RemoteServiceContextFactory; import org.collectionspace.services.common.context.ServiceContext; import org.collectionspace.services.common.context.ServiceContextFactory; @@ -52,21 +56,40 @@ import org.collectionspace.services.common.query.UriInfoImpl; import org.collectionspace.services.common.storage.StorageClient; import org.collectionspace.services.common.storage.TransactionContext; import org.collectionspace.services.common.storage.jpa.JpaStorageUtils; +import org.collectionspace.services.config.ServiceConfig; import org.collectionspace.services.config.tenant.EmailConfig; import org.collectionspace.services.config.tenant.TenantBindingType; import org.jboss.resteasy.util.HttpResponseCodes; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.springframework.security.web.csrf.CsrfToken; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; + +import freemarker.core.ParseException; +import freemarker.template.Configuration; +import freemarker.template.MalformedTemplateNameException; +import freemarker.template.Template; +import freemarker.template.TemplateException; +import freemarker.template.TemplateNotFoundException; + +import java.io.IOException; +import java.io.StringWriter; +import java.io.Writer; import java.net.URI; import java.net.URISyntaxException; import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.Collections; +import java.util.HashMap; import java.util.List; +import java.util.Map; +import java.util.function.Predicate; import javax.persistence.NoResultException; +import javax.servlet.http.HttpServletRequest; import javax.ws.rs.Consumes; import javax.ws.rs.DELETE; import javax.ws.rs.GET; @@ -77,6 +100,7 @@ import javax.ws.rs.PathParam; import javax.ws.rs.Produces; import javax.ws.rs.QueryParam; import javax.ws.rs.core.Context; +import javax.ws.rs.core.MediaType; import javax.ws.rs.core.MultivaluedMap; import javax.ws.rs.core.PathSegment; import javax.ws.rs.core.Response; @@ -93,8 +117,6 @@ public class AccountResource extends SecurityResourceBaseThe token %s is not valid.", tokenId); + } + + Map uiConfig = new HashMap<>(); + + CsrfToken csrfToken = (CsrfToken) request.getAttribute(CsrfToken.class.getName()); + + if (csrfToken != null) { + Map csrfConfig = new HashMap<>(); + + csrfConfig.put("parameterName", csrfToken.getParameterName()); + csrfConfig.put("token", csrfToken.getToken()); + + uiConfig.put("csrf", csrfConfig); + } + + uiConfig.put("token", tokenId); + uiConfig.put("tenantId", token.getTenantId()); + + String uiConfigJS; + + try { + uiConfigJS = new ObjectMapper().writeValueAsString(uiConfig); + } catch (JsonProcessingException e) { + logger.error("Error generating login page UI configuration", e); + + uiConfigJS = ""; + } + + Map dataModel = new HashMap<>(); + + dataModel.put("uiConfig", uiConfigJS); + + Configuration freeMarkerConfig = ServiceMain.getInstance().getFreeMarkerConfig(); + Template template = freeMarkerConfig.getTemplate("service-ui.ftlh"); + Writer out = new StringWriter(); + + template.process(dataModel, out); + + out.close(); + + return out.toString(); + } + /** * Resets an accounts password. * @@ -248,7 +327,7 @@ public class AccountResource extends SecurityResourceBase uiConfig = new HashMap<>(); + + CsrfToken csrfToken = (CsrfToken) request.getAttribute(CsrfToken.class.getName()); + + if (csrfToken != null) { + Map csrfConfig = new HashMap<>(); + + csrfConfig.put("parameterName", csrfToken.getParameterName()); + csrfConfig.put("token", csrfToken.getToken()); + + uiConfig.put("csrf", csrfConfig); + } + + String tenantId = request.getParameter(AuthN.TENANT_ID_QUERY_PARAM); + + if (tenantId != null) { + uiConfig.put("tenantId", tenantId); + } + + String uiConfigJS; + + try { + uiConfigJS = new ObjectMapper().writeValueAsString(uiConfig); + } catch (JsonProcessingException e) { + logger.error("Error generating login page UI configuration", e); + + uiConfigJS = ""; + } + + Map dataModel = new HashMap<>(); + + dataModel.put("uiConfig", uiConfigJS); + + Configuration freeMarkerConfig = ServiceMain.getInstance().getFreeMarkerConfig(); + Template template = freeMarkerConfig.getTemplate("service-ui.ftlh"); + Writer out = new StringWriter(); + + template.process(dataModel, out); + + out.close(); + + return out.toString(); + } + @POST - @Path(PASSWORD_RESET_PATH) + @Path(AccountClient.PASSWORD_RESET_PATH_COMPONENT) public Response requestPasswordReset(@Context UriInfo ui) { - Response response = null; - MultivaluedMap queryParams = ui.getQueryParameters(); String email = queryParams.getFirst(AccountClient.EMAIL_QUERY_PARAM); - if (email == null || email.isEmpty()) { - response = Response.status(Response.Status.BAD_REQUEST).entity("You must specify an 'email' query paramater.").type("text/plain").build(); - return response; + + if (StringUtils.isEmpty(email)) { + return Response.status(Response.Status.BAD_REQUEST).entity("You must specify an 'email' query paramater.").type("text/plain").build(); } - String tenantId = queryParams.getFirst(AuthN.TENANT_ID_QUERY_PARAM); - if (tenantId == null || tenantId.isEmpty()) { - response = Response.status(Response.Status.BAD_REQUEST).entity("You must specify an 'tid' (tenant ID) query paramater.").type("text/plain").build(); - return response; + final String tenantId = queryParams.getFirst(AuthN.TENANT_ID_QUERY_PARAM); + + ui = new UriInfoWrapper(ui); + + if (StringUtils.isEmpty(tenantId)) { + // If no tenant ID was supplied, pick an arbitrary one for purposes of account search. + // It doesn't matter which, because all accounts will be returned regardless of the + // tenant ID used to list the accounts. + + TenantBindingConfigReaderImpl tenantBindingConfigReader = ServiceMain.getInstance().getTenantBindingConfigReader(); + String effectiveTenantId = tenantBindingConfigReader.getTenantIds().get(0); + + ui.getQueryParameters().putSingle(AuthN.TENANT_ID_QUERY_PARAM, effectiveTenantId); } + // - // Search for an account with the provided email and tenant ID + // Search for an account with the provided email and (optional) tenant ID. // - boolean found = false; AccountListItem accountListItem = null; AccountsCommonList accountList = getAccountList(ui); + if (accountList != null || accountList.getTotalItems() > 0) { - List itemsList = accountList.getAccountListItem(); - for (AccountListItem item : itemsList) { - if (item != null && item.getTenantid() != null && item.getTenantid().equalsIgnoreCase(tenantId)) { - accountListItem = item; - found = true; - break; - } - } - } + accountListItem = accountList.getAccountListItem().stream() + .filter(new Predicate() { + @Override + public boolean test(AccountListItem item) { + if (item == null) { + return false; + } + + if (StringUtils.isEmpty(tenantId)) { + return true; + } + + String itemTenantId = item.getTenantid(); + + return (itemTenantId != null && itemTenantId.equalsIgnoreCase(tenantId)); + } + }) + .findFirst() + .orElse(null); + } - if (found == true) { - try { - response = requestPasswordReset(ui, tenantId, accountListItem); - } catch (Exception e) { - response = Response.status(Response.Status.INTERNAL_SERVER_ERROR).entity(e.getMessage()).type("text/plain").build(); - } - } else { - String msg = String.format("Could not locate an account associated with the email '%s' and tenant ID '%s'", - email , tenantId); - response = Response.status(Response.Status.NOT_FOUND).entity(msg).type("text/plain").build(); - } + if (accountListItem == null) { + String msg = String.format( + StringUtils.isEmpty(tenantId) + ? "Could not locate an account associated with the email %s" + : "Could not locate an account associated with the email %s and tenant ID '%s'", + email, tenantId + ); - return response; + return Response.status(Response.Status.NOT_FOUND).entity(msg).type("text/plain").build(); + } + + // If no tenant ID was supplied, use the account's first associated tenant ID for purposes + // of password reset. This is the same way that a tenant is selected for the account when + // logging in. In practice, accounts are only associated with one tenant anyway. + + String targetTenantId = StringUtils.isEmpty(tenantId) + ? accountListItem.getTenants().get(0).getTenantId() + : tenantId; + + try { + return requestPasswordReset(ui, targetTenantId, accountListItem); + } catch (Exception e) { + return Response.status(Response.Status.INTERNAL_SERVER_ERROR).entity(e.getMessage()).type("text/plain").build(); + } } private boolean contains(String targetTenantID, List accountTenantList) { @@ -447,7 +604,8 @@ public class AccountResource extends SecurityResourceBase> { @@ -69,9 +67,15 @@ public class AccountDocumentHandler public void handleCreate(DocumentWrapper wrapDoc) throws Exception { String id = UUID.randomUUID().toString(); AccountsCommon account = wrapDoc.getWrappedObject(); + account.setCsid(id); + setTenant(account); - account.setStatus(Status.ACTIVE); + + if (account.getStatus() == null) { + account.setStatus(Status.ACTIVE); + } + // We do not allow creation of locked accounts through the services. account.setMetadataProtection(null); account.setRolesProtection(null); @@ -92,8 +96,8 @@ public class AccountDocumentHandler // // First, delete the existing accountroles // - AccountRoleSubResource subResource = - new AccountRoleSubResource(AccountRoleSubResource.ACCOUNT_ACCOUNTROLE_SERVICE); + AccountRoleSubResource subResource = + new AccountRoleSubResource(AccountRoleSubResource.ACCOUNT_ACCOUNTROLE_SERVICE); subResource.deleteAccountRole(getServiceContext(), accountFound.getCsid(), SubjectType.ROLE); // // Check to see if the payload has new roles to relate to the account @@ -103,7 +107,7 @@ public class AccountDocumentHandler // // Next, create the new accountroles // - AccountRole accountRole = AccountRoleFactory.createAccountRoleInstance(accountFound, + AccountRole accountRole = AccountRoleFactory.createAccountRoleInstance(accountFound, roleValueList, true, true); String accountRoleCsid = subResource.createAccountRole(getServiceContext(), accountRole, SubjectType.ROLE); // @@ -143,6 +147,7 @@ public class AccountDocumentHandler if (from.getPersonRefName() != null) { to.setPersonRefName(from.getPersonRefName()); } + // Note that we do not allow update of locks //fixme update for tenant association @@ -173,11 +178,11 @@ public class AccountDocumentHandler subResource.createAccountRole(this.getServiceContext(), accountRole, SubjectType.ROLE); } } - + @Override public void completeUpdate(DocumentWrapper wrapDoc) throws Exception { AccountsCommon upAcc = wrapDoc.getWrappedObject(); - getServiceContext().setOutput(upAcc); + getServiceContext().setOutput(upAcc); } @Override @@ -198,7 +203,7 @@ public class AccountDocumentHandler @Override public AccountsCommon extractCommonPart(DocumentWrapper wrapDoc) throws Exception { AccountsCommon account = wrapDoc.getWrappedObject(); - + String includeRolesQueryParamValue = (String) getServiceContext().getQueryParams().getFirst(AccountClient.INCLUDE_ROLES_QP); boolean includeRoles = Tools.isTrue(includeRolesQueryParamValue); if (includeRoles) { @@ -208,7 +213,7 @@ public class AccountDocumentHandler SubjectType.ROLE); account.setRoleList(AccountRoleFactory.convert(accountRole.getRole())); } - + return wrapDoc.getWrappedObject(); } @@ -313,13 +318,13 @@ public class AccountDocumentHandler AccountsCommon account = wrapDoc.getWrappedObject(); sanitize(account); } - + private void sanitize(AccountsCommon account) { account.setPassword(null); if (!SecurityUtils.isCSpaceAdmin()) { account.setTenants(new ArrayList(0)); } - } + } /* (non-Javadoc) * @see org.collectionspace.services.common.document.DocumentHandler#initializeDocumentFilter(org.collectionspace.services.common.context.ServiceContext) diff --git a/services/account/service/src/main/java/org/collectionspace/services/account/storage/AccountValidatorHandler.java b/services/account/service/src/main/java/org/collectionspace/services/account/storage/AccountValidatorHandler.java index bec10d722..43462797e 100644 --- a/services/account/service/src/main/java/org/collectionspace/services/account/storage/AccountValidatorHandler.java +++ b/services/account/service/src/main/java/org/collectionspace/services/account/storage/AccountValidatorHandler.java @@ -67,7 +67,7 @@ import org.slf4j.LoggerFactory; /** * - * @author + * @author */ public class AccountValidatorHandler implements ValidatorHandler { @@ -105,7 +105,7 @@ public class AccountValidatorHandler implements ValidatorHandler { if (account.getPassword() == null || account.getPassword().length == 0) { invalid = true; msgBldr.append("\npassword : missing"); - } + } if (account.getEmail() == null || account.getEmail().isEmpty()) { invalid = true; msgBldr.append("\nemail : missing"); diff --git a/services/account/service/src/main/java/org/collectionspace/services/account/storage/csidp/TokenStorageClient.java b/services/account/service/src/main/java/org/collectionspace/services/account/storage/csidp/TokenStorageClient.java index 7a69d118a..226abe67e 100644 --- a/services/account/service/src/main/java/org/collectionspace/services/account/storage/csidp/TokenStorageClient.java +++ b/services/account/service/src/main/java/org/collectionspace/services/account/storage/csidp/TokenStorageClient.java @@ -60,9 +60,9 @@ public class TokenStorageClient { * @return user */ static public Token create(String accountCsid, String tenantId, BigInteger expireSeconds) { - EntityManagerFactory emf = JpaStorageUtils.getEntityManagerFactory(); + EntityManagerFactory emf = JpaStorageUtils.getEntityManagerFactory(); Token token = new Token(); - + try { EntityManager em = emf.createEntityManager(); @@ -72,7 +72,7 @@ public class TokenStorageClient { token.setExpireSeconds(expireSeconds); token.setEnabled(true); token.setCreatedAtItem(new Date()); - + em.getTransaction().begin(); em.persist(token); em.getTransaction().commit(); @@ -82,7 +82,7 @@ public class TokenStorageClient { JpaStorageUtils.releaseEntityManagerFactory(emf); } } - + return token; } @@ -90,11 +90,11 @@ public class TokenStorageClient { * Update a token for given an id * @param id * @param enabledFlag - * @throws TransactionException + * @throws TransactionException */ static public void update(TransactionContext transactionContext, String id, boolean enabledFlag) throws DocumentNotFoundException, TransactionException { Token tokenFound = null; - + tokenFound = get((JPATransactionContext)transactionContext, id); if (tokenFound != null) { tokenFound.setEnabled(enabledFlag); @@ -112,11 +112,11 @@ public class TokenStorageClient { * Get token for given ID * @param em EntityManager * @param id - */ + */ public static Token get(JPATransactionContext jpaTransactionContext, String id) throws DocumentNotFoundException, TransactionException { Token tokenFound = null; - - tokenFound = (Token) jpaTransactionContext.find(Token.class, id); + + tokenFound = (Token) jpaTransactionContext.find(Token.class, id); if (tokenFound == null) { String msg = "Could not find token with ID=" + id; logger.error(msg); @@ -125,14 +125,14 @@ public class TokenStorageClient { return tokenFound; } - + static public Token get(String id) throws DocumentNotFoundException { Token tokenFound = null; EntityManagerFactory emf = JpaStorageUtils.getEntityManagerFactory(); try { EntityManager em = emf.createEntityManager(); - tokenFound = (Token) em.find(Token.class, id); + tokenFound = (Token) em.find(Token.class, id); if (tokenFound == null) { String msg = "Could not find token with ID=" + id; logger.error(msg); @@ -143,9 +143,9 @@ public class TokenStorageClient { JpaStorageUtils.releaseEntityManagerFactory(emf); } } - + return tokenFound; - } + } /** * Deletes the token with given id @@ -157,11 +157,11 @@ public class TokenStorageClient { try { EntityManager em = emf.createEntityManager(); - + StringBuilder tokenDelStr = new StringBuilder("DELETE FROM "); tokenDelStr.append(Token.class.getCanonicalName()); tokenDelStr.append(" WHERE id = :id"); - + Query tokenDel = em.createQuery(tokenDelStr.toString()); tokenDel.setParameter("id", id); int tokenDelCount = tokenDel.executeUpdate(); @@ -174,7 +174,7 @@ public class TokenStorageClient { if (emf != null) { JpaStorageUtils.releaseEntityManagerFactory(emf); } - } + } } private String getEncPassword(String userId, byte[] password) throws BadRequestException { @@ -185,8 +185,7 @@ public class TokenStorageClient { } catch (Exception e) { throw new BadRequestException(e.getMessage()); } - String secEncPasswd = SecurityUtils.createPasswordHash( - userId, new String(password), null); + String secEncPasswd = SecurityUtils.createPasswordHash(new String(password)); return secEncPasswd; } } diff --git a/services/account/service/src/main/java/org/collectionspace/services/account/storage/csidp/UserStorageClient.java b/services/account/service/src/main/java/org/collectionspace/services/account/storage/csidp/UserStorageClient.java index 07f0a1c46..4c337433b 100644 --- a/services/account/service/src/main/java/org/collectionspace/services/account/storage/csidp/UserStorageClient.java +++ b/services/account/service/src/main/java/org/collectionspace/services/account/storage/csidp/UserStorageClient.java @@ -82,14 +82,14 @@ public class UserStorageClient { logger.error(msg); throw new DocumentNotFoundException(msg); } - + return userFound; } - + @SuppressWarnings("rawtypes") public User get(ServiceContext ctx, String userId) throws DocumentNotFoundException, TransactionException { User userFound = null; - + JPATransactionContext jpaConnectionContext = (JPATransactionContext)ctx.openConnection(); try { userFound = (User) jpaConnectionContext.find(User.class, userId); @@ -101,9 +101,9 @@ public class UserStorageClient { } finally { ctx.closeConnection(); } - + return userFound; - } + } /** * updateUser for given userId @@ -158,8 +158,7 @@ public class UserStorageClient { } catch (Exception e) { throw new BadRequestException(e.getMessage()); } - String secEncPasswd = SecurityUtils.createPasswordHash( - userId, new String(password), salt); + String secEncPasswd = SecurityUtils.createPasswordHash(new String(password)); return secEncPasswd; } } diff --git a/services/authentication/pstore/src/main/resources/db/postgresql/authentication.sql b/services/authentication/pstore/src/main/resources/db/postgresql/authentication.sql index eff2a4216..210959dbf 100644 --- a/services/authentication/pstore/src/main/resources/db/postgresql/authentication.sql +++ b/services/authentication/pstore/src/main/resources/db/postgresql/authentication.sql @@ -8,9 +8,14 @@ CREATE TABLE IF NOT EXISTS users ( ); -- Upgrade older users tables to 6.0 + ALTER TABLE users ADD COLUMN IF NOT EXISTS lastlogin TIMESTAMP; ALTER TABLE users ADD COLUMN IF NOT EXISTS salt VARCHAR(128); +-- Upgrade older users tables to 8.0 + +UPDATE users SET passwd = concat('{SHA-256}', '{', salt, '}', passwd) WHERE left(passwd, 1) <> '{'; + CREATE TABLE IF NOT EXISTS tokens ( id VARCHAR(128) NOT NULL PRIMARY KEY, account_csid VARCHAR(128) NOT NULL, diff --git a/services/authentication/service/pom.xml b/services/authentication/service/pom.xml index 4fea7a4d8..d9770168a 100644 --- a/services/authentication/service/pom.xml +++ b/services/authentication/service/pom.xml @@ -69,9 +69,9 @@ provided - org.springframework.security.oauth - spring-security-oauth2 - ${spring.security.oauth2.version} + org.springframework.security + spring-security-oauth2-authorization-server + ${spring.security.authorization.server.version} provided diff --git a/services/authentication/service/src/main/java/org/collectionspace/authentication/CSpaceAuthenticationSuccessEvent.java b/services/authentication/service/src/main/java/org/collectionspace/authentication/CSpaceAuthenticationSuccessEvent.java index b8faec555..363365e2a 100644 --- a/services/authentication/service/src/main/java/org/collectionspace/authentication/CSpaceAuthenticationSuccessEvent.java +++ b/services/authentication/service/src/main/java/org/collectionspace/authentication/CSpaceAuthenticationSuccessEvent.java @@ -12,40 +12,37 @@ import org.collectionspace.authentication.realm.db.CSpaceDbRealm; import org.postgresql.util.PSQLState; import org.springframework.context.ApplicationListener; import org.springframework.security.authentication.event.AuthenticationSuccessEvent; -import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.Authentication; public class CSpaceAuthenticationSuccessEvent implements ApplicationListener { - - final private static String UPDATE_USER_SQL = + + private static final String UPDATE_USER_SQL = "UPDATE users SET lastlogin = now() WHERE username = ?"; @Override public void onApplicationEvent(AuthenticationSuccessEvent event) { - // TODO Auto-generated method stub - System.out.println(); //org.springframework.security.authentication.UsernamePasswordAuthenticationToken@8a633e91: Principal: org.collectionspace.authentication.CSpaceUser@b122ec20: Username: admin@core.collectionspace.org; Password: [PROTECTED]; Enabled: true; AccountNonExpired: true; credentialsNonExpired: true; AccountNonLocked: true; Granted Authorities: ROLE_1_TENANT_ADMINISTRATOR,ROLE_SPRING_ADMIN; Credentials: [PROTECTED]; Authenticated: true; Details: {grant_type=password, username=admin@core.collectionspace.org}; Granted Authorities: ROLE_1_TENANT_ADMINISTRATOR, ROLE_SPRING_ADMIN - String username = null; - CSpaceDbRealm cspaceDbRealm = new CSpaceDbRealm(); - - if (event.getSource() instanceof UsernamePasswordAuthenticationToken) { - UsernamePasswordAuthenticationToken eventSource = (UsernamePasswordAuthenticationToken)event.getSource(); + if (event.getSource() instanceof Authentication) { + Authentication eventSource = (Authentication) event.getSource(); + if (eventSource.getPrincipal() instanceof CSpaceUser) { + CSpaceDbRealm cspaceDbRealm = new CSpaceDbRealm(); CSpaceUser cspaceUser = (CSpaceUser) eventSource.getPrincipal(); - username = cspaceUser.getUsername(); + String username = cspaceUser.getUsername(); + try { setLastLogin(cspaceDbRealm, username); } catch (Exception e) { - // TODO Auto-generated catch block e.printStackTrace(); } } } } - + private void setLastLogin(CSpaceDbRealm cspaceDbRealm, String username) throws AccountException { Connection conn = null; PreparedStatement ps = null; ResultSet rs = null; - + try { conn = cspaceDbRealm.getConnection(); ps = conn.prepareStatement(UPDATE_USER_SQL); diff --git a/services/authentication/service/src/main/java/org/collectionspace/authentication/CSpaceSaltSource.java b/services/authentication/service/src/main/java/org/collectionspace/authentication/CSpaceSaltSource.java deleted file mode 100644 index 34c3471df..000000000 --- a/services/authentication/service/src/main/java/org/collectionspace/authentication/CSpaceSaltSource.java +++ /dev/null @@ -1,13 +0,0 @@ -package org.collectionspace.authentication; - -import org.springframework.security.authentication.dao.ReflectionSaltSource; -import org.springframework.security.core.userdetails.UserDetails; - -public class CSpaceSaltSource extends ReflectionSaltSource { - - @Override - public Object getSalt(UserDetails user) { - return super.getSalt(user); - } - -} diff --git a/services/authentication/service/src/main/java/org/collectionspace/authentication/CSpaceTenant.java b/services/authentication/service/src/main/java/org/collectionspace/authentication/CSpaceTenant.java index 68180b6a6..046f93939 100644 --- a/services/authentication/service/src/main/java/org/collectionspace/authentication/CSpaceTenant.java +++ b/services/authentication/service/src/main/java/org/collectionspace/authentication/CSpaceTenant.java @@ -26,17 +26,31 @@ package org.collectionspace.authentication; import org.apache.commons.lang3.builder.EqualsBuilder; import org.apache.commons.lang3.builder.HashCodeBuilder; import org.apache.commons.lang3.builder.ToStringBuilder; +import org.collectionspace.authentication.jackson2.CSpaceTenantDeserializer; + +import com.fasterxml.jackson.annotation.JsonAutoDetect; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonTypeInfo; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; /** * A CollectionSpace tenant. */ +@JsonTypeInfo(use = JsonTypeInfo.Id.CLASS, include = JsonTypeInfo.As.PROPERTY) +@JsonDeserialize(using = CSpaceTenantDeserializer.class) +@JsonAutoDetect( + fieldVisibility = JsonAutoDetect.Visibility.ANY, + getterVisibility = JsonAutoDetect.Visibility.NONE, + isGetterVisibility = JsonAutoDetect.Visibility.NONE +) +@JsonIgnoreProperties(ignoreUnknown = true) public class CSpaceTenant { private final String id; private final String name; /** * Creates a CSpaceTenant with a given id and name. - * + * * @param id the tenant id, e.g. "1" * @param name the tenant name, e.g. "core.collectionspace.org" */ @@ -50,7 +64,7 @@ public class CSpaceTenant { // The tenant id uniquely identifies the tenant, // regardless of other properties. CSpaceTenants // with the same id should hash identically. - + return new HashCodeBuilder(83, 61) .append(id) .build(); @@ -65,17 +79,17 @@ public class CSpaceTenant { if (obj == null) { return false; } - + if (obj == this) { return true; } - + if (obj.getClass() != getClass()) { return false; } - + CSpaceTenant rhs = (CSpaceTenant) obj; - + return new EqualsBuilder() .append(id, rhs.getId()) .isEquals(); @@ -88,7 +102,7 @@ public class CSpaceTenant { append("name", name). toString(); } - + public String getId() { return id; } diff --git a/services/authentication/service/src/main/java/org/collectionspace/authentication/CSpaceUser.java b/services/authentication/service/src/main/java/org/collectionspace/authentication/CSpaceUser.java index ed0931210..7f3ff714d 100644 --- a/services/authentication/service/src/main/java/org/collectionspace/authentication/CSpaceUser.java +++ b/services/authentication/service/src/main/java/org/collectionspace/authentication/CSpaceUser.java @@ -2,30 +2,44 @@ package org.collectionspace.authentication; import java.util.Set; +import org.collectionspace.authentication.jackson2.CSpaceUserDeserializer; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.userdetails.User; +import com.fasterxml.jackson.annotation.JsonAutoDetect; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonTypeInfo; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; + /** * A CollectionSpace user. This class implements the Spring UserDetails interface, * but the enabled, accountNonExpired, credentialsNonExpired, and accountNonLocked * properties are not meaningful and will always be true. CollectionSpace users * may be disabled (aka inactive), but this check is done outside of Spring Security, * after Spring authentication has succeeded. - * + * * @See org.collectionspace.services.common.security.SecurityInterceptor. */ +@JsonTypeInfo(use = JsonTypeInfo.Id.CLASS, include = JsonTypeInfo.As.PROPERTY) +@JsonDeserialize(using = CSpaceUserDeserializer.class) +@JsonAutoDetect( + fieldVisibility = JsonAutoDetect.Visibility.ANY, + getterVisibility = JsonAutoDetect.Visibility.NONE, + isGetterVisibility = JsonAutoDetect.Visibility.NONE +) +@JsonIgnoreProperties(ignoreUnknown = true) public class CSpaceUser extends User { - + private static final long serialVersionUID = 3326192720134327612L; private Set tenants; private CSpaceTenant primaryTenant; private String salt; - + /** * Creates a CSpaceUser with the given username, hashed password, associated * tenants, and granted authorities. - * + * * @param username the username, e.g. "admin@core.collectionspace.org" * @param password the hashed password, e.g. "59PnafP1k9rcuGNMxbCfyQ3TphxKBqecsJI2Yv5vrms=" * @param tenants the tenants associated with the user @@ -44,30 +58,30 @@ public class CSpaceUser extends User { this.tenants = tenants; this.salt = salt; - + if (!tenants.isEmpty()) { primaryTenant = tenants.iterator().next(); } } - + /** * Retrieves the tenants associated with the user. - * + * * @return the tenants */ public Set getTenants() { return tenants; } - + /** * Retrieves the primary tenant associated with the user. - * + * * @return the tenants */ public CSpaceTenant getPrimaryTenant() { return primaryTenant; } - + /** * Returns a "salt" string to use when encrypting a user's password * @return @@ -75,5 +89,4 @@ public class CSpaceUser extends User { public String getSalt() { return salt != null ? salt : ""; } - } diff --git a/services/authentication/service/src/main/java/org/collectionspace/authentication/jackson2/CSpaceTenantDeserializer.java b/services/authentication/service/src/main/java/org/collectionspace/authentication/jackson2/CSpaceTenantDeserializer.java new file mode 100644 index 000000000..f297fca7d --- /dev/null +++ b/services/authentication/service/src/main/java/org/collectionspace/authentication/jackson2/CSpaceTenantDeserializer.java @@ -0,0 +1,31 @@ +package org.collectionspace.authentication.jackson2; + +import java.io.IOException; + +import org.collectionspace.authentication.CSpaceTenant; + +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JsonDeserializer; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.MissingNode; + +public class CSpaceTenantDeserializer extends JsonDeserializer { + + @Override + public CSpaceTenant deserialize(JsonParser parser, DeserializationContext context) throws IOException, JsonProcessingException { + ObjectMapper mapper = (ObjectMapper) parser.getCodec(); + JsonNode jsonNode = mapper.readTree(parser); + + String id = readJsonNode(jsonNode, "id").asText(); + String name = readJsonNode(jsonNode, "name").asText(); + + return new CSpaceTenant(id, name); + } + + private JsonNode readJsonNode(JsonNode jsonNode, String field) { + return jsonNode.has(field) ? jsonNode.get(field) : MissingNode.getInstance(); + } +} diff --git a/services/authentication/service/src/main/java/org/collectionspace/authentication/jackson2/CSpaceUserDeserializer.java b/services/authentication/service/src/main/java/org/collectionspace/authentication/jackson2/CSpaceUserDeserializer.java new file mode 100644 index 000000000..acaa97b82 --- /dev/null +++ b/services/authentication/service/src/main/java/org/collectionspace/authentication/jackson2/CSpaceUserDeserializer.java @@ -0,0 +1,52 @@ +package org.collectionspace.authentication.jackson2; + +import java.io.IOException; +import java.util.Set; + +import org.collectionspace.authentication.CSpaceTenant; +import org.collectionspace.authentication.CSpaceUser; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.SimpleGrantedAuthority; + +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JsonDeserializer; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.MissingNode; + +public class CSpaceUserDeserializer extends JsonDeserializer { + private static final TypeReference> SIMPLE_GRANTED_AUTHORITY_SET = new TypeReference>() { + }; + + private static final TypeReference> CSPACE_TENANT_SET = new TypeReference>() { + }; + + @Override + public CSpaceUser deserialize(JsonParser parser, DeserializationContext context) throws IOException, JsonProcessingException { + ObjectMapper mapper = (ObjectMapper) parser.getCodec(); + JsonNode jsonNode = mapper.readTree(parser); + + Set authorities = mapper.convertValue(jsonNode.get("authorities"), SIMPLE_GRANTED_AUTHORITY_SET); + Set tenants = mapper.convertValue(jsonNode.get("tenants"), CSPACE_TENANT_SET); + + JsonNode passwordNode = readJsonNode(jsonNode, "password"); + String username = readJsonNode(jsonNode, "username").asText(); + String password = passwordNode.asText(""); + String salt = readJsonNode(jsonNode, "salt").asText(); + + CSpaceUser result = new CSpaceUser(username, password, salt, tenants, authorities); + + if (passwordNode.asText(null) == null) { + result.eraseCredentials(); + } + + return result; + } + + private JsonNode readJsonNode(JsonNode jsonNode, String field) { + return jsonNode.has(field) ? jsonNode.get(field) : MissingNode.getInstance(); + } +} diff --git a/services/authentication/service/src/main/java/org/collectionspace/authentication/realm/CSpaceRealm.java b/services/authentication/service/src/main/java/org/collectionspace/authentication/realm/CSpaceRealm.java index c70a14ccc..bfbd20755 100644 --- a/services/authentication/service/src/main/java/org/collectionspace/authentication/realm/CSpaceRealm.java +++ b/services/authentication/service/src/main/java/org/collectionspace/authentication/realm/CSpaceRealm.java @@ -38,7 +38,7 @@ import org.collectionspace.authentication.CSpaceTenant; * Interface for the CollectionSpace realm. */ public interface CSpaceRealm { - + /** * Retrieves the "salt" used to encrypt the user's password * @param username @@ -49,7 +49,7 @@ public interface CSpaceRealm { /** * Retrieves the hashed password used to authenticate a user. - * + * * @param username * @return the password * @throws AccountNotFoundException if the user is not found @@ -59,7 +59,7 @@ public interface CSpaceRealm { /** * Retrieves the roles for a user. - * + * * @param username * @return a collection of roles * @throws AccountException if the roles could not be retrieved @@ -68,7 +68,7 @@ public interface CSpaceRealm { /** * Retrieves the enabled tenants associated with a user. - * + * * @param username * @return a collection of tenants * @throws AccountException if the tenants could not be retrieved @@ -77,12 +77,11 @@ public interface CSpaceRealm { /** * Retrieves the tenants associated with a user, optionally including disabled tenants. - * + * * @param username * @param includeDisabledTenants if true, include disabled tenants * @return a collection of tenants * @throws AccountException if the tenants could not be retrieved */ public Set getTenants(String username, boolean includeDisabledTenants) throws AccountException; - } diff --git a/services/authentication/service/src/main/java/org/collectionspace/authentication/realm/db/CSpaceDbRealm.java b/services/authentication/service/src/main/java/org/collectionspace/authentication/realm/db/CSpaceDbRealm.java index 814ec72fa..09942bd30 100644 --- a/services/authentication/service/src/main/java/org/collectionspace/authentication/realm/db/CSpaceDbRealm.java +++ b/services/authentication/service/src/main/java/org/collectionspace/authentication/realm/db/CSpaceDbRealm.java @@ -74,13 +74,13 @@ import org.slf4j.LoggerFactory; /** * CSpaceDbRealm provides access to user, password, role, tenant database - * @author + * @author */ public class CSpaceDbRealm implements CSpaceRealm { public static String DEFAULT_DATASOURCE_NAME = "CspaceDS"; - + private Logger logger = LoggerFactory.getLogger(CSpaceDbRealm.class); - + private String datasourceName; private String principalsQuery; private String saltQuery; @@ -96,7 +96,7 @@ public class CSpaceDbRealm implements CSpaceRealm { private long delayBetweenAttemptsMillis = DELAY_BETWEEN_ATTEMPTS_MILLISECONDS; private static final String DELAY_BETWEEN_ATTEMPTS_MILLISECONDS_STR = "delayBetweenAttemptsMillis"; private static final long DELAY_BETWEEN_ATTEMPTS_MILLISECONDS = 200; - + protected void setMaxRetrySeconds(Map options) { Object optionsObj = options.get(MAX_RETRY_SECONDS_STR); if (optionsObj != null) { @@ -109,11 +109,11 @@ public class CSpaceDbRealm implements CSpaceRealm { } } } - + protected long getMaxRetrySeconds() { return this.maxRetrySeconds; } - + protected void setDelayBetweenAttemptsMillis(Map options) { Object optionsObj = options.get(DELAY_BETWEEN_ATTEMPTS_MILLISECONDS_STR); if (optionsObj != null) { @@ -126,15 +126,15 @@ public class CSpaceDbRealm implements CSpaceRealm { } } } - + protected long getDelayBetweenAttemptsMillis() { return this.delayBetweenAttemptsMillis; } - + public CSpaceDbRealm() { datasourceName = DEFAULT_DATASOURCE_NAME; } - + /** * CSpace Database Realm * @param datasourceName datasource name @@ -168,10 +168,10 @@ public class CSpaceDbRealm implements CSpaceRealm { if (tmp != null) { suspendResume = Boolean.valueOf(tmp.toString()).booleanValue(); } - + this.setMaxRetrySeconds(options); this.setDelayBetweenAttemptsMillis(options); - + if (logger.isTraceEnabled()) { logger.trace("DatabaseServerLoginModule, dsJndiName=" + datasourceName); logger.trace("principalsQuery=" + principalsQuery); @@ -270,14 +270,14 @@ public class CSpaceDbRealm implements CSpaceRealm { if (logger.isDebugEnabled()) { logger.debug("No roles found"); } - + return roles; } do { String roleName = rs.getString(1); roles.add(roleName); - + } while (rs.next()); } catch (SQLException ex) { AccountException ae = new AccountException("Query failed"); @@ -316,7 +316,7 @@ public class CSpaceDbRealm implements CSpaceRealm { public Set getTenants(String username) throws AccountException { return getTenants(username, false); } - + private boolean userIsTenantManager(Connection conn, String username) { String acctQuery = "SELECT csid FROM accounts_common WHERE userid=?"; PreparedStatement ps = null; @@ -356,7 +356,7 @@ public class CSpaceDbRealm implements CSpaceRealm { } return accountIsTenantManager; } - + /** * Execute the tenantsQuery against the datasourceName to obtain the tenants for * the authenticated user. @@ -366,13 +366,13 @@ public class CSpaceDbRealm implements CSpaceRealm { public Set getTenants(String username, boolean includeDisabledTenants) throws AccountException { String tenantsQuery = getTenantQuery(includeDisabledTenants); - + if (logger.isDebugEnabled()) { logger.debug("getTenants using tenantsQuery: " + tenantsQuery + ", username: " + username); } Set tenants = new LinkedHashSet(); - + Connection conn = null; PreparedStatement ps = null; ResultSet rs = null; @@ -393,7 +393,7 @@ public class CSpaceDbRealm implements CSpaceRealm { if (logger.isDebugEnabled()) { logger.debug("GetTenants called with tenantManager - synthesizing the pseudo-tenant"); } - + tenants.add(new CSpaceTenant(AuthN.TENANT_MANAGER_ACCT_ID, "PseudoTenant")); } else { if (logger.isDebugEnabled()) { @@ -403,7 +403,7 @@ public class CSpaceDbRealm implements CSpaceRealm { // empty Tenants set. // FIXME should this be allowed? } - + return tenants; } @@ -461,7 +461,7 @@ public class CSpaceDbRealm implements CSpaceRealm { if (requestAttempts > 0) { Thread.sleep(getDelayBetweenAttemptsMillis()); // Wait a little time between reattempts. } - + try { // proceed to the original request by calling doFilter() result = this.getConnection(getDataSourceName()); @@ -482,7 +482,7 @@ public class CSpaceDbRealm implements CSpaceRealm { requestAttempts++; // keep track of how many times we've tried the request } } while (System.currentTimeMillis() < quittingTime); // keep trying until we run out of time - + // // Add a warning to the logs if we encountered *any* failures on our re-attempts. Only add the warning // if we were eventually successful. @@ -498,10 +498,10 @@ public class CSpaceDbRealm implements CSpaceRealm { // If we get here, it means all of our attempts to get a successful call to chain.doFilter() have failed. throw lastException; } - + return result; } - + /* * Don't call this method directly. Instead, use the getConnection() method that take no arguments. */ @@ -509,52 +509,52 @@ public class CSpaceDbRealm implements CSpaceRealm { InitialContext ctx = null; Connection conn = null; DataSource ds = null; - + try { ctx = new InitialContext(); try { ds = (DataSource) ctx.lookup(dataSourceName); } catch (Exception e) {} - + try { Context envCtx = (Context) ctx.lookup("java:comp/env"); ds = (DataSource) envCtx.lookup(dataSourceName); } catch (Exception e) {} - + try { Context envCtx = (Context) ctx.lookup("java:comp"); ds = (DataSource) envCtx.lookup(dataSourceName); } catch (Exception e) {} - + try { Context envCtx = (Context) ctx.lookup("java:"); ds = (DataSource) envCtx.lookup(dataSourceName); } catch (Exception e) {} - + try { Context envCtx = (Context) ctx.lookup("java"); ds = (DataSource) envCtx.lookup(dataSourceName); } catch (Exception e) {} - + try { ds = (DataSource) ctx.lookup("java:/" + dataSourceName); - } catch (Exception e) {} + } catch (Exception e) {} if (ds == null) { ds = AuthN.getDataSource(); } - + if (ds == null) { throw new IllegalArgumentException("datasource not found: " + dataSourceName); } - + conn = ds.getConnection(); if (conn == null) { conn = AuthN.getDataSource().getConnection(); //FIXME:REM - This is the result of some type of JNDI mess. Should try to solve this problem and clean up this code. } - + return conn; - + } catch (NamingException ex) { AccountException ae = new AccountException("Error looking up DataSource from: " + dataSourceName); ae.initCause(ex); @@ -619,7 +619,7 @@ public class CSpaceDbRealm implements CSpaceRealm { this.tenantsQueryNoDisabled = tenantQuery; } */ - + /* * This method crawls the exception chain looking for network related exceptions and * returns 'true' if it finds one. @@ -633,13 +633,13 @@ public class CSpaceDbRealm implements CSpaceRealm { result = true; break; } - + cause = cause.getCause(); } return result; } - + /* * Return 'true' if the exception is in the "java.net" package. */ @@ -713,8 +713,7 @@ public class CSpaceDbRealm implements CSpaceRealm { } } } - + return salt; } - } diff --git a/services/authentication/service/src/main/java/org/collectionspace/authentication/spring/CSpaceJwtAuthenticationToken.java b/services/authentication/service/src/main/java/org/collectionspace/authentication/spring/CSpaceJwtAuthenticationToken.java new file mode 100644 index 000000000..7b26ed76a --- /dev/null +++ b/services/authentication/service/src/main/java/org/collectionspace/authentication/spring/CSpaceJwtAuthenticationToken.java @@ -0,0 +1,27 @@ +package org.collectionspace.authentication.spring; + +import java.util.Objects; + +import org.collectionspace.authentication.CSpaceUser; +import org.springframework.security.oauth2.jwt.Jwt; +import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationToken; + +/** + * A JwtAuthenticationToken whose principal is a CSpaceUser. + */ +public class CSpaceJwtAuthenticationToken extends JwtAuthenticationToken { + private final CSpaceUser user; + + public CSpaceJwtAuthenticationToken(Jwt jwt, CSpaceUser user) { + super(jwt, user.getAuthorities(), user.getUsername()); + + this.user = Objects.requireNonNull(user); + + this.setAuthenticated(true); + } + + @Override + public Object getPrincipal() { + return user; + } +} diff --git a/services/authentication/service/src/main/java/org/collectionspace/authentication/spring/CSpaceLogoutSuccessHandler.java b/services/authentication/service/src/main/java/org/collectionspace/authentication/spring/CSpaceLogoutSuccessHandler.java new file mode 100644 index 000000000..d0e42afc7 --- /dev/null +++ b/services/authentication/service/src/main/java/org/collectionspace/authentication/spring/CSpaceLogoutSuccessHandler.java @@ -0,0 +1,50 @@ +package org.collectionspace.authentication.spring; + +import java.util.Set; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.security.web.authentication.logout.SimpleUrlLogoutSuccessHandler; + +/** + * A LogoutSuccessHandler that reads the post-logout redirect URL from a parameter in the logout + * request. As an anti-phishing security measure, the URL is checked against a list of permitted + * redirect URLs (originating from tenant binding configuration or OAuth client configuration). + */ +public class CSpaceLogoutSuccessHandler extends SimpleUrlLogoutSuccessHandler { + final Logger logger = LoggerFactory.getLogger(CSpaceLogoutSuccessHandler.class); + + public static final String REDIRECT_PARAMETER_NAME = "redirect"; + + private Set permittedRedirectUris; + + public CSpaceLogoutSuccessHandler(String defaultTargetUrl, Set permittedRedirectUris) { + super(); + + this.setDefaultTargetUrl(defaultTargetUrl); + + this.permittedRedirectUris = permittedRedirectUris; + } + + @Override + protected String determineTargetUrl(HttpServletRequest request, HttpServletResponse response) { + String redirectUrl = request.getParameter(REDIRECT_PARAMETER_NAME); + + if (redirectUrl != null && !isPermitted(redirectUrl)) { + logger.warn("Logout redirect url not permitted: {}", redirectUrl); + + redirectUrl = null; + } + + return (redirectUrl != null) + ? redirectUrl + : super.determineTargetUrl(request, response); + } + + private boolean isPermitted(String redirectUrl) { + return permittedRedirectUris.contains(redirectUrl); + } +} diff --git a/services/authentication/service/src/main/java/org/collectionspace/authentication/spring/CSpacePasswordEncoderFactory.java b/services/authentication/service/src/main/java/org/collectionspace/authentication/spring/CSpacePasswordEncoderFactory.java new file mode 100644 index 000000000..acccdf3f7 --- /dev/null +++ b/services/authentication/service/src/main/java/org/collectionspace/authentication/spring/CSpacePasswordEncoderFactory.java @@ -0,0 +1,39 @@ +package org.collectionspace.authentication.spring; + +import java.util.HashMap; +import java.util.Map; + +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.security.crypto.password.DelegatingPasswordEncoder; +import org.springframework.security.crypto.password.MessageDigestPasswordEncoder; +import org.springframework.security.crypto.password.NoOpPasswordEncoder; +import org.springframework.security.crypto.password.PasswordEncoder; + +/** + * A password encoder factory that creates PasswordEncoders supporting three algorithms: + * - bcrypt (the default for new passwords) + * - SHA-256 (to support legacy passwords generated before 8.0) + * - noop (for testing only) + */ +public class CSpacePasswordEncoderFactory { + private static PasswordEncoder instance = null; + + public static PasswordEncoder createDefaultPasswordEncoder() { + if (instance == null) { + Map encoders = new HashMap(); + + // Passwords in CollectionSpace pre-8.0 were SHA-256 hashed and Base64 encoded. Continue to + // support these. + MessageDigestPasswordEncoder legacyPasswordEncoder = new MessageDigestPasswordEncoder("SHA-256"); + legacyPasswordEncoder.setEncodeHashAsBase64(true); + + encoders.put("bcrypt", new BCryptPasswordEncoder()); + encoders.put("noop", NoOpPasswordEncoder.getInstance()); + encoders.put("SHA-256", legacyPasswordEncoder); + + instance = new DelegatingPasswordEncoder("bcrypt", encoders); + } + + return instance; + } +} diff --git a/services/authentication/service/src/main/java/org/collectionspace/authentication/spring/CSpaceUserAttributeFilter.java b/services/authentication/service/src/main/java/org/collectionspace/authentication/spring/CSpaceUserAttributeFilter.java index 230709b23..a4bd5c507 100644 --- a/services/authentication/service/src/main/java/org/collectionspace/authentication/spring/CSpaceUserAttributeFilter.java +++ b/services/authentication/service/src/main/java/org/collectionspace/authentication/spring/CSpaceUserAttributeFilter.java @@ -12,23 +12,45 @@ import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.web.filter.OncePerRequestFilter; /** - * A filter that sets a request attribute containing the username of the - * authenticated CollectionSpace user. This attribute may then be used - * to log the username via tomcat's standard access log valve. + * A filter that sets a request attribute containing the username of the authenticated + * CollectionSpace user. This attribute may be used to log the username via tomcat's standard + * access log valve. + * + * This filter should run before org.springframework.security.web.authentication.logout.LogoutFilter. */ public class CSpaceUserAttributeFilter extends OncePerRequestFilter { public static final String ATTRIBUTE_NAME = "org.collectionspace.authentication.user"; - + @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException { + + // Get the username before running LogoutFilter, in case this is a logout request that + // would delete the authenticated user. + + String beforeLogoutUsername = getUsername(); + chain.doFilter(request, response); + String username = getUsername(); + + if (username == null && beforeLogoutUsername != null) { + username = beforeLogoutUsername; + } + + if (username != null) { + request.setAttribute(ATTRIBUTE_NAME, username); + } + } + + private String getUsername() { Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); if (authentication != null) { - request.setAttribute(ATTRIBUTE_NAME, authentication.getName()); + return authentication.getName(); } + + return null; } } diff --git a/services/authentication/service/src/main/java/org/collectionspace/authentication/spring/CSpaceUserAuthenticationConverter.java b/services/authentication/service/src/main/java/org/collectionspace/authentication/spring/CSpaceUserAuthenticationConverter.java deleted file mode 100644 index 3d81539a6..000000000 --- a/services/authentication/service/src/main/java/org/collectionspace/authentication/spring/CSpaceUserAuthenticationConverter.java +++ /dev/null @@ -1,63 +0,0 @@ -package org.collectionspace.authentication.spring; - -import java.util.LinkedHashMap; -import java.util.Map; - -import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; -import org.springframework.security.core.Authentication; -import org.springframework.security.core.userdetails.UserDetails; -import org.springframework.security.core.userdetails.UserDetailsService; -import org.springframework.security.core.userdetails.UsernameNotFoundException; -import org.springframework.security.oauth2.provider.token.UserAuthenticationConverter; - -/** - * Converter for CSpace user authentication information to and from Maps. - * This is used to serialize/deserialize user information to/from JWTs. - * When extracting the user authentication from a map, only the username - * is required. The full user information is retrieved from a UserDetailsService. - */ -public class CSpaceUserAuthenticationConverter implements UserAuthenticationConverter { - - private UserDetailsService userDetailsService; - - /** - * Creates a converter that uses the given UserDetailsService when extracting - * the authentication information. - * - * @param userDetailsService the UserDetailsService to use - */ - public CSpaceUserAuthenticationConverter(UserDetailsService userDetailsService) { - this.userDetailsService = userDetailsService; - } - - @Override - public Map convertUserAuthentication(Authentication userAuthentication) { - // In extractAuthentication we use a UserDetailsService to look up - // the user's roles and tenants, so there's no need to serialize - // those. We just need the username. - - Map response = new LinkedHashMap(); - - response.put(USERNAME, userAuthentication.getName()); - - return response; - } - - @Override - public Authentication extractAuthentication(Map map) { - if (!map.containsKey(USERNAME) || userDetailsService == null) { - return null; - } - - String username = (String) map.get(USERNAME); - - try { - UserDetails user = userDetailsService.loadUserByUsername(username); - - return new UsernamePasswordAuthenticationToken(user, "N/A", user.getAuthorities()); - } - catch(UsernameNotFoundException e) { - return null; - } - } -} diff --git a/services/authentication/service/src/main/java/org/collectionspace/authentication/spring/CSpaceUserDetailsService.java b/services/authentication/service/src/main/java/org/collectionspace/authentication/spring/CSpaceUserDetailsService.java index fba9868ff..74901a35e 100644 --- a/services/authentication/service/src/main/java/org/collectionspace/authentication/spring/CSpaceUserDetailsService.java +++ b/services/authentication/service/src/main/java/org/collectionspace/authentication/spring/CSpaceUserDetailsService.java @@ -77,7 +77,7 @@ public class CSpaceUserDetailsService implements UserDetailsService { String salt = null; Set tenants = null; Set grantedAuthorities = null; - + try { password = realm.getPassword(username); salt = realm.getSalt(username); @@ -90,32 +90,32 @@ public class CSpaceUserDetailsService implements UserDetailsService { catch (AccountException e) { throw new AuthenticationServiceException(e.getMessage(), e); } - - CSpaceUser cspaceUser = + + CSpaceUser cspaceUser = new CSpaceUser( username, password, salt, tenants, grantedAuthorities); - + return cspaceUser; } - + protected Set getAuthorities(String username) throws AccountException { Set roles = realm.getRoles(username); Set authorities = new LinkedHashSet(roles.size()); - + for (String role : roles) { authorities.add(new SimpleGrantedAuthority(role)); } - + return authorities; } - + protected Set getTenants(String username) throws AccountException { Set tenants = realm.getTenants(username); - + return tenants; } } diff --git a/services/authentication/service/src/main/java/org/collectionspace/authentication/spring/SpringAuthNContext.java b/services/authentication/service/src/main/java/org/collectionspace/authentication/spring/SpringAuthNContext.java index c1dc8dd68..309f856ba 100644 --- a/services/authentication/service/src/main/java/org/collectionspace/authentication/spring/SpringAuthNContext.java +++ b/services/authentication/service/src/main/java/org/collectionspace/authentication/spring/SpringAuthNContext.java @@ -37,23 +37,23 @@ public class SpringAuthNContext implements AuthNContext { /** * Returns the username of the authenticated user. - * + * * @return the username */ @Override public String getUserId() { Authentication authToken = SecurityContextHolder.getContext().getAuthentication(); - + if (authToken == null) { return AuthN.ANONYMOUS_USER; } - + return authToken.getName(); } /** * Returns the authenticated CSpaceUser user. - * + * * @return the user */ @Override @@ -67,38 +67,38 @@ public class SpringAuthNContext implements AuthNContext { if (principal instanceof CSpaceUser ) { result = (CSpaceUser) principal; } - } - + } + return result; } /** * Returns the id of the primary tenant associated with the authenticated user. - * + * * @return the tenant id */ @Override public String getCurrentTenantId() { String result = null; - + CSpaceUser cspaceUser = getUser(); if (cspaceUser != null) { result = getCurrentTenant().getId(); } else { - String username = getUserId(); + String username = getUserId(); if (username.equals(AuthN.ANONYMOUS_USER)) { result = AuthN.ANONYMOUS_TENANT_ID; } else if (username.equals(AuthN.SPRING_ADMIN_USER)) { result = AuthN.ADMIN_TENANT_ID; } } - + return result; } /** * Returns the name of the primary tenant associated with the authenticated user. - * + * * @return the tenant name */ @Override @@ -112,13 +112,13 @@ public class SpringAuthNContext implements AuthNContext { /** * Returns the primary tenant associated with the authenticated user. - * + * * @return the tenant */ @Override public CSpaceTenant getCurrentTenant() { CSpaceTenant result = null; - + CSpaceUser cspaceUser = getUser(); if (cspaceUser != null) { result = getUser().getPrimaryTenant(); @@ -128,9 +128,9 @@ public class SpringAuthNContext implements AuthNContext { result = new CSpaceTenant(AuthN.ANONYMOUS_TENANT_ID, AuthN.ANONYMOUS_TENANT_NAME); } else if (username.equals(AuthN.SPRING_ADMIN_USER)) { result = new CSpaceTenant(AuthN.ADMIN_TENANT_ID, AuthN.ADMIN_TENANT_NAME); - } + } } - + return result; } } diff --git a/services/authorization-mgt/import/build.xml b/services/authorization-mgt/import/build.xml index 8afdd2ab4..b0b53a92e 100644 --- a/services/authorization-mgt/import/build.xml +++ b/services/authorization-mgt/import/build.xml @@ -117,6 +117,7 @@ +
@@ -149,7 +150,7 @@ - + diff --git a/services/authorization/pstore/src/main/resources/db/postgresql/README.txt b/services/authorization/pstore/src/main/resources/db/postgresql/README.txt index fbc2b8919..727de8acd 100644 --- a/services/authorization/pstore/src/main/resources/db/postgresql/README.txt +++ b/services/authorization/pstore/src/main/resources/db/postgresql/README.txt @@ -1,20 +1,20 @@ -The file authorization.sql is basically generated by the gen_ddl ant target. -However, you must modify the result of that to make the +The file authorization.sql is basically generated by the gen_ddl ant target, except for the Spring +Security table definitions. However, you must modify the result of that to make the - DROP TABLE + DROP TABLE -statements be - - DROP TABLE IF EXISTS table CASCADE +statements be + + DROP TABLE IF EXISTS table CASCADE This ensures that first time setup does not fail, and that later invocations can deal with dependencies. You must also make the - DROP SEQUENCE + DROP SEQUENCE -statements be +statements be DROP SEQUENCE IF EXISTS @@ -24,8 +24,8 @@ You must also remove (comment out) the statement (which is superfluous with the alter table permissions_actions drop constraint FK85F82042E2DC84FD; -When using the account_tenants table on insert, you have to specify "nextval('hibernate_sequence')" -as the value for the HJID column. +When using the account_tenants table on insert, you have to specify "nextval('hibernate_sequence')" +as the value for the HJID column. Note that because of the way gen_ddl does its work per-sub-project, there is a single shared sequence for both this and the authorization.sql script. This should be okay, even if it does diff --git a/services/authorization/pstore/src/main/resources/db/postgresql/acl.sql b/services/authorization/pstore/src/main/resources/db/postgresql/acl.sql index a3f9f7f66..726046451 100644 --- a/services/authorization/pstore/src/main/resources/db/postgresql/acl.sql +++ b/services/authorization/pstore/src/main/resources/db/postgresql/acl.sql @@ -7,50 +7,59 @@ -- -- Table structure for table acl_class -- -CREATE TABLE IF NOT EXISTS acl_class ( - id BIGSERIAL NOT NULL PRIMARY KEY, - class VARCHAR(100) NOT NULL UNIQUE + +CREATE TABLE IF NOT EXISTS acl_class( + id BIGSERIAL NOT NULL PRIMARY KEY, + class VARCHAR(100) NOT NULL, + CONSTRAINT unique_uk_2 UNIQUE(class) ); -- -- Table structure for table acl_sid -- -CREATE TABLE IF NOT EXISTS acl_sid ( - id BIGSERIAL NOT NULL PRIMARY KEY, - principal BOOLEAN NOT NULL, - sid VARCHAR(100) NOT NULL, - UNIQUE (sid, principal) + +CREATE TABLE IF NOT EXISTS acl_sid( + id BIGSERIAL NOT NULL PRIMARY KEY, + principal BOOLEAN NOT NULL, + sid VARCHAR(100) NOT NULL, + CONSTRAINT unique_uk_1 UNIQUE(sid,principal) ); -- -- Table structure for table acl_object_identity -- -CREATE TABLE IF NOT EXISTS acl_object_identity ( - id BIGSERIAL PRIMARY KEY, - object_id_class BIGINT NOT NULL, - object_id_identity BIGINT NOT NULL, - parent_object BIGINT, - owner_sid BIGINT, - entries_inheriting BOOLEAN NOT NULL, - UNIQUE (object_id_class, object_id_identity), - FOREIGN KEY (parent_object) REFERENCES acl_object_identity (id), - FOREIGN KEY (object_id_class) REFERENCES acl_class (id), - FOREIGN KEY (owner_sid) REFERENCES acl_sid (id) + +CREATE TABLE IF NOT EXISTS acl_object_identity( + id BIGSERIAL PRIMARY KEY, + object_id_class BIGINT NOT NULL, + object_id_identity VARCHAR(36) NOT NULL, + parent_object BIGINT, + owner_sid BIGINT, + entries_inheriting BOOLEAN NOT NULL, + CONSTRAINT unique_uk_3 UNIQUE(object_id_class,object_id_identity), + CONSTRAINT foreign_fk_1 FOREIGN KEY(parent_object) REFERENCES acl_object_identity(id), + CONSTRAINT foreign_fk_2 FOREIGN KEY(object_id_class) REFERENCES acl_class(id), + CONSTRAINT foreign_fk_3 FOREIGN KEY(owner_sid) REFERENCES acl_sid(id) ); +-- Upgrade older acl_object_identity tables to 8.0 + +ALTER TABLE acl_object_identity ALTER COLUMN object_id_identity TYPE VARCHAR(36); + -- -- Table structure for table acl_entry -- -CREATE TABLE IF NOT EXISTS acl_entry ( - id BIGSERIAL PRIMARY KEY, - acl_object_identity BIGINT NOT NULL, - ace_order INT NOT NULL, - sid BIGINT NOT NULL, - mask INTEGER NOT NULL, - granting BOOLEAN NOT NULL, - audit_success BOOLEAN NOT NULL, - audit_failure BOOLEAN NOT NULL, - UNIQUE(acl_object_identity,ace_order), - FOREIGN KEY (acl_object_identity) REFERENCES acl_object_identity (id), - FOREIGN KEY (sid) REFERENCES acl_sid (id) + +CREATE TABLE IF NOT EXISTS acl_entry( + id BIGSERIAL PRIMARY KEY, + acl_object_identity BIGINT NOT NULL, + ace_order INT NOT NULL, + sid BIGINT NOT NULL, + mask INTEGER NOT NULL, + granting BOOLEAN NOT NULL, + audit_success BOOLEAN NOT NULL, + audit_failure BOOLEAN NOT NULL, + CONSTRAINT unique_uk_4 UNIQUE(acl_object_identity,ace_order), + CONSTRAINT foreign_fk_4 FOREIGN KEY(acl_object_identity) REFERENCES acl_object_identity(id), + CONSTRAINT foreign_fk_5 FOREIGN KEY(sid) REFERENCES acl_sid(id) ); diff --git a/services/authorization/pstore/src/main/resources/db/postgresql/authorization.sql b/services/authorization/pstore/src/main/resources/db/postgresql/authorization.sql index 1c8cc3958..c321adfb0 100644 --- a/services/authorization/pstore/src/main/resources/db/postgresql/authorization.sql +++ b/services/authorization/pstore/src/main/resources/db/postgresql/authorization.sql @@ -59,3 +59,50 @@ CREATE TABLE IF NOT EXISTS roles ( ); CREATE SEQUENCE IF NOT EXISTS hibernate_sequence; + +-- Spring Security Authorization Server (OAuth) tables + +CREATE TABLE IF NOT EXISTS oauth2_authorization ( + id varchar(100) NOT NULL, + registered_client_id varchar(100) NOT NULL, + principal_name varchar(200) NOT NULL, + authorization_grant_type varchar(100) NOT NULL, + authorized_scopes varchar(1000) DEFAULT NULL, + attributes text DEFAULT NULL, + state varchar(500) DEFAULT NULL, + authorization_code_value text DEFAULT NULL, + authorization_code_issued_at timestamp DEFAULT NULL, + authorization_code_expires_at timestamp DEFAULT NULL, + authorization_code_metadata text DEFAULT NULL, + access_token_value text DEFAULT NULL, + access_token_issued_at timestamp DEFAULT NULL, + access_token_expires_at timestamp DEFAULT NULL, + access_token_metadata text DEFAULT NULL, + access_token_type varchar(100) DEFAULT NULL, + access_token_scopes varchar(1000) DEFAULT NULL, + oidc_id_token_value text DEFAULT NULL, + oidc_id_token_issued_at timestamp DEFAULT NULL, + oidc_id_token_expires_at timestamp DEFAULT NULL, + oidc_id_token_metadata text DEFAULT NULL, + refresh_token_value text DEFAULT NULL, + refresh_token_issued_at timestamp DEFAULT NULL, + refresh_token_expires_at timestamp DEFAULT NULL, + refresh_token_metadata text DEFAULT NULL, + PRIMARY KEY (id) +); + +CREATE TABLE IF NOT EXISTS oauth2_registered_client ( + id varchar(100) NOT NULL, + client_id varchar(100) NOT NULL, + client_id_issued_at timestamp DEFAULT CURRENT_TIMESTAMP NOT NULL, + client_secret varchar(200) DEFAULT NULL, + client_secret_expires_at timestamp DEFAULT NULL, + client_name varchar(200) NOT NULL, + client_authentication_methods varchar(1000) NOT NULL, + authorization_grant_types varchar(1000) NOT NULL, + redirect_uris varchar(1000) DEFAULT NULL, + scopes varchar(1000) NOT NULL, + client_settings varchar(2000) NOT NULL, + token_settings varchar(2000) NOT NULL, + PRIMARY KEY (id) +); diff --git a/services/authorization/service/pom.xml b/services/authorization/service/pom.xml index fb0dc78b1..fff5e4746 100644 --- a/services/authorization/service/pom.xml +++ b/services/authorization/service/pom.xml @@ -72,12 +72,6 @@ ${spring.security.version} provided - - org.springframework.security.oauth - spring-security-oauth2 - ${spring.security.oauth2.version} - provided - org.springframework spring-context diff --git a/services/authorization/service/src/main/java/org/collectionspace/services/authorization/spring/CSpaceOAuth2RequestFactory.java b/services/authorization/service/src/main/java/org/collectionspace/services/authorization/spring/CSpaceOAuth2RequestFactory.java deleted file mode 100644 index 7cd8075ca..000000000 --- a/services/authorization/service/src/main/java/org/collectionspace/services/authorization/spring/CSpaceOAuth2RequestFactory.java +++ /dev/null @@ -1,100 +0,0 @@ -/** - * This document is a part of the source code and related artifacts - * for CollectionSpace, an open source collections management system - * for museums and related institutions: - - * http://www.collectionspace.org - * http://wiki.collectionspace.org - - * Copyright 2009 University of California at Berkeley - - * Licensed under the Educational Community License (ECL), Version 2.0. - * You may not use this file except in compliance with this License. - - * You may obtain a copy of the ECL 2.0 License at - - * https://source.collectionspace.org/collection-space/LICENSE.txt - - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - *//** - * This document is a part of the source code and related artifacts - * for CollectionSpace, an open source collections management system - * for museums and related institutions: - - * http://www.collectionspace.org - * http://wiki.collectionspace.org - - * Copyright 2009 University of California at Berkeley - - * Licensed under the Educational Community License (ECL), Version 2.0. - * You may not use this file except in compliance with this License. - - * You may obtain a copy of the ECL 2.0 License at - - * https://source.collectionspace.org/collection-space/LICENSE.txt - - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.collectionspace.services.authorization.spring; - -import java.nio.charset.StandardCharsets; -import java.util.HashMap; -import java.util.Map; - -import javax.xml.bind.DatatypeConverter; - -import org.springframework.security.oauth2.provider.AuthorizationRequest; -import org.springframework.security.oauth2.provider.ClientDetails; -import org.springframework.security.oauth2.provider.ClientDetailsService; -import org.springframework.security.oauth2.provider.TokenRequest; -import org.springframework.security.oauth2.provider.request.DefaultOAuth2RequestFactory; - -/** - * An OAuth2RequestFactory that expects the password to be base64 encoded. This implementation - * copies the parameters, decodes the password if present, and passes the result to - * DefaultOAuth2RequestFactory. - */ -public class CSpaceOAuth2RequestFactory extends DefaultOAuth2RequestFactory { - private final String PASSWORD_PARAMETER = "password"; - - public CSpaceOAuth2RequestFactory(ClientDetailsService clientDetailsService) { - super(clientDetailsService); - } - - @Override - public AuthorizationRequest createAuthorizationRequest( - Map authorizationParameters) { - return super.createAuthorizationRequest(decodePassword(authorizationParameters)); - } - - @Override - public TokenRequest createTokenRequest( - Map requestParameters, - ClientDetails authenticatedClient) { - return super.createTokenRequest(decodePassword(requestParameters), authenticatedClient); - } - - private Map decodePassword(Map parameters) { - if (parameters.containsKey(PASSWORD_PARAMETER)) { - String base64EncodedPassword = parameters.get(PASSWORD_PARAMETER); - String password = new String(DatatypeConverter.parseBase64Binary(base64EncodedPassword), StandardCharsets.UTF_8); - - Map parametersCopy = new HashMap(parameters); - - parametersCopy.put(PASSWORD_PARAMETER, password); - - return parametersCopy; - } - - return parameters; - } -} diff --git a/services/common-api/src/main/java/org/collectionspace/services/common/api/Tools.java b/services/common-api/src/main/java/org/collectionspace/services/common/api/Tools.java index 599d83916..27595fb78 100644 --- a/services/common-api/src/main/java/org/collectionspace/services/common/api/Tools.java +++ b/services/common-api/src/main/java/org/collectionspace/services/common/api/Tools.java @@ -37,9 +37,9 @@ import java.util.regex.Matcher; * @author Laramie Crocker * v.1.4 */ -public class Tools { +public class Tools { private static final String PROPERTY_VAR_REGEX = "\\$\\{([A-Za-z0-9_\\.]+)\\}"; - + /** @return first glued to second with the separator string, at most one time - useful for appending paths. */ public static String glue(String first, String separator, String second){ @@ -101,15 +101,15 @@ public class Tools { public static boolean isTrue(String test) { return notEmpty(test) && (new Boolean(test)).booleanValue(); } - + /** Handles null value with 'true' result. */ public static boolean isFalse(String test) { if (test == null) { return true; } - + return (new Boolean(test)).booleanValue() == false; - } + } public static String searchAndReplace(String source, String find, String replace){ Pattern pattern = Pattern.compile(find); @@ -117,7 +117,7 @@ public class Tools { String output = matcher.replaceAll(replace); return output; } - + public static String searchAndReplaceWithQuoteReplacement(String source, String find, String replace){ Pattern pattern = Pattern.compile(find); Matcher matcher = pattern.matcher(source); @@ -127,7 +127,7 @@ public class Tools { static boolean m_fileSystemIsDOS = "\\".equals(File.separator); static boolean m_fileSystemIsMac = ":".equals(File.separator); - + public final static String FILE_EXTENSION_SEPARATOR = "."; public final static String OPTIONAL_VALUE_SUFFIX = "_OPT"; @@ -160,7 +160,7 @@ public class Tools { } return dir + file; } - + public static String getFilenameExtension(String filename) { int dot = filename.lastIndexOf(FILE_EXTENSION_SEPARATOR); return (dot>=0)?filename.substring(dot + 1):null; @@ -177,7 +177,7 @@ public class Tools { public static String getStackTrace(Throwable e){ return getStackTrace(e, -1); } - + public static String implode(String strings[], String sep) { String implodedString; if (strings.length == 0) { @@ -195,7 +195,7 @@ public class Tools { } return implodedString; } - + @@ -262,7 +262,7 @@ public class Tools { /** * Return a set of properties from a properties file. - * + * * @param clientPropertiesFilename * @return */ @@ -288,32 +288,32 @@ public class Tools { return inProperties; } - + static public Properties loadProperties(String clientPropertiesFilename, boolean filterPasswords) throws Exception { Properties result = loadProperties(clientPropertiesFilename); if (filterPasswords) { result = filterPropertiesWithEnvVars(result); } - + return result; } - + /** * Looks for property values if the form ${foo} and tries to find environment property "foo" value to replace with. - * + * * For example, a property value of "${foo}" would be replaced with the value of the environment variable "foo" if a * value for "foo" exists in the current environment. - * + * * @param inProperties * @return * @throws Exception */ static public Properties filterPropertiesWithEnvVars(Properties inProperties) throws Exception { final String filteredFlag = "fe915b1b-7411-4aaa-887f"; - final String filteredKey = filteredFlag; + final String filteredKey = filteredFlag; Properties result = inProperties; - + if (inProperties.containsKey(filteredKey) == false) { // Only process the properties once if (inProperties != null && inProperties.size() > 0) { @@ -327,28 +327,28 @@ public class Tools { inProperties.setProperty(filteredKey, filteredFlag); // set to indicated we've already process these properties } } - + return result; } - + static public boolean isOptional(String properyValue) { boolean result = false; - + result = properyValue.endsWith(OPTIONAL_VALUE_SUFFIX); - + return result; } - + /** * Try to find the value of a property variable in the system or JVM environment. This code substitutes only property values formed * like ${cspace.password.mysecret} or ${cspace_password_mysecret_secret}. The corresponding environment variables would * be "cspace.password.mysecret" and "cspace.password.mysecret.secret". - * + * * Returns null if the passed in property value is not a property variable -i.e., not something of the form {$cspace.password.foo} - * + * * Throws an exception if the passed in property value has a valid variable form but the corresponding environment variable is not * set. - * + * * @param propertyValue * @return * @throws Exception @@ -361,7 +361,7 @@ public class Tools { // Pattern pattern = Pattern.compile(PROPERTY_VAR_REGEX); // For example, "${cspace.password.mysecret}" or "${password_strong_longpassword}" Matcher matcher = pattern.matcher(propertyValue); - String key = null; + String key = null; if (matcher.find()) { key = matcher.group(1); // Gets the string inside the ${} enclosure. For example, gets "cspace.password.mysecret" from "${cspace.password.mysecret}" result = System.getenv(key); @@ -371,7 +371,7 @@ public class Tools { } if (result == null || result.isEmpty()) { - String errMsg = String.format("Could find neither an environment variable nor a systen variable named '%s'", key); + String errMsg = String.format("Could find neither an environment variable nor a system variable named '%s'", key); if (isOptional(key) == true) { System.err.println(errMsg); } else { @@ -379,10 +379,10 @@ public class Tools { } } } - + return result; } - + /** * Test to see if 'propertyValue' is actually a property variable * @param propertyValue @@ -390,7 +390,7 @@ public class Tools { */ static public boolean isValuePropretyVar(String propertyValue) { boolean result = false; - + if (propertyValue != null) { Pattern pattern = Pattern.compile(PROPERTY_VAR_REGEX); // For example, "${cspace.password.mysecret}" or "${password_strong_longpassword}" Matcher matcher = pattern.matcher(propertyValue); @@ -398,7 +398,7 @@ public class Tools { result = true; } } - + return result; } @@ -409,16 +409,16 @@ public class Tools { return true; } } - + static public boolean listContainsIgnoreCase(List theList, String searchStr) { boolean result = false; - + for (String listItem : theList) { if (StringUtils.containsIgnoreCase(listItem, searchStr)) { return true; } } - + return result; } } diff --git a/services/common/build.xml b/services/common/build.xml index 875ed7c1c..bca1f527f 100644 --- a/services/common/build.xml +++ b/services/common/build.xml @@ -164,6 +164,7 @@ + - + + + +
diff --git a/services/common/lib/spring/commons-codec-1.10.jar b/services/common/lib/spring/commons-codec-1.10.jar new file mode 100644 index 000000000..1d7417c40 Binary files /dev/null and b/services/common/lib/spring/commons-codec-1.10.jar differ diff --git a/services/common/lib/spring/cryptacular-1.1.4.jar b/services/common/lib/spring/cryptacular-1.1.4.jar new file mode 100644 index 000000000..02b895413 Binary files /dev/null and b/services/common/lib/spring/cryptacular-1.1.4.jar differ diff --git a/services/common/lib/spring/guava-20.0.jar b/services/common/lib/spring/guava-20.0.jar new file mode 100644 index 000000000..632772f3a Binary files /dev/null and b/services/common/lib/spring/guava-20.0.jar differ diff --git a/services/common/lib/spring/httpclient-4.5.13.jar b/services/common/lib/spring/httpclient-4.5.13.jar new file mode 100644 index 000000000..218ee25f2 Binary files /dev/null and b/services/common/lib/spring/httpclient-4.5.13.jar differ diff --git a/services/common/lib/spring/httpcore-4.4.13.jar b/services/common/lib/spring/httpcore-4.4.13.jar new file mode 100644 index 000000000..163dc438c Binary files /dev/null and b/services/common/lib/spring/httpcore-4.4.13.jar differ diff --git a/services/common/lib/spring/jackson-annotations-2.14.3.jar b/services/common/lib/spring/jackson-annotations-2.14.3.jar new file mode 100644 index 000000000..f10f7802d Binary files /dev/null and b/services/common/lib/spring/jackson-annotations-2.14.3.jar differ diff --git a/services/common/lib/spring/jackson-annotations-2.8.0.jar b/services/common/lib/spring/jackson-annotations-2.8.0.jar deleted file mode 100644 index d19b67b0f..000000000 Binary files a/services/common/lib/spring/jackson-annotations-2.8.0.jar and /dev/null differ diff --git a/services/common/lib/spring/jackson-core-2.14.3.jar b/services/common/lib/spring/jackson-core-2.14.3.jar new file mode 100644 index 000000000..b1fb3f270 Binary files /dev/null and b/services/common/lib/spring/jackson-core-2.14.3.jar differ diff --git a/services/common/lib/spring/jackson-core-2.8.0.jar b/services/common/lib/spring/jackson-core-2.8.0.jar deleted file mode 100644 index a078720cd..000000000 Binary files a/services/common/lib/spring/jackson-core-2.8.0.jar and /dev/null differ diff --git a/services/common/lib/spring/jackson-databind-2.14.3.jar b/services/common/lib/spring/jackson-databind-2.14.3.jar new file mode 100644 index 000000000..a4791e503 Binary files /dev/null and b/services/common/lib/spring/jackson-databind-2.14.3.jar differ diff --git a/services/common/lib/spring/jackson-databind-2.8.0.jar b/services/common/lib/spring/jackson-databind-2.8.0.jar deleted file mode 100644 index 3565ff515..000000000 Binary files a/services/common/lib/spring/jackson-databind-2.8.0.jar and /dev/null differ diff --git a/services/common/lib/spring/jackson-datatype-jsr310-2.14.3.jar b/services/common/lib/spring/jackson-datatype-jsr310-2.14.3.jar new file mode 100644 index 000000000..38825795e Binary files /dev/null and b/services/common/lib/spring/jackson-datatype-jsr310-2.14.3.jar differ diff --git a/services/common/lib/spring/java-support-7.5.2.jar b/services/common/lib/spring/java-support-7.5.2.jar new file mode 100644 index 000000000..c9021f6a3 Binary files /dev/null and b/services/common/lib/spring/java-support-7.5.2.jar differ diff --git a/services/common/lib/spring/joda-time-2.9.jar b/services/common/lib/spring/joda-time-2.9.jar new file mode 100644 index 000000000..340af06ad Binary files /dev/null and b/services/common/lib/spring/joda-time-2.9.jar differ diff --git a/services/common/lib/spring/metrics-core-3.1.5.jar b/services/common/lib/spring/metrics-core-3.1.5.jar new file mode 100644 index 000000000..5e6aed8dd Binary files /dev/null and b/services/common/lib/spring/metrics-core-3.1.5.jar differ diff --git a/services/common/lib/spring/nimbus-jose-jwt-9.24.4.jar b/services/common/lib/spring/nimbus-jose-jwt-9.24.4.jar new file mode 100644 index 000000000..df56a4ce8 Binary files /dev/null and b/services/common/lib/spring/nimbus-jose-jwt-9.24.4.jar differ diff --git a/services/common/lib/spring/opensaml-core-3.4.6.jar b/services/common/lib/spring/opensaml-core-3.4.6.jar new file mode 100644 index 000000000..e0cb404fe Binary files /dev/null and b/services/common/lib/spring/opensaml-core-3.4.6.jar differ diff --git a/services/common/lib/spring/opensaml-messaging-api-3.4.6.jar b/services/common/lib/spring/opensaml-messaging-api-3.4.6.jar new file mode 100644 index 000000000..92be1d8f1 Binary files /dev/null and b/services/common/lib/spring/opensaml-messaging-api-3.4.6.jar differ diff --git a/services/common/lib/spring/opensaml-profile-api-3.4.6.jar b/services/common/lib/spring/opensaml-profile-api-3.4.6.jar new file mode 100644 index 000000000..a9899fb92 Binary files /dev/null and b/services/common/lib/spring/opensaml-profile-api-3.4.6.jar differ diff --git a/services/common/lib/spring/opensaml-saml-api-3.4.6.jar b/services/common/lib/spring/opensaml-saml-api-3.4.6.jar new file mode 100644 index 000000000..e4492fb76 Binary files /dev/null and b/services/common/lib/spring/opensaml-saml-api-3.4.6.jar differ diff --git a/services/common/lib/spring/opensaml-saml-impl-3.4.6.jar b/services/common/lib/spring/opensaml-saml-impl-3.4.6.jar new file mode 100644 index 000000000..0356dfa00 Binary files /dev/null and b/services/common/lib/spring/opensaml-saml-impl-3.4.6.jar differ diff --git a/services/common/lib/spring/opensaml-security-api-3.4.6.jar b/services/common/lib/spring/opensaml-security-api-3.4.6.jar new file mode 100644 index 000000000..4c7b328db Binary files /dev/null and b/services/common/lib/spring/opensaml-security-api-3.4.6.jar differ diff --git a/services/common/lib/spring/opensaml-security-impl-3.4.6.jar b/services/common/lib/spring/opensaml-security-impl-3.4.6.jar new file mode 100644 index 000000000..6a6f8c17c Binary files /dev/null and b/services/common/lib/spring/opensaml-security-impl-3.4.6.jar differ diff --git a/services/common/lib/spring/opensaml-soap-api-3.4.6.jar b/services/common/lib/spring/opensaml-soap-api-3.4.6.jar new file mode 100644 index 000000000..45f65e35b Binary files /dev/null and b/services/common/lib/spring/opensaml-soap-api-3.4.6.jar differ diff --git a/services/common/lib/spring/opensaml-storage-api-3.4.6.jar b/services/common/lib/spring/opensaml-storage-api-3.4.6.jar new file mode 100644 index 000000000..c383fa886 Binary files /dev/null and b/services/common/lib/spring/opensaml-storage-api-3.4.6.jar differ diff --git a/services/common/lib/spring/opensaml-xmlsec-api-3.4.6.jar b/services/common/lib/spring/opensaml-xmlsec-api-3.4.6.jar new file mode 100644 index 000000000..023a3af6f Binary files /dev/null and b/services/common/lib/spring/opensaml-xmlsec-api-3.4.6.jar differ diff --git a/services/common/lib/spring/opensaml-xmlsec-impl-3.4.6.jar b/services/common/lib/spring/opensaml-xmlsec-impl-3.4.6.jar new file mode 100644 index 000000000..577b2c983 Binary files /dev/null and b/services/common/lib/spring/opensaml-xmlsec-impl-3.4.6.jar differ diff --git a/services/common/lib/spring/spring-aop-4.3.16.RELEASE.jar b/services/common/lib/spring/spring-aop-4.3.16.RELEASE.jar deleted file mode 100644 index e054cf688..000000000 Binary files a/services/common/lib/spring/spring-aop-4.3.16.RELEASE.jar and /dev/null differ diff --git a/services/common/lib/spring/spring-aop-5.3.28.jar b/services/common/lib/spring/spring-aop-5.3.28.jar new file mode 100644 index 000000000..50f033eb9 Binary files /dev/null and b/services/common/lib/spring/spring-aop-5.3.28.jar differ diff --git a/services/common/lib/spring/spring-beans-4.3.16.RELEASE.jar b/services/common/lib/spring/spring-beans-4.3.16.RELEASE.jar deleted file mode 100644 index f52417f29..000000000 Binary files a/services/common/lib/spring/spring-beans-4.3.16.RELEASE.jar and /dev/null differ diff --git a/services/common/lib/spring/spring-beans-5.3.28.jar b/services/common/lib/spring/spring-beans-5.3.28.jar new file mode 100644 index 000000000..354d40030 Binary files /dev/null and b/services/common/lib/spring/spring-beans-5.3.28.jar differ diff --git a/services/common/lib/spring/spring-context-4.3.16.RELEASE.jar b/services/common/lib/spring/spring-context-4.3.16.RELEASE.jar deleted file mode 100644 index f30358342..000000000 Binary files a/services/common/lib/spring/spring-context-4.3.16.RELEASE.jar and /dev/null differ diff --git a/services/common/lib/spring/spring-context-5.3.28.jar b/services/common/lib/spring/spring-context-5.3.28.jar new file mode 100644 index 000000000..6b5dd6924 Binary files /dev/null and b/services/common/lib/spring/spring-context-5.3.28.jar differ diff --git a/services/common/lib/spring/spring-context-support-4.3.16.RELEASE.jar b/services/common/lib/spring/spring-context-support-4.3.16.RELEASE.jar deleted file mode 100644 index 3818a4d77..000000000 Binary files a/services/common/lib/spring/spring-context-support-4.3.16.RELEASE.jar and /dev/null differ diff --git a/services/common/lib/spring/spring-context-support-5.3.28.jar b/services/common/lib/spring/spring-context-support-5.3.28.jar new file mode 100644 index 000000000..3b136aac8 Binary files /dev/null and b/services/common/lib/spring/spring-context-support-5.3.28.jar differ diff --git a/services/common/lib/spring/spring-core-4.3.16.RELEASE.jar b/services/common/lib/spring/spring-core-4.3.16.RELEASE.jar deleted file mode 100644 index 883ce390c..000000000 Binary files a/services/common/lib/spring/spring-core-4.3.16.RELEASE.jar and /dev/null differ diff --git a/services/common/lib/spring/spring-core-5.3.28.jar b/services/common/lib/spring/spring-core-5.3.28.jar new file mode 100644 index 000000000..f6a5a711b Binary files /dev/null and b/services/common/lib/spring/spring-core-5.3.28.jar differ diff --git a/services/common/lib/spring/spring-expression-4.3.16.RELEASE.jar b/services/common/lib/spring/spring-expression-4.3.16.RELEASE.jar deleted file mode 100644 index 1d6abd1b2..000000000 Binary files a/services/common/lib/spring/spring-expression-4.3.16.RELEASE.jar and /dev/null differ diff --git a/services/common/lib/spring/spring-expression-5.3.28.jar b/services/common/lib/spring/spring-expression-5.3.28.jar new file mode 100644 index 000000000..038f18e29 Binary files /dev/null and b/services/common/lib/spring/spring-expression-5.3.28.jar differ diff --git a/services/common/lib/spring/spring-instrument-4.3.1.RELEASE.jar b/services/common/lib/spring/spring-instrument-4.3.1.RELEASE.jar deleted file mode 100644 index 73d9d578d..000000000 Binary files a/services/common/lib/spring/spring-instrument-4.3.1.RELEASE.jar and /dev/null differ diff --git a/services/common/lib/spring/spring-jdbc-4.3.16.RELEASE.jar b/services/common/lib/spring/spring-jdbc-4.3.16.RELEASE.jar deleted file mode 100644 index a3876b5ac..000000000 Binary files a/services/common/lib/spring/spring-jdbc-4.3.16.RELEASE.jar and /dev/null differ diff --git a/services/common/lib/spring/spring-jdbc-5.3.28.jar b/services/common/lib/spring/spring-jdbc-5.3.28.jar new file mode 100644 index 000000000..f61fdefc1 Binary files /dev/null and b/services/common/lib/spring/spring-jdbc-5.3.28.jar differ diff --git a/services/common/lib/spring/spring-security-acl-4.2.5.RELEASE.jar b/services/common/lib/spring/spring-security-acl-4.2.5.RELEASE.jar deleted file mode 100644 index c8fbdd80c..000000000 Binary files a/services/common/lib/spring/spring-security-acl-4.2.5.RELEASE.jar and /dev/null differ diff --git a/services/common/lib/spring/spring-security-acl-5.8.4.jar b/services/common/lib/spring/spring-security-acl-5.8.4.jar new file mode 100644 index 000000000..45caeb3f0 Binary files /dev/null and b/services/common/lib/spring/spring-security-acl-5.8.4.jar differ diff --git a/services/common/lib/spring/spring-security-config-4.2.5.RELEASE.jar b/services/common/lib/spring/spring-security-config-4.2.5.RELEASE.jar deleted file mode 100644 index c92232de4..000000000 Binary files a/services/common/lib/spring/spring-security-config-4.2.5.RELEASE.jar and /dev/null differ diff --git a/services/common/lib/spring/spring-security-config-5.8.4.jar b/services/common/lib/spring/spring-security-config-5.8.4.jar new file mode 100644 index 000000000..618feb050 Binary files /dev/null and b/services/common/lib/spring/spring-security-config-5.8.4.jar differ diff --git a/services/common/lib/spring/spring-security-core-4.2.5.RELEASE.jar b/services/common/lib/spring/spring-security-core-4.2.5.RELEASE.jar deleted file mode 100644 index 9fb9fe4c9..000000000 Binary files a/services/common/lib/spring/spring-security-core-4.2.5.RELEASE.jar and /dev/null differ diff --git a/services/common/lib/spring/spring-security-core-5.8.4.jar b/services/common/lib/spring/spring-security-core-5.8.4.jar new file mode 100644 index 000000000..efb41540c Binary files /dev/null and b/services/common/lib/spring/spring-security-core-5.8.4.jar differ diff --git a/services/common/lib/spring/spring-security-crypto-5.8.4.jar b/services/common/lib/spring/spring-security-crypto-5.8.4.jar new file mode 100644 index 000000000..3e77b937f Binary files /dev/null and b/services/common/lib/spring/spring-security-crypto-5.8.4.jar differ diff --git a/services/common/lib/spring/spring-security-jwt-1.0.4.RELEASE.jar b/services/common/lib/spring/spring-security-jwt-1.0.4.RELEASE.jar deleted file mode 100644 index ac8dec15a..000000000 Binary files a/services/common/lib/spring/spring-security-jwt-1.0.4.RELEASE.jar and /dev/null differ diff --git a/services/common/lib/spring/spring-security-oauth2-2.0.10.RELEASE.jar b/services/common/lib/spring/spring-security-oauth2-2.0.10.RELEASE.jar deleted file mode 100644 index 354b7688c..000000000 Binary files a/services/common/lib/spring/spring-security-oauth2-2.0.10.RELEASE.jar and /dev/null differ diff --git a/services/common/lib/spring/spring-security-oauth2-authorization-server-0.4.3.jar b/services/common/lib/spring/spring-security-oauth2-authorization-server-0.4.3.jar new file mode 100644 index 000000000..e036a7e3d Binary files /dev/null and b/services/common/lib/spring/spring-security-oauth2-authorization-server-0.4.3.jar differ diff --git a/services/common/lib/spring/spring-security-oauth2-core-5.8.4.jar b/services/common/lib/spring/spring-security-oauth2-core-5.8.4.jar new file mode 100644 index 000000000..e29154b95 Binary files /dev/null and b/services/common/lib/spring/spring-security-oauth2-core-5.8.4.jar differ diff --git a/services/common/lib/spring/spring-security-oauth2-jose-5.8.4.jar b/services/common/lib/spring/spring-security-oauth2-jose-5.8.4.jar new file mode 100644 index 000000000..38fb705c7 Binary files /dev/null and b/services/common/lib/spring/spring-security-oauth2-jose-5.8.4.jar differ diff --git a/services/common/lib/spring/spring-security-oauth2-resource-server-5.8.4.jar b/services/common/lib/spring/spring-security-oauth2-resource-server-5.8.4.jar new file mode 100644 index 000000000..9907ad2b7 Binary files /dev/null and b/services/common/lib/spring/spring-security-oauth2-resource-server-5.8.4.jar differ diff --git a/services/common/lib/spring/spring-security-saml2-service-provider-5.8.4.jar b/services/common/lib/spring/spring-security-saml2-service-provider-5.8.4.jar new file mode 100644 index 000000000..2bbf75ccd Binary files /dev/null and b/services/common/lib/spring/spring-security-saml2-service-provider-5.8.4.jar differ diff --git a/services/common/lib/spring/spring-security-web-4.2.5.RELEASE.jar b/services/common/lib/spring/spring-security-web-4.2.5.RELEASE.jar deleted file mode 100644 index 7af90e403..000000000 Binary files a/services/common/lib/spring/spring-security-web-4.2.5.RELEASE.jar and /dev/null differ diff --git a/services/common/lib/spring/spring-security-web-5.8.4.jar b/services/common/lib/spring/spring-security-web-5.8.4.jar new file mode 100644 index 000000000..19bad1ce9 Binary files /dev/null and b/services/common/lib/spring/spring-security-web-5.8.4.jar differ diff --git a/services/common/lib/spring/spring-tx-4.3.16.RELEASE.jar b/services/common/lib/spring/spring-tx-4.3.16.RELEASE.jar deleted file mode 100644 index 379a761c6..000000000 Binary files a/services/common/lib/spring/spring-tx-4.3.16.RELEASE.jar and /dev/null differ diff --git a/services/common/lib/spring/spring-tx-5.3.28.jar b/services/common/lib/spring/spring-tx-5.3.28.jar new file mode 100644 index 000000000..7b4d6db93 Binary files /dev/null and b/services/common/lib/spring/spring-tx-5.3.28.jar differ diff --git a/services/common/lib/spring/spring-web-4.3.16.RELEASE.jar b/services/common/lib/spring/spring-web-4.3.16.RELEASE.jar deleted file mode 100644 index 1f5e46825..000000000 Binary files a/services/common/lib/spring/spring-web-4.3.16.RELEASE.jar and /dev/null differ diff --git a/services/common/lib/spring/spring-web-5.3.28.jar b/services/common/lib/spring/spring-web-5.3.28.jar new file mode 100644 index 000000000..6cec39eba Binary files /dev/null and b/services/common/lib/spring/spring-web-5.3.28.jar differ diff --git a/services/common/lib/spring/spring-webmvc-4.3.16.RELEASE.jar b/services/common/lib/spring/spring-webmvc-4.3.16.RELEASE.jar deleted file mode 100644 index bfc0bcfcb..000000000 Binary files a/services/common/lib/spring/spring-webmvc-4.3.16.RELEASE.jar and /dev/null differ diff --git a/services/common/lib/spring/xmlsec-2.0.10.jar b/services/common/lib/spring/xmlsec-2.0.10.jar new file mode 100644 index 000000000..c5eb7e5b1 Binary files /dev/null and b/services/common/lib/spring/xmlsec-2.0.10.jar differ diff --git a/services/common/pom.xml b/services/common/pom.xml index 93660cc75..e0ac45539 100644 --- a/services/common/pom.xml +++ b/services/common/pom.xml @@ -39,6 +39,16 @@ org.collectionspace.services.systeminfo.client ${project.version} + + org.collectionspace.services + org.collectionspace.services.login.client + ${project.version} + + + org.collectionspace.services + org.collectionspace.services.logout.client + ${project.version} + org.collectionspace.services org.collectionspace.services.account.client @@ -348,11 +358,22 @@ spring-aop ${spring.version} + + org.springframework.security + spring-security-oauth2-authorization-server + ${spring.security.authorization.server.version} + provided + + + org.springframework.security + spring-security-config + ${spring.security.version} + provided + com.fasterxml.jackson.core jackson-core - 2.8.0 org.mybatis @@ -372,6 +393,11 @@ 2.2.1 test + + org.freemarker + freemarker + 2.3.32 + diff --git a/services/common/src/main/cspace/config/services/service-config-security.xml b/services/common/src/main/cspace/config/services/service-config-security.xml new file mode 100644 index 000000000..34ef8c46a --- /dev/null +++ b/services/common/src/main/cspace/config/services/service-config-security.xml @@ -0,0 +1,65 @@ + + + + + + P1D + + + + PT1H + + + + cspace-ui + CollectionSpace UI + + none + + authorization_code + + cspace.full + + + + false + + + PT12H + + + + + + diff --git a/services/common/src/main/cspace/config/services/service-config.xml b/services/common/src/main/cspace/config/services/service-config.xml index 1561b7548..67e7ef839 100644 --- a/services/common/src/main/cspace/config/services/service-config.xml +++ b/services/common/src/main/cspace/config/services/service-config.xml @@ -19,7 +19,7 @@ @DB_NUXEO_NAME@ @DB_CSPACE_NAME@ true - + diff --git a/services/common/src/main/cspace/config/services/tenants/bonsai/bonsai-tenant-bindings.delta.xml b/services/common/src/main/cspace/config/services/tenants/bonsai/bonsai-tenant-bindings.delta.xml index 3802fb6e3..8ea12ea32 100644 --- a/services/common/src/main/cspace/config/services/tenants/bonsai/bonsai-tenant-bindings.delta.xml +++ b/services/common/src/main/cspace/config/services/tenants/bonsai/bonsai-tenant-bindings.delta.xml @@ -11,4 +11,3 @@ - diff --git a/services/common/src/main/cspace/config/services/tenants/botgarden/botgarden-tenant-bindings.delta.xml b/services/common/src/main/cspace/config/services/tenants/botgarden/botgarden-tenant-bindings.delta.xml index ac379ed4b..06028f4ed 100644 --- a/services/common/src/main/cspace/config/services/tenants/botgarden/botgarden-tenant-bindings.delta.xml +++ b/services/common/src/main/cspace/config/services/tenants/botgarden/botgarden-tenant-bindings.delta.xml @@ -24,11 +24,11 @@ 425 - + org.collectionspace.services.collectionobject.nuxeo.BotGardenCollectionObjectValidatorHandler - + @@ -43,6 +43,6 @@ - + diff --git a/services/common/src/main/cspace/config/services/tenants/core/core-tenant-bindings.delta.xml b/services/common/src/main/cspace/config/services/tenants/core/core-tenant-bindings.delta.xml index 585d1cd05..129faa0e4 100644 --- a/services/common/src/main/cspace/config/services/tenants/core/core-tenant-bindings.delta.xml +++ b/services/common/src/main/cspace/config/services/tenants/core/core-tenant-bindings.delta.xml @@ -2,7 +2,7 @@ - + diff --git a/services/common/src/main/cspace/config/services/tenants/dvp/dvp-tenant-bindings.delta.xml b/services/common/src/main/cspace/config/services/tenants/dvp/dvp-tenant-bindings.delta.xml deleted file mode 100644 index 0ef38c1af..000000000 --- a/services/common/src/main/cspace/config/services/tenants/dvp/dvp-tenant-bindings.delta.xml +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - - - - - - diff --git a/services/common/src/main/cspace/config/services/tenants/lhmc/lhmc-tenant-bindings.delta.xml b/services/common/src/main/cspace/config/services/tenants/lhmc/lhmc-tenant-bindings.delta.xml index 1a946bfe7..af1af828c 100644 --- a/services/common/src/main/cspace/config/services/tenants/lhmc/lhmc-tenant-bindings.delta.xml +++ b/services/common/src/main/cspace/config/services/tenants/lhmc/lhmc-tenant-bindings.delta.xml @@ -2,7 +2,7 @@ - + diff --git a/services/common/src/main/cspace/config/services/tenants/materials/materials-tenant-bindings.delta.xml b/services/common/src/main/cspace/config/services/tenants/materials/materials-tenant-bindings.delta.xml index 67acbf24d..624494ad5 100644 --- a/services/common/src/main/cspace/config/services/tenants/materials/materials-tenant-bindings.delta.xml +++ b/services/common/src/main/cspace/config/services/tenants/materials/materials-tenant-bindings.delta.xml @@ -8,7 +8,7 @@ - + org.collectionspace.services.nuxeo.elasticsearch.materials.MaterialsESDocumentWriter diff --git a/services/common/src/main/cspace/config/services/tenants/tenant-bindings-proto-unified.xml b/services/common/src/main/cspace/config/services/tenants/tenant-bindings-proto-unified.xml index 7052bdee3..a3ce605f4 100644 --- a/services/common/src/main/cspace/config/services/tenants/tenant-bindings-proto-unified.xml +++ b/services/common/src/main/cspace/config/services/tenants/tenant-bindings-proto-unified.xml @@ -2,6 +2,12 @@ + + authorize + authorized + logout?success + + org.collectionspace.services.listener.UpdateObjectLocationOnMove diff --git a/services/common/src/main/cspace/config/services/tenants/testsci/testsci-tenant-bindings.delta.xml b/services/common/src/main/cspace/config/services/tenants/testsci/testsci-tenant-bindings.delta.xml index 9228a7c65..b6b9c8d94 100644 --- a/services/common/src/main/cspace/config/services/tenants/testsci/testsci-tenant-bindings.delta.xml +++ b/services/common/src/main/cspace/config/services/tenants/testsci/testsci-tenant-bindings.delta.xml @@ -1,30 +1,30 @@ + xmlns:merge="http://xmlmerge.el4j.elca.ch" + xmlns:tenant="http://collectionspace.org/services/config/tenant"> - - - + + + - + - - - - testsci-key0 - value0 - - - testsci-key1 - value1 - - - testsci-key2 - value2 - - - - - + + + + testsci-key0 + value0 + + + testsci-key1 + value1 + + + testsci-key2 + value2 + + + + + diff --git a/services/common/src/main/java/org/collectionspace/services/common/ServiceMain.java b/services/common/src/main/java/org/collectionspace/services/common/ServiceMain.java index d11f79681..0ce3d507c 100644 --- a/services/common/src/main/java/org/collectionspace/services/common/ServiceMain.java +++ b/services/common/src/main/java/org/collectionspace/services/common/ServiceMain.java @@ -7,6 +7,7 @@ import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.FileReader; +import java.io.IOException; import java.io.InputStream; import java.math.BigInteger; import java.nio.file.Path; @@ -66,6 +67,9 @@ import org.dom4j.tree.DefaultElement; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import freemarker.template.Configuration; +import freemarker.template.TemplateExceptionHandler; + /** * Main class for Services layer. It reads configuration and performs service * level initialization. It is a singleton. @@ -88,29 +92,30 @@ public class ServiceMain { ServiceMain.logger.info(str); } - /** - * volatile is used here to assume about ordering (post JDK 1.5) - */ - private static volatile ServiceMain instance = null; - private static volatile boolean initFailed = false; + /** + * volatile is used here to assume about ordering (post JDK 1.5) + */ + private static volatile ServiceMain instance = null; + private static volatile boolean initFailed = false; - private static final String SERVER_HOME_PROPERTY = "catalina.base"; - private static final boolean USE_APP_GENERATED_CONFIG = true; + private static final String SERVER_HOME_PROPERTY = "catalina.base"; + private static final boolean USE_APP_GENERATED_CONFIG = true; - private static ServletContext servletContext = null; + private static ServletContext servletContext = null; - private NuxeoConnectorEmbedded nuxeoConnector; - private String serverRootDir = null; - private ServicesConfigReaderImpl servicesConfigReader; - private TenantBindingConfigReaderImpl tenantBindingConfigReader; - private UriTemplateRegistry uriTemplateRegistry = new UriTemplateRegistry(); + private NuxeoConnectorEmbedded nuxeoConnector; + private String serverRootDir = null; + private ServicesConfigReaderImpl servicesConfigReader; + private TenantBindingConfigReaderImpl tenantBindingConfigReader; + private UriTemplateRegistry uriTemplateRegistry = new UriTemplateRegistry(); + private Configuration freeMarkerConfig = null; - private static final String DROP_DATABASE_SQL_CMD = "DROP DATABASE"; - private static final String DROP_DATABASE_IF_EXISTS_SQL_CMD = DROP_DATABASE_SQL_CMD + " IF EXISTS %s;"; - private static final String DROP_USER_SQL_CMD = "DROP USER"; - private static final String DROP_USER_IF_EXISTS_SQL_CMD = DROP_USER_SQL_CMD + " IF EXISTS %s;"; - private static final String DROP_OBJECTS_SQL_COMMENT = "-- drop all the objects before dropping roles"; - private static final String CSPACE_JEESERVER_HOME = "CSPACE_JEESERVER_HOME"; + private static final String DROP_DATABASE_SQL_CMD = "DROP DATABASE"; + private static final String DROP_DATABASE_IF_EXISTS_SQL_CMD = DROP_DATABASE_SQL_CMD + " IF EXISTS %s;"; + private static final String DROP_USER_SQL_CMD = "DROP USER"; + private static final String DROP_USER_IF_EXISTS_SQL_CMD = DROP_USER_SQL_CMD + " IF EXISTS %s;"; + private static final String DROP_OBJECTS_SQL_COMMENT = "-- drop all the objects before dropping roles"; + private static final String CSPACE_JEESERVER_HOME = "CSPACE_JEESERVER_HOME"; private ServiceMain() { // Intentionally blank @@ -229,6 +234,7 @@ public class ServiceMain { // // initializeEventListeners(); + initializeFreeMarker(); // // Mark if a tenant's bindings have changed since the last time we started, by comparing the MD5 hash of each tenant's bindings with that of @@ -298,6 +304,18 @@ public class ServiceMain { return result; } + private void initializeFreeMarker() throws IOException { + Configuration config = new Configuration(Configuration.VERSION_2_3_32); + TenantBindingConfigReaderImpl tenantBindingConfigReader = this.getTenantBindingConfigReader(); + String templateDir = tenantBindingConfigReader.getResourcesDir() + File.separator + "templates"; + + config.setDirectoryForTemplateLoading(new File(templateDir)); + config.setDefaultEncoding("UTF-8"); + config.setTemplateExceptionHandler(TemplateExceptionHandler.RETHROW_HANDLER); + + this.freeMarkerConfig = config; + } + /** * Initialize the event listeners. We're essentially registering listeners with tenants. This ensures that listeners ignore events * caused by other tenants. @@ -897,6 +915,10 @@ public class ServiceMain { return result; } + public Configuration getFreeMarkerConfig() { + return this.freeMarkerConfig; + } + /* * Look through the tenant bindings and create the required Nuxeo databases -each tenant can declare * their own Nuxeo repository/database. diff --git a/services/common/src/main/java/org/collectionspace/services/common/authorization_mgt/AuthorizationCommon.java b/services/common/src/main/java/org/collectionspace/services/common/authorization_mgt/AuthorizationCommon.java index ed28f4766..2dd9ce10b 100644 --- a/services/common/src/main/java/org/collectionspace/services/common/authorization_mgt/AuthorizationCommon.java +++ b/services/common/src/main/java/org/collectionspace/services/common/authorization_mgt/AuthorizationCommon.java @@ -15,6 +15,7 @@ import java.util.List; import java.util.Map; import java.util.UUID; import javax.naming.NamingException; +import javax.ws.rs.core.UriBuilder; import org.collectionspace.authentication.AuthN; import org.collectionspace.services.account.AccountListItem; @@ -35,6 +36,7 @@ import org.collectionspace.services.authorization.perms.ActionType; import org.collectionspace.services.authorization.perms.EffectType; import org.collectionspace.services.authorization.perms.Permission; import org.collectionspace.services.authorization.perms.PermissionAction; +import org.collectionspace.services.client.AccountClient; import org.collectionspace.services.client.PermissionClient; import org.collectionspace.services.client.Profiler; import org.collectionspace.services.client.RoleClient; @@ -66,15 +68,15 @@ import org.slf4j.LoggerFactory; public class AuthorizationCommon { - + final public static String REFRESH_AUTHZ_PROP = "refreshAuthZOnStartup"; - + // // For token generation and password reset // final private static String DEFAULT_PASSWORD_RESET_EMAIL_MESSAGE = "Hello {{greeting}},\n\r\n\rYou've started the process to reset your CollectionSpace account password. To finish resetting your password, go to the Reset Password page {{link}} on CollectionSpace.\n\r\n\rIf clicking the link doesn't work, copy and paste the following link into your browser address bar and click Go.\n\r\n\r{{link}}\n\r Thanks,\n\r\n\r CollectionSpace Administrator\n\r\n\rPlease do not reply to this email. This mailbox is not monitored and you will not receive a response. For assistance, contact your CollectionSpace Administrator directly."; private static final String DEFAULT_PASSWORD_RESET_EMAIL_SUBJECT = "Password reset for CollectionSpace account"; - + // // Keep track of the MD5 hash value for the tenant bindings // @@ -83,7 +85,7 @@ public class AuthorizationCommon { // // ActionGroup labels/constants // - + // for READ-WRITE-DELETE final public static String ACTIONGROUP_CRUDL_NAME = "CRUDL"; final public static ActionType[] ACTIONSET_CRUDL = {ActionType.CREATE, ActionType.READ, ActionType.UPDATE, ActionType.DELETE, ActionType.SEARCH}; @@ -93,11 +95,11 @@ public class AuthorizationCommon { // for READ-ONLY final public static String ACTIONGROUP_RL_NAME = "RL"; final public static ActionType[] ACTIONSET_RL = {ActionType.READ, ActionType.SEARCH}; - + static ActionGroup ACTIONGROUP_CRUDL; static ActionGroup ACTIONGROUP_CRUL; static ActionGroup ACTIONGROUP_RL; - + // A static block to initialize the predefined action groups static { // For admin @@ -113,44 +115,44 @@ public class AuthorizationCommon { ACTIONGROUP_CRUL.name = ACTIONGROUP_CRUL_NAME; ACTIONGROUP_CRUL.actions = ACTIONSET_CRUL; } - + final static Logger logger = LoggerFactory.getLogger(AuthorizationCommon.class); final public static String ROLE_TENANT_ADMINISTRATOR = "TENANT_ADMINISTRATOR"; final public static String ROLE_TENANT_READER = "TENANT_READER"; - - public static final String TENANT_MANAGER_USER = "tenantManager"; - public static final String TENANT_MANAGER_SCREEN_NAME = TENANT_MANAGER_USER; - public static final String DEFAULT_TENANT_MANAGER_PASSWORD = "manage"; - public static final String DEFAULT_TENANT_MANAGER_EMAIL = "tenantManager@collectionspace.org"; - - public static final String TENANT_ADMIN_ACCT_PREFIX = "admin@"; - public static final String TENANT_READER_ACCT_PREFIX = "reader@"; - public static final String ROLE_PREFIX = "ROLE_"; - public static final String TENANT_ADMIN_ROLE_SUFFIX = "_TENANT_ADMINISTRATOR"; - public static final String TENANT_READER_ROLE_SUFFIX = "_TENANT_READER"; + + public static final String TENANT_MANAGER_USER = "tenantManager"; + public static final String TENANT_MANAGER_SCREEN_NAME = TENANT_MANAGER_USER; + public static final String DEFAULT_TENANT_MANAGER_PASSWORD = "manage"; + public static final String DEFAULT_TENANT_MANAGER_EMAIL = "tenantManager@collectionspace.org"; + + public static final String TENANT_ADMIN_ACCT_PREFIX = "admin@"; + public static final String TENANT_READER_ACCT_PREFIX = "reader@"; + public static final String ROLE_PREFIX = "ROLE_"; + public static final String TENANT_ADMIN_ROLE_SUFFIX = "_TENANT_ADMINISTRATOR"; + public static final String TENANT_READER_ROLE_SUFFIX = "_TENANT_READER"; public static final String DEFAULT_ADMIN_PASSWORD = "Administrator"; public static final String DEFAULT_READER_PASSWORD = "reader"; - + // SQL for init tasks - final private static String INSERT_ACCOUNT_ROLE_SQL_MYSQL = + final private static String INSERT_ACCOUNT_ROLE_SQL_MYSQL = "INSERT INTO accounts_roles(account_id, user_id, role_id, role_name, created_at)" +" VALUES(?, ?, ?, ?, now())"; final private static String INSERT_ACCOUNT_ROLE_SQL_POSTGRES = "INSERT INTO accounts_roles(HJID, account_id, user_id, role_id, role_name, created_at)" +" VALUES(nextval('hibernate_sequence'), ?, ?, ?, ?, now())"; - final private static String QUERY_USERS_SQL = + final private static String QUERY_USERS_SQL = "SELECT username FROM users WHERE username LIKE '" +TENANT_ADMIN_ACCT_PREFIX+"%' OR username LIKE '"+TENANT_READER_ACCT_PREFIX+"%'"; final private static String INSERT_USER_SQL = - "INSERT INTO users (username,passwd,salt, created_at) VALUES (?,?,?, now())"; - final private static String INSERT_ACCOUNT_SQL = + "INSERT INTO users (username,passwd,created_at) VALUES (?,?,now())"; + final private static String INSERT_ACCOUNT_SQL = "INSERT INTO accounts_common " + "(csid, email, userid, status, screen_name, metadata_protection, roles_protection, created_at) " + "VALUES (?,?,?,'ACTIVE',?, 'immutable', 'immutable', now())"; - + // TENANT MANAGER specific SQL - final private static String QUERY_TENANT_MGR_USER_SQL = + final private static String QUERY_TENANT_MGR_USER_SQL = "SELECT username FROM users WHERE username = '"+TENANT_MANAGER_USER+"'"; final private static String GET_TENANT_MGR_ROLE_SQL = "SELECT csid from roles WHERE tenant_id='" + AuthN.ALL_TENANTS_MANAGER_TENANT_ID + "' and rolename=?"; @@ -161,23 +163,23 @@ public class AuthorizationCommon { public static String getTenantConfigMD5Hash(String tenantId) { return tenantConfigMD5HashTable.get(tenantId); } - + public static String setTenantConfigMD5Hash(String tenantId, String md5hash) { return tenantConfigMD5HashTable.put(tenantId, md5hash); - } - + } + public static Role getRole(JPATransactionContext jpaTransactionContext, String tenantId, String displayName) { Role role = null; - + String roleName = AuthorizationCommon.getQualifiedRoleName(tenantId, displayName); role = AuthorizationStore.getRoleByName(jpaTransactionContext, roleName, tenantId); - + return role; } - + /** * Create a new role instance to be persisted later. - * + * * @param tenantId * @param name * @param description @@ -186,10 +188,10 @@ public class AuthorizationCommon { */ public static Role createRole(String tenantId, String name, String description, boolean immutable) { Role role = new Role(); - + role.setCreatedAtItem(new Date()); role.setDisplayName(name); - String roleName = AuthorizationCommon.getQualifiedRoleName(tenantId, name); + String roleName = AuthorizationCommon.getQualifiedRoleName(tenantId, name); role.setRoleName(roleName); String id = UUID.randomUUID().toString(); //FIXME: The qualified role name should be unique enough to use as an ID/key role.setCsid(id); @@ -199,10 +201,10 @@ public class AuthorizationCommon { role.setMetadataProtection(RoleClient.IMMUTABLE); role.setPermsProtection(RoleClient.IMMUTABLE); } - + return role; } - + /** * Add permission to the Spring Security tables * with assumption that resource is of type URI @@ -220,47 +222,47 @@ public class AuthorizationCommon { + " with permissionId=" + permRole.getPermission().get(0).getPermissionId() + " for permission with csid=" + perm.getCsid()); } - - List principals = new ArrayList(); + + List principals = new ArrayList(); for (RoleValue roleValue : permRole.getRole()) { principals.add(roleValue.getRoleName()); } - + boolean grant = perm.getEffect().equals(EffectType.PERMIT) ? true : false; List permActions = perm.getAction(); ArrayList resources = new ArrayList(); for (PermissionAction permAction : permActions) { - CSpaceAction action = URIResourceImpl.getAction(permAction.getName()); + CSpaceAction action = URIResourceImpl.getAction(permAction.getName()); URIResourceImpl uriRes = new URIResourceImpl(perm.getTenantId(), perm.getResourceName(), action); resources.add(uriRes); } AuthZ.get().addPermissions(resources.toArray(new CSpaceResource[0]), principals.toArray(new String[0]), grant); // CSPACE-4967 jpaTransactionContext.setAclTablesUpdateFlag(true); // Tell the containing JPA transaction that we've committed changes to the Spring Tables } - + private static Connection getConnection(String databaseName) throws NamingException, SQLException { return JDBCTools.getConnection(JDBCTools.CSPACE_DATASOURCE_NAME, databaseName); } - + /* * Spring security seems to require that all of our role names start * with the ROLE_PREFIX string. */ public static String getQualifiedRoleName(String tenantId, String name) { String result = name; - - String qualifiedName = ROLE_PREFIX + tenantId.toUpperCase() + "_" + name.toUpperCase(); + + String qualifiedName = ROLE_PREFIX + tenantId.toUpperCase() + "_" + name.toUpperCase(); if (name.equals(qualifiedName) == false) { result = qualifiedName; } - + return result; } - + private static ActionGroup getActionGroup(String actionGroupStr) { ActionGroup result = null; - + if (actionGroupStr.equalsIgnoreCase(ACTIONGROUP_CRUDL_NAME)) { result = ACTIONGROUP_CRUDL; } else if (actionGroupStr.equalsIgnoreCase(ACTIONGROUP_RL_NAME)) { @@ -268,23 +270,23 @@ public class AuthorizationCommon { } else if (actionGroupStr.equalsIgnoreCase(ACTIONGROUP_CRUL_NAME)) { result = ACTIONGROUP_CRUL; } - + return result; } - + public static Permission createPermission(String tenantId, String resourceName, String description, String actionGroupStr, boolean immutable) { Permission result = null; - + ActionGroup actionGroup = getActionGroup(actionGroupStr); result = createPermission(tenantId, resourceName, description, actionGroup, immutable); - + return result; } - + private static Permission createPermission(String tenantId, String resourceName, String description, @@ -300,7 +302,7 @@ public class AuthorizationCommon { perm.setResourceName(resourceName.toLowerCase().trim()); perm.setEffect(EffectType.PERMIT); perm.setTenantId(tenantId); - + perm.setActionGroup(actionGroup.name); ArrayList pas = new ArrayList(); perm.setAction(pas); @@ -308,15 +310,15 @@ public class AuthorizationCommon { PermissionAction permAction = createPermissionAction(perm, actionType); pas.add(permAction); } - + if (immutable) { perm.setMetadataProtection(PermissionClient.IMMUTABLE); perm.setActionsProtection(PermissionClient.IMMUTABLE); } - + return perm; } - + private static Permission createWorkflowPermission(TenantBindingType tenantBinding, ServiceBindingType serviceBinding, String transitionVerb, @@ -330,10 +332,10 @@ public class AuthorizationCommon { transitionName = transitionVerb; workFlowServiceSuffix = WorkflowClient.SERVICE_AUTHZ_SUFFIX; } else { - transitionName = ""; //since the transitionDef was null, we're assuming that this is the base workflow permission to be created + transitionName = ""; //since the transitionDef was null, we're assuming that this is the base workflow permission to be created workFlowServiceSuffix = WorkflowClient.SERVICE_PATH; } - + String tenantId = tenantBinding.getId(); String resourceName = "/" + serviceBinding.getName().toLowerCase().trim() @@ -341,7 +343,7 @@ public class AuthorizationCommon { + transitionName; String description = "A generated workflow permission for actiongroup " + actionGroup.name; result = createPermission(tenantId, resourceName, description, actionGroup, immutable); - + if (logger.isDebugEnabled() == true) { logger.debug("Generated a workflow permission: " + result.getResourceName() @@ -349,17 +351,17 @@ public class AuthorizationCommon { + ":" + "tenant id=" + result.getTenantId() + ":" + actionGroup.name); } - + return result; } - + private static PermissionRole createPermissionRole( Permission permission, Role role, boolean enforceTenancy) throws DocumentException { PermissionRole permRole = new PermissionRole(); - + // // Check to see if the tenant ID of the permission and the tenant ID of the role match // @@ -367,7 +369,7 @@ public class AuthorizationCommon { if (tenantIdsMatch == false && enforceTenancy == false) { tenantIdsMatch = true; // If we don't need to enforce tenancy then we'll just consider them matched. } - + if (tenantIdsMatch == true) { permRole.setSubject(SubjectType.ROLE); // @@ -395,10 +397,10 @@ public class AuthorizationCommon { + " did not match the tenant ID of the permission: " + permission.getTenantId(); throw new DocumentException(errMsg); } - + return permRole; } - + private static Hashtable getTenantNamesFromConfig(TenantBindingConfigReaderImpl tenantBindingConfigReader) { // Note that this only handles tenants not marked as "createDisabled" @@ -415,7 +417,7 @@ public class AuthorizationCommon { } return tenantInfo; } - + private static ArrayList compileExistingTenants(Connection conn, Hashtable tenantInfo) throws SQLException, Exception { Statement stmt = null; @@ -447,8 +449,8 @@ public class AuthorizationCommon { return existingTenants; } - - private static ArrayList findOrCreateDefaultUsers(Connection conn, Hashtable tenantInfo) + + private static ArrayList findOrCreateDefaultUsers(Connection conn, Hashtable tenantInfo) throws SQLException, Exception { // Second find or create the users Statement stmt = null; @@ -466,12 +468,9 @@ public class AuthorizationCommon { for(String tName : tenantInfo.values()) { String adminAcctName = getDefaultAdminUserID(tName); if(!usersInRepo.contains(adminAcctName)) { - String salt = UUID.randomUUID().toString(); - String secEncPasswd = SecurityUtils.createPasswordHash( - adminAcctName, DEFAULT_ADMIN_PASSWORD, salt); + String secEncPasswd = SecurityUtils.createPasswordHash(DEFAULT_ADMIN_PASSWORD); pstmt.setString(1, adminAcctName); // set username param pstmt.setString(2, secEncPasswd); // set passwd param - pstmt.setString(3, salt); if (logger.isDebugEnabled()) { logger.debug("createDefaultUsersAndAccounts adding user: " +adminAcctName+" for tenant: "+tName); @@ -485,12 +484,9 @@ public class AuthorizationCommon { String readerAcctName = getDefaultReaderUserID(tName); if(!usersInRepo.contains(readerAcctName)) { - String salt = UUID.randomUUID().toString(); - String secEncPasswd = SecurityUtils.createPasswordHash( - readerAcctName, DEFAULT_READER_PASSWORD, salt); + String secEncPasswd = SecurityUtils.createPasswordHash(DEFAULT_READER_PASSWORD); pstmt.setString(1, readerAcctName); // set username param pstmt.setString(2, secEncPasswd); // set passwd param - pstmt.setString(3, salt); if (logger.isDebugEnabled()) { logger.debug("createDefaultUsersAndAccounts adding user: " +readerAcctName+" for tenant: "+tName); @@ -512,10 +508,10 @@ public class AuthorizationCommon { } return usersInRepo; } - + private static void findOrCreateDefaultAccounts(Connection conn, Hashtable tenantInfo, ArrayList usersInRepo, - Hashtable tenantAdminAcctCSIDs, Hashtable tenantReaderAcctCSIDs) + Hashtable tenantAdminAcctCSIDs, Hashtable tenantReaderAcctCSIDs) throws SQLException, Exception { // Third, create the accounts. Assume that if the users were already there, // then the accounts were as well @@ -542,7 +538,7 @@ public class AuthorizationCommon { +" already exists - skipping account generation."); } - String readerCSID = UUID.randomUUID().toString(); + String readerCSID = UUID.randomUUID().toString(); tenantReaderAcctCSIDs.put(tId, readerCSID); String readerAcctName = getDefaultReaderUserID(tName); if(!usersInRepo.contains(readerAcctName)) { @@ -568,8 +564,8 @@ public class AuthorizationCommon { pstmt.close(); } } - - private static boolean findOrCreateTenantManagerUserAndAccount(Connection conn) + + private static boolean findOrCreateTenantManagerUserAndAccount(Connection conn) throws SQLException, Exception { // Find or create the special tenant manager account. // Later can make the user name for tenant manager be configurable, settable. @@ -587,13 +583,10 @@ public class AuthorizationCommon { } rs.close(); if(!foundTMgrUser) { - String salt = UUID.randomUUID().toString(); pstmt = conn.prepareStatement(INSERT_USER_SQL); // create a statement - String secEncPasswd = SecurityUtils.createPasswordHash( - TENANT_MANAGER_USER, DEFAULT_TENANT_MANAGER_PASSWORD, salt); + String secEncPasswd = SecurityUtils.createPasswordHash(DEFAULT_TENANT_MANAGER_PASSWORD); pstmt.setString(1, TENANT_MANAGER_USER); // set username param pstmt.setString(2, secEncPasswd); // set passwd param - pstmt.setString(3, salt); if (logger.isDebugEnabled()) { logger.debug("findOrCreateTenantManagerUserAndAccount adding tenant manager user: " +TENANT_MANAGER_USER); @@ -627,10 +620,10 @@ public class AuthorizationCommon { } return created; } - + private static void bindDefaultAccountsToTenants(Connection conn, DatabaseProductType databaseProductType, Hashtable tenantInfo, ArrayList usersInRepo, - Hashtable tenantAdminAcctCSIDs, Hashtable tenantReaderAcctCSIDs) + Hashtable tenantAdminAcctCSIDs, Hashtable tenantReaderAcctCSIDs) throws SQLException, Exception { // Fourth, bind accounts to tenants. Assume that if the users were already there, // then the accounts were bound to tenants correctly @@ -680,12 +673,12 @@ public class AuthorizationCommon { pstmt.close(); } } - + /** * Creates the default Admin and Reader roles for all the configured tenants. - * + * * Returns the CSID of the Spring Admin role. - * + * * @param conn * @param tenantInfo * @param tenantAdminRoleCSIDs @@ -695,7 +688,7 @@ public class AuthorizationCommon { * @throws Exception */ private static String findOrCreateDefaultRoles(Connection conn, Hashtable tenantInfo, - Hashtable tenantAdminRoleCSIDs, Hashtable tenantReaderRoleCSIDs) + Hashtable tenantAdminRoleCSIDs, Hashtable tenantReaderRoleCSIDs) throws SQLException, Exception { String springAdminRoleCSID = null; @@ -722,7 +715,7 @@ public class AuthorizationCommon { } rs.close(); rs = null; - + // // Look for and save each tenants default Admin and Reader roles // @@ -766,17 +759,17 @@ public class AuthorizationCommon { if (stmt != null) stmt.close(); if (pstmt != null) pstmt.close(); } - + return springAdminRoleCSID; } - private static String findTenantManagerRole(Connection conn ) + private static String findTenantManagerRole(Connection conn ) throws SQLException, RuntimeException, Exception { String tenantMgrRoleCSID = null; PreparedStatement pstmt = null; try { - String rolename = getQualifiedRoleName(AuthN.ALL_TENANTS_MANAGER_TENANT_ID, - AuthN.ROLE_ALL_TENANTS_MANAGER); + String rolename = getQualifiedRoleName(AuthN.ALL_TENANTS_MANAGER_TENANT_ID, + AuthN.ROLE_ALL_TENANTS_MANAGER); pstmt = conn.prepareStatement(GET_TENANT_MGR_ROLE_SQL); // create a statement ResultSet rs = null; pstmt.setString(1, rolename); // set rolename param @@ -804,7 +797,7 @@ public class AuthorizationCommon { Hashtable tenantInfo, ArrayList usersInRepo, String springAdminRoleCSID, Hashtable tenantAdminRoleCSIDs, Hashtable tenantReaderRoleCSIDs, - Hashtable tenantAdminAcctCSIDs, Hashtable tenantReaderAcctCSIDs) + Hashtable tenantAdminAcctCSIDs, Hashtable tenantReaderAcctCSIDs) throws SQLException, Exception { // Sixth, bind the accounts to roles. If the users already existed, // we'll assume they were set up correctly. @@ -816,7 +809,7 @@ public class AuthorizationCommon { } else { throw new Exception("Unrecognized database system."); } - + pstmt = conn.prepareStatement(insertAccountRoleSQL); // create a statement for (String tId : tenantInfo.keySet()) { String adminUserId = getDefaultAdminUserID(tenantInfo.get(tId)); @@ -855,9 +848,9 @@ public class AuthorizationCommon { } } } - + private static void bindTenantManagerAccountRole(Connection conn, DatabaseProductType databaseProductType, - String tenantManagerUserID, String tenantManagerAccountID, String tenantManagerRoleID, String tenantManagerRoleName ) + String tenantManagerUserID, String tenantManagerAccountID, String tenantManagerRoleID, String tenantManagerRoleName ) throws SQLException, Exception { PreparedStatement pstmt = null; try { @@ -879,7 +872,7 @@ public class AuthorizationCommon { pstmt.setString(3, tenantManagerRoleID); // set role_id param pstmt.setString(4, tenantManagerRoleName); // set rolename param pstmt.executeUpdate(); - + /* At this point, tenant manager should not need the Spring Admin Role pstmt.setString(3, springAdminRoleCSID); // set role_id param pstmt.setString(4, SPRING_ADMIN_ROLE); // set rolename param @@ -889,7 +882,7 @@ public class AuthorizationCommon { } pstmt.executeUpdate(); */ - + pstmt.close(); } catch(Exception e) { throw e; @@ -898,7 +891,7 @@ public class AuthorizationCommon { pstmt.close(); } } - + /* * Using the tenant bindings, ensure there are corresponding Tenant records (db columns). */ @@ -931,9 +924,9 @@ public class AuthorizationCommon { } } } - + /** - * + * * @param tenantBindingConfigReader * @param databaseProductType * @param cspaceDatabaseName @@ -946,20 +939,20 @@ public class AuthorizationCommon { String cspaceDatabaseName) throws Exception { logger.debug("ServiceMain.createDefaultAccounts starting..."); - + Hashtable tenantInfo = getTenantNamesFromConfig(tenantBindingConfigReader); Connection conn = null; // TODO - need to put in tests for existence first. // We could just look for the accounts per tenant up front, and assume that // the rest is there if the accounts are. - // Could add a sql script to remove these if need be - Spring only does roles, - // and we're not touching that, so we could safely toss the + // Could add a sql script to remove these if need be - Spring only does roles, + // and we're not touching that, so we could safely toss the // accounts, users, account-tenants, account-roles, and start over. try { conn = getConnection(cspaceDatabaseName); - + ArrayList usersInRepo = findOrCreateDefaultUsers(conn, tenantInfo); - + Hashtable tenantAdminAcctCSIDs = new Hashtable(); Hashtable tenantReaderAcctCSIDs = new Hashtable(); findOrCreateDefaultAccounts(conn, tenantInfo, usersInRepo, @@ -967,24 +960,24 @@ public class AuthorizationCommon { bindDefaultAccountsToTenants(conn, databaseProductType, tenantInfo, usersInRepo, tenantAdminAcctCSIDs, tenantReaderAcctCSIDs); - + Hashtable tenantAdminRoleCSIDs = new Hashtable(); Hashtable tenantReaderRoleCSIDs = new Hashtable(); String springAdminRoleCSID = findOrCreateDefaultRoles(conn, tenantInfo, tenantAdminRoleCSIDs, tenantReaderRoleCSIDs); - + bindAccountsToRoles(conn, databaseProductType, tenantInfo, usersInRepo, springAdminRoleCSID, tenantAdminRoleCSIDs, tenantReaderRoleCSIDs, tenantAdminAcctCSIDs, tenantReaderAcctCSIDs); - + boolean createdTenantMgrAccount = findOrCreateTenantManagerUserAndAccount(conn); if (createdTenantMgrAccount) { // If we created the account, we need to create the bindings. Otherwise, assume they // are all set (from previous initialization). String tenantManagerRoleCSID = findTenantManagerRole(conn); - bindTenantManagerAccountRole(conn, databaseProductType, - TENANT_MANAGER_USER, AuthN.TENANT_MANAGER_ACCT_ID, + bindTenantManagerAccountRole(conn, databaseProductType, + TENANT_MANAGER_USER, AuthN.TENANT_MANAGER_ACCT_ID, tenantManagerRoleCSID, AuthN.ROLE_ALL_TENANTS_MANAGER); } } catch (Exception e) { @@ -1000,25 +993,25 @@ public class AuthorizationCommon { logger.debug("SQL Exception closing statement/connection: " + sqle.getLocalizedMessage()); } } - } + } } - + private static String getDefaultAdminRole(String tenantId) { return ROLE_PREFIX + tenantId + TENANT_ADMIN_ROLE_SUFFIX; } - + private static String getDefaultReaderRole(String tenantId) { return ROLE_PREFIX+tenantId+TENANT_READER_ROLE_SUFFIX; } - + private static String getDefaultAdminUserID(String tenantName) { return TENANT_ADMIN_ACCT_PREFIX + tenantName; } - + private static String getDefaultReaderUserID(String tenantName) { return TENANT_READER_ACCT_PREFIX + tenantName; } - + static private PermissionAction createPermissionAction(Permission perm, ActionType actionType) { PermissionAction pa = new PermissionAction(); @@ -1029,13 +1022,13 @@ public class AuthorizationCommon { pa.setName(actionType); pa.setObjectIdentity(uriRes.getHashedId().toString()); pa.setObjectIdentityResource(uriRes.getId()); - + return pa; } - + private static HashSet getTransitionVerbList(TenantBindingType tenantBinding, ServiceBindingType serviceBinding) { HashSet result = new HashSet(); - + TransitionDefList transitionDefList = getTransitionDefList(tenantBinding, serviceBinding); for (TransitionDef transitionDef : transitionDefList.getTransitionDef()) { String transitionVerb = transitionDef.getName(); @@ -1045,12 +1038,12 @@ public class AuthorizationCommon { return result; } - + private static TransitionDefList getTransitionDefList(TenantBindingType tenantBinding, ServiceBindingType serviceBinding) { TransitionDefList result = null; try { String serviceObjectName = serviceBinding.getObject().getName(); - + @SuppressWarnings("rawtypes") DocumentHandler docHandler = ServiceConfigUtils.createDocumentHandlerInstance( tenantBinding, serviceBinding); @@ -1061,7 +1054,7 @@ public class AuthorizationCommon { } catch (Exception e) { // Ignore this exception and return an empty non-null TransitionDefList } - + if (result == null) { if (serviceBinding.getType().equalsIgnoreCase(ServiceBindingUtils.SERVICE_TYPE_SECURITY) == false) { logger.debug("Could not retrieve a lifecycle transition definition list from: " @@ -1069,7 +1062,7 @@ public class AuthorizationCommon { + " with tenant ID = " + tenantBinding.getId()); } - // return an empty list + // return an empty list result = new TransitionDefList(); } else { logger.debug("Successfully retrieved a lifecycle transition definition list from: " @@ -1077,13 +1070,13 @@ public class AuthorizationCommon { + " with tenant ID = " + tenantBinding.getId()); } - + return result; } - + /** * Creates the immutable workflow permission sets for the default admin and reader roles. - * + * * @param tenantBindingConfigReader * @param databaseProductType * @param cspaceDatabaseName @@ -1092,13 +1085,13 @@ public class AuthorizationCommon { public static void createDefaultWorkflowPermissions( JPATransactionContext jpaTransactionContext, TenantBindingConfigReaderImpl tenantBindingConfigReader, - DatabaseProductType databaseProductType, + DatabaseProductType databaseProductType, String cspaceDatabaseName) throws Exception { java.util.logging.Logger logger = java.util.logging.Logger.getAnonymousLogger(); AuthZ.get().login(); //login to Spring Security manager - + try { Hashtable tenantBindings = tenantBindingConfigReader.getTenantBindings(); for (String tenantId : tenantBindings.keySet()) { @@ -1107,16 +1100,16 @@ public class AuthorizationCommon { if (tenantBinding.isConfigChangedSinceLastStart() == false) { continue; // skip the rest of the loop and go to the next tenant } - + Role adminRole = AuthorizationCommon.getRole(jpaTransactionContext, tenantBinding.getId(), ROLE_TENANT_ADMINISTRATOR); Role readonlyRole = AuthorizationCommon.getRole(jpaTransactionContext, tenantBinding.getId(), ROLE_TENANT_READER); - + if (adminRole == null || readonlyRole == null) { String msg = String.format("One or more of the required default CollectionSpace administrator roles is missing or was never created. If you're setting up a new instance of CollectionSpace, shutdown the Tomcat server and run the 'ant import' command from the root/top level CollectionSpace 'Services' source directory. Then try restarting Tomcat."); logger.info(msg); throw new RuntimeException("One or more of the required default CollectionSpace administrator roles is missing or was never created."); } - + for (ServiceBindingType serviceBinding : tenantBinding.getServiceBindings()) { String prop = ServiceBindingUtils.getPropertyValue(serviceBinding, REFRESH_AUTHZ_PROP); if (prop == null ? true : Boolean.parseBoolean(prop)) { @@ -1130,7 +1123,7 @@ public class AuthorizationCommon { persist(jpaTransactionContext, adminPerm, adminRole, true, ACTIONGROUP_CRUDL); // // Create the permission for the read-only role - Permission readonlyPerm = createWorkflowPermission(tenantBinding, serviceBinding, transitionVerb, ACTIONGROUP_RL, true); + Permission readonlyPerm = createWorkflowPermission(tenantBinding, serviceBinding, transitionVerb, ACTIONGROUP_RL, true); persist(jpaTransactionContext, readonlyPerm, readonlyRole, true, ACTIONGROUP_RL); // Persist/store the permission and permrole records and related Spring Security info } jpaTransactionContext.commitTransaction(); @@ -1151,11 +1144,11 @@ public class AuthorizationCommon { throw e; } } - + private static void createMissingTenants(Connection conn, Hashtable tenantInfo, ArrayList existingTenants) throws SQLException, Exception { // Need to define and look for a createDisabled attribute in tenant config - final String insertTenantSQL = + final String insertTenantSQL = "INSERT INTO tenants (id,name,authorities_initialized,disabled,created_at) VALUES (?,?,FALSE,FALSE,now())"; PreparedStatement pstmt = null; try { @@ -1183,13 +1176,13 @@ public class AuthorizationCommon { pstmt.close(); } } - + public static String getPersistedMD5Hash(String tenantId, String cspaceDatabaseName) throws Exception { String result = null; - + // First find or create the tenants final String queryTenantSQL = String.format("SELECT id, name, config_md5hash FROM tenants WHERE id = '%s'", tenantId); - + Statement stmt = null; Connection conn; int rowCount = 0; @@ -1214,7 +1207,7 @@ public class AuthorizationCommon { } finally { if (stmt != null) stmt.close(); } - + return result; } @@ -1223,22 +1216,22 @@ public class AuthorizationCommon { String permissionId, String RoleId) { PermissionRoleRel result = null; - + try { String whereClause = "where permissionId = :id and roleId = :roleId"; HashMap params = new HashMap(); params.put("id", permissionId); - params.put("roleId", RoleId); - + params.put("roleId", RoleId); + result = (PermissionRoleRel) JpaStorageUtils.getEntity(jpaTransactionContext, PermissionRoleRel.class.getCanonicalName(), whereClause, params); } catch (Exception e) { //Do nothing. Will return null; } - + return result; } - + /* * Persists the Permission, PermissionRoleRel, and Spring Security table entries all in one transaction */ @@ -1246,7 +1239,7 @@ public class AuthorizationCommon { AuthorizationStore authzStore = new AuthorizationStore(); // First persist the Permission record authzStore.store(jpaTransactionContext, permission); - + // If the PermRoleRel doesn't already exists then relate the permission and the role in a new PermissionRole (the service payload) // Create a PermissionRoleRel (the database relation table for the permission and role) PermissionRoleRel permRoleRel = findPermRoleRel(jpaTransactionContext, permission.getCsid(), role.getCsid()); @@ -1269,19 +1262,19 @@ public class AuthorizationCommon { + ":" + actionGroup.getName() + ":" + profiler.getCumulativeTime()); } - + } - + public static boolean hasTokenExpired(EmailConfig emailConfig, Token token) throws NoSuchAlgorithmException { boolean result = false; - + int maxConfigSeconds = emailConfig.getPasswordResetConfig().getTokenExpirationSeconds().intValue(); int maxTokenSeconds = token.getExpireSeconds().intValue(); - - long createdTime = token.getCreatedAtItem().getTime(); + + long createdTime = token.getCreatedAtItem().getTime(); long configExpirationTime = createdTime + maxConfigSeconds * 1000; // the current tenant config for how long a token stays valid long tokenDefinedExirationTime = createdTime + maxTokenSeconds * 1000; // the tenant config for how long a token stays valid when the token was created. - + if (configExpirationTime != tokenDefinedExirationTime) { String msg = String.format("The configured expiration time for the token = '%s' changed from when the token was created.", token.getId()); @@ -1293,66 +1286,66 @@ public class AuthorizationCommon { if (System.currentTimeMillis() >= configExpirationTime) { result = true; } - + return result; } - + /* * Validate that the password reset configuration is correct. */ private static String validatePasswordResetConfig(PasswordResetConfig passwordResetConfig) { String result = null; - + if (passwordResetConfig != null) { result = passwordResetConfig.getMessage(); if (result == null || result.length() == 0) { result = DEFAULT_PASSWORD_RESET_EMAIL_MESSAGE; logger.warn("Could not find a password reset message in the tenant's configuration. Using the default one"); } - + if (result.contains("{{link}}") == false) { logger.warn("The tenant's password reset message does not contain a required '{{link}}' marker."); result = null; } - - if (passwordResetConfig.getLoginpage() == null || passwordResetConfig.getLoginpage().trim().isEmpty()) { - logger.warn("The tenant's password reset configuration is missing a 'loginpage' value. It should be set to something like '/collectionspace/ui/core/html/index.html'."); - result = null; - } - - String subject = passwordResetConfig.getSubject(); - if (subject == null || subject.trim().isEmpty()) { - passwordResetConfig.setSubject(DEFAULT_PASSWORD_RESET_EMAIL_SUBJECT); - } + String subject = passwordResetConfig.getSubject(); + + if (subject == null || subject.trim().isEmpty()) { + passwordResetConfig.setSubject(DEFAULT_PASSWORD_RESET_EMAIL_SUBJECT); + } } - + return result; } - + /* * Generate a password reset message. Embeds an authorization token to reset a user's password. */ public static String generatePasswordResetEmailMessage(EmailConfig emailConfig, AccountListItem accountListItem, Token token) throws Exception { String result = null; - + result = validatePasswordResetConfig(emailConfig.getPasswordResetConfig()); if (result == null) { String errMsg = String.format("The password reset configuration for the tenant ID='%s' is missing or malformed. Could not initiate a password reset for user ID='%s. See the log files for more details.", token.getTenantId(), accountListItem.getEmail()); throw new Exception(errMsg); } - - String link = emailConfig.getBaseurl() + emailConfig.getPasswordResetConfig().getLoginpage() + "?token=" + token.getId(); + + String link = UriBuilder.fromUri(emailConfig.getBaseurl()) + .path(AccountClient.PROCESS_PASSWORD_RESET_PATH) + .replaceQuery("token=" + token.getId()) + .build() + .toString(); + result = result.replaceAll("\\{\\{link\\}\\}", link); - + if (result.contains("{{greeting}}")) { String greeting = accountListItem.getScreenName(); result = result.replaceAll("\\{\\{greeting\\}\\}", greeting); result = result.replaceAll("\\\\n", "\\\n"); result = result.replaceAll("\\\\r", "\\\r"); - } - + } + return result; } diff --git a/services/common/src/main/java/org/collectionspace/services/common/config/TenantBindingConfigReaderImpl.java b/services/common/src/main/java/org/collectionspace/services/common/config/TenantBindingConfigReaderImpl.java index cc4fbdc06..7cc4243fc 100644 --- a/services/common/src/main/java/org/collectionspace/services/common/config/TenantBindingConfigReaderImpl.java +++ b/services/common/src/main/java/org/collectionspace/services/common/config/TenantBindingConfigReaderImpl.java @@ -181,6 +181,11 @@ public class TenantBindingConfigReaderImpl extends AbstractConfigReaderImpl configFiles, boolean useAppGeneratedBindings) throws Exception { + throw new UnsupportedOperationException("Not implemented"); + } + @Override public void read(String tenantRootDirPath, boolean useAppGeneratedBindings) throws Exception { File tenantsRootDir = new File(tenantRootDirPath); diff --git a/services/common/src/main/java/org/collectionspace/services/common/security/SecurityConfig.java b/services/common/src/main/java/org/collectionspace/services/common/security/SecurityConfig.java new file mode 100644 index 000000000..8e13db12c --- /dev/null +++ b/services/common/src/main/java/org/collectionspace/services/common/security/SecurityConfig.java @@ -0,0 +1,543 @@ +package org.collectionspace.services.common.security; + +import java.net.MalformedURLException; +import java.security.KeyPair; +import java.security.KeyPairGenerator; +import java.security.interfaces.RSAPrivateKey; +import java.security.interfaces.RSAPublicKey; +import java.time.Duration; +import java.util.Arrays; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.UUID; + +import javax.servlet.http.HttpServletRequest; +import javax.sql.DataSource; + +import org.collectionspace.authentication.CSpaceUser; +import org.collectionspace.authentication.spring.CSpaceJwtAuthenticationToken; +import org.collectionspace.authentication.spring.CSpaceLogoutSuccessHandler; +import org.collectionspace.authentication.spring.CSpacePasswordEncoderFactory; +import org.collectionspace.authentication.spring.CSpaceUserAttributeFilter; +import org.collectionspace.authentication.spring.CSpaceUserDetailsService; +import org.collectionspace.services.client.AccountClient; +import org.collectionspace.services.common.ServiceMain; +import org.collectionspace.services.common.config.ConfigUtils; +import org.collectionspace.services.common.config.TenantBindingConfigReaderImpl; +import org.collectionspace.services.config.OAuthAuthorizationGrantTypeEnum; +import org.collectionspace.services.config.OAuthClientAuthenticationMethodEnum; +import org.collectionspace.services.config.OAuthClientSettingsType; +import org.collectionspace.services.config.OAuthClientType; +import org.collectionspace.services.config.OAuthScopeEnum; +import org.collectionspace.services.config.OAuthTokenSettingsType; +import org.collectionspace.services.config.OAuthType; +import org.collectionspace.services.config.ServiceConfig; +import org.collectionspace.services.config.tenant.TenantBindingType; +import org.collectionspace.authentication.realm.db.CSpaceDbRealm; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.context.ApplicationEventPublisher; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.Ordered; +import org.springframework.core.annotation.Order; +import org.springframework.core.convert.converter.Converter; +import org.springframework.http.HttpMethod; +import org.springframework.jdbc.core.JdbcOperations; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.lang.Nullable; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.authentication.ProviderManager; +import org.springframework.security.authentication.dao.DaoAuthenticationProvider; +import org.springframework.security.config.Customizer; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.config.annotation.web.configurers.AnonymousConfigurer; +import org.springframework.security.config.annotation.web.configurers.AuthorizeHttpRequestsConfigurer; +import org.springframework.security.config.annotation.web.configurers.CorsConfigurer; +import org.springframework.security.config.annotation.web.configurers.CsrfConfigurer; +import org.springframework.security.config.annotation.web.configurers.ExceptionHandlingConfigurer; +import org.springframework.security.config.annotation.web.configurers.FormLoginConfigurer; +import org.springframework.security.config.annotation.web.configurers.HttpBasicConfigurer; +import org.springframework.security.config.annotation.web.configurers.LogoutConfigurer; +import org.springframework.security.config.annotation.web.configurers.oauth2.server.resource.OAuth2ResourceServerConfigurer; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.security.core.userdetails.UsernameNotFoundException; +import org.springframework.security.oauth2.core.AuthorizationGrantType; +import org.springframework.security.oauth2.core.ClientAuthenticationMethod; +import org.springframework.security.oauth2.jwt.Jwt; +import org.springframework.security.oauth2.jwt.JwtDecoder; +import org.springframework.security.oauth2.server.authorization.JdbcOAuth2AuthorizationService; +import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationService; +import org.springframework.security.oauth2.server.authorization.client.JdbcRegisteredClientRepository; +import org.springframework.security.oauth2.server.authorization.client.RegisteredClient; +import org.springframework.security.oauth2.server.authorization.client.RegisteredClientRepository; +import org.springframework.security.oauth2.server.authorization.config.annotation.web.configuration.OAuth2AuthorizationServerConfiguration; +import org.springframework.security.oauth2.server.authorization.settings.AuthorizationServerSettings; +import org.springframework.security.oauth2.server.authorization.settings.ClientSettings; +import org.springframework.security.oauth2.server.authorization.settings.TokenSettings; +import org.springframework.security.web.SecurityFilterChain; +import org.springframework.security.web.authentication.LoginUrlAuthenticationEntryPoint; +import org.springframework.security.web.authentication.logout.LogoutFilter; +import org.springframework.security.web.util.matcher.AntPathRequestMatcher; +import org.springframework.security.web.util.matcher.OrRequestMatcher; +import org.springframework.web.cors.CorsConfiguration; +import org.springframework.web.cors.CorsConfigurationSource; + +import com.nimbusds.jose.jwk.JWKSet; +import com.nimbusds.jose.jwk.RSAKey; +import com.nimbusds.jose.jwk.source.ImmutableJWKSet; +import com.nimbusds.jose.jwk.source.JWKSource; +import com.nimbusds.jose.proc.SecurityContext; + +@Configuration +@EnableWebSecurity +public class SecurityConfig { + private final Logger logger = LoggerFactory.getLogger(SecurityConfig.class); + + public static final String LOGIN_FORM_URL = "/login"; + public static final String LOGOUT_FORM_URL = "/logout"; + + // The default login success URL, handled by LoginResource. + public static final String DEFAULT_LOGIN_SUCCESS_URL = "/"; + + private CorsConfiguration defaultCorsConfiguration = null; + private CorsConfiguration oauthServerCorsConfiguration = null; + + private void initializeCorsConfigurations() { + ServiceConfig serviceConfig = ServiceMain.getInstance().getServiceConfig(); + Duration maxAge = ConfigUtils.getCorsMaxAge(serviceConfig); + List allowedOrigins = ConfigUtils.getCorsAllowedOrigins(serviceConfig); + + if (this.defaultCorsConfiguration == null) { + this.defaultCorsConfiguration = defaultCorsConfiguration(allowedOrigins, maxAge); + } + + if (this.oauthServerCorsConfiguration == null) { + this.oauthServerCorsConfiguration = oauthServerCorsConfiguration(allowedOrigins, maxAge); + } + } + + private CorsConfiguration defaultCorsConfiguration(List allowedOrigins, Duration maxAge) { + CorsConfiguration configuration = new CorsConfiguration(); + + configuration.setAllowedOrigins(allowedOrigins); + + if (maxAge != null) { + configuration.setMaxAge(maxAge); + } + + configuration.setAllowedHeaders(Arrays.asList( + "Authorization", + "Content-Type" + )); + + configuration.setAllowedMethods(Arrays.asList( + HttpMethod.POST.toString(), + HttpMethod.GET.toString(), + HttpMethod.PUT.toString(), + HttpMethod.DELETE.toString() + )); + + configuration.setExposedHeaders(Arrays.asList( + "Location", + "Content-Disposition", + "Www-Authenticate" + )); + + return configuration; + } + + private CorsConfiguration oauthServerCorsConfiguration(List allowedOrigins, Duration maxAge) { + CorsConfiguration configuration = new CorsConfiguration(); + + configuration.setAllowedOrigins(allowedOrigins); + + if (maxAge != null) { + configuration.setMaxAge(maxAge); + } + + configuration.setAllowedMethods(Arrays.asList( + HttpMethod.POST.toString(), + HttpMethod.GET.toString() + )); + + return configuration; + } + + @Bean + public JdbcOperations jdbcOperations(DataSource cspaceDataSource) { + return new JdbcTemplate(cspaceDataSource); + } + + @Bean + public AuthorizationServerSettings authorizationServerSettings() { + return AuthorizationServerSettings.builder().build(); + } + + @Bean + public OAuth2AuthorizationService authorizationService(JdbcOperations jdbcOperations, RegisteredClientRepository registeredClientRepository) { + return new JdbcOAuth2AuthorizationService(jdbcOperations, registeredClientRepository); + } + + @Bean + @Order(Ordered.HIGHEST_PRECEDENCE) + public SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http) throws Exception { + this.initializeCorsConfigurations(); + + OAuth2AuthorizationServerConfiguration.applyDefaultSecurity(http); + + return http + .exceptionHandling(new Customizer>() { + @Override + public void customize(ExceptionHandlingConfigurer configurer) { + configurer.authenticationEntryPoint(new LoginUrlAuthenticationEntryPoint(LOGIN_FORM_URL)); + } + }) + .cors(new Customizer>() { + @Override + public void customize(CorsConfigurer configurer) { + configurer.configurationSource(new CorsConfigurationSource() { + @Override + @Nullable + public CorsConfiguration getCorsConfiguration(HttpServletRequest request) { + return SecurityConfig.this.oauthServerCorsConfiguration; + } + }); + } + }) + .build(); + } + + @Bean + @Order(2) + public SecurityFilterChain defaultSecurityFilterChain( + HttpSecurity http, + final AuthenticationManager authenticationManager, + final UserDetailsService userDetailsService, + final RegisteredClientRepository registeredClientRepository, + final ApplicationEventPublisher appEventPublisher + ) throws Exception { + + this.initializeCorsConfigurations(); + + http + .authorizeHttpRequests(new Customizer.AuthorizationManagerRequestMatcherRegistry>() { + @Override + public void customize(AuthorizeHttpRequestsConfigurer.AuthorizationManagerRequestMatcherRegistry configurer) { + configurer + // Exclude the login form, which needs to be accessible anonymously. + .requestMatchers(LOGIN_FORM_URL).permitAll() + + // Exclude the logout form, since it's harmless to log out when you're not logged in. + .requestMatchers(LOGOUT_FORM_URL).permitAll() + + // Exclude the resource path to public items' content from AuthN and AuthZ. Lets us publish resources with anonymous access. + .requestMatchers("/publicitems/*/*/content").permitAll() + + // Exclude the resource path to handle an account password reset request from AuthN and AuthZ. Lets us process password resets anonymous access. + .requestMatchers("/accounts/requestpasswordreset").permitAll() + + // Exclude the resource path to account process a password resets from AuthN and AuthZ. Lets us process password resets anonymous access. + .requestMatchers("/accounts/processpasswordreset").permitAll() + + // Exclude the resource path to request system info. + .requestMatchers("/systeminfo").permitAll() + + // Handle CORS (preflight OPTIONS requests must be anonymous). + .requestMatchers(HttpMethod.OPTIONS).permitAll() + + // All other paths must be authenticated. + .anyRequest().fullyAuthenticated(); + } + }) + .oauth2ResourceServer(new Customizer>() { + @Override + public void customize(OAuth2ResourceServerConfigurer configurer) { + configurer.jwt(new Customizer.JwtConfigurer>() { + @Override + public void customize(OAuth2ResourceServerConfigurer.JwtConfigurer jwtConfigurer) { + // By default, authentication results in a JwtAuthenticationToken, where the principal is a Jwt instance. + // We want the principal to be a CSpaceUser instance, so that authentication functions will continue to + // work as they do with basic auth and session auth. This conversion code is based on comments in + // https://github.com/spring-projects/spring-security/issues/7834 + + jwtConfigurer.jwtAuthenticationConverter(new Converter() { + @Override + @Nullable + public CSpaceJwtAuthenticationToken convert(Jwt jwt) { + CSpaceUser user = null; + String username = (String) jwt.getClaims().get("sub"); + + try { + user = (CSpaceUser) userDetailsService.loadUserByUsername(username); + } catch (UsernameNotFoundException e) { + user = null; + } + + return new CSpaceJwtAuthenticationToken(jwt, user); + } + }); + } + }); + } + }) + .httpBasic(new Customizer>() { + @Override + public void customize(HttpBasicConfigurer configurer) {} + }) + .formLogin(new Customizer>() { + @Override + public void customize(FormLoginConfigurer configurer) { + configurer + .loginPage(LOGIN_FORM_URL) + .defaultSuccessUrl(DEFAULT_LOGIN_SUCCESS_URL); + } + }) + .logout(new Customizer>() { + @Override + public void customize(LogoutConfigurer configurer) { + // Add a custom logout success handler that redirects to a URL (passed as a parameter) + // after logout. + + // TODO: This seems to be automatic in Spring Authorization Server 1.1, so it should be + // possible to remove this when we upgrade. + // See https://docs.spring.io/spring-authorization-server/docs/current/api/org/springframework/security/oauth2/server/authorization/client/RegisteredClient.Builder.html#postLogoutRedirectUri(java.lang.String) + + ServiceConfig serviceConfig = ServiceMain.getInstance().getServiceConfig(); + List clientsConfig = ConfigUtils.getOAuthClientRegistrations(serviceConfig); + Set permittedRedirectUris = new HashSet<>(); + + for (OAuthClientType clientConfig : clientsConfig) { + String clientId = clientConfig.getId(); + RegisteredClient client = registeredClientRepository.findByClientId(clientId); + + permittedRedirectUris.addAll(client.getRedirectUris()); + } + + configurer + .logoutSuccessHandler(new CSpaceLogoutSuccessHandler(LOGIN_FORM_URL + "?logout", permittedRedirectUris)); + } + }) + .csrf(new Customizer>() { + @Override + public void customize(CsrfConfigurer configurer) { + configurer.requireCsrfProtectionMatcher(new OrRequestMatcher( + new AntPathRequestMatcher(LOGIN_FORM_URL, HttpMethod.POST.toString()), + new AntPathRequestMatcher(AccountClient.PASSWORD_RESET_PATH, HttpMethod.POST.toString()), + new AntPathRequestMatcher(AccountClient.PROCESS_PASSWORD_RESET_PATH, HttpMethod.POST.toString()) + )); + } + }) + .anonymous(new Customizer>() { + @Override + public void customize(AnonymousConfigurer configurer) { + configurer.principal("anonymous"); + } + }) + .cors(new Customizer>() { + @Override + public void customize(CorsConfigurer configurer) { + configurer.configurationSource(new CorsConfigurationSource() { + @Override + @Nullable + public CorsConfiguration getCorsConfiguration(HttpServletRequest request) { + return SecurityConfig.this.defaultCorsConfiguration; + } + }); + } + }) + // Insert the username from the security context into a request attribute for logging. + .addFilterBefore(new CSpaceUserAttributeFilter(), LogoutFilter.class); + + return http.build(); + } + + @Bean + public DaoAuthenticationProvider daoAuthenticationProvider(UserDetailsService userDetailsService) { + DaoAuthenticationProvider provider = new DaoAuthenticationProvider(); + + provider.setUserDetailsService(userDetailsService); + provider.setPasswordEncoder(CSpacePasswordEncoderFactory.createDefaultPasswordEncoder()); + + return provider; + } + + @Bean + public AuthenticationManager authenticationManager(DaoAuthenticationProvider provider) { + return new ProviderManager(provider); + } + + @Bean + public RegisteredClientRepository registeredClientRepository(JdbcOperations jdbcOperations) { + JdbcRegisteredClientRepository registeredClientRepository = new JdbcRegisteredClientRepository(jdbcOperations); + ServiceConfig serviceConfig = ServiceMain.getInstance().getServiceConfig(); + OAuthType oauthConfig = ConfigUtils.getOAuth(serviceConfig); + List clientsConfig = ConfigUtils.getOAuthClientRegistrations(serviceConfig); + + Duration defaultAccessTokenTimeToLive = Duration.parse(oauthConfig.getDefaultAccessTokenTimeToLive()); + + for (OAuthClientType clientConfig : clientsConfig) { + RegisteredClient.Builder registeredClientBuilder = RegisteredClient.withId(clientConfig.getId()); + + if (clientConfig.getClientId() != null) { + registeredClientBuilder.clientId(clientConfig.getClientId()); + } + + if (clientConfig.getClientName() != null) { + registeredClientBuilder.clientName(clientConfig.getClientName()); + } + + if (clientConfig.getClientAuthenticationMethod() != null) { + for (OAuthClientAuthenticationMethodEnum method : clientConfig.getClientAuthenticationMethod()) { + registeredClientBuilder.clientAuthenticationMethod(new ClientAuthenticationMethod(method.value())); + } + } + + if (clientConfig.getAuthorizationGrantType() != null) { + for (OAuthAuthorizationGrantTypeEnum type : clientConfig.getAuthorizationGrantType()) { + registeredClientBuilder.authorizationGrantType(new AuthorizationGrantType(type.value())); + } + } + + if (clientConfig.getScope() != null) { + for (OAuthScopeEnum scope : clientConfig.getScope()) { + registeredClientBuilder.scope(scope.value()); + } + } + + OAuthClientSettingsType clientSettingsConfig = clientConfig.getClientSettings(); + + if (clientSettingsConfig != null) { + ClientSettings.Builder clientSettingsBuilder = ClientSettings.builder(); + + if (clientSettingsConfig.isRequireAuthorizationConsent() != null) { + clientSettingsBuilder.requireAuthorizationConsent(clientSettingsConfig.isRequireAuthorizationConsent()); + } + + registeredClientBuilder.clientSettings(clientSettingsBuilder.build()); + } + + OAuthTokenSettingsType tokenSettingsConfig = clientConfig.getTokenSettings(); + + if (tokenSettingsConfig != null) { + TokenSettings.Builder tokenSettingsBuilder = TokenSettings.builder(); + + if (tokenSettingsConfig.getAccessTokenTimeToLive() != null) { + tokenSettingsBuilder.accessTokenTimeToLive(Duration.parse(tokenSettingsConfig.getAccessTokenTimeToLive())); + } else { + tokenSettingsBuilder.accessTokenTimeToLive(defaultAccessTokenTimeToLive); + } + + registeredClientBuilder.tokenSettings(tokenSettingsBuilder.build()); + } + + if (clientConfig.getRedirectUri() != null) { + for (String redirectUri : clientConfig.getRedirectUri()) { + registeredClientBuilder.redirectUri(redirectUri); + } + } + + if (clientConfig.getId().equals("cspace-ui")) { + populateUIRedirectUris(registeredClientBuilder); + } + + registeredClientRepository.save(registeredClientBuilder.build()); + } + + return registeredClientRepository; + } + + private void populateUIRedirectUris(RegisteredClient.Builder registeredClientBuilder) { + // Add the configured authorization success and logout success URLs for each active tenant + // to the allowed redirect URIs for the OAuth client. + + TenantBindingConfigReaderImpl tenantBindingConfigReader = ServiceMain.getInstance().getTenantBindingConfigReader(); + + for (TenantBindingType tenantBinding : tenantBindingConfigReader.getTenantBindings().values()) { + try { + // Add allowed post-authorization redirects from tenant config. + + registeredClientBuilder.redirectUri(ConfigUtils.getUIAuthorizationSuccessUrl(tenantBinding)); + } catch (MalformedURLException e) { + logger.warn( + "Malformed authorizationSuccessUrl in tenant bindings config: name={} id={}", + tenantBinding.getName(), + tenantBinding.getId() + ); + } + + try { + // Add allowed post-logout redirects from tenant config. + + // TODO: RegisteredClient.Builder#postLogoutRedirectUri is available in Spring Authorization + // Server 1.1, and should be used for this when we upgrade. For now we store the allowed + // post-logout redirects alongside the allowed post-authorization redirects. + + registeredClientBuilder.redirectUri(ConfigUtils.getUILogoutSuccessUrl(tenantBinding)); + } catch (MalformedURLException e) { + logger.warn( + "Malformed logoutSuccessUrl in tenant bindings config: name={} id={}", + tenantBinding.getName(), + tenantBinding.getId() + ); + } + } + } + + private static KeyPair generateRsaKey() { + KeyPair keyPair; + + try { + KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA"); + keyPairGenerator.initialize(2048); + keyPair = keyPairGenerator.generateKeyPair(); + } + catch (Exception ex) { + throw new IllegalStateException(ex); + } + + return keyPair; + } + + @Bean + public JWKSource jwkSource() { + KeyPair keyPair = generateRsaKey(); + + RSAPublicKey publicKey = (RSAPublicKey) keyPair.getPublic(); + RSAPrivateKey privateKey = (RSAPrivateKey) keyPair.getPrivate(); + + RSAKey rsaKey = new RSAKey.Builder(publicKey) + .privateKey(privateKey) + .keyID(UUID.randomUUID().toString()) + .build(); + + JWKSet jwkSet = new JWKSet(rsaKey); + + return new ImmutableJWKSet<>(jwkSet); + } + + @Bean + public JwtDecoder jwtDecoder(JWKSource jwkSource) { + return OAuth2AuthorizationServerConfiguration.jwtDecoder(jwkSource); + } + + @Bean + public UserDetailsService userDetailsService() { + Map options = new HashMap(); + + options.put("dsJndiName", "CspaceDS"); + options.put("principalsQuery", "select passwd from users where username=?"); + options.put("saltQuery", "select salt from users where username=?"); + options.put("rolesQuery", "select r.rolename from roles as r, accounts_roles as ar where ar.user_id=? and ar.role_id=r.csid"); + options.put("tenantsQueryWithDisabled", "select t.id, t.name from accounts_common as a, accounts_tenants as at, tenants as t where a.userid=? and a.csid = at.TENANTS_ACCOUNTS_COMMON_CSID and at.tenant_id = t.id order by t.id"); + options.put("tenantsQueryNoDisabled", "select t.id, t.name from accounts_common as a, accounts_tenants as at, tenants as t where a.userid=? and a.csid = at.TENANTS_ACCOUNTS_COMMON_CSID and at.tenant_id = t.id and NOT t.disabled order by t.id"); + options.put("maxRetrySeconds", 5000); + options.put("delayBetweenAttemptsMillis", 200); + + return new CSpaceUserDetailsService(new CSpaceDbRealm(options)); + } +} diff --git a/services/common/src/main/java/org/collectionspace/services/common/security/SecurityInterceptor.java b/services/common/src/main/java/org/collectionspace/services/common/security/SecurityInterceptor.java index 87aca18f7..bdcf75c77 100644 --- a/services/common/src/main/java/org/collectionspace/services/common/security/SecurityInterceptor.java +++ b/services/common/src/main/java/org/collectionspace/services/common/security/SecurityInterceptor.java @@ -27,15 +27,12 @@ */ package org.collectionspace.services.common.security; +import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.security.Principal; import java.util.HashMap; import java.util.Set; - - - -//import org.jboss.resteasy.core.ResourceMethod; import org.jboss.resteasy.core.ResourceMethodInvoker; import org.jboss.resteasy.core.ServerResponse; import org.jboss.resteasy.spi.interception.PostProcessInterceptor; @@ -63,8 +60,9 @@ import org.collectionspace.services.common.CollectionSpaceResource; import org.collectionspace.services.common.ServiceMain; import org.collectionspace.services.common.document.JaxbUtils; import org.collectionspace.services.common.storage.jpa.JpaStorageUtils; -import org.collectionspace.services.common.security.SecurityUtils; import org.collectionspace.services.config.tenant.TenantBindingType; +import org.collectionspace.services.login.LoginClient; +import org.collectionspace.services.logout.LogoutClient; import org.collectionspace.services.systeminfo.SystemInfoClient; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -85,6 +83,8 @@ public class SecurityInterceptor implements PreProcessInterceptor, PostProcessIn /** The Constant logger. */ private static final Logger logger = LoggerFactory.getLogger(SecurityInterceptor.class); + private static final String LOGIN = LoginClient.SERVICE_NAME; + private static final String LOGOUT = LogoutClient.SERVICE_NAME; private static final String SYSTEM_INFO = SystemInfoClient.SERVICE_NAME; private static final String NUXEO_ADMIN = null; // @@ -105,7 +105,10 @@ public class SecurityInterceptor implements PreProcessInterceptor, PostProcessIn switch (resName) { case AuthZ.PASSWORD_RESET: case AuthZ.PROCESS_PASSWORD_RESET: + case LOGIN: + case LOGOUT: case SYSTEM_INFO: + case "": return true; } @@ -155,6 +158,7 @@ public class SecurityInterceptor implements PreProcessInterceptor, PostProcessIn @Override public ServerResponse preProcess(HttpRequest request, ResourceMethodInvoker resourceMethodInvoker) throws Failure, CSWebApplicationException { + ServerResponse result = null; // A null value essentially means success for this method Method resourceMethod = resourceMethodInvoker.getMethod(); @@ -329,8 +333,8 @@ public class SecurityInterceptor implements PreProcessInterceptor, PostProcessIn throw new CSWebApplicationException(response); } } - - } catch (Exception e) { + } + catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException e) { String msg = "User's account is in invalid state, userId=" + userId; Response response = Response.status( Response.Status.FORBIDDEN).entity(msg).type("text/plain").build(); diff --git a/services/common/src/main/java/org/collectionspace/services/common/security/SecurityUtils.java b/services/common/src/main/java/org/collectionspace/services/common/security/SecurityUtils.java index 062f25f72..f4938f782 100644 --- a/services/common/src/main/java/org/collectionspace/services/common/security/SecurityUtils.java +++ b/services/common/src/main/java/org/collectionspace/services/common/security/SecurityUtils.java @@ -38,6 +38,7 @@ import org.collectionspace.services.client.workflow.WorkflowClient; import org.collectionspace.services.common.api.Tools; import org.collectionspace.services.config.service.ServiceBindingType; import org.collectionspace.authentication.AuthN; +import org.collectionspace.authentication.spring.CSpacePasswordEncoderFactory; import javax.ws.rs.core.MultivaluedMap; import javax.ws.rs.core.UriInfo; @@ -45,40 +46,12 @@ import javax.ws.rs.core.UriInfo; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import org.springframework.security.authentication.encoding.BasePasswordEncoder; +import org.springframework.security.crypto.password.PasswordEncoder; import org.jboss.crypto.digest.DigestCallback; import org.jboss.resteasy.spi.HttpRequest; import org.jboss.security.Base64Encoder; import org.jboss.security.Base64Utils; -/** - * Extends Spring Security's base class for encoding passwords. We use only the - * mergePasswordAndSalt() method. - * @author remillet - * - */ -class CSpacePasswordEncoder extends BasePasswordEncoder { - public CSpacePasswordEncoder() { - //Do nothing - } - - String mergePasswordAndSalt(String password, String salt) { - return this.mergePasswordAndSalt(password, salt, false); - } - - @Override - public String encodePassword(String rawPass, Object salt) { - // TODO Auto-generated method stub - return null; - } - - @Override - public boolean isPasswordValid(String encPass, String rawPass, Object salt) { - // TODO Auto-generated method stub - return false; - } -} - /** * * @author @@ -99,18 +72,13 @@ public class SecurityUtils { /** * createPasswordHash creates password has using configured digest algorithm * and encoding - * @param user * @param password in cleartext * @return hashed password */ - public static String createPasswordHash(String username, String password, String salt) { - //TODO: externalize digest algo and encoding - return createPasswordHash("SHA-256", //digest algo - "base64", //encoding - null, //charset - username, - password, - salt); + public static String createPasswordHash(String password) { + PasswordEncoder encoder = CSpacePasswordEncoderFactory.createDefaultPasswordEncoder(); + + return encoder.encode(password); } /** @@ -351,118 +319,4 @@ public class SecurityUtils { return result; } - - public static String createPasswordHash(String hashAlgorithm, String hashEncoding, String hashCharset, - String username, String password, String salt) - { - return createPasswordHash(hashAlgorithm, hashEncoding, hashCharset, username, password, salt, null); - } - - public static String createPasswordHash(String hashAlgorithm, String hashEncoding, String hashCharset, - String username, String password, String salt, DigestCallback callback) - { - CSpacePasswordEncoder passwordEncoder = new CSpacePasswordEncoder(); - String saltedPassword = passwordEncoder.mergePasswordAndSalt(password, salt); // - - String passwordHash = null; - byte passBytes[]; - try - { - if(hashCharset == null) - passBytes = saltedPassword.getBytes(); - else - passBytes = saltedPassword.getBytes(hashCharset); - } - catch(UnsupportedEncodingException uee) - { - logger.error((new StringBuilder()).append("charset ").append(hashCharset).append(" not found. Using platform default.").toString(), uee); - passBytes = saltedPassword.getBytes(); - } - try - { - MessageDigest md = MessageDigest.getInstance(hashAlgorithm); - if(callback != null) - callback.preDigest(md); - md.update(passBytes); - if(callback != null) - callback.postDigest(md); - byte hash[] = md.digest(); - if(hashEncoding.equalsIgnoreCase("BASE64")) - passwordHash = encodeBase64(hash); - else - if(hashEncoding.equalsIgnoreCase("HEX")) - passwordHash = encodeBase16(hash); - else - if(hashEncoding.equalsIgnoreCase("RFC2617")) - passwordHash = encodeRFC2617(hash); - else - logger.error((new StringBuilder()).append("Unsupported hash encoding format ").append(hashEncoding).toString()); - } - catch(Exception e) - { - logger.error("Password hash calculation failed ", e); - } - return passwordHash; - } - - public static String encodeRFC2617(byte data[]) - { - char hash[] = new char[32]; - for(int i = 0; i < 16; i++) - { - int j = data[i] >> 4 & 0xf; - hash[i * 2] = MD5_HEX[j]; - j = data[i] & 0xf; - hash[i * 2 + 1] = MD5_HEX[j]; - } - - return new String(hash); - } - - public static String encodeBase16(byte bytes[]) - { - StringBuffer sb = new StringBuffer(bytes.length * 2); - for(int i = 0; i < bytes.length; i++) - { - byte b = bytes[i]; - char c = (char)(b >> 4 & 0xf); - if(c > '\t') - c = (char)((c - 10) + 97); - else - c += '0'; - sb.append(c); - c = (char)(b & 0xf); - if(c > '\t') - c = (char)((c - 10) + 97); - else - c += '0'; - sb.append(c); - } - - return sb.toString(); - } - - public static String encodeBase64(byte bytes[]) - { - String base64 = null; - try - { - base64 = Base64Encoder.encode(bytes); - } - catch(Exception e) { } - return base64; - } - - public static String tob64(byte buffer[]) - { - return Base64Utils.tob64(buffer); - } - - public static byte[] fromb64(String str) - throws NumberFormatException - { - return Base64Utils.fromb64(str); - } - - } diff --git a/services/common/src/main/java/org/collectionspace/services/common/storage/spring/DataSourceConfiguration.java b/services/common/src/main/java/org/collectionspace/services/common/storage/spring/DataSourceConfiguration.java new file mode 100644 index 000000000..d71af7e24 --- /dev/null +++ b/services/common/src/main/java/org/collectionspace/services/common/storage/spring/DataSourceConfiguration.java @@ -0,0 +1,18 @@ +package org.collectionspace.services.common.storage.spring; + +import javax.naming.NamingException; +import javax.sql.DataSource; + +import org.collectionspace.services.common.storage.JDBCTools; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class DataSourceConfiguration { + @Bean + @Qualifier("cspaceDataSource") + public DataSource cspaceDataSource() throws NamingException { + return JDBCTools.getDataSource(JDBCTools.CSPACE_DATASOURCE_NAME); + } +} diff --git a/services/config/src/main/java/org/collectionspace/services/common/config/AbstractConfigReaderImpl.java b/services/config/src/main/java/org/collectionspace/services/common/config/AbstractConfigReaderImpl.java index 8afe58795..eff40cbf4 100644 --- a/services/config/src/main/java/org/collectionspace/services/common/config/AbstractConfigReaderImpl.java +++ b/services/config/src/main/java/org/collectionspace/services/common/config/AbstractConfigReaderImpl.java @@ -63,6 +63,9 @@ public abstract class AbstractConfigReaderImpl implements ConfigReader { @Override abstract public void read(String configFile, boolean useAppGeneratedBindings) throws Exception; + @Override + abstract public void read(List configFiles, boolean useAppGeneratedBindings) throws Exception; + @Override abstract public T getConfiguration(); diff --git a/services/config/src/main/java/org/collectionspace/services/common/config/ConfigReader.java b/services/config/src/main/java/org/collectionspace/services/common/config/ConfigReader.java index 6bc2e4de4..3a1f537ce 100644 --- a/services/config/src/main/java/org/collectionspace/services/common/config/ConfigReader.java +++ b/services/config/src/main/java/org/collectionspace/services/common/config/ConfigReader.java @@ -24,6 +24,8 @@ package org.collectionspace.services.common.config; import java.io.File; +import java.util.List; + import org.collectionspace.services.common.api.JEEServerDeployment; /** @@ -54,6 +56,13 @@ public interface ConfigReader { */ public void read(String configFile, boolean useAppGeneratedBindings) throws Exception; + /** + * Merge the given configuration files, and parse the result. + * @param configFiles fully qualified file names + * @throws Exception + */ + public void read(List configFiles, boolean useAppGeneratedBindings) throws Exception; + /** * getConfig get configuration binding * @return diff --git a/services/config/src/main/java/org/collectionspace/services/common/config/ConfigUtils.java b/services/config/src/main/java/org/collectionspace/services/common/config/ConfigUtils.java index 3792c2df5..ab9ad75d4 100644 --- a/services/config/src/main/java/org/collectionspace/services/common/config/ConfigUtils.java +++ b/services/config/src/main/java/org/collectionspace/services/common/config/ConfigUtils.java @@ -1,16 +1,25 @@ package org.collectionspace.services.common.config; +import java.net.MalformedURLException; +import java.time.Duration; import java.util.ArrayList; import java.util.List; +import org.collectionspace.services.config.CORSType; +import org.collectionspace.services.config.OAuthClientRegistrationsType; +import org.collectionspace.services.config.OAuthClientType; +import org.collectionspace.services.config.OAuthType; +import org.collectionspace.services.config.SecurityType; +import org.collectionspace.services.config.ServiceConfig; import org.collectionspace.services.config.tenant.RepositoryDomainType; import org.collectionspace.services.config.tenant.TenantBindingType; +import org.collectionspace.services.config.tenant.UIConfig; import org.slf4j.Logger; import org.slf4j.LoggerFactory; public class ConfigUtils { final static Logger logger = LoggerFactory.getLogger(ConfigUtils.class); - + public static final String EXTENSION_XPATH = "/extension[@point='%s']"; public static final String COMPONENT_EXTENSION_XPATH = "/component" + EXTENSION_XPATH; public static final String DATASOURCE_EXTENSION_POINT_XPATH = String.format(COMPONENT_EXTENSION_XPATH, "datasources"); @@ -19,21 +28,21 @@ public class ConfigUtils { public static final String CONFIGURATION_EXTENSION_POINT_XPATH = String.format(COMPONENT_EXTENSION_XPATH, "configuration"); public static final String ELASTICSEARCH_INDEX_EXTENSION_XPATH = String.format(EXTENSION_XPATH, "elasticSearchIndex"); public static final String ELASTICSEARCH_EXTENSIONS_EXPANDER_STR = "%elasticSearchIndex_extensions%"; - - + + // Default database names - + // public static String DEFAULT_CSPACE_DATABASE_NAME = "cspace"; public static String DEFAULT_NUXEO_REPOSITORY_NAME = "default"; public static String DEFAULT_NUXEO_DATABASE_NAME = "nuxeo"; public static String DEFAULT_ELASTICSEARCH_INDEX_NAME = "nuxeo"; - + /* * Returns the list of repository/DB names defined by a tenant bindings file */ public static List getRepositoryNameList(TenantBindingType tenantBindingType) { List result = null; - + List repoDomainList = tenantBindingType.getRepositoryDomain(); if (repoDomainList != null && repoDomainList.isEmpty() == false) { result = new ArrayList(); @@ -41,10 +50,10 @@ public class ConfigUtils { result.add(repoDomain.getRepositoryName()); } } - + return result; } - + /* * Returns 'true' if the tenant declares the default repository. */ @@ -59,13 +68,13 @@ public class ConfigUtils { } } } - + return result; } - + public static String getRepositoryName(TenantBindingType tenantBindingType, String domainName) { String result = null; - + if (domainName != null && domainName.trim().isEmpty() == false) { List repoDomainList = tenantBindingType.getRepositoryDomain(); if (repoDomainList != null && repoDomainList.isEmpty() == false) { @@ -84,8 +93,103 @@ public class ConfigUtils { logger.trace(String.format("Could not find the repository name for tenent name='%s' and domain='%s'", tenantBindingType.getName(), domainName)); } - + return result; } - + + public static CORSType getCors(ServiceConfig serviceConfig) { + SecurityType security = serviceConfig.getSecurity(); + + if (security != null) { + CORSType cors = security.getCors(); + + return cors; + } + + return null; + } + + public static List getCorsAllowedOrigins(ServiceConfig serviceConfig) { + CORSType cors = getCors(serviceConfig); + + if (cors != null) { + List allowedOrigin = cors.getAllowedOrigin(); + + if (allowedOrigin != null) { + return allowedOrigin; + } + } + + return new ArrayList(); + } + + public static Duration getCorsMaxAge(ServiceConfig serviceConfig) { + CORSType cors = getCors(serviceConfig); + + if (cors != null) { + String maxAge = cors.getMaxAge(); + + if (maxAge != null) { + return Duration.parse(maxAge); + } + } + + return null; + } + + public static OAuthType getOAuth(ServiceConfig serviceConfig) { + SecurityType security = serviceConfig.getSecurity(); + + if (security != null) { + OAuthType oauth = security.getOauth(); + + return oauth; + } + + return null; + } + + public static List getOAuthClientRegistrations(ServiceConfig serviceConfig) { + OAuthType oauth = getOAuth(serviceConfig); + + if (oauth != null) { + OAuthClientRegistrationsType registrations = oauth.getClientRegistrations(); + + if (registrations != null) { + return registrations.getClient(); + } + } + + return null; + } + + public static String getUILoginSuccessUrl(TenantBindingType tenantBinding) throws MalformedURLException { + UIConfig uiConfig = tenantBinding.getUiConfig(); + + if (uiConfig != null) { + return uiConfig.getBaseUrl() + uiConfig.getLoginSuccessUrl(); + } + + return null; + } + + public static String getUIAuthorizationSuccessUrl(TenantBindingType tenantBinding) throws MalformedURLException { + UIConfig uiConfig = tenantBinding.getUiConfig(); + + if (uiConfig != null) { + return uiConfig.getBaseUrl() + uiConfig.getAuthorizationSuccessUrl(); + } + + return null; + } + + public static String getUILogoutSuccessUrl(TenantBindingType tenantBinding) throws MalformedURLException { + UIConfig uiConfig = tenantBinding.getUiConfig(); + + if (uiConfig != null) { + return uiConfig.getBaseUrl() + uiConfig.getLogoutSuccessUrl(); + } + + return null; + } } diff --git a/services/config/src/main/java/org/collectionspace/services/common/config/ServicesConfigReaderImpl.java b/services/config/src/main/java/org/collectionspace/services/common/config/ServicesConfigReaderImpl.java index d92a96adc..883437321 100644 --- a/services/config/src/main/java/org/collectionspace/services/common/config/ServicesConfigReaderImpl.java +++ b/services/config/src/main/java/org/collectionspace/services/common/config/ServicesConfigReaderImpl.java @@ -24,12 +24,28 @@ package org.collectionspace.services.common.config; import java.io.File; - +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.Comparator; +import java.util.List; +import java.util.function.Consumer; +import java.util.function.Predicate; + +import org.apache.commons.io.FileUtils; +import org.collectionspace.services.common.api.JEEServerDeployment; import org.collectionspace.services.config.ClientType; import org.collectionspace.services.config.ServiceConfig; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import ch.elca.el4j.services.xmlmerge.Configurer; +import ch.elca.el4j.services.xmlmerge.config.AttributeMergeConfigurer; +import ch.elca.el4j.services.xmlmerge.config.ConfigurableXmlMerge; + /** * ServicesConfigReader reads service layer specific configuration * @@ -39,7 +55,11 @@ import org.slf4j.LoggerFactory; public class ServicesConfigReaderImpl extends AbstractConfigReaderImpl { - final private static String CONFIG_FILE_NAME = "service-config.xml"; + private static final String CONFIG_FILE_NAME = "service-config.xml"; + private static final String SECURITY_CONFIG_FILE_NAME = "service-config-security.xml"; + private static final String LOCAL_CONFIG_DIR_NAME = "local"; + private static final String MERGED_FILE_NAME = "service-config.merged.xml"; + final Logger logger = LoggerFactory.getLogger(ServicesConfigReaderImpl.class); private ServiceConfig serviceConfig; private ClientType clientType; @@ -56,34 +76,80 @@ public class ServicesConfigReaderImpl @Override public void read(boolean useAppGeneratedBindings) throws Exception { - String configFileName = getAbsoluteFileName(CONFIG_FILE_NAME); - read(configFileName, useAppGeneratedBindings); + String localConfigDirName = getAbsoluteFileName(LOCAL_CONFIG_DIR_NAME); + File localConfigDir = new File(localConfigDirName); + List localXmlConfigFiles = new ArrayList<>(); + + if (localConfigDir.exists()) { + List localConfigDirFiles = getFiles(localConfigDir); + + Collections.sort(localConfigDirFiles, new Comparator() { + @Override + public int compare(File file1, File file2) { + return file1.getName().compareTo(file2.getName()); + } + }); + + for (File candidateFile : localConfigDirFiles) { + if (candidateFile.getName().endsWith(".xml")) { + localXmlConfigFiles.add(candidateFile.getAbsolutePath()); + } + } + } + + List configFileNames = new ArrayList<>(); + + configFileNames.add(getAbsoluteFileName(CONFIG_FILE_NAME)); + configFileNames.add(getAbsoluteFileName(SECURITY_CONFIG_FILE_NAME)); + configFileNames.addAll(localXmlConfigFiles); + + read(configFileNames, useAppGeneratedBindings); } @Override public void read(String configFileName, boolean useAppGeneratedBindings) throws Exception { - if (logger.isDebugEnabled()) { - logger.debug("read() config file=" + configFileName); + read(Arrays.asList(configFileName), useAppGeneratedBindings); + } + + @Override + public void read(List configFileNames, boolean useAppGeneratedBindings) throws Exception { + List files = new ArrayList(); + + for (String configFileName : configFileNames) { + File configFile = new File(configFileName); + + if (configFile.exists()) { + logger.info("Using config file " + configFileName); + + files.add(configFile); + } else { + logger.warn("Could not find config file " + configFile.getAbsolutePath()); + } } - File configFile = new File(configFileName); - if (!configFile.exists()) { - String msg = "Could not find configuration file " + configFile.getAbsolutePath(); //configFileName; - logger.error(msg); - throw new RuntimeException(msg); + + if (files.size() == 0) { + throw new RuntimeException("No config files found"); } - serviceConfig = (ServiceConfig) parse(configFile, ServiceConfig.class); + + InputStream mergedConfigStream = merge(files); + + serviceConfig = (ServiceConfig) parse(mergedConfigStream, ServiceConfig.class); clientType = serviceConfig.getRepositoryClient().getClientType(); + if (clientType == null) { String msg = "Missing in "; logger.error(msg); throw new IllegalArgumentException(msg); } + clientClassName = serviceConfig.getRepositoryClient().getClientClass(); + if (clientClassName == null) { String msg = "Missing in "; logger.error(msg); throw new IllegalArgumentException(msg); } + if (logger.isDebugEnabled()) { logger.debug("using client=" + clientType.toString() + " class=" + clientClassName); } @@ -101,4 +167,40 @@ public class ServicesConfigReaderImpl public String getClientClass() { return clientClassName; } + + private InputStream merge(List files) throws IOException { + InputStream result = null; + List inputStreams = new ArrayList<>(); + + for (File file : files) { + inputStreams.add(new FileInputStream(file)); + } + + try { + Configurer configurer = new AttributeMergeConfigurer(); + + result = new ConfigurableXmlMerge(configurer).merge(inputStreams.toArray(new InputStream[0])); + } catch (Exception e) { + logger.error("Could not merge configuration files", e); + } + + // Save the merge output to a file that is suffixed with ".merged.xml" in the same + // directory as the first file. + + if (result != null) { + File outputDir = files.get(0).getParentFile(); + String mergedFileName = outputDir.getAbsolutePath() + File.separator + MERGED_FILE_NAME; + File mergedOutFile = new File(mergedFileName); + + try { + FileUtils.copyInputStreamToFile(result, mergedOutFile); + } catch (IOException e) { + logger.warn("Could not create a copy of the merged configuration at: " + mergedFileName, e); + } + + result.reset(); + } + + return result; + } } diff --git a/services/config/src/main/resources/service-config.xsd b/services/config/src/main/resources/service-config.xsd index 0f57a48c5..c3e57baf4 100644 --- a/services/config/src/main/resources/service-config.xsd +++ b/services/config/src/main/resources/service-config.xsd @@ -10,7 +10,7 @@ Schema for service layer configuration --> - - + - - + + + - - + + - - + + - + - + - + @@ -60,17 +61,17 @@ - + - + - + - + @@ -79,6 +80,101 @@ + + + Configures security. + + + + + + - + + + + + + + + + + + + + + + + + The default TTL for access tokens, if not specified in the token settings of + the client registration. + + Specified in ISO-8601 duration format: PnDTnHnMn.nS + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/services/config/src/main/resources/tenant.xsd b/services/config/src/main/resources/tenant.xsd index 586ea2cab..6f4018708 100644 --- a/services/config/src/main/resources/tenant.xsd +++ b/services/config/src/main/resources/tenant.xsd @@ -51,6 +51,7 @@ + @@ -67,6 +68,8 @@ + + @@ -121,6 +124,55 @@ + + + Configuration of the CollectionSpace UI. + + + + + + + The base URL of the UI. Other configured URLs are appended this URL. + This should end with a slash. Example: http://core.dev.collectionspace.org/cspace/core/ + + + + + + + + The URL in the UI that should be opened after a login to the tenant succeeds, + relative to the baseUrl (see above). + See LoginResource.rootRedirect. + + + + + + + + The URL in the UI that can be opened after an OAuth authorization succeeds, + relative to the baseUrl (see above). + This is not used directly by the services; it is only added to the list of + URIs to which clients are permitted to ask for a redirect. + + + + + + + + The URL in the UI that can be opened after a logout succeeds, relative to + the baseUrl (see above). + This is not used directly by the services; it is only added to the list of + URIs to which clients are permitted to ask for a redirect. + + + + + + Configuration of a tenant's Elasticsearch index @@ -161,7 +213,6 @@ - diff --git a/services/login/client/pom.xml b/services/login/client/pom.xml new file mode 100644 index 000000000..338b51ac9 --- /dev/null +++ b/services/login/client/pom.xml @@ -0,0 +1,18 @@ + + + + org.collectionspace.services + org.collectionspace.services.login + ${revision} + + + 4.0.0 + org.collectionspace.services.login.client + services.login.client + + + collectionspace-services-login-client + + diff --git a/services/login/client/src/main/java/org/collectionspace/services/login/LoginClient.java b/services/login/client/src/main/java/org/collectionspace/services/login/LoginClient.java new file mode 100644 index 000000000..be501198b --- /dev/null +++ b/services/login/client/src/main/java/org/collectionspace/services/login/LoginClient.java @@ -0,0 +1,7 @@ +package org.collectionspace.services.login; + +public class LoginClient { + public static final String SERVICE_NAME = "login"; + public static final String SERVICE_PATH_COMPONENT = SERVICE_NAME; + public static final String SERVICE_PATH = "/" + SERVICE_PATH_COMPONENT; +} diff --git a/services/login/pom.xml b/services/login/pom.xml new file mode 100644 index 000000000..d1b2f217f --- /dev/null +++ b/services/login/pom.xml @@ -0,0 +1,18 @@ + + + + org.collectionspace.services + org.collectionspace.services.main + ${revision} + + + 4.0.0 + org.collectionspace.services.login + services.login + pom + + + client + service + + diff --git a/services/login/service/pom.xml b/services/login/service/pom.xml new file mode 100644 index 000000000..62935bd69 --- /dev/null +++ b/services/login/service/pom.xml @@ -0,0 +1,54 @@ + + + + + org.collectionspace.services + org.collectionspace.services.login + ${revision} + + + 4.0.0 + org.collectionspace.services.login.service + services.login.service + jar + + + + org.collectionspace.services + org.collectionspace.services.common + ${project.version} + + + org.collectionspace.services + org.collectionspace.services.authentication.service + ${project.version} + provided + + + org.collectionspace.services + org.collectionspace.services.login.client + ${project.version} + + + + org.apache.tomcat + tomcat-servlet-api + ${tomcat.version} + provided + + + org.jboss.resteasy + resteasy-jaxrs + + + org.springframework.security + spring-security-web + ${spring.security.version} + provided + + + + + collectionspace-services-login-service + + diff --git a/services/login/service/src/main/java/org/collectionspace/services/login/LoginResource.java b/services/login/service/src/main/java/org/collectionspace/services/login/LoginResource.java new file mode 100644 index 000000000..cdb2d22a9 --- /dev/null +++ b/services/login/service/src/main/java/org/collectionspace/services/login/LoginResource.java @@ -0,0 +1,142 @@ +package org.collectionspace.services.login; + +import java.io.IOException; +import java.io.StringWriter; +import java.io.Writer; +import java.net.MalformedURLException; +import java.net.URI; +import java.net.URISyntaxException; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpSession; +import javax.ws.rs.GET; +import javax.ws.rs.Path; +import javax.ws.rs.Produces; +import javax.ws.rs.core.Context; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; + +import org.collectionspace.authentication.AuthN; +import org.collectionspace.services.common.ServiceMain; +import org.collectionspace.services.common.config.ConfigUtils; +import org.collectionspace.services.common.config.TenantBindingConfigReaderImpl; +import org.collectionspace.services.config.ServiceConfig; +import org.collectionspace.services.config.tenant.TenantBindingType; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.security.core.AuthenticationException; +import org.springframework.security.web.WebAttributes; +import org.springframework.security.web.csrf.CsrfToken; +import org.springframework.security.web.savedrequest.HttpSessionRequestCache; +import org.springframework.security.web.savedrequest.SavedRequest; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; + +import freemarker.core.ParseException; +import freemarker.template.Configuration; +import freemarker.template.MalformedTemplateNameException; +import freemarker.template.Template; +import freemarker.template.TemplateException; +import freemarker.template.TemplateNotFoundException; + +@Path("/") +public class LoginResource { + final Logger logger = LoggerFactory.getLogger(LoginResource.class); + + @GET + public Response rootRedirect() throws URISyntaxException, MalformedURLException { + String tenantId = AuthN.get().getCurrentTenantId(); + TenantBindingConfigReaderImpl tenantBindingConfigReader = ServiceMain.getInstance().getTenantBindingConfigReader(); + TenantBindingType tenantBinding = tenantBindingConfigReader.getTenantBinding(tenantId); + URI uri = new URI(ConfigUtils.getUILoginSuccessUrl(tenantBinding)); + + return Response.temporaryRedirect(uri).build(); + } + + @GET + @Path(LoginClient.SERVICE_PATH) + @Produces(MediaType.TEXT_HTML) + public String getHtml(@Context HttpServletRequest request) throws TemplateNotFoundException, MalformedTemplateNameException, ParseException, IOException, TemplateException { + ServiceConfig serviceConfig = ServiceMain.getInstance().getServiceConfig(); + + Map uiConfig = new HashMap<>(); + CsrfToken csrfToken = (CsrfToken) request.getAttribute(CsrfToken.class.getName()); + + if (csrfToken != null) { + Map csrfConfig = new HashMap<>(); + + csrfConfig.put("parameterName", csrfToken.getParameterName()); + csrfConfig.put("token", csrfToken.getToken()); + + uiConfig.put("csrf", csrfConfig); + } + + String tid = null; + SavedRequest savedRequest = new HttpSessionRequestCache().getRequest(request, null); + + if (savedRequest != null) { + String[] tidValues = savedRequest.getParameterValues(AuthN.TENANT_ID_QUERY_PARAM); + + if (tidValues != null && tidValues.length > 0) { + tid = tidValues[0]; + } + } + + if (tid != null) { + uiConfig.put("tenantId", tid); + } + + if (request.getParameter("error") != null) { + uiConfig.put("error", getLoginErrorMessage(request)); + } + + if (request.getParameter("logout") != null) { + uiConfig.put("isLogoutSuccess", true); + } + + String uiConfigJS; + + try { + uiConfigJS = new ObjectMapper().writeValueAsString(uiConfig); + } catch (JsonProcessingException e) { + logger.error("Error generating login page UI configuration", e); + + uiConfigJS = ""; + } + + Map dataModel = new HashMap<>(); + + dataModel.put("uiConfig", uiConfigJS); + + Configuration freeMarkerConfig = ServiceMain.getInstance().getFreeMarkerConfig(); + Template template = freeMarkerConfig.getTemplate("service-ui.ftlh"); + Writer out = new StringWriter(); + + template.process(dataModel, out); + + out.close(); + + return out.toString(); + } + + private String getLoginErrorMessage(HttpServletRequest request) { + HttpSession session = request.getSession(false); + + if (session != null) { + Object exception = session.getAttribute(WebAttributes.AUTHENTICATION_EXCEPTION); + + if (exception != null && exception instanceof AuthenticationException) { + AuthenticationException authException = (AuthenticationException) exception; + + return authException.getMessage(); + } + } + + return "Invalid credentials"; + } +} diff --git a/services/logout/client/pom.xml b/services/logout/client/pom.xml new file mode 100644 index 000000000..839deae48 --- /dev/null +++ b/services/logout/client/pom.xml @@ -0,0 +1,18 @@ + + + + org.collectionspace.services + org.collectionspace.services.logout + ${revision} + + + 4.0.0 + org.collectionspace.services.logout.client + services.logout.client + + + collectionspace-services-logout-client + + diff --git a/services/logout/client/src/main/java/org/collectionspace/services/logout/LogoutClient.java b/services/logout/client/src/main/java/org/collectionspace/services/logout/LogoutClient.java new file mode 100644 index 000000000..be03581ab --- /dev/null +++ b/services/logout/client/src/main/java/org/collectionspace/services/logout/LogoutClient.java @@ -0,0 +1,7 @@ +package org.collectionspace.services.logout; + +public class LogoutClient { + public static final String SERVICE_NAME = "logout"; + public static final String SERVICE_PATH_COMPONENT = SERVICE_NAME; + public static final String SERVICE_PATH = "/" + SERVICE_PATH_COMPONENT; +} diff --git a/services/logout/pom.xml b/services/logout/pom.xml new file mode 100644 index 000000000..e0f6bab09 --- /dev/null +++ b/services/logout/pom.xml @@ -0,0 +1,18 @@ + + + + org.collectionspace.services + org.collectionspace.services.main + ${revision} + + + 4.0.0 + org.collectionspace.services.logout + services.logout + pom + + + client + service + + diff --git a/services/logout/service/pom.xml b/services/logout/service/pom.xml new file mode 100644 index 000000000..fab7d1292 --- /dev/null +++ b/services/logout/service/pom.xml @@ -0,0 +1,54 @@ + + + + + org.collectionspace.services + org.collectionspace.services.logout + ${revision} + + + 4.0.0 + org.collectionspace.services.logout.service + services.logout.service + jar + + + + org.collectionspace.services + org.collectionspace.services.common + ${project.version} + + + org.collectionspace.services + org.collectionspace.services.authentication.service + ${project.version} + provided + + + org.collectionspace.services + org.collectionspace.services.logout.client + ${project.version} + + + + org.apache.tomcat + tomcat-servlet-api + ${tomcat.version} + provided + + + org.jboss.resteasy + resteasy-jaxrs + + + org.springframework.security + spring-security-web + ${spring.security.version} + provided + + + + + collectionspace-services-logout-service + + diff --git a/services/logout/service/src/main/java/org/collectionspace/services/logout/LogoutResource.java b/services/logout/service/src/main/java/org/collectionspace/services/logout/LogoutResource.java new file mode 100644 index 000000000..f9fba20ff --- /dev/null +++ b/services/logout/service/src/main/java/org/collectionspace/services/logout/LogoutResource.java @@ -0,0 +1,77 @@ +package org.collectionspace.services.logout; + +import java.io.IOException; +import java.io.StringWriter; +import java.io.Writer; +import java.util.HashMap; +import java.util.Map; + + +import javax.servlet.http.HttpServletRequest; +import javax.ws.rs.GET; +import javax.ws.rs.Path; +import javax.ws.rs.Produces; +import javax.ws.rs.core.Context; +import javax.ws.rs.core.MediaType; + +import org.collectionspace.services.common.ServiceMain; +import org.collectionspace.services.config.ServiceConfig; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.security.web.csrf.CsrfToken; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; + +import freemarker.core.ParseException; +import freemarker.template.Configuration; +import freemarker.template.MalformedTemplateNameException; +import freemarker.template.Template; +import freemarker.template.TemplateException; +import freemarker.template.TemplateNotFoundException; + +@Path(LogoutClient.SERVICE_PATH) +public class LogoutResource { + final Logger logger = LoggerFactory.getLogger(LogoutResource.class); + + @GET + @Produces(MediaType.TEXT_HTML) + public String getHtml(/* @Context UriInfo ui, */ @Context HttpServletRequest request) throws TemplateNotFoundException, MalformedTemplateNameException, ParseException, IOException, TemplateException { + Map uiConfig = new HashMap<>(); + + CsrfToken csrfToken = (CsrfToken) request.getAttribute(CsrfToken.class.getName()); + + if (csrfToken != null) { + Map csrfConfig = new HashMap<>(); + + csrfConfig.put("parameterName", csrfToken.getParameterName()); + csrfConfig.put("token", csrfToken.getToken()); + + uiConfig.put("csrf", csrfConfig); + } + + String uiConfigJS; + + try { + uiConfigJS = new ObjectMapper().writeValueAsString(uiConfig); + } catch (JsonProcessingException e) { + logger.error("Error generating login page UI configuration", e); + + uiConfigJS = ""; + } + + Map dataModel = new HashMap<>(); + + dataModel.put("uiConfig", uiConfigJS); + + Configuration freeMarkerConfig = ServiceMain.getInstance().getFreeMarkerConfig(); + Template template = freeMarkerConfig.getTemplate("service-ui.ftlh"); + Writer out = new StringWriter(); + + template.process(dataModel, out); + + out.close(); + + return out.toString(); + } +} diff --git a/services/pom.xml b/services/pom.xml index e5462fa7b..90d73faa2 100644 --- a/services/pom.xml +++ b/services/pom.xml @@ -100,6 +100,8 @@ IntegrationTests PerformanceTests security + login + logout JaxRsServiceProvider diff --git a/services/systeminfo/client/src/main/java/org/collectionspace/services/systeminfo/SystemInfoClient.java b/services/systeminfo/client/src/main/java/org/collectionspace/services/systeminfo/SystemInfoClient.java index 99455ef5a..1a319e30a 100644 --- a/services/systeminfo/client/src/main/java/org/collectionspace/services/systeminfo/SystemInfoClient.java +++ b/services/systeminfo/client/src/main/java/org/collectionspace/services/systeminfo/SystemInfoClient.java @@ -1,7 +1,7 @@ package org.collectionspace.services.systeminfo; /** - * Client class for Structureddate service. + * Client class for system info service. * @author remillet * */