]> git.aero2k.de Git - tmp/jakarta-migration.git/commitdiff
Upgrade spring and spring security. (#365)
authorRay Lee <ray.lee@lyrasis.org>
Thu, 21 Sep 2023 03:53:27 +0000 (23:53 -0400)
committerGitHub <noreply@github.com>
Thu, 21 Sep 2023 03:53:27 +0000 (23:53 -0400)
150 files changed:
.gitignore
3rdparty/nuxeo/nuxeo-server/9.10-HF30/lib/jackson-core-2.7.9.jar [deleted file]
build.properties
build.xml
cspace-ui/build.xml
cspace-ui/build_js.sh
cspace-ui/service-ui.ftlh [new file with mode: 0644]
pom.xml
services/IntegrationTests/src/test/java/org/collectionspace/services/IntegrationTests/test/JsonIntegrationTest.java
services/IntegrationTests/src/test/resources/test-data/xmlreplay/xml-replay-master.xml
services/JaxRsServiceProvider/pom.xml
services/JaxRsServiceProvider/src/main/java/org/collectionspace/services/jaxrs/CollectionSpaceJaxRsApplication.java
services/JaxRsServiceProvider/src/main/webapp/META-INF/context.xml
services/JaxRsServiceProvider/src/main/webapp/WEB-INF/applicationContext-security.xml
services/JaxRsServiceProvider/src/main/webapp/WEB-INF/oauth-servlet.xml [deleted file]
services/JaxRsServiceProvider/src/main/webapp/WEB-INF/web.xml
services/account/client/src/main/java/org/collectionspace/services/client/AccountClient.java
services/account/jaxb/src/main/resources/accounts_common.xsd
services/account/jaxb/src/main/resources/accounts_common_list.xsd
services/account/service/pom.xml
services/account/service/src/main/java/org/collectionspace/services/account/AccountResource.java
services/account/service/src/main/java/org/collectionspace/services/account/storage/AccountDocumentHandler.java
services/account/service/src/main/java/org/collectionspace/services/account/storage/AccountValidatorHandler.java
services/account/service/src/main/java/org/collectionspace/services/account/storage/csidp/TokenStorageClient.java
services/account/service/src/main/java/org/collectionspace/services/account/storage/csidp/UserStorageClient.java
services/authentication/pstore/src/main/resources/db/postgresql/authentication.sql
services/authentication/service/pom.xml
services/authentication/service/src/main/java/org/collectionspace/authentication/CSpaceAuthenticationSuccessEvent.java
services/authentication/service/src/main/java/org/collectionspace/authentication/CSpaceSaltSource.java [deleted file]
services/authentication/service/src/main/java/org/collectionspace/authentication/CSpaceTenant.java
services/authentication/service/src/main/java/org/collectionspace/authentication/CSpaceUser.java
services/authentication/service/src/main/java/org/collectionspace/authentication/jackson2/CSpaceTenantDeserializer.java [new file with mode: 0644]
services/authentication/service/src/main/java/org/collectionspace/authentication/jackson2/CSpaceUserDeserializer.java [new file with mode: 0644]
services/authentication/service/src/main/java/org/collectionspace/authentication/realm/CSpaceRealm.java
services/authentication/service/src/main/java/org/collectionspace/authentication/realm/db/CSpaceDbRealm.java
services/authentication/service/src/main/java/org/collectionspace/authentication/spring/CSpaceJwtAuthenticationToken.java [new file with mode: 0644]
services/authentication/service/src/main/java/org/collectionspace/authentication/spring/CSpaceLogoutSuccessHandler.java [new file with mode: 0644]
services/authentication/service/src/main/java/org/collectionspace/authentication/spring/CSpacePasswordEncoderFactory.java [new file with mode: 0644]
services/authentication/service/src/main/java/org/collectionspace/authentication/spring/CSpaceUserAttributeFilter.java
services/authentication/service/src/main/java/org/collectionspace/authentication/spring/CSpaceUserAuthenticationConverter.java [deleted file]
services/authentication/service/src/main/java/org/collectionspace/authentication/spring/CSpaceUserDetailsService.java
services/authentication/service/src/main/java/org/collectionspace/authentication/spring/SpringAuthNContext.java
services/authorization-mgt/import/build.xml
services/authorization/pstore/src/main/resources/db/postgresql/README.txt
services/authorization/pstore/src/main/resources/db/postgresql/acl.sql
services/authorization/pstore/src/main/resources/db/postgresql/authorization.sql
services/authorization/service/pom.xml
services/authorization/service/src/main/java/org/collectionspace/services/authorization/spring/CSpaceOAuth2RequestFactory.java [deleted file]
services/common-api/src/main/java/org/collectionspace/services/common/api/Tools.java
services/common/build.xml
services/common/lib/spring/commons-codec-1.10.jar [new file with mode: 0644]
services/common/lib/spring/cryptacular-1.1.4.jar [new file with mode: 0644]
services/common/lib/spring/guava-20.0.jar [new file with mode: 0644]
services/common/lib/spring/httpclient-4.5.13.jar [new file with mode: 0644]
services/common/lib/spring/httpcore-4.4.13.jar [new file with mode: 0644]
services/common/lib/spring/jackson-annotations-2.14.3.jar [new file with mode: 0644]
services/common/lib/spring/jackson-annotations-2.8.0.jar [deleted file]
services/common/lib/spring/jackson-core-2.14.3.jar [new file with mode: 0644]
services/common/lib/spring/jackson-core-2.8.0.jar [deleted file]
services/common/lib/spring/jackson-databind-2.14.3.jar [new file with mode: 0644]
services/common/lib/spring/jackson-databind-2.8.0.jar [deleted file]
services/common/lib/spring/jackson-datatype-jsr310-2.14.3.jar [new file with mode: 0644]
services/common/lib/spring/java-support-7.5.2.jar [new file with mode: 0644]
services/common/lib/spring/joda-time-2.9.jar [new file with mode: 0644]
services/common/lib/spring/metrics-core-3.1.5.jar [new file with mode: 0644]
services/common/lib/spring/nimbus-jose-jwt-9.24.4.jar [new file with mode: 0644]
services/common/lib/spring/opensaml-core-3.4.6.jar [new file with mode: 0644]
services/common/lib/spring/opensaml-messaging-api-3.4.6.jar [new file with mode: 0644]
services/common/lib/spring/opensaml-profile-api-3.4.6.jar [new file with mode: 0644]
services/common/lib/spring/opensaml-saml-api-3.4.6.jar [new file with mode: 0644]
services/common/lib/spring/opensaml-saml-impl-3.4.6.jar [new file with mode: 0644]
services/common/lib/spring/opensaml-security-api-3.4.6.jar [new file with mode: 0644]
services/common/lib/spring/opensaml-security-impl-3.4.6.jar [new file with mode: 0644]
services/common/lib/spring/opensaml-soap-api-3.4.6.jar [new file with mode: 0644]
services/common/lib/spring/opensaml-storage-api-3.4.6.jar [new file with mode: 0644]
services/common/lib/spring/opensaml-xmlsec-api-3.4.6.jar [new file with mode: 0644]
services/common/lib/spring/opensaml-xmlsec-impl-3.4.6.jar [new file with mode: 0644]
services/common/lib/spring/spring-aop-4.3.16.RELEASE.jar [deleted file]
services/common/lib/spring/spring-aop-5.3.28.jar [new file with mode: 0644]
services/common/lib/spring/spring-beans-4.3.16.RELEASE.jar [deleted file]
services/common/lib/spring/spring-beans-5.3.28.jar [new file with mode: 0644]
services/common/lib/spring/spring-context-4.3.16.RELEASE.jar [deleted file]
services/common/lib/spring/spring-context-5.3.28.jar [new file with mode: 0644]
services/common/lib/spring/spring-context-support-4.3.16.RELEASE.jar [deleted file]
services/common/lib/spring/spring-context-support-5.3.28.jar [new file with mode: 0644]
services/common/lib/spring/spring-core-4.3.16.RELEASE.jar [deleted file]
services/common/lib/spring/spring-core-5.3.28.jar [new file with mode: 0644]
services/common/lib/spring/spring-expression-4.3.16.RELEASE.jar [deleted file]
services/common/lib/spring/spring-expression-5.3.28.jar [new file with mode: 0644]
services/common/lib/spring/spring-instrument-4.3.1.RELEASE.jar [deleted file]
services/common/lib/spring/spring-jdbc-4.3.16.RELEASE.jar [deleted file]
services/common/lib/spring/spring-jdbc-5.3.28.jar [new file with mode: 0644]
services/common/lib/spring/spring-security-acl-4.2.5.RELEASE.jar [deleted file]
services/common/lib/spring/spring-security-acl-5.8.4.jar [new file with mode: 0644]
services/common/lib/spring/spring-security-config-4.2.5.RELEASE.jar [deleted file]
services/common/lib/spring/spring-security-config-5.8.4.jar [new file with mode: 0644]
services/common/lib/spring/spring-security-core-4.2.5.RELEASE.jar [deleted file]
services/common/lib/spring/spring-security-core-5.8.4.jar [new file with mode: 0644]
services/common/lib/spring/spring-security-crypto-5.8.4.jar [new file with mode: 0644]
services/common/lib/spring/spring-security-jwt-1.0.4.RELEASE.jar [deleted file]
services/common/lib/spring/spring-security-oauth2-2.0.10.RELEASE.jar [deleted file]
services/common/lib/spring/spring-security-oauth2-authorization-server-0.4.3.jar [new file with mode: 0644]
services/common/lib/spring/spring-security-oauth2-core-5.8.4.jar [new file with mode: 0644]
services/common/lib/spring/spring-security-oauth2-jose-5.8.4.jar [new file with mode: 0644]
services/common/lib/spring/spring-security-oauth2-resource-server-5.8.4.jar [new file with mode: 0644]
services/common/lib/spring/spring-security-saml2-service-provider-5.8.4.jar [new file with mode: 0644]
services/common/lib/spring/spring-security-web-4.2.5.RELEASE.jar [deleted file]
services/common/lib/spring/spring-security-web-5.8.4.jar [new file with mode: 0644]
services/common/lib/spring/spring-tx-4.3.16.RELEASE.jar [deleted file]
services/common/lib/spring/spring-tx-5.3.28.jar [new file with mode: 0644]
services/common/lib/spring/spring-web-4.3.16.RELEASE.jar [deleted file]
services/common/lib/spring/spring-web-5.3.28.jar [new file with mode: 0644]
services/common/lib/spring/spring-webmvc-4.3.16.RELEASE.jar [deleted file]
services/common/lib/spring/xmlsec-2.0.10.jar [new file with mode: 0644]
services/common/pom.xml
services/common/src/main/cspace/config/services/service-config-security.xml [new file with mode: 0644]
services/common/src/main/cspace/config/services/service-config.xml
services/common/src/main/cspace/config/services/tenants/bonsai/bonsai-tenant-bindings.delta.xml
services/common/src/main/cspace/config/services/tenants/botgarden/botgarden-tenant-bindings.delta.xml
services/common/src/main/cspace/config/services/tenants/core/core-tenant-bindings.delta.xml
services/common/src/main/cspace/config/services/tenants/dvp/dvp-tenant-bindings.delta.xml [deleted file]
services/common/src/main/cspace/config/services/tenants/lhmc/lhmc-tenant-bindings.delta.xml
services/common/src/main/cspace/config/services/tenants/materials/materials-tenant-bindings.delta.xml
services/common/src/main/cspace/config/services/tenants/tenant-bindings-proto-unified.xml
services/common/src/main/cspace/config/services/tenants/testsci/testsci-tenant-bindings.delta.xml
services/common/src/main/java/org/collectionspace/services/common/ServiceMain.java
services/common/src/main/java/org/collectionspace/services/common/authorization_mgt/AuthorizationCommon.java
services/common/src/main/java/org/collectionspace/services/common/config/TenantBindingConfigReaderImpl.java
services/common/src/main/java/org/collectionspace/services/common/security/SecurityConfig.java [new file with mode: 0644]
services/common/src/main/java/org/collectionspace/services/common/security/SecurityInterceptor.java
services/common/src/main/java/org/collectionspace/services/common/security/SecurityUtils.java
services/common/src/main/java/org/collectionspace/services/common/storage/spring/DataSourceConfiguration.java [new file with mode: 0644]
services/config/src/main/java/org/collectionspace/services/common/config/AbstractConfigReaderImpl.java
services/config/src/main/java/org/collectionspace/services/common/config/ConfigReader.java
services/config/src/main/java/org/collectionspace/services/common/config/ConfigUtils.java
services/config/src/main/java/org/collectionspace/services/common/config/ServicesConfigReaderImpl.java
services/config/src/main/resources/service-config.xsd
services/config/src/main/resources/tenant.xsd
services/login/client/pom.xml [new file with mode: 0644]
services/login/client/src/main/java/org/collectionspace/services/login/LoginClient.java [new file with mode: 0644]
services/login/pom.xml [new file with mode: 0644]
services/login/service/pom.xml [new file with mode: 0644]
services/login/service/src/main/java/org/collectionspace/services/login/LoginResource.java [new file with mode: 0644]
services/logout/client/pom.xml [new file with mode: 0644]
services/logout/client/src/main/java/org/collectionspace/services/logout/LogoutClient.java [new file with mode: 0644]
services/logout/pom.xml [new file with mode: 0644]
services/logout/service/pom.xml [new file with mode: 0644]
services/logout/service/src/main/java/org/collectionspace/services/logout/LogoutResource.java [new file with mode: 0644]
services/pom.xml
services/systeminfo/client/src/main/java/org/collectionspace/services/systeminfo/SystemInfoClient.java

index 479e62cfc58f0548e75db779017b191bb09688e5..c2a6a58d8a5f3bcb16404d98a6e08fbd61888b4d 100644 (file)
@@ -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 (file)
index e87c08a..0000000
Binary files a/3rdparty/nuxeo/nuxeo-server/9.10-HF30/lib/jackson-core-2.7.9.jar and /dev/null differ
index a973faef579abec73bc02d94b5b38ba466f4b3a3..5d1206ce2a25fe57999279ceac0e21e5e9d48136 100644 (file)
@@ -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
index a2cd3e2b3ac0c1fb5fd53d3d3deb4f226fda71f0..aa244070164254507d08f4058afe319838559a8c 100644 (file)
--- a/build.xml
+++ b/build.xml
                <delete failonerror="false" dir="${jee.server.cspace}/cspace/services/config" />
                <delete failonerror="false" dir="${jee.server.cspace}/cspace/services/scripts" />
                <delete failonerror="false" dir="${jee.server.cspace}/cspace/services/db/jdbc_drivers" />
-               <delete failonerror="false" dir="${jee.server.cspace}/cspace/config/services" />
+               <delete failonerror="false">
+                       <fileset dir="${jee.server.cspace}/cspace/config/services" excludes="local/**" />
+               </delete>
 
                <!-- Delete mysql-ds.xml to clean up pre-1.8 bundles -->
                <delete failonerror="false" file="${jee.deploy.cspace}/mysql-ds.xml" />
index 4ae1cc20d1ab30d963ecec2f77fe2b84832b4597..3fceeabe6a25ac3f0a85b22f010558dd5b219880 100644 (file)
@@ -47,7 +47,7 @@
        <target name="install_nvm" if="${cspace.ui.build}">
                <exec executable="bash" failonerror="true">
                        <arg value="-c" />
-                       <arg line='"curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.3/install.sh | bash"' />
+                       <arg line='"curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.5/install.sh | bash"' />
                </exec>
        </target>
 
@@ -61,7 +61,7 @@
        <target name="build_cspace_ui_js" depends="ensure_staging_dir,ensure_source_dir,install_node" if="${cspace.ui.build}">
                <exec executable="bash" failonerror="true">
                        <arg value="-c" />
-                       <arg value="./build_js.sh ${cspace.ui.package.name} ${cspace.ui.build.branch} ${source.dir} ${staging.dir} ${cspace.ui.library.name}"/>
+                       <arg value="./build_js.sh ${cspace.ui.package.name} ${cspace.ui.build.branch} ${source.dir} ${staging.dir} ${cspace.ui.library.name} ${service.ui.library.name}"/>
                </exec>
        </target>
 
                </exec>
        </target>
 
+       <target name="download_service_ui_js" depends="ensure_staging_dir" unless="${cspace.ui.build}">
+               <exec executable="curl" failonerror="true">
+                       <arg line="-o ${staging.dir}/${service.ui.library.name}@${cspace.ui.version}.min.js --fail --insecure --location https://cdn.jsdelivr.net/npm/cspace-ui@${cspace.ui.version}/dist/${service.ui.library.name}.min.js"/>
+               </exec>
+       </target>
+
        <target name="deploy_cspace_ui_js" depends="build_cspace_ui_js,download_cspace_ui_js">
                <pathconvert property="cspace.ui.install.filename" targetos="unix">
                        <first>
                <copy file="${staging.dir}/${cspace.ui.install.filename}" todir="${jee.deploy.cspace.ui.shared}" />
        </target>
 
+       <target name="deploy_service_ui_js" depends="build_cspace_ui_js,download_service_ui_js">
+               <pathconvert property="service.ui.install.filename" targetos="unix">
+                       <first>
+                               <fileset dir="${staging.dir}" includes="${service.ui.library.name}@*.min.js" />
+                       </first>
+
+                       <mapper type="flatten" />
+               </pathconvert>
+
+               <copy file="${staging.dir}/${service.ui.install.filename}" todir="${jee.deploy.cspace.ui.shared}" />
+       </target>
+
        <target name="deploy_tenants" depends="deploy_cspace_ui_js">
                <subant target="deploy_tenant" genericantfile="${ant.file}" inheritall="true">
                        <dirset dir="." includes="*" excludes="${build.dir.name}" />
                </copy>
        </target>
 
+       <target name="deploy_service_ui_template" depends="deploy_service_ui_js">
+               <filter token="SERVICE_UI_FILENAME" value="${service.ui.install.filename}" />
+
+               <copy file="service-ui.ftlh" todir="${jee.server.cspace}/cspace/config/services/resources/templates" filtering="true" overwrite="true" />
+       </target>
+
+       <target name="undeploy_service_ui_template">
+               <delete file="${jee.server.cspace}/cspace/config/services/resources/templates/service-ui.ftlh" />
+       </target>
+
        <target name="undeploy_tenants">
                <subant target="undeploy_tenant" genericantfile="${ant.file}" inheritall="true">
                        <dirset dir="." includes="*" />
                <delete dir="${jee.deploy.cspace.ui.shared}" />
        </target>
 
-       <target name="deploy" depends="clean,deploy_tenants" description="deploy cspace ui to ${jee.server.cspace}" />
+       <target name="deploy" depends="clean,deploy_service_ui_js,deploy_service_ui_template,deploy_tenants" description="deploy cspace ui to ${jee.server.cspace}" />
 
-       <target name="undeploy" depends="undeploy_tenants,undeploy_js" description="undeploy collectionspace ui components from ${jee.server.cspace}" />
+       <target name="undeploy" depends="undeploy_service_ui_template,undeploy_tenants,undeploy_js" description="undeploy collectionspace ui components from ${jee.server.cspace}" />
 </project>
index 2bc2b4cd4b9e77e89575c82f2d22bff04455ea11..147d6ea9824047e9e7416b8b469fb2a1260cff88 100755 (executable)
@@ -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 (file)
index 0000000..f563ffb
--- /dev/null
@@ -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.
+-->
+<html>
+       <head>
+               <meta charset="UTF-8" />
+       </head>
+       <body>
+               <div id="cspace"></div>
+               <script src="/cspace-ui/@SERVICE_UI_FILENAME@"></script>
+               <script>
+                       cspaceUI(
+<#outputformat "JavaScript">
+                               ${uiConfig}
+</#outputformat>
+                       );
+               </script>
+       </body>
+</html>
diff --git a/pom.xml b/pom.xml
index 194be9de2c337be34ce11925e4bb9e5554d7af05..5a7f39845a097c7b9f5c83affdcc1c21efa4a1c4 100644 (file)
--- a/pom.xml
+++ b/pom.xml
                <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
                <cspace.services.version>${revision}</cspace.services.version>
                <cspace.services.client.version>${revision}</cspace.services.client.version>
+               <jackson.version>2.14.3</jackson.version>
                <nuxeo.general.release>9.10-HF30</nuxeo.general.release>
                <nuxeo.shell.version>${nuxeo.general.release}</nuxeo.shell.version>
                <nuxeo.platform.version>${nuxeo.general.release}</nuxeo.platform.version>
                <nuxeo.core.version>${nuxeo.general.release}</nuxeo.core.version>
                <chemistry.opencmis.version.nx>0.12.0-NX2</chemistry.opencmis.version.nx>
-               <spring.version>4.3.16.RELEASE</spring.version>
-               <spring.security.version>4.1.1.RELEASE</spring.security.version>
-               <spring.security.oauth2.version>2.0.10.RELEASE</spring.security.oauth2.version>
+               <spring.version>5.3.28</spring.version>
+               <spring.security.version>5.8.4</spring.security.version>
+               <spring.security.authorization.server.version>0.4.3</spring.security.authorization.server.version>
                <aspectj.version>1.7.4</aspectj.version>
                <log4j.version>2.17.1</log4j.version>
        </properties>
                                <scope>provided</scope>
                        </dependency>
 
+                       <!-- Jackson -->
+
+                       <dependency>
+                               <groupId>com.fasterxml.jackson.core</groupId>
+                               <artifactId>jackson-core</artifactId>
+                               <version>${jackson.version}</version>
+                               <scope>provided</scope>
+                       </dependency>
+                       <dependency>
+                               <groupId>com.fasterxml.jackson.core</groupId>
+                               <artifactId>jackson-databind</artifactId>
+                               <version>${jackson.version}</version>
+                               <scope>provided</scope>
+                       </dependency>
+                       <dependency>
+                               <groupId>com.fasterxml.jackson.core</groupId>
+                               <artifactId>jackson-annotations</artifactId>
+                               <version>${jackson.version}</version>
+                               <scope>provided</scope>
+                       </dependency>
+                       <dependency>
+                               <groupId>com.fasterxml.jackson.datatype</groupId>
+                               <artifactId>jackson-datatype-jsr310</artifactId>
+                               <version>${jackson.version}</version>
+                               <scope>provided</scope>
+                       </dependency>
                </dependencies>
        </dependencyManagement>
 
index fb14d63087d6a1e13d29dc8378c73a35be8435f6..6b56485c1c1e06d2834cebbc64cd196bcc26afb8 100644 (file)
@@ -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<String, String> 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<String>() {
+                @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<Integer>() {
+                @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<String, String> getLoginForm(String path) throws ClientProtocolException, IOException {
+        return authExecutor.execute(Request.Get(getUrl(path)))
+            .handleResponse(new ResponseHandler<Pair<String, String>>() {
+                @Override
+                public Pair<String, 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());
+                    }
+
+                    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<String>() {
+                @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<String> {
+    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<String>() {
+                @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<JsonNode> {
 
         @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<Integer> {
+    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());
     }
 }
index 83d598153799b26c41ab03be508cb6c357253ef2..81a6bdc0348ac37337ec182ffef305967370ad18 100644 (file)
@@ -16,7 +16,8 @@
         <auth ID="admin@core.collectionspace.org">YWRtaW5AY29yZS5jb2xsZWN0aW9uc3BhY2Uub3JnOkFkbWluaXN0cmF0b3I=</auth>
     </auths>
 
-    <run controlFile="security/security-oauth.xml" />
+    <!-- FIXME: Update these tests and re-enable. -->
+    <!-- <run controlFile="security/security-oauth.xml" /> -->
     <run controlFile="security/security.xml" />
     <run controlFile="objectexit/object-exit.xml" testGroup="makeone" />
     <run controlFile="objectexit/object-exit.xml" testGroup="checkList" />
index 2f45fcb8adbcc08cfcea7da3a33db244796c2bb4..cd82ee78c78fea0d215b8d8b813e704ca63d1d1e 100644 (file)
                        <version>6.6.1</version>
                </dependency>
 
-        <!-- CollectionSpace dependencies -->
-        <dependency>
-            <groupId>org.collectionspace.services</groupId>
-            <artifactId>org.collectionspace.services.authorization.service</artifactId>
-            <version>${project.version}</version>
-        </dependency>
-        <dependency>
+               <!-- CollectionSpace dependencies -->
+
+               <dependency>
                        <groupId>org.collectionspace.services</groupId>
-               <artifactId>org.collectionspace.services.authentication.service</artifactId>
-               <version>${project.version}</version>
-               <exclusions>
+                       <artifactId>org.collectionspace.services.authorization.service</artifactId>
+                       <version>${project.version}</version>
+               </dependency>
+               <dependency>
+                       <groupId>org.collectionspace.services</groupId>
+                       <artifactId>org.collectionspace.services.authentication.service</artifactId>
+                       <version>${project.version}</version>
+                       <scope>provided</scope>
+                       <exclusions>
                                <exclusion>
                                        <artifactId>servlet-api-2.5</artifactId>
                                        <groupId>org.mortbay.jetty</groupId>
             <groupId>org.collectionspace.services</groupId>
             <artifactId>org.collectionspace.services.account.service</artifactId>
             <version>${project.version}</version>
-        </dependency>
+        </dependency>   
         <dependency>
             <groupId>org.collectionspace.services</groupId>
             <artifactId>org.collectionspace.services.authorization-mgt.service</artifactId>
             <artifactId>org.collectionspace.services.loanout.service</artifactId>
             <version>${project.version}</version>
         </dependency>
+        <dependency>
+            <groupId>org.collectionspace.services</groupId>
+            <artifactId>org.collectionspace.services.login.service</artifactId>
+            <version>${project.version}</version>
+        </dependency>
+        <dependency>
+            <groupId>org.collectionspace.services</groupId>
+            <artifactId>org.collectionspace.services.logout.service</artifactId>
+            <version>${project.version}</version>
+        </dependency>
         <dependency>
             <groupId>org.collectionspace.services</groupId>
             <artifactId>org.collectionspace.services.transport.service</artifactId>
             </exclusions>
         </dependency>
         <dependency>
-            <groupId>org.springframework.security.oauth</groupId>
-            <artifactId>spring-security-oauth2</artifactId>
-            <version>${spring.security.oauth2.version}</version>
+            <groupId>org.springframework.security</groupId>
+            <artifactId>spring-security-oauth2-authorization-server</artifactId>
+            <version>${spring.security.authorization.server.version}</version>
             <scope>provided</scope>
             <exclusions>
-               <exclusion>
-                       <artifactId>spring-core</artifactId>
-                       <groupId>org.springframework</groupId>
-               </exclusion>
-               <exclusion>
-                       <artifactId>spring-beans</artifactId>
-                       <groupId>org.springframework</groupId>
-               </exclusion>
+                <exclusion>
+                    <artifactId>spring-core</artifactId>
+                    <groupId>org.springframework</groupId>
+                </exclusion>
+                <exclusion>
+                    <artifactId>spring-beans</artifactId>
+                    <groupId>org.springframework</groupId>
+                </exclusion>
             </exclusions>
         </dependency>
         <dependency>
index 506f56393f70299fac57d5c5c3c776adc6289b86..0ef40bd9b7a758d4c90ba00ec6a2d7a9f1313723 100644 (file)
@@ -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());
index e0026b701468c7516dea5894533a8b24790b7f61..92518bba5c80dd92e4de9a4bbab0ac12d1a89271 100644 (file)
@@ -13,7 +13,7 @@
 
     <!-- Disable HTTP Session persistence between restart since webengine session objects are not serializable -->
     <Manager pathname=""/>
-
+       
     <!-- define custom loader that is responsible to start nuxeo runtime (it extends the default one) -->
 
     <!-- Disabled since these are specific to the default Nuxeo DM webapp
index 6ebc272ca6e41632c28d4086d4219fd86876d32e..65b4c2cd838159f1a0e6f1072fea20b1e4fd4de6 100644 (file)
 <beans xmlns="http://www.springframework.org/schema/beans"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xmlns:sec="http://www.springframework.org/schema/security"
-       xmlns:oauth="http://www.springframework.org/schema/security/oauth2"
        xmlns:util="http://www.springframework.org/schema/util"
        xmlns:context="http://www.springframework.org/schema/context"
        xsi:schemaLocation="
        http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/security http://www.springframework.org/schema/security/spring-security.xsd
-       http://www.springframework.org/schema/security/oauth2 http://www.springframework.org/schema/security/spring-security-oauth2.xsd
        http://www.springframework.org/schema/util http://www.springframework.org/schema/util/spring-util.xsd
        http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd">
 
-       <bean class="org.collectionspace.authentication.CSpaceAuthenticationSuccessEvent" />
+    <!-- Load Java configuration. -->
+    <context:annotation-config />
+    <context:component-scan base-package="org.collectionspace.services.common.storage.spring,org.collectionspace.services.common.security" />
 
-    <bean class="org.springframework.context.support.PropertySourcesPlaceholderConfigurer">
-        <!-- Read properties from security.properties file in the classpath. -->
-        <!-- Values in the file override the defaults set below. -->
-        <property name="ignoreResourceNotFound" value="true" />
-        <property name="locations" value="classpath:security.properties" />
+    <bean class="org.collectionspace.authentication.CSpaceAuthenticationSuccessEvent" />
 
-        <!-- Default property values. -->
-        <property name="properties">
-            <props>
-                <prop key="cors.allowed.origins"></prop>
-            </props>
-        </property>
-    </bean>
-
-    <!-- Convert string properties to complex types. -->
-    <bean id="conversionService" class="org.springframework.context.support.ConversionServiceFactoryBean" />
-
-    <!-- Require client id and client secret via basic auth when granting tokens (https://tools.ietf.org/html/rfc6749#section-4.3.2).
-         Note that public (https://tools.ietf.org/html/rfc6749#section-2.1) clients, such as the CSpace web UI, may supply a
-         blank or publicly known "secret." The clientAuthenticationManager bean handles this client authentication. -->
-    <sec:http pattern="/oauth/token/**" create-session="stateless" authentication-manager-ref="clientAuthenticationManager">
-        <sec:intercept-url pattern="/oauth/token/**" access="isFullyAuthenticated()"/>
-        <sec:http-basic entry-point-ref="clientAuthenticationEntryPoint"/>
-        <sec:anonymous enabled="false"/>
-        <sec:csrf disabled="true"/>
-        <sec:access-denied-handler ref="oauthAccessDeniedHandler"/>
-
-        <!-- Handle CORS (preflight OPTIONS requests must be anonymous) -->
-        <sec:intercept-url method="OPTIONS" pattern="/oauth/token/**" access="isAnonymous()"/>
-        <sec:cors configuration-source-ref="corsSource" />
-    </sec:http>
-
-    <sec:http realm="org.collectionspace.services" create-session="stateless" authentication-manager-ref="userAuthenticationManager">
-        <!-- Exclude the resource path to public items' content from AuthN and AuthZ. Lets us publish resources with anonymous access. -->
-        <sec:intercept-url pattern="/publicitems/*/*/content" access="permitAll" />
-
-        <!-- Exclude the resource path to handle an account password reset request from AuthN and AuthZ. Lets us process password resets anonymous access. -->
-        <sec:intercept-url pattern="/accounts/requestpasswordreset" access="permitAll" />
-
-        <!-- Exclude the resource path to account process a password resets from AuthN and AuthZ. Lets us process password resets anonymous access. -->
-        <sec:intercept-url pattern="/accounts/processpasswordreset" access="permitAll" />
-
-        <!-- Exclude the resource path to request system info -->
-        <sec:intercept-url pattern="/systeminfo" access="permitAll" />
-
-        <!-- All other paths must be authenticated. -->
-        <sec:intercept-url pattern="/**" access="isFullyAuthenticated()" />
-
-        <sec:http-basic />
-        <sec:anonymous username="anonymous" />
-        <sec:csrf disabled="true" />
-
-        <!-- Handle CORS (preflight OPTIONS requests must be anonymous) -->
-        <sec:intercept-url method="OPTIONS" pattern="/**" access="isAnonymous()"/>
-        <sec:cors configuration-source-ref="corsSource" />
-
-        <!-- Insert the username from the security context into a request attribute for logging -->
-        <sec:custom-filter ref="userAttributeFilter" after="SECURITY_CONTEXT_FILTER" />
-
-        <!-- Handle token auth -->
-        <sec:custom-filter ref="oauthResourceServerFilter" before="PRE_AUTH_FILTER" />
-    </sec:http>
-
-    <sec:authentication-manager id="userAuthenticationManager">
-        <sec:authentication-provider ref="daoAuthenticationProvider"/>
-    </sec:authentication-manager>
-
-    <bean id="daoAuthenticationProvider" class="org.springframework.security.authentication.dao.DaoAuthenticationProvider">
-        <property name="userDetailsService" ref="userDetailsService" />
-        <property name="saltSource" ref="saltSource"/>
-        <property name="passwordEncoder">
-            <bean class="org.springframework.security.authentication.encoding.ShaPasswordEncoder">
-                <constructor-arg value="256"/>
-                <property name="encodeHashAsBase64" value="true" />
-            </bean>
-        </property>
-    </bean>
-    
-    <bean id="saltSource" class="org.collectionspace.authentication.CSpaceSaltSource">
-        <property name="userPropertyToUse" value="salt" />
-    </bean>
-
-    <bean id="userDetailsService" class="org.collectionspace.authentication.spring.CSpaceUserDetailsService">
-        <constructor-arg>
-            <bean class="org.collectionspace.authentication.realm.db.CSpaceDbRealm">
-                <constructor-arg>
-                    <util:map>
-                        <entry key="dsJndiName" value="CspaceDS" />
-                        <entry key="principalsQuery" value="select passwd from users where username=?" />
-                        <entry key="saltQuery" value="select salt from users where username=?" />
-                        <entry key="rolesQuery" value="select r.rolename from roles as r, accounts_roles as ar where ar.user_id=? and ar.role_id=r.csid" />
-                        <entry key="tenantsQueryWithDisabled" value="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" />
-                        <entry key="tenantsQueryNoDisabled" value="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" />
-                        <entry key="maxRetrySeconds" value="5000" />
-                        <entry key="delayBetweenAttemptsMillis" value="200" />
-                    </util:map>
-                </constructor-arg>
-            </bean>
-        </constructor-arg>
-    </bean>
-
-    <oauth:resource-server id="oauthResourceServerFilter" resource-id="cspace-services" token-services-ref="tokenServices" />
-
-    <sec:authentication-manager id="clientAuthenticationManager">
-        <sec:authentication-provider user-service-ref="clientDetailsUserDetailsService"/>
-    </sec:authentication-manager>
-
-    <bean id="clientDetailsUserDetailsService" class="org.springframework.security.oauth2.provider.client.ClientDetailsUserDetailsService">
-        <constructor-arg ref="clientDetails"/>
-    </bean>
-
-    <!-- The scope attribute below is a meaningless placeholder. In the future we may want to use it to limit
-         the permissions of particular clients. Currently a client has the full permissions of the user on
-         whose behalf it is acting. -->
-    <oauth:client-details-service id="clientDetails">
-        <oauth:client
-            client-id="cspace-ui"
-            resource-ids="cspace-services"
-            authorized-grant-types="password,refresh_token"
-            scope="full"
-            access-token-validity="3600"
-            refresh-token-validity="43200" />
-    </oauth:client-details-service>
-
-    <bean id="clientAuthenticationEntryPoint" class="org.springframework.security.oauth2.provider.error.OAuth2AuthenticationEntryPoint">
-        <property name="realmName" value="org.collectionspace.services/client"/>
-        <property name="typeName" value="Basic"/>
-    </bean>
-
-    <bean id="oauthAccessDeniedHandler" class="org.springframework.security.oauth2.provider.error.OAuth2AccessDeniedHandler"/>
-
-    <bean id="tokenServices" class="org.springframework.security.oauth2.provider.token.DefaultTokenServices">
-        <property name="tokenStore" ref="tokenStore" />
-        <property name="tokenEnhancer" ref="tokenEnhancer" />
-        <property name="supportRefreshToken" value="true" />
-        <property name="clientDetailsService" ref="clientDetails" />
-    </bean>
-
-    <bean id="tokenStore" class="org.springframework.security.oauth2.provider.token.store.JwtTokenStore">
-        <constructor-arg ref="tokenEnhancer" />
-    </bean>
-
-    <bean id="tokenEnhancer" class="org.springframework.security.oauth2.provider.token.store.JwtAccessTokenConverter">
-        <!--
-            Can specify a signing key here. By default a random one is generated on bean instantiation,
-            which means that when CSpace is restarted, all granted tokens will become invalid. A
-            public/private key pair may also be supplied, using keyPair.
-        -->
-        <!-- <property name="signingKey" value="" /> -->
-        <property name="accessTokenConverter">
-            <bean class="org.springframework.security.oauth2.provider.token.DefaultAccessTokenConverter">
-                <property name="userTokenConverter">
-                    <bean class="org.collectionspace.authentication.spring.CSpaceUserAuthenticationConverter">
-                        <constructor-arg ref="userDetailsService" />
-                    </bean>
-                </property>
-            </bean>
-        </property>
-    </bean>
-
-    <bean id="corsSource" class="org.springframework.web.cors.UrlBasedCorsConfigurationSource">
-        <property name="corsConfigurations">
-            <util:map>
-                <entry key="/**">
-                    <bean class="org.springframework.web.cors.CorsConfiguration">
-                        <property name="allowCredentials" value="true" />
-                        <property name="allowedHeaders">
-                            <list>
-                                <value>Authorization</value>
-                                <value>Content-Type</value>
-                            </list>
-                        </property>
-                        <property name="allowedMethods">
-                            <list>
-                                <value>POST</value>
-                                <value>GET</value>
-                                <value>PUT</value>
-                                <value>DELETE</value>
-                            </list>
-                        </property>
-                        <property name="allowedOrigins" value="${cors.allowed.origins}" />
-                        <property name="exposedHeaders">
-                            <list>
-                                <value>Location</value>
-                                <value>Content-Disposition</value>
-                            </list>
-                        </property>
-                        <property name="maxAge" value="86400" />
-                    </bean>
-                </entry>
-            </util:map>
-        </property>
-    </bean>
-
-    <bean id="userAttributeFilter"
-        class="org.collectionspace.authentication.spring.CSpaceUserAttributeFilter">
-    </bean>
-    
-    <!-- Switches on the AOP (AspectJ) load-time weaving -->
-    <context:load-time-weaver/>
-    
+    <!-- Switch on AOP (AspectJ) load-time weaving. -->
+    <context:load-time-weaver />
 </beans>
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 (file)
index 2f37963..0000000
+++ /dev/null
@@ -1,35 +0,0 @@
-<beans xmlns="http://www.springframework.org/schema/beans"
-       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
-       xmlns:mvc="http://www.springframework.org/schema/mvc"
-       xmlns:oauth="http://www.springframework.org/schema/security/oauth2"
-       xsi:schemaLocation="
-       http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
-       http://www.springframework.org/schema/mvc http://www.springframework.org/schema/mvc/spring-mvc-3.1.xsd
-       http://www.springframework.org/schema/security/oauth2 http://www.springframework.org/schema/security/spring-security-oauth2.xsd">
-
-    <oauth:authorization-server
-        client-details-service-ref="clientDetails"
-        token-services-ref="tokenServices"
-        authorization-request-manager-ref="oauthRequestManager"
-    >
-        <oauth:refresh-token />
-        <oauth:password authentication-manager-ref="userAuthenticationManager" />
-    </oauth:authorization-server>
-
-    <mvc:annotation-driven />
-
-    <mvc:default-servlet-handler />
-
-    <bean id="oauthRequestManager" class="org.collectionspace.services.authorization.spring.CSpaceOAuth2RequestFactory">
-        <constructor-arg ref="clientDetails" />
-    </bean>
-
-    <bean id="viewResolver" class="org.springframework.web.servlet.view.ContentNegotiatingViewResolver">
-        <property name="defaultViews">
-            <list>
-                <bean class="org.springframework.web.servlet.view.xml.MappingJackson2XmlView" />
-                <bean class="org.springframework.web.servlet.view.json.MappingJackson2JsonView" />
-            </list>
-        </property>
-    </bean>
-</beans>
index 5e785a495fce24cdb63cd43d658344902a744482..f1749217aeed37edf71c763fc7a95f343a8a637c 100644 (file)
@@ -7,11 +7,11 @@
     Description:
         service layer web application
 -->
-<web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://java.sun.com/xml/ns/javaee" xmlns:web="http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd" xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd" id="WebApp_ID" version="2.5">    
+<web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://java.sun.com/xml/ns/javaee" xmlns:web="http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd" xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd" id="WebApp_ID" version="2.5">
     <display-name>CollectionSpace Services</display-name>
-       
+
        <env-entry>
-        <description>Sets the logging context for the Tiger web-app</description>
+        <description>Sets the logging context for the web-app</description>
         <env-entry-name>cspace-logging-context</env-entry-name>
         <env-entry-type>java.lang.String</env-entry-type>
         <env-entry-value>CSpaceLoggingContext</env-entry-value>
@@ -58,7 +58,7 @@
        -               delayBetweenAttemptsMillis - How long to wait between retries.
        -
        -->
-       <!-- 
+       <!--
        <filter>
                <filter-name>networkErrorRetryFilter</filter-name>
                <filter-class>org.collectionspace.services.common.NetworkErrorRetryFilter</filter-class>
                <param-value>200</param-value>
                </init-param>
        </filter>
-       
+
        <filter-mapping>
                <filter-name>networkErrorRetryFilter</filter-name>
                <url-pattern>/*</url-pattern>
        </filter-mapping>
      -->
-     
+
        <!--
                A filter that logs profiling information.
         -->
@@ -85,7 +85,7 @@
                <filter-name>CSpaceFilter</filter-name>
         <filter-class>org.collectionspace.services.common.profile.CSpaceFilter</filter-class>
        </filter>
-       
+
        <filter-mapping>
                <filter-name>CSpaceFilter</filter-name>
                <url-pattern>/*</url-pattern>
         <filter-name>JsonToXmlFilter</filter-name>
         <filter-class>org.collectionspace.services.common.xmljson.JsonToXmlFilter</filter-class>
     </filter>
-    
+
     <filter-mapping>
         <filter-name>JsonToXmlFilter</filter-name>
         <url-pattern>/*</url-pattern>
     </filter-mapping>
-    
+
     <!--
         A filter that converts XML responses to JSON if needed.
     -->
         <filter-name>XmlToJsonFilter</filter-name>
         <filter-class>org.collectionspace.services.common.xmljson.XmlToJsonFilter</filter-class>
     </filter>
-    
+
     <filter-mapping>
         <filter-name>XmlToJsonFilter</filter-name>
         <url-pattern>/*</url-pattern>
             org.collectionspace.services.common.CollectionSpaceServiceContextListener
         </listener-class>
     </listener>
-    
+
        <!-- The CollectionSpace listener that starts up the RESTEasy/JAX-RS service framework. -->
     <listener>
         <listener-class>
             org.collectionspace.services.jaxrs.CSpaceResteasyBootstrap
         </listener-class>
     </listener>
-
-    <servlet>
-        <servlet-name>oauth</servlet-name>
-        <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
-        <load-on-startup>1</load-on-startup>
-    </servlet>
-    <servlet-mapping>
-        <servlet-name>oauth</servlet-name>
-        <url-pattern>/oauth/token/*</url-pattern>
-    </servlet-mapping>
-
+    
     <servlet>
-            <servlet-name>Resteasy</servlet-name>
-            <servlet-class>
-                org.jboss.resteasy.plugins.server.servlet.HttpServletDispatcher
-            </servlet-class>
+        <servlet-name>Resteasy</servlet-name>
+        <servlet-class>
+            org.jboss.resteasy.plugins.server.servlet.HttpServletDispatcher
+        </servlet-class>
     </servlet>
     <servlet-mapping>
         <servlet-name>Resteasy</servlet-name>
index 6f9f6d9c0b633c3456db491c4bd32ba7b8da4bbd..950d301ef9f7b2c73e1fd3b46e04ec73fee58d94 100644 (file)
@@ -44,11 +44,15 @@ public class AccountClient extends AbstractServiceClientImpl<AccountsCommonList,
     public static final String SERVICE_PATH_COMPONENT = SERVICE_NAME;
     public static final String SERVICE_PATH = "/" + SERVICE_PATH_COMPONENT;
     public static final String SERVICE_COMMON_PART_NAME = SERVICE_NAME + PART_LABEL_SEPARATOR + PART_COMMON_LABEL;
-    public final static String IMMUTABLE = "immutable";
-    public final static String EMAIL_QUERY_PARAM = "email";
+    public static final String IMMUTABLE = "immutable";
+    public static final String EMAIL_QUERY_PARAM = "email";
        public static final String PASSWORD_RESET_TOKEN_QP = "token";
        public static final String PASSWORD_RESET_PASSWORD_QP = "password";
        public static final String INCLUDE_ROLES_QP = "showRoles";
+    public static final String PASSWORD_RESET_PATH_COMPONENT = "/requestpasswordreset";
+    public static final String PASSWORD_RESET_PATH = SERVICE_PATH + PASSWORD_RESET_PATH_COMPONENT;
+    public static final String PROCESS_PASSWORD_RESET_PATH_COMPONENT = "/processpasswordreset";
+    public static final String PROCESS_PASSWORD_RESET_PATH = SERVICE_PATH + PROCESS_PASSWORD_RESET_PATH_COMPONENT;
 
        public AccountClient() throws Exception {
                super();
index adf195228ea1a20ac29fbd062062fb69a2be5988..3ac4545b93cff738a94c28a99e91747969b9c3a5 100644 (file)
@@ -1,5 +1,5 @@
 <?xml version="1.0" encoding="UTF-8" standalone="yes"?>
-<xs:schema 
+<xs:schema
     xmlns:xs="http://www.w3.org/2001/XMLSchema"
     xmlns:jaxb="http://java.sun.com/xml/ns/jaxb"
     jaxb:version="2.1" elementFormDefault="unqualified"
@@ -25,7 +25,7 @@
                        </jaxb:globalBindings>
                </xs:appinfo>
        </xs:annotation>
-               
+
     <!-- accounts-common -->
     <!-- convention: <servicename>-common  -->
     <xs:element name="accounts_common">
             </xs:attribute>
         </xs:complexType>
     </xs:element>
-    
+
     <xs:complexType name="roleList">
         <xs:annotation>
             <xs:documentation>
         <xs:sequence>
             <xs:element name="role" type="role_value" minOccurs="1" maxOccurs="unbounded"/>
         </xs:sequence>
-    </xs:complexType>    
+    </xs:complexType>
 
     <xs:complexType name="role_value" >
         <xs:annotation>
             </xs:element>
         </xs:sequence>
     </xs:complexType>
-       
+
     <!-- FIXME tenant definition could be in a separate schema -->
        <xs:element name="tenant">
                <xs:complexType>
                                                        </hj:basic>
                                                </xs:appinfo>
                                        </xs:annotation>
-                               </xs:element>                           
+                               </xs:element>
                                <xs:element name="authoritiesInitialized" type="xs:boolean">
                                        <xs:annotation>
                                                <xs:appinfo>
                </xs:complexType>
        </xs:element>
 </xs:schema>
-
index 30fdbbef4a1ddba144e010295f340f7420ff5108..6c88ecdc74e891931affb26f6d7c1c5c97775b61 100644 (file)
@@ -1,5 +1,5 @@
 <?xml version="1.0" encoding="UTF-8" standalone="yes"?>
-<xs:schema 
+<xs:schema
     xmlns:xs="http://www.w3.org/2001/XMLSchema"
     xmlns:jaxb="http://java.sun.com/xml/ns/jaxb"
     jaxb:version="2.1" elementFormDefault="unqualified"
@@ -19,7 +19,7 @@
     Avoid XmlRootElement nightmare:
     See http://weblogs.java.net/blog/kohsuke/archive/2006/03/why_does_jaxb_p.html
        -->
-       
+
     <!-- This is the base class for paginated lists -->
     <xs:complexType name="abstractCommonList">
         <xs:annotation>
                 </xs:appinfo>
             </xs:annotation>
             <xs:complexContent>
-                <xs:extension base="abstractCommonList">                    
-                           <xs:sequence>
-                               <xs:element name="account-list-item" maxOccurs="unbounded">
-                                   <xs:complexType>
-                                       <xs:annotation>
-                                           <xs:appinfo>
-                                               <hj:ignored/>
-                                           </xs:appinfo>
-                                       </xs:annotation>
-                                       <xs:sequence>
-                                           <xs:element name="screenName" type="xs:string" minOccurs="1"/>
-                                           <xs:element name="userid" type="xs:string" minOccurs="1" />
-                                           <xs:element name="tenantid" type="xs:string" minOccurs="1" />
-                                           <xs:element name="tenants" type="account_tenant" minOccurs="1" maxOccurs="unbounded">
-                                                   <xs:annotation>
-                                                       <xs:documentation>
-                                                           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
-                                                       </xs:documentation>
-                                                   </xs:annotation>
-                                                       </xs:element>                                       
-                                           <xs:element name="personRefName" type="xs:string" minOccurs="1" />
-                                           <xs:element name="email" type="xs:string" minOccurs="1" />
-                                           <xs:element name="status" type="status" minOccurs="1" />
-                                           <!-- uri to retrive collection object details -->
-                                           <xs:element name="uri" type="xs:anyURI" minOccurs="1" />
-                                           <xs:element name="csid" type="xs:string" minOccurs="1" />
-                                       </xs:sequence>
-                                   </xs:complexType>
-                               </xs:element>
-                           </xs:sequence>
+                <xs:extension base="abstractCommonList">
+                    <xs:sequence>
+                        <xs:element name="account-list-item" maxOccurs="unbounded">
+                            <xs:complexType>
+                                <xs:annotation>
+                                    <xs:appinfo>
+                                        <hj:ignored/>
+                                    </xs:appinfo>
+                                </xs:annotation>
+                                <xs:sequence>
+                                    <xs:element name="screenName" type="xs:string" minOccurs="1"/>
+                                    <xs:element name="userid" type="xs:string" minOccurs="1" />
+                                    <xs:element name="tenantid" type="xs:string" minOccurs="1" />
+                                    <xs:element name="tenants" type="account_tenant" minOccurs="1" maxOccurs="unbounded">
+                                    <xs:annotation>
+                                        <xs:documentation>
+                                            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
+                                        </xs:documentation>
+                                    </xs:annotation>
+                                    </xs:element>
+                                    <xs:element name="personRefName" type="xs:string" minOccurs="1" />
+                                    <xs:element name="email" type="xs:string" minOccurs="1" />
+                                    <xs:element name="status" type="status" minOccurs="1" />
+                                    <!-- uri to retrive collection object details -->
+                                    <xs:element name="uri" type="xs:anyURI" minOccurs="1" />
+                                    <xs:element name="csid" type="xs:string" minOccurs="1" />
+                                </xs:sequence>
+                            </xs:complexType>
+                        </xs:element>
+                    </xs:sequence>
                 </xs:extension>
-            </xs:complexContent>        
+            </xs:complexContent>
         </xs:complexType>
     </xs:element>
 
@@ -93,7 +93,7 @@
                 </xs:appinfo>
             </xs:annotation>
             <xs:complexContent>
-                <xs:extension base="abstractCommonList">                    
+                <xs:extension base="abstractCommonList">
                            <xs:sequence>
                                <xs:element name="tenant-list-item" maxOccurs="unbounded">
                                    <xs:complexType>
                                </xs:element>
                            </xs:sequence>
                 </xs:extension>
-            </xs:complexContent>        
+            </xs:complexContent>
         </xs:complexType>
     </xs:element>
 </xs:schema>
-
index 15076db322008fc2a824cf2e71f7c079d6698439..9d1465b1acb99c5d59ebf022d25088b0245f0a42 100644 (file)
             <groupId>org.testng</groupId>
             <artifactId>testng</artifactId>
         </dependency>
+        <dependency>
+            <groupId>org.springframework.security</groupId>
+            <artifactId>spring-security-web</artifactId>
+            <version>${spring.security.version}</version>
+            <scope>provided</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.tomcat</groupId>
+            <artifactId>tomcat-servlet-api</artifactId>
+            <version>${tomcat.version}</version>
+            <scope>provided</scope>
+        </dependency>
 
         <!-- apache -->
         <dependency>
index 46a6c7f9a81bbbee3c85aa5c0cea80791be77115..cc21837f7744514c1df46c6fe369ccae15c90267 100644 (file)
@@ -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 SecurityResourceBase<AccountsCommon, Accoun
 
        final Logger logger = LoggerFactory.getLogger(AccountResource.class);
     final StorageClient storageClient = new AccountStorageClient();
-    private static final String PASSWORD_RESET_PATH = "/requestpasswordreset";
-       private static final String PROCESS_PASSWORD_RESET_PATH = "/processpasswordreset";
 
     @Override
     protected String getVersionString() {
@@ -226,7 +248,7 @@ public class AccountResource extends SecurityResourceBase<AccountsCommon, Accoun
     public AccountsCommon updateAccount(@Context UriInfo ui, @PathParam("csid") String csid, AccountsCommon theUpdate) {
         return (AccountsCommon)update(ui, csid, theUpdate, AccountsCommon.class);
     }
-    
+
     /*
      * Use this when you have an existing and active ServiceContext. //FIXME: Use this only for password reset
      */
@@ -234,6 +256,63 @@ public class AccountResource extends SecurityResourceBase<AccountsCommon, Accoun
         return (AccountsCommon)update(parentContext, ui, csid, theUpdate, AccountsCommon.class, false);
     }
 
+    @GET
+    @Path(AccountClient.PROCESS_PASSWORD_RESET_PATH_COMPONENT)
+    @Produces(MediaType.TEXT_HTML)
+    public String processPasswordResetForm(@Context HttpServletRequest request) throws TemplateNotFoundException, MalformedTemplateNameException, ParseException, IOException, TemplateException {
+        String tokenId = request.getParameter(AccountClient.PASSWORD_RESET_TOKEN_QP);
+        Token token = null;
+
+        try {
+            token = TokenStorageClient.get(tokenId);
+        } catch (DocumentNotFoundException e) {
+        }
+
+        if (token == null || !token.isEnabled()) {
+            return String.format("<html><body>The token %s is not valid.</body></html>", tokenId);
+        }
+
+        Map<String, Object> uiConfig = new HashMap<>();
+
+        CsrfToken csrfToken = (CsrfToken) request.getAttribute(CsrfToken.class.getName());
+
+        if (csrfToken != null) {
+            Map<String, Object> 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<String, String> 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<AccountsCommon, Accoun
      * @throws IOException
      */
     @POST
-    @Path(PROCESS_PASSWORD_RESET_PATH)
+    @Path(AccountClient.PROCESS_PASSWORD_RESET_PATH_COMPONENT)
     synchronized public Response processPasswordReset(Passwordreset passwordreset, @Context UriInfo ui) {
        Response response = null;
 
@@ -344,7 +423,7 @@ public class AccountResource extends SecurityResourceBase<AccountsCommon, Accoun
                                }
                                } catch (Throwable t) {
                                        transactionCtx.markForRollback();
-                                       transactionCtx.close(); // https://jira.ets.berkeley.edu/jira/browse/CC-241                                     
+                                       transactionCtx.close(); // https://jira.ets.berkeley.edu/jira/browse/CC-241
                                        String errMsg = String.format("Could not reset password using token ID='%s'. Error: '%s'",
                                                        t.getMessage(), token.getId());
                                response = Response.status(Response.Status.BAD_REQUEST).entity(errMsg).type("text/plain").build();
@@ -365,53 +444,131 @@ public class AccountResource extends SecurityResourceBase<AccountsCommon, Accoun
        return response;
     }
 
+    @GET
+    @Path(AccountClient.PASSWORD_RESET_PATH_COMPONENT)
+    @Produces(MediaType.TEXT_HTML)
+    public String requestPasswordResetForm(@Context HttpServletRequest request) throws TemplateNotFoundException, MalformedTemplateNameException, ParseException, IOException, TemplateException {
+        Map<String, Object> uiConfig = new HashMap<>();
+
+        CsrfToken csrfToken = (CsrfToken) request.getAttribute(CsrfToken.class.getName());
+
+        if (csrfToken != null) {
+            Map<String, Object> 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<String, String> 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<String,String> 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<AccountListItem> 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<AccountListItem>() {
+                    @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<AccountTenant> accountTenantList) {
@@ -447,7 +604,8 @@ public class AccountResource extends SecurityResourceBase<AccountsCommon, Accoun
                String deprecatedConfigBaseUrl = emailConfig.getBaseurl();
 
                Object[] emptyValues = new String[0];
-               String baseUrl = baseUrlBuilder.replacePath(null).build(emptyValues).toString();
+               String baseUrl = baseUrlBuilder.build(emptyValues).toString();
+
                emailConfig.setBaseurl(baseUrl);
                //
                // Configuring (via config files) the base URL is not supported as of CSpace v5.0.  Log a warning if we find config for it.
@@ -465,15 +623,15 @@ public class AccountResource extends SecurityResourceBase<AccountsCommon, Accoun
                String message = AuthorizationCommon.generatePasswordResetEmailMessage(emailConfig, accountListItem, token);
                String status = EmailUtil.sendMessage(emailConfig, accountListItem.getEmail(), message);
                if (status != null) {
-                       String errMsg = String.format("Could not send a password request email to user ID='%s'.  Error: '%s'",
+                       String errMsg = String.format("Could not send email to %s: %s",
                                        accountListItem.email, status);
                result = Response.status(Response.Status.INTERNAL_SERVER_ERROR).entity(errMsg).type("text/plain").build();
                } else {
-                       String okMsg = String.format("Password reset email sent to '%s'.", accountListItem.getEmail());
+                       String okMsg = accountListItem.getEmail();
                        result = Response.status(Response.Status.OK).entity(okMsg).type("text/plain").build();
                }
        } else {
-               String errMsg = String.format("The email configuration for tenant ID='%s' is missing.  Please ask your CollectionSpace administrator to check the configuration.",
+               String errMsg = String.format("The email configuration for tenant %s is missing. Please ask your CollectionSpace administrator to check the configuration.",
                                targetTenantID);
                result = Response.status(Response.Status.BAD_REQUEST).entity(errMsg).type("text/plain").build();
        }
index 2a17628ba4868a0fa241982256278259b70bab16..9539b143a4ba2f4b6b570edc1fa63cd62fff78af 100644 (file)
@@ -31,17 +31,15 @@ import java.util.UUID;
 import org.collectionspace.services.account.AccountTenant;
 import org.collectionspace.services.account.AccountsCommon;
 import org.collectionspace.services.account.AccountsCommonList;
+import org.apache.commons.text.RandomStringGenerator;
 import org.collectionspace.services.account.AccountListItem;
 import org.collectionspace.services.account.AccountRoleSubResource;
 import org.collectionspace.services.account.Status;
 import org.collectionspace.services.authorization.AccountRole;
-import org.collectionspace.services.authorization.PermissionRole;
-import org.collectionspace.services.authorization.PermissionRoleSubResource;
 import org.collectionspace.services.authorization.SubjectType;
 import org.collectionspace.services.account.RoleValue;
 import org.collectionspace.services.client.AccountClient;
 import org.collectionspace.services.client.AccountRoleFactory;
-import org.collectionspace.services.client.RoleClient;
 import org.collectionspace.services.common.storage.TransactionContext;
 import org.collectionspace.services.common.storage.jpa.JpaDocumentHandler;
 import org.collectionspace.services.common.api.Tools;
@@ -56,7 +54,7 @@ import org.slf4j.LoggerFactory;
 
 /**
  *
- * @author 
+ * @author
  */
 public class AccountDocumentHandler
         extends JpaDocumentHandler<AccountsCommon, AccountsCommonList, AccountsCommon, List<AccountsCommon>> {
@@ -69,9 +67,15 @@ public class AccountDocumentHandler
     public void handleCreate(DocumentWrapper<AccountsCommon> 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<AccountsCommon> 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<AccountsCommon> 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<AccountTenant>(0));
         }
-    }    
+    }
 
     /* (non-Javadoc)
      * @see org.collectionspace.services.common.document.DocumentHandler#initializeDocumentFilter(org.collectionspace.services.common.context.ServiceContext)
index bec10d722e9f03635a286406fb9804242cc6b37a..43462797e32b158b6752cabbd039877ab234adca 100644 (file)
@@ -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");
index 7a69d118abf58e42903b91faff9d22e9095edc61..226abe67eaf0525944dbab63f4abf3d4eb752c3d 100644 (file)
@@ -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;
     }
 }
index 07f0a1c46b7666f1d2bd82b26e051991948615b6..4c337433bde74c34f479361910ecb436d4b02c00 100644 (file)
@@ -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;
     }
 }
index eff2a4216e9745b83cb107155cae175fedeb92e5..210959dbfdf6485431a41fffeec0ed0127d6dfa7 100644 (file)
@@ -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,
index 4fea7a4d8161202aa4a7b5d3c235c31925fc69bc..d9770168a015ffc60a5006c8e5fc95496649d6b2 100644 (file)
@@ -69,9 +69,9 @@
             <scope>provided</scope>
         </dependency>
         <dependency>
-            <groupId>org.springframework.security.oauth</groupId>
-            <artifactId>spring-security-oauth2</artifactId>
-            <version>${spring.security.oauth2.version}</version>
+            <groupId>org.springframework.security</groupId>
+            <artifactId>spring-security-oauth2-authorization-server</artifactId>
+            <version>${spring.security.authorization.server.version}</version>
             <scope>provided</scope>
         </dependency>
         <dependency>
index b8faec5551de65ffa402d282dfe9c34bb9ccbd71..363365e2a0371e2a64a94974b131b3328175cc5f 100644 (file)
@@ -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<AuthenticationSuccessEvent> {
-       
-       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 (file)
index 34c3471..0000000
+++ /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);
-       }
-
-}
index 68180b6a65c3029baa120a2a53f2051d45883b31..046f93939a4fd6497da3084bdba107fb1380901c 100644 (file)
@@ -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;
     }
index ed0931210a9be0daec2336acf88b9442c4b4f3cd..7f3ff714d4348bb08ed4ab81206c8dbe037628d2 100644 (file)
@@ -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<CSpaceTenant> 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<CSpaceTenant> 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 (file)
index 0000000..f297fca
--- /dev/null
@@ -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<CSpaceTenant> {
+
+       @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 (file)
index 0000000..acaa97b
--- /dev/null
@@ -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<CSpaceUser> {
+       private static final TypeReference<Set<SimpleGrantedAuthority>> SIMPLE_GRANTED_AUTHORITY_SET = new TypeReference<Set<SimpleGrantedAuthority>>() {
+       };
+
+  private static final TypeReference<Set<CSpaceTenant>> CSPACE_TENANT_SET = new TypeReference<Set<CSpaceTenant>>() {
+       };
+
+       @Override
+       public CSpaceUser deserialize(JsonParser parser, DeserializationContext context) throws IOException, JsonProcessingException {
+               ObjectMapper mapper = (ObjectMapper) parser.getCodec();
+               JsonNode jsonNode = mapper.readTree(parser);
+
+               Set<? extends GrantedAuthority> authorities = mapper.convertValue(jsonNode.get("authorities"), SIMPLE_GRANTED_AUTHORITY_SET);
+               Set<CSpaceTenant> 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();
+       }
+}
index c70a14ccc5f6935f8ec05acaac79aabae930660a..bfbd207551ef4d6933018e6b7bafb34399db46a3 100644 (file)
@@ -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<CSpaceTenant> getTenants(String username, boolean includeDisabledTenants) throws AccountException;
-
 }
index 814ec72fa83010d21293e4e42755270dc73fd950..09942bd3021f636219f4c579bfe202e51707a175 100644 (file)
@@ -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<String, ?> 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<String, ?> 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<CSpaceTenant> 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<CSpaceTenant> getTenants(String username, boolean includeDisabledTenants) throws AccountException {
 
        String tenantsQuery = getTenantQuery(includeDisabledTenants);
-       
+
         if (logger.isDebugEnabled()) {
             logger.debug("getTenants using tenantsQuery: " + tenantsQuery + ", username: " + username);
         }
 
         Set<CSpaceTenant> tenants = new LinkedHashSet<CSpaceTenant>();
-        
+
         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 (file)
index 0000000..7b26ed7
--- /dev/null
@@ -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 (file)
index 0000000..d0e42af
--- /dev/null
@@ -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<String> permittedRedirectUris;
+
+  public CSpaceLogoutSuccessHandler(String defaultTargetUrl, Set<String> 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 (file)
index 0000000..acccdf3
--- /dev/null
@@ -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<String, PasswordEncoder> encoders = new HashMap<String, PasswordEncoder>();
+
+      // 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;
+  }
+}
index 230709b2300cbc2bec83f6d4a326d045b2cc1a39..a4bd5c50704b7a5c2969ce1c84342def27f2288b 100644 (file)
@@ -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 (file)
index 3d81539..0000000
+++ /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<String, ?> 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<String, Object> response = new LinkedHashMap<String, Object>();
-        
-        response.put(USERNAME, userAuthentication.getName());
-        
-        return response;
-    }
-
-    @Override
-    public Authentication extractAuthentication(Map<String, ?> 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;
-        }
-    }
-}
index fba9868ffe0cfebb215284e838a595f600c4356c..74901a35e3429aae87f267f326ebac100b147d12 100644 (file)
@@ -77,7 +77,7 @@ public class CSpaceUserDetailsService implements UserDetailsService {
         String salt = null;
         Set<CSpaceTenant> tenants = null;
         Set<GrantedAuthority> 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<GrantedAuthority> getAuthorities(String username) throws AccountException {
         Set<String> roles = realm.getRoles(username);
         Set<GrantedAuthority> authorities = new LinkedHashSet<GrantedAuthority>(roles.size());
-        
+
         for (String role : roles) {
             authorities.add(new SimpleGrantedAuthority(role));
         }
-        
+
         return authorities;
     }
-    
+
     protected Set<CSpaceTenant> getTenants(String username) throws AccountException {
         Set<CSpaceTenant> tenants = realm.getTenants(username);
-        
+
         return tenants;
     }
 }
index c1dc8dd681dec25d0f8b1dae1282bb15981d2ea8..309f856baf8bfc895da23f4f4efba075d3c54e9f 100644 (file)
@@ -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;
     }
 }
index 8afdd2ab4bdf290376baf797d8117c4b6341d518..b0b53a92e3787bb89418050e35e583634358947f 100644 (file)
             <arg value="${basedir}/pom.xml" />
             <arg value="-N" />
             <arg value="${mvn.opts}" />
+            <arg value="-X" />
         </exec>
     </target>
     <target name="import-windows" if="osfamily-windows" depends="setup_hibernate.cfg">
         <copy tofile="${dest.appContext.cfg}" file="${src.appContext.cfg}" filtering="true"/>
     </target>
 
-    
+
     <target name="deploy" depends="install"
             description="deploy authorization-mgt import in ${jee.server.cspace}">
     </target>
index fbc2b89190e0a325b9663a8d63f1259ee1d3df67..727de8acda50814cc2bdcdfd54bb645c8e5314ee 100644 (file)
@@ -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
index a3f9f7f66e61b58630fc1f1ed2c246de550ceb6d..726046451cb99e609cf41cc8f9dbffb9feb6d209 100644 (file)
@@ -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)
 );
index 1c8cc395862f556711c41f1b1b4eeb6512e745e2..c321adfb0d8746936c491951e50fb66b5be52b53 100644 (file)
@@ -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)
+);
index fb0dc78b1a6ad52c444d7410a4a869d9473dd9e3..fff5e474651d7f3bef55723197852642ccfc328f 100644 (file)
             <version>${spring.security.version}</version>
             <scope>provided</scope>
         </dependency>
-        <dependency>
-            <groupId>org.springframework.security.oauth</groupId>
-            <artifactId>spring-security-oauth2</artifactId>
-            <version>${spring.security.oauth2.version}</version>
-            <scope>provided</scope>
-        </dependency>
         <dependency>
             <groupId>org.springframework</groupId>
             <artifactId>spring-context</artifactId>
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 (file)
index 7cd8075..0000000
+++ /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<String, String> authorizationParameters) {
-        return super.createAuthorizationRequest(decodePassword(authorizationParameters));
-    }
-
-    @Override
-    public TokenRequest createTokenRequest(
-            Map<String, String> requestParameters,
-            ClientDetails authenticatedClient) {
-        return super.createTokenRequest(decodePassword(requestParameters), authenticatedClient);
-    }
-    
-    private Map<String, String> decodePassword(Map<String, String> parameters) {
-        if (parameters.containsKey(PASSWORD_PARAMETER)) {
-            String base64EncodedPassword = parameters.get(PASSWORD_PARAMETER);
-            String password = new String(DatatypeConverter.parseBase64Binary(base64EncodedPassword), StandardCharsets.UTF_8);
-
-            Map<String, String> parametersCopy = new HashMap<String, String>(parameters);
-
-            parametersCopy.put(PASSWORD_PARAMETER, password);
-
-            return parametersCopy;
-        }
-
-        return parameters;
-    }
-}
index 599d83916e7e68ea47916134aacd9164be3bb028..27595fb7820d2931a023f7bc1ee8e1ba10eec556 100644 (file)
@@ -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<String> theList, String searchStr) {
        boolean result = false;
-       
+
        for (String listItem : theList) {
                if (StringUtils.containsIgnoreCase(listItem, searchStr)) {
                        return true;
                }
        }
-       
+
        return result;
     }
 }
index 875ed7c1cb8dd5c23715ee0a1bc1967b4c2eb4d4..bca1f527f890a25f014f86c217d1985a76a9d6b4 100644 (file)
                                <filter token="DB_CSADMIN_NAME" value="${db.csadmin.name}" />
                                <filter token="DB_NUXEO_NAME" value="${db.nuxeo.name}" />
                                <filter token="DB_CSPACE_NAME" value="${db.cspace.name}" />
+                               <filter token="SERVICE_UI_JS_URL" value="${service.ui.js.url}" />
                        </filterset>
         </copy>
                <!--
         <delete failonerror="false" file="${jee.server.cspace}/conf/jboss-log4j-release.xml"/>
                -->
         <delete failonerror="false" file="${jee.server.cspace}/lib/${common.jar}"/>
-        <delete failonerror="false" dir="${jee.server.cspace}/cspace/config/services"/>
+
+        <delete failonerror="false">
+            <fileset dir="${jee.server.cspace}/cspace/config/services" excludes="local/**" />
+        </delete>
 
     </target>
 
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 (file)
index 0000000..1d7417c
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 (file)
index 0000000..02b8954
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 (file)
index 0000000..632772f
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 (file)
index 0000000..218ee25
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 (file)
index 0000000..163dc43
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 (file)
index 0000000..f10f780
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 (file)
index d19b67b..0000000
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 (file)
index 0000000..b1fb3f2
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 (file)
index a078720..0000000
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 (file)
index 0000000..a4791e5
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 (file)
index 3565ff5..0000000
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 (file)
index 0000000..3882579
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 (file)
index 0000000..c9021f6
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 (file)
index 0000000..340af06
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 (file)
index 0000000..5e6aed8
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 (file)
index 0000000..df56a4c
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 (file)
index 0000000..e0cb404
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 (file)
index 0000000..92be1d8
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 (file)
index 0000000..a9899fb
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 (file)
index 0000000..e4492fb
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 (file)
index 0000000..0356dfa
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 (file)
index 0000000..4c7b328
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 (file)
index 0000000..6a6f8c1
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 (file)
index 0000000..45f65e3
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 (file)
index 0000000..c383fa8
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 (file)
index 0000000..023a3af
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 (file)
index 0000000..577b2c9
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 (file)
index e054cf6..0000000
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 (file)
index 0000000..50f033e
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 (file)
index f52417f..0000000
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 (file)
index 0000000..354d400
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 (file)
index f303583..0000000
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 (file)
index 0000000..6b5dd69
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 (file)
index 3818a4d..0000000
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 (file)
index 0000000..3b136aa
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 (file)
index 883ce39..0000000
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 (file)
index 0000000..f6a5a71
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 (file)
index 1d6abd1..0000000
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 (file)
index 0000000..038f18e
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 (file)
index 73d9d57..0000000
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 (file)
index a3876b5..0000000
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 (file)
index 0000000..f61fdef
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 (file)
index c8fbdd8..0000000
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 (file)
index 0000000..45caeb3
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 (file)
index c92232d..0000000
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 (file)
index 0000000..618feb0
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 (file)
index 9fb9fe4..0000000
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 (file)
index 0000000..efb4154
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 (file)
index 0000000..3e77b93
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 (file)
index ac8dec1..0000000
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 (file)
index 354b768..0000000
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 (file)
index 0000000..e036a7e
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 (file)
index 0000000..e29154b
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 (file)
index 0000000..38fb705
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 (file)
index 0000000..9907ad2
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 (file)
index 0000000..2bbf75c
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 (file)
index 7af90e4..0000000
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 (file)
index 0000000..19bad1c
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 (file)
index 379a761..0000000
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 (file)
index 0000000..7b4d6db
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 (file)
index 1f5e468..0000000
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 (file)
index 0000000..6cec39e
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 (file)
index bfc0bcf..0000000
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 (file)
index 0000000..c5eb7e5
Binary files /dev/null and b/services/common/lib/spring/xmlsec-2.0.10.jar differ
index 93660cc756fd0509263ff7af80db23d671e62e01..e0ac45539c813cc1c1990c1ce57cacd606eeb6fb 100644 (file)
                        <artifactId>org.collectionspace.services.systeminfo.client</artifactId>
                        <version>${project.version}</version>
                </dependency>
+               <dependency>
+                       <groupId>org.collectionspace.services</groupId>
+                       <artifactId>org.collectionspace.services.login.client</artifactId>
+                       <version>${project.version}</version>
+               </dependency>
+               <dependency>
+                       <groupId>org.collectionspace.services</groupId>
+                       <artifactId>org.collectionspace.services.logout.client</artifactId>
+                       <version>${project.version}</version>
+               </dependency>
                <dependency>
                        <groupId>org.collectionspace.services</groupId>
                        <artifactId>org.collectionspace.services.account.client</artifactId>
                        <artifactId>spring-aop</artifactId>
                        <version>${spring.version}</version>
                </dependency>
+               <dependency>
+                       <groupId>org.springframework.security</groupId>
+                       <artifactId>spring-security-oauth2-authorization-server</artifactId>
+                       <version>${spring.security.authorization.server.version}</version>
+                       <scope>provided</scope>
+               </dependency>
+               <dependency>
+                       <groupId>org.springframework.security</groupId>
+                       <artifactId>spring-security-config</artifactId>
+                       <version>${spring.security.version}</version>
+                       <scope>provided</scope>
+               </dependency>
 
                <dependency>
                        <groupId>com.fasterxml.jackson.core</groupId>
                        <artifactId>jackson-core</artifactId>
-                       <version>2.8.0</version>
                </dependency>
                <dependency>
                                <groupId>org.mybatis</groupId>
                        <version>2.2.1</version>
                        <scope>test</scope>
                </dependency>
+               <dependency>
+                       <groupId>org.freemarker</groupId>
+                       <artifactId>freemarker</artifactId>
+                       <version>2.3.32</version>
+               </dependency>
        </dependencies>
 
        <build>
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 (file)
index 0000000..34ef8c4
--- /dev/null
@@ -0,0 +1,65 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+<svc:service-config
+    xmlns:svc='http://collectionspace.org/services/config'
+    xmlns:merge='http://xmlmerge.el4j.elca.ch'
+>
+    <security merge:action="insert">
+        <cors>
+            <max-age>P1D</max-age>
+        </cors>
+
+        <oauth>
+            <default-access-token-time-to-live>PT1H</default-access-token-time-to-live>
+
+            <client-registrations>
+                <client id="cspace-ui">
+                    <client-id>cspace-ui</client-id>
+                    <client-name>CollectionSpace UI</client-name>
+                    <!--
+                        cspace-ui is a public client that cannot keep a secret, so it does not use
+                        client authentication.
+                    -->
+                    <client-authentication-method>none</client-authentication-method>
+                    <!--
+                        Spring does not allow refresh token grants for public clients (those with
+                               ClientAuthenticationMethod.NONE), so the cspace-ui client only supports
+                        AuthorizationGrantType.AUTHORIZATION_CODE.
+                    -->
+                    <authorization-grant-type>authorization_code</authorization-grant-type>
+                    <!-- <authorization-grant-type>refresh</authorization-grant-type> -->
+                    <scope>cspace.full</scope>
+                    <!--
+                        For the cspace-ui client, the allowed redirect URIs are now automatically
+                        populated from tenant config. The lines below serve as examples for other
+                        clients.
+                    -->
+                    <!--
+                    <redirect-uri>http://localhost:8180/cspace/core/authorized</redirect-uri>
+                    <redirect-uri>http://localhost:8180/cspace/anthro/authorized</redirect-uri>
+                    <redirect-uri>http://localhost:8180/cspace/bonsai/authorized</redirect-uri>
+                    <redirect-uri>http://localhost:8180/cspace/botgarden/authorized</redirect-uri>
+                    <redirect-uri>http://localhost:8180/cspace/fcart/authorized</redirect-uri>
+                    <redirect-uri>http://localhost:8180/cspace/lhmc/authorized</redirect-uri>
+                    <redirect-uri>http://localhost:8180/cspace/materials/authorized</redirect-uri>
+                    <redirect-uri>http://localhost:8180/cspace/publicart/authorized</redirect-uri>
+                    <redirect-uri>http://localhost:8180/cspace/core/logout?success</redirect-uri>
+                    <redirect-uri>http://localhost:8180/cspace/anthro/logout?success</redirect-uri>
+                    <redirect-uri>http://localhost:8180/cspace/bonsai/logout?success</redirect-uri>
+                    <redirect-uri>http://localhost:8180/cspace/botgarden/logout?success</redirect-uri>
+                    <redirect-uri>http://localhost:8180/cspace/fcart/logout?success</redirect-uri>
+                    <redirect-uri>http://localhost:8180/cspace/lhmc/logout?success</redirect-uri>
+                    <redirect-uri>http://localhost:8180/cspace/materials/logout?success</redirect-uri>
+                    <redirect-uri>http://localhost:8180/cspace/publicart/logout?success</redirect-uri>
+                    -->
+                    <client-settings>
+                        <require-authorization-consent>false</require-authorization-consent>
+                    </client-settings>
+                    <token-settings>
+                        <access-token-time-to-live>PT12H</access-token-time-to-live>
+                    </token-settings>
+                </client>
+            </client-registrations>
+        </oauth>
+    </security>
+</svc:service-config>
index 1561b754861abf1c46de2ecb36199c1a7f382010..67e7ef839292d33fa702b5cae5120e23405f123c 100644 (file)
@@ -19,7 +19,7 @@
        <db-nuxeo-name>@DB_NUXEO_NAME@</db-nuxeo-name>
        <db-cspace-name>@DB_CSPACE_NAME@</db-cspace-name>
        <use-app-generated-tenant-bindings>true</use-app-generated-tenant-bindings>
-    
+
     <!-- name of the repository client is referred in each service binding -->
     <repository-client name="nuxeo-java" default="true">
         <!-- ip of network interface to which Nuxeo server is listening on -->
index ac379ed4b7005e9f230e6858d6d33288883353b0..06028f4ed94e2e6670152488b5a98f456785eb7e 100644 (file)
                 <types:value>425</types:value>
             </types:item>
         </tenant:properties>
-        
+
         <tenant:serviceBindings merge:matcher="id" id="CollectionObjects">
             <service:validatorHandler xmlns:service="http://collectionspace.org/services/config/service" merge:matcher="tag" merge:action="replace">org.collectionspace.services.collectionobject.nuxeo.BotGardenCollectionObjectValidatorHandler</service:validatorHandler>
         </tenant:serviceBindings>
+
         <tenant:serviceBindings merge:matcher="id" id="idgenerators">
             <service:initHandler xmlns:service="http://collectionspace.org/services/config/service">
                 <service:params>
@@ -43,6 +43,6 @@
                 </service:params>
             </service:initHandler>
         </tenant:serviceBindings>
-        
+
     </tenant:tenantBinding>
 </tenant:TenantBindingConfig>
index 585d1cd05bb25a86f5e76e7f2ddf84a2a6a11c96..129faa0e4ae9b904e1fa9ca6f0d6dadd8cfd7446 100644 (file)
@@ -2,7 +2,7 @@
 <tenant:TenantBindingConfig
         xmlns:merge='http://xmlmerge.el4j.elca.ch'
         xmlns:tenant='http://collectionspace.org/services/config/tenant'>
-    
+
     <!-- Add your changes, if any, within the following tag pair. -->
     <!-- The value of the 'id' attribute, below, should match the corresponding -->
     <!-- value in cspace/config/services/tenants/core-tenant-bindings-proto.xml -->
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 (file)
index 0ef38c1..0000000
+++ /dev/null
@@ -1,13 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<tenant:TenantBindingConfig
-        xmlns:merge="http://xmlmerge.el4j.elca.ch"
-        xmlns:tenant="http://collectionspace.org/services/config/tenant">
-
-    <!-- Add your changes, if any, within the following tag pair. -->
-    <!-- The value of the 'id' attribute, below, should match the corresponding -->
-    <!-- value in cspace/config/services/tenants/testsci-tenant-bindings-proto.xml -->
-
-    <tenant:tenantBinding id="1975">   
-    </tenant:tenantBinding>
-
-</tenant:TenantBindingConfig>
index 1a946bfe773d21dae6043fc2f430339e1e103fc6..af1af828c15e5a4e8b8e0e8071e9cb67c57245c2 100644 (file)
@@ -2,7 +2,7 @@
 <tenant:TenantBindingConfig
        xmlns:merge='http://xmlmerge.el4j.elca.ch'
        xmlns:tenant='http://collectionspace.org/services/config/tenant'>
-    
+
     <!-- Add your changes, if any, within the following tag pair. -->
     <!-- The value of the 'id' attribute, below, should match the corresponding -->
     <!-- value in cspace/config/services/tenants/lhmc-tenant-bindings-proto.xml -->
index 67acbf24d2ba5bf31c765cf7cfd7c424dd860925..624494ad54c04c9eaad6e625990cd0b3b970afcc 100644 (file)
@@ -8,7 +8,7 @@
     <!-- value in cspace/config/services/tenants/materials-tenant-bindings-proto.xml -->
 
     <tenant:tenantBinding id="2000">
-        <tenant:elasticSearchDocumentWriter merge:action="replace">
+      <tenant:elasticSearchDocumentWriter merge:action="replace">
           org.collectionspace.services.nuxeo.elasticsearch.materials.MaterialsESDocumentWriter
         </tenant:elasticSearchDocumentWriter>
 
index 7052bdee36a7abee64e83e0fd789cbb27dc587b8..a3ce605f420081726c40101a95810168e9c0b9db 100644 (file)
@@ -2,6 +2,12 @@
 <tenant:TenantBindingConfig xmlns:merge="http://xmlmerge.el4j.elca.ch" xmlns:tenant="http://collectionspace.org/services/config/tenant">
 
        <tenant:tenantBinding>
+               <tenant:uiConfig>
+                       <tenant:loginSuccessUrl>authorize</tenant:loginSuccessUrl>
+                       <tenant:authorizationSuccessUrl>authorized</tenant:authorizationSuccessUrl>
+                       <tenant:logoutSuccessUrl>logout?success</tenant:logoutSuccessUrl>
+               </tenant:uiConfig>
+
                <tenant:eventListenerConfigurations id="default">
                        <tenant:eventListenerConfig id="UpdateObjectLocationOnMove">
                                <tenant:className>org.collectionspace.services.listener.UpdateObjectLocationOnMove</tenant:className>
index 9228a7c6594c36b596da680bcb607336b00fd172..b6b9c8d9423cf87d614ee8c23236ebe45d9cc068 100644 (file)
@@ -1,30 +1,30 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <tenant:TenantBindingConfig
-        xmlns:merge="http://xmlmerge.el4j.elca.ch"
-        xmlns:tenant="http://collectionspace.org/services/config/tenant">
+               xmlns:merge="http://xmlmerge.el4j.elca.ch"
+               xmlns:tenant="http://collectionspace.org/services/config/tenant">
 
-    <!-- Add your changes, if any, within the following tag pair. -->
-    <!-- The value of the 'id' attribute, below, should match the corresponding -->
-    <!-- value in cspace/config/services/tenants/testsci-tenant-bindings-proto.xml -->
+       <!-- Add your changes, if any, within the following tag pair. -->
+       <!-- The value of the 'id' attribute, below, should match the corresponding -->
+       <!-- value in cspace/config/services/tenants/testsci-tenant-bindings-proto.xml -->
 
-    <tenant:tenantBinding id="2">
+       <tenant:tenantBinding id="2">
                <tenant:eventListenerConfigurations id="default" merge:matcher="id">
-            <tenant:eventListenerConfig id="UpdateObjectLocationOnMove" merge:matcher="id">
-                <tenant:paramList id="default" merge:matcher="id" merge:action="replace">
-                       <tenant:param>
-                           <tenant:key>testsci-key0</tenant:key>
-                           <tenant:value>value0</tenant:value>
-                           </tenant:param>
-                       <tenant:param>
-                           <tenant:key>testsci-key1</tenant:key>
-                           <tenant:value>value1</tenant:value>
-                           </tenant:param>
-                       <tenant:param>
-                           <tenant:key>testsci-key2</tenant:key>
-                           <tenant:value>value2</tenant:value>
-                           </tenant:param>
-                   </tenant:paramList>
-            </tenant:eventListenerConfig>
-        </tenant:eventListenerConfigurations>  
-    </tenant:tenantBinding>
+                       <tenant:eventListenerConfig id="UpdateObjectLocationOnMove" merge:matcher="id">
+                               <tenant:paramList id="default" merge:matcher="id" merge:action="replace">
+                                       <tenant:param>
+                                               <tenant:key>testsci-key0</tenant:key>
+                                               <tenant:value>value0</tenant:value>
+                               </tenant:param>
+                                       <tenant:param>
+                                               <tenant:key>testsci-key1</tenant:key>
+                                               <tenant:value>value1</tenant:value>
+                               </tenant:param>
+                                       <tenant:param>
+                                               <tenant:key>testsci-key2</tenant:key>
+                                               <tenant:value>value2</tenant:value>
+                               </tenant:param>
+                               </tenant:paramList>
+                       </tenant:eventListenerConfig>
+               </tenant:eventListenerConfigurations>
+       </tenant:tenantBinding>
 </tenant:TenantBindingConfig>
index d11f79681962b26f2656ff2e91557047029152e2..0ce3d507c9ceeaeb9185156464e2891d80c1e1ce 100644 (file)
@@ -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.
index ed28f4766678eb556f94ca8d41e0e1ce65e83338..2dd9ce10b69c69bb67e87c008ad2436135361d08 100644 (file)
@@ -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<String> principals = new ArrayList<String>();        
+
+        List<String> principals = new ArrayList<String>();
         for (RoleValue roleValue : permRole.getRole()) {
             principals.add(roleValue.getRoleName());
         }
-        
+
         boolean grant = perm.getEffect().equals(EffectType.PERMIT) ? true : false;
         List<PermissionAction> permActions = perm.getAction();
         ArrayList<CSpaceResource> resources = new ArrayList<CSpaceResource>();
         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<PermissionAction> pas = new ArrayList<PermissionAction>();
         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<String, String> 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<String> compileExistingTenants(Connection conn, Hashtable<String, String> tenantInfo)
        throws SQLException, Exception {
        Statement stmt = null;
@@ -447,8 +449,8 @@ public class AuthorizationCommon {
 
        return existingTenants;
     }
-        
-    private static ArrayList<String> findOrCreateDefaultUsers(Connection conn, Hashtable<String, String> tenantInfo) 
+
+    private static ArrayList<String> findOrCreateDefaultUsers(Connection conn, Hashtable<String, String> 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<String, String> tenantInfo,
                ArrayList<String> usersInRepo,
-               Hashtable<String, String> tenantAdminAcctCSIDs, Hashtable<String, String> tenantReaderAcctCSIDs) 
+               Hashtable<String, String> tenantAdminAcctCSIDs, Hashtable<String, String> 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<String, String> tenantInfo, ArrayList<String> usersInRepo,
-               Hashtable<String, String> tenantAdminAcctCSIDs, Hashtable<String, String> tenantReaderAcctCSIDs) 
+               Hashtable<String, String> tenantAdminAcctCSIDs, Hashtable<String, String> 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<String, String> tenantInfo,
-               Hashtable<String, String> tenantAdminRoleCSIDs, Hashtable<String, String> tenantReaderRoleCSIDs) 
+               Hashtable<String, String> tenantAdminRoleCSIDs, Hashtable<String, String> 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<String, String> tenantInfo, ArrayList<String> usersInRepo,
                String springAdminRoleCSID,
                Hashtable<String, String> tenantAdminRoleCSIDs, Hashtable<String, String> tenantReaderRoleCSIDs,
-               Hashtable<String, String> tenantAdminAcctCSIDs, Hashtable<String, String> tenantReaderAcctCSIDs) 
+               Hashtable<String, String> tenantAdminAcctCSIDs, Hashtable<String, String> 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<String, String> 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<String> usersInRepo = findOrCreateDefaultUsers(conn, tenantInfo);
-               
+
                Hashtable<String, String> tenantAdminAcctCSIDs = new Hashtable<String, String>();
                Hashtable<String, String> tenantReaderAcctCSIDs = new Hashtable<String, String>();
                findOrCreateDefaultAccounts(conn, tenantInfo, usersInRepo,
@@ -967,24 +960,24 @@ public class AuthorizationCommon {
 
                bindDefaultAccountsToTenants(conn, databaseProductType, tenantInfo, usersInRepo,
                                tenantAdminAcctCSIDs, tenantReaderAcctCSIDs);
-               
+
                Hashtable<String, String> tenantAdminRoleCSIDs = new Hashtable<String, String>();
                Hashtable<String, String> tenantReaderRoleCSIDs = new Hashtable<String, String>();
                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<String> getTransitionVerbList(TenantBindingType tenantBinding, ServiceBindingType serviceBinding) {
                HashSet<String> result = new HashSet<String>();
-               
+
                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<String, TenantBindingType> 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<String, String> tenantInfo,
                ArrayList<String> 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<String, Object> params = new HashMap<String, Object>();
                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;
        }
 
index cc4fbdc066f1430841e71f83bb4cce93d69a1ce9..7cc4243fcf7dd0f8aa63b9c3be9bea5a2ebcb25b 100644 (file)
@@ -181,6 +181,11 @@ public class TenantBindingConfigReaderImpl extends AbstractConfigReaderImpl<List
                read(tenantsRootPath, useAppGeneratedBindings);
        }
 
+       @Override
+       public void read(List<String> 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 (file)
index 0000000..8e13db1
--- /dev/null
@@ -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<String> 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<String> 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<String> 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<ExceptionHandlingConfigurer<HttpSecurity>>() {
+                               @Override
+                               public void customize(ExceptionHandlingConfigurer<HttpSecurity> configurer) {
+                                       configurer.authenticationEntryPoint(new LoginUrlAuthenticationEntryPoint(LOGIN_FORM_URL));
+                               }
+                       })
+                       .cors(new Customizer<CorsConfigurer<HttpSecurity>>() {
+                               @Override
+                               public void customize(CorsConfigurer<HttpSecurity> 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<AuthorizeHttpRequestsConfigurer<HttpSecurity>.AuthorizationManagerRequestMatcherRegistry>() {
+                               @Override
+                               public void customize(AuthorizeHttpRequestsConfigurer<HttpSecurity>.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<OAuth2ResourceServerConfigurer<HttpSecurity>>() {
+                               @Override
+                               public void customize(OAuth2ResourceServerConfigurer<HttpSecurity> configurer) {
+                                       configurer.jwt(new Customizer<OAuth2ResourceServerConfigurer<HttpSecurity>.JwtConfigurer>() {
+                                               @Override
+                                               public void customize(OAuth2ResourceServerConfigurer<HttpSecurity>.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<Jwt,CSpaceJwtAuthenticationToken>() {
+                                                               @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<HttpBasicConfigurer<HttpSecurity>>() {
+                               @Override
+                               public void customize(HttpBasicConfigurer<HttpSecurity> configurer) {}
+                       })
+                       .formLogin(new Customizer<FormLoginConfigurer<HttpSecurity>>() {
+                               @Override
+                               public void customize(FormLoginConfigurer<HttpSecurity> configurer) {
+                                       configurer
+                                               .loginPage(LOGIN_FORM_URL)
+                                               .defaultSuccessUrl(DEFAULT_LOGIN_SUCCESS_URL);
+                               }
+                       })
+                       .logout(new Customizer<LogoutConfigurer<HttpSecurity>>() {
+                               @Override
+                               public void customize(LogoutConfigurer<HttpSecurity> 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<OAuthClientType> clientsConfig = ConfigUtils.getOAuthClientRegistrations(serviceConfig);
+                                       Set<String> 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<CsrfConfigurer<HttpSecurity>>() {
+                               @Override
+                               public void customize(CsrfConfigurer<HttpSecurity> 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<AnonymousConfigurer<HttpSecurity>>() {
+                               @Override
+                               public void customize(AnonymousConfigurer<HttpSecurity> configurer) {
+                                       configurer.principal("anonymous");
+                               }
+                       })
+                       .cors(new Customizer<CorsConfigurer<HttpSecurity>>() {
+                               @Override
+                               public void customize(CorsConfigurer<HttpSecurity> 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<OAuthClientType> 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<SecurityContext> 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<SecurityContext> jwkSource) {
+               return OAuth2AuthorizationServerConfiguration.jwtDecoder(jwkSource);
+       }
+
+       @Bean
+       public UserDetailsService userDetailsService() {
+               Map<String, Object> options = new HashMap<String, Object>();
+
+               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));
+       }
+}
index 87aca18f764a6fc593958e75f12d09209b7e6df2..bdcf75c774b677bf87d40c512b4072c0052016ae 100644 (file)
  */
 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();
index 062f25f72db253fd0b5c85f47776522fec13d067..f4938f782fa05730602058332384fd06011d4d2d 100644 (file)
@@ -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 (file)
index 0000000..d71af7e
--- /dev/null
@@ -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);
+       }
+}
index 8afe5879515ac1dde1725be2a17f4bc45c9d04bb..eff40cbf44d559efc702e281d65df21fd8419bc1 100644 (file)
@@ -63,6 +63,9 @@ public abstract class AbstractConfigReaderImpl<T> implements ConfigReader<T> {
        @Override
        abstract public void read(String configFile, boolean useAppGeneratedBindings) throws Exception;
 
+       @Override
+       abstract public void read(List<String> configFiles, boolean useAppGeneratedBindings) throws Exception;
+
        @Override
        abstract public T getConfiguration();
 
index 6bc2e4de4a4da09f1130fc1eb0c1240611fa28bd..3a1f537ceba9ab98d59d443cf12368f50f72957e 100644 (file)
@@ -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<T> {
      */
     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<String> configFiles, boolean useAppGeneratedBindings) throws Exception;
+
     /**
      * getConfig get configuration binding
      * @return
index 3792c2df563c3c275fa5bd4ef886825fc15c20e1..ab9ad75d40645b0c6e8c324ba2748797b8a40f9a 100644 (file)
@@ -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<String> getRepositoryNameList(TenantBindingType tenantBindingType) {
        List<String> result = null;
-       
+
                List<RepositoryDomainType> repoDomainList = tenantBindingType.getRepositoryDomain();
                if (repoDomainList != null && repoDomainList.isEmpty() == false) {
                        result = new ArrayList<String>();
@@ -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<RepositoryDomainType> 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<String> getCorsAllowedOrigins(ServiceConfig serviceConfig) {
+               CORSType cors = getCors(serviceConfig);
+
+               if (cors != null) {
+                       List<String> allowedOrigin = cors.getAllowedOrigin();
+
+                       if (allowedOrigin != null) {
+                               return allowedOrigin;
+                       }
+               }
+
+               return new ArrayList<String>();
+       }
+
+       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<OAuthClientType> 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;
+       }
 }
index d92a96adc77e9a08c52b193500db10a1ab157693..883437321b5d6bc28df97bc2bd03fdddbbe266bf 100644 (file)
 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<ServiceConfig> {
 
-    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<String> localXmlConfigFiles = new ArrayList<>();
+
+        if (localConfigDir.exists()) {
+            List<File> localConfigDirFiles = getFiles(localConfigDir);
+
+            Collections.sort(localConfigDirFiles, new Comparator<File>() {
+                @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<String> 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<String> configFileNames, boolean useAppGeneratedBindings) throws Exception {
+        List<File> files = new ArrayList<File>();
+
+        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 <client-type> in <repository-client>";
             logger.error(msg);
             throw new IllegalArgumentException(msg);
         }
+
         clientClassName = serviceConfig.getRepositoryClient().getClientClass();
+
         if (clientClassName == null) {
             String msg = "Missing <client-class> in <repository-client>";
             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<File> files) throws IOException {
+        InputStream result = null;
+        List<InputStream> 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;
+       }
 }
index 0f57a48c5e253ca02e226def5cc7d4c68a3e1886..c3e57baf4f8e5f98489285fe07876d69aa9efd95 100644 (file)
@@ -10,7 +10,7 @@
         Schema for service layer configuration
 -->
 
-<xs:schema 
+<xs:schema
   xmlns:xs="http://www.w3.org/2001/XMLSchema"
   xmlns="http://collectionspace.org/services/config"
   xmlns:types="http://collectionspace.org/services/config/types"
     <xs:element name="service-config">
         <xs:complexType>
             <xs:sequence>
-               <xs:element name="use-app-generated-tenant-bindings" type="xs:boolean" minOccurs="0"  maxOccurs="1"/>
+               <xs:element name="use-app-generated-tenant-bindings" type="xs:boolean" minOccurs="0" maxOccurs="1"/>
                <xs:element name="cspace-instance-id" type="xs:string" default="" minOccurs="0"  maxOccurs="1"/>
                <xs:element name="db-csadmin-name" type="xs:string" minOccurs="1"  maxOccurs="1"/>
                <xs:element name="db-cspace-name" type="xs:string" minOccurs="1"  maxOccurs="1"/>
                <xs:element name="db-nuxeo-name" type="xs:string" minOccurs="1"  maxOccurs="1"/>
                 <!-- assumption: there is only one type of repository client used  -->
-                <xs:element name="repository-client" type="RepositoryClientConfigType" minOccurs="1"  maxOccurs="1"/>
-                <xs:element name="repository-workspace" type="RepositoryWorkspaceType" minOccurs="0"  maxOccurs="1" />
+                <xs:element name="repository-client" type="RepositoryClientConfigType" minOccurs="1" maxOccurs="1"/>
+                <xs:element name="repository-workspace" type="RepositoryWorkspaceType" minOccurs="0" maxOccurs="1" />
+                <xs:element name="security" type="SecurityType" minOccurs="0" maxOccurs="1" />
             </xs:sequence>
         </xs:complexType>
     </xs:element>
 
     <xs:complexType name="RepositoryClientConfigType">
         <xs:sequence>
-            <xs:element name="host" type="xs:string" minOccurs="1"  maxOccurs="1" />
-            <xs:element name="port" type="xs:int" minOccurs="1"  maxOccurs="1" />
+            <xs:element name="host" type="xs:string" minOccurs="1" maxOccurs="1" />
+            <xs:element name="port" type="xs:int" minOccurs="1" maxOccurs="1" />
             <!-- protocol (http/https) is only applicable for rest client -->
-            <xs:element name="protocol" type="xs:string" minOccurs="0"  maxOccurs="1" />
-            <xs:element name="user" type="xs:string" minOccurs="1"  maxOccurs="1" />
+            <xs:element name="protocol" type="xs:string" minOccurs="0" maxOccurs="1" />
+            <xs:element name="user" type="xs:string" minOccurs="1" maxOccurs="1" />
             <!-- password should not be in cleartext -->
-            <xs:element name="password" type="xs:string" minOccurs="1"  maxOccurs="1" />
+            <xs:element name="password" type="xs:string" minOccurs="1" maxOccurs="1" />
             <!-- default client is java -->
-            <xs:element name="client-type" type="ClientType" minOccurs="1"  maxOccurs="1" />
+            <xs:element name="client-type" type="ClientType" minOccurs="1" maxOccurs="1" />
             <!-- default client is org.collectionspace.services.nuxeo.client.java.RepositoryJavaClient -->
-            <xs:element name="client-class" type="xs:string" minOccurs="1"  maxOccurs="1" />
+            <xs:element name="client-class" type="xs:string" minOccurs="1" maxOccurs="1" />
                                                <xs:element name="properties" type="types:PropertyType" minOccurs="0" maxOccurs="unbounded"/>
         </xs:sequence>
         <!-- name of the client -->
             <xs:element name="workspace" maxOccurs="unbounded" >
                 <xs:complexType>
                     <xs:sequence>
-                        <xs:element name="service-name" type="xs:string" minOccurs="1"  maxOccurs="1" />
+                        <xs:element name="service-name" type="xs:string" minOccurs="1" maxOccurs="1" />
                         <!-- workspace name is required for Repository Java client -->
-                        <xs:element name="workspace-name" type="xs:string" minOccurs="1"  maxOccurs="1" />
+                        <xs:element name="workspace-name" type="xs:string" minOccurs="1" maxOccurs="1" />
                         <!-- workspace ids are required only for Repository REST client -->
-                        <xs:element name="workspace-id" type="xs:string" minOccurs="1"  maxOccurs="1" />
+                        <xs:element name="workspace-id" type="xs:string" minOccurs="1" maxOccurs="1" />
                     </xs:sequence>
                 </xs:complexType>
             </xs:element>
         </xs:sequence>
     </xs:complexType>
-    
+
     <!-- enumeration defining the type repository client -->
     <xs:simpleType name="ClientType">
         <xs:restriction base="xs:string">
         </xs:restriction>
     </xs:simpleType>
 
+    <xs:complexType name="SecurityType">
+        <xs:annotation>
+            <xs:documentation>Configures security.</xs:documentation>
+        </xs:annotation>
+        <xs:sequence>
+            <xs:element name="cors" type="CORSType" minOccurs="0" maxOccurs="1" />
+            <xs:element name="oauth" type="OAuthType" minOccurs="0" maxOccurs="1" />
+        </xs:sequence>
+    </xs:complexType>
 
-</xs:schema>
+    <xs:complexType name="CORSType">
+        <xs:sequence>
+            <!-- An origin for which cross-origin requests are allowed. -->
+            <xs:element name="allowed-origin" type="xs:string" minOccurs="0" maxOccurs="unbounded" />
+
+            <!-- How long, as a duration, the response from a pre-flight request can be cached by clients. -->
+            <!-- Specified in ISO-8601 duration format: PnDTnHnMn.nS -->
+            <xs:element name="max-age" type="xs:string" minOccurs="0" maxOccurs="1" />
+        </xs:sequence>
+    </xs:complexType>
+
+    <xs:complexType name="OAuthType">
+        <xs:sequence>
+            <xs:element name="default-access-token-time-to-live" type="xs:string" minOccurs="1" maxOccurs="1">
+                <xs:annotation>
+                    <xs:documentation>
+                        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
+                    </xs:documentation>
+                </xs:annotation>
+            </xs:element>
+
+            <xs:element name="client-registrations" type="OAuthClientRegistrationsType" minOccurs="0" maxOccurs="1" />
+        </xs:sequence>
+    </xs:complexType>
+
+    <xs:complexType name="OAuthClientRegistrationsType">
+        <xs:sequence>
+            <xs:element name="client" type="OAuthClientType" minOccurs="0" maxOccurs="unbounded" />
+        </xs:sequence>
+    </xs:complexType>
+
+    <xs:complexType name="OAuthClientType">
+        <xs:sequence>
+            <xs:element name="client-id" type="xs:string" minOccurs="0" maxOccurs="1" />
+            <xs:element name="client-name" type="xs:string" minOccurs="0" maxOccurs="1" />
+            <xs:element name="client-authentication-method" type="OAuthClientAuthenticationMethodEnum" minOccurs="0" maxOccurs="unbounded" />
+            <xs:element name="authorization-grant-type" type="OAuthAuthorizationGrantTypeEnum" minOccurs="0" maxOccurs="unbounded" />
+            <xs:element name="scope" type="OAuthScopeEnum" minOccurs="0" maxOccurs="unbounded" />
+            <xs:element name="redirect-uri" type="xs:string" minOccurs="0" maxOccurs="unbounded" />
+            <xs:element name="client-settings" type="OAuthClientSettingsType" minOccurs="0" maxOccurs="1" />
+            <xs:element name="token-settings" type="OAuthTokenSettingsType" minOccurs="0" maxOccurs="1" />
+        </xs:sequence>
 
+        <xs:attribute name="id" type="xs:string" use="required" />
+    </xs:complexType>
+
+    <xs:simpleType name="OAuthClientAuthenticationMethodEnum">
+        <xs:restriction base="xs:string">
+            <xs:enumeration value="basic"/>
+            <xs:enumeration value="client_secret_basic"/>
+            <xs:enumeration value="post"/>
+            <xs:enumeration value="client_secret_post"/>
+            <xs:enumeration value="client_secret_jwt"/>
+            <xs:enumeration value="private_key_jwt"/>
+            <xs:enumeration value="none"/>
+        </xs:restriction>
+    </xs:simpleType>
+
+    <xs:simpleType name="OAuthAuthorizationGrantTypeEnum">
+        <xs:restriction base="xs:string">
+            <xs:enumeration value="authorization_code"/>
+            <xs:enumeration value="implicit"/>
+            <xs:enumeration value="refresh_token"/>
+            <xs:enumeration value="client_credentials"/>
+        </xs:restriction>
+    </xs:simpleType>
+
+    <xs:simpleType name="OAuthScopeEnum">
+        <xs:restriction base="xs:string">
+            <xs:enumeration value="cspace.full"/>
+        </xs:restriction>
+    </xs:simpleType>
+
+    <xs:complexType name="OAuthClientSettingsType">
+        <xs:sequence>
+            <xs:element name="require-authorization-consent" type="xs:boolean" minOccurs="0" maxOccurs="1" />
+        </xs:sequence>
+    </xs:complexType>
+
+    <xs:complexType name="OAuthTokenSettingsType">
+        <xs:sequence>
+            <xs:element name="access-token-time-to-live" type="xs:string" minOccurs="0" maxOccurs="1" />
+        </xs:sequence>
+    </xs:complexType>
+</xs:schema>
index 586ea2cabe0caa639aa76e1644ee3f5b63d9440f..6f4018708524e69e02e4b71f5293149ae5797cd8 100644 (file)
@@ -51,6 +51,7 @@
                        <xs:element name="properties" type="types:PropertyType" minOccurs="0" maxOccurs="unbounded"/>
                        <xs:element name="remoteClientConfigurations" type="RemoteClientConfigurations" minOccurs="0" maxOccurs="1"/>
                        <xs:element name="emailConfig" type="EmailConfig" minOccurs="0" maxOccurs="1"/>
+                       <xs:element name="uiConfig" type="UIConfig" minOccurs="0" maxOccurs="1"/>
                        <xs:element name="elasticSearchDocumentWriter" type="xs:string" minOccurs="0" maxOccurs="1"/>
                        <xs:element name="elasticSearchIndexConfig" type="ElasticSearchIndexConfig" minOccurs="0" maxOccurs="1"/>
                        <xs:element name="serviceBindings" type="service:ServiceBindingType" minOccurs="0" maxOccurs="unbounded"/>
@@ -67,6 +68,8 @@
                <!-- domain name including subdomain but not TLD -->
                <!-- e.g. hearstmuseum.berkeley or movingimage.us -->
                <xs:attribute name="name" type="xs:string" use="required"/>
+               <!-- Short name (not a domain), e.g. mmi, pahma -->
+               <xs:attribute name="shortName" type="xs:string" use="required"/>
                <!-- display name as Museum of Moving Images -->
                <xs:attribute name="displayName" type="xs:string" use="required"/>
                <xs:attribute name="version" type="types:VersionType" use="required"/>
                </xs:sequence>
        </xs:complexType>
 
+       <xs:complexType name="UIConfig">
+               <xs:annotation>
+                       <xs:documentation>Configuration of the CollectionSpace UI.</xs:documentation>
+               </xs:annotation>
+
+               <xs:sequence>
+                       <xs:element name="baseUrl" type="xs:string" minOccurs="0" maxOccurs="1">
+                               <xs:annotation>
+                                       <xs:documentation>
+                                               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/
+                                       </xs:documentation>
+                               </xs:annotation>
+                       </xs:element>
+
+                       <xs:element name="loginSuccessUrl" type="xs:string" minOccurs="0" maxOccurs="1">
+                               <xs:annotation>
+                                       <xs:documentation>
+                                               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.
+                                       </xs:documentation>
+                               </xs:annotation>
+                       </xs:element>
+
+                       <xs:element name="authorizationSuccessUrl" type="xs:string" minOccurs="0" maxOccurs="1">
+                               <xs:annotation>
+                                       <xs:documentation>
+                                               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.
+                                       </xs:documentation>
+                               </xs:annotation>
+                       </xs:element>
+
+                       <xs:element name="logoutSuccessUrl" type="xs:string" minOccurs="0" maxOccurs="1">
+                               <xs:annotation>
+                                       <xs:documentation>
+                                               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.
+                                       </xs:documentation>
+                               </xs:annotation>
+                       </xs:element>
+               </xs:sequence>
+       </xs:complexType>
+
        <xs:complexType name="ElasticSearchIndexConfig">
                <xs:annotation>
                        <xs:documentation>Configuration of a tenant's Elasticsearch index</xs:documentation>
                </xs:annotation>
                <xs:sequence>
                        <xs:element name="tokenExpirationSeconds" type="xs:integer" minOccurs="1" maxOccurs="1"/>
-                       <xs:element name="loginpage" type="xs:string" minOccurs="1" maxOccurs="1"/>
                        <xs:element name="subject" type="xs:string" minOccurs="1" maxOccurs="1"/>
                        <xs:element name="message" type="xs:string" minOccurs="1" maxOccurs="1"/>
                </xs:sequence>
diff --git a/services/login/client/pom.xml b/services/login/client/pom.xml
new file mode 100644 (file)
index 0000000..338b51a
--- /dev/null
@@ -0,0 +1,18 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+    xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
+    <parent>
+        <groupId>org.collectionspace.services</groupId>
+        <artifactId>org.collectionspace.services.login</artifactId>
+        <version>${revision}</version>
+    </parent>
+
+    <modelVersion>4.0.0</modelVersion>
+    <artifactId>org.collectionspace.services.login.client</artifactId>
+    <name>services.login.client</name>
+
+    <build>
+        <finalName>collectionspace-services-login-client</finalName>
+    </build>
+</project>
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 (file)
index 0000000..be50119
--- /dev/null
@@ -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 (file)
index 0000000..d1b2f21
--- /dev/null
@@ -0,0 +1,18 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
+       <parent>
+               <groupId>org.collectionspace.services</groupId>
+               <artifactId>org.collectionspace.services.main</artifactId>
+           <version>${revision}</version>
+       </parent>
+       
+       <modelVersion>4.0.0</modelVersion>
+       <artifactId>org.collectionspace.services.login</artifactId>
+       <name>services.login</name>
+       <packaging>pom</packaging>
+    
+    <modules>
+        <module>client</module>
+        <module>service</module>
+    </modules>
+</project>
diff --git a/services/login/service/pom.xml b/services/login/service/pom.xml
new file mode 100644 (file)
index 0000000..62935bd
--- /dev/null
@@ -0,0 +1,54 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
+
+    <parent>
+        <groupId>org.collectionspace.services</groupId>
+        <artifactId>org.collectionspace.services.login</artifactId>
+        <version>${revision}</version>
+    </parent>
+
+    <modelVersion>4.0.0</modelVersion>
+    <artifactId>org.collectionspace.services.login.service</artifactId>
+    <name>services.login.service</name>
+    <packaging>jar</packaging>
+
+    <dependencies>
+        <dependency>
+            <groupId>org.collectionspace.services</groupId>
+            <artifactId>org.collectionspace.services.common</artifactId>
+            <version>${project.version}</version>
+        </dependency>
+        <dependency>
+            <groupId>org.collectionspace.services</groupId>
+            <artifactId>org.collectionspace.services.authentication.service</artifactId>
+            <version>${project.version}</version>
+            <scope>provided</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.collectionspace.services</groupId>
+            <artifactId>org.collectionspace.services.login.client</artifactId>
+            <version>${project.version}</version>
+        </dependency>
+
+        <dependency>
+            <groupId>org.apache.tomcat</groupId>
+            <artifactId>tomcat-servlet-api</artifactId>
+            <version>${tomcat.version}</version>
+            <scope>provided</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.jboss.resteasy</groupId>
+            <artifactId>resteasy-jaxrs</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.springframework.security</groupId>
+            <artifactId>spring-security-web</artifactId>
+            <version>${spring.security.version}</version>
+            <scope>provided</scope>
+        </dependency>
+    </dependencies>
+
+    <build>
+        <finalName>collectionspace-services-login-service</finalName>
+    </build>
+</project>
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 (file)
index 0000000..cdb2d22
--- /dev/null
@@ -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<String, Object> uiConfig = new HashMap<>();
+        CsrfToken csrfToken = (CsrfToken) request.getAttribute(CsrfToken.class.getName());
+
+        if (csrfToken != null) {
+            Map<String, Object> 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<String, String> 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 (file)
index 0000000..839deae
--- /dev/null
@@ -0,0 +1,18 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+    xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
+    <parent>
+        <groupId>org.collectionspace.services</groupId>
+        <artifactId>org.collectionspace.services.logout</artifactId>
+        <version>${revision}</version>
+    </parent>
+
+    <modelVersion>4.0.0</modelVersion>
+    <artifactId>org.collectionspace.services.logout.client</artifactId>
+    <name>services.logout.client</name>
+
+    <build>
+        <finalName>collectionspace-services-logout-client</finalName>
+    </build>
+</project>
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 (file)
index 0000000..be03581
--- /dev/null
@@ -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 (file)
index 0000000..e0f6bab
--- /dev/null
@@ -0,0 +1,18 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
+       <parent>
+               <groupId>org.collectionspace.services</groupId>
+               <artifactId>org.collectionspace.services.main</artifactId>
+               <version>${revision}</version>
+       </parent>
+
+       <modelVersion>4.0.0</modelVersion>
+       <artifactId>org.collectionspace.services.logout</artifactId>
+       <name>services.logout</name>
+       <packaging>pom</packaging>
+
+       <modules>
+               <module>client</module>
+               <module>service</module>
+       </modules>
+</project>
diff --git a/services/logout/service/pom.xml b/services/logout/service/pom.xml
new file mode 100644 (file)
index 0000000..fab7d12
--- /dev/null
@@ -0,0 +1,54 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
+
+    <parent>
+        <groupId>org.collectionspace.services</groupId>
+        <artifactId>org.collectionspace.services.logout</artifactId>
+        <version>${revision}</version>
+    </parent>
+
+    <modelVersion>4.0.0</modelVersion>
+    <artifactId>org.collectionspace.services.logout.service</artifactId>
+    <name>services.logout.service</name>
+    <packaging>jar</packaging>
+
+    <dependencies>
+        <dependency>
+            <groupId>org.collectionspace.services</groupId>
+            <artifactId>org.collectionspace.services.common</artifactId>
+            <version>${project.version}</version>
+        </dependency>
+        <dependency>
+            <groupId>org.collectionspace.services</groupId>
+            <artifactId>org.collectionspace.services.authentication.service</artifactId>
+            <version>${project.version}</version>
+            <scope>provided</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.collectionspace.services</groupId>
+            <artifactId>org.collectionspace.services.logout.client</artifactId>
+            <version>${project.version}</version>
+        </dependency>
+
+        <dependency>
+            <groupId>org.apache.tomcat</groupId>
+            <artifactId>tomcat-servlet-api</artifactId>
+            <version>${tomcat.version}</version>
+            <scope>provided</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.jboss.resteasy</groupId>
+            <artifactId>resteasy-jaxrs</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.springframework.security</groupId>
+            <artifactId>spring-security-web</artifactId>
+            <version>${spring.security.version}</version>
+            <scope>provided</scope>
+        </dependency>
+    </dependencies>
+
+    <build>
+        <finalName>collectionspace-services-logout-service</finalName>
+    </build>
+</project>
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 (file)
index 0000000..f9fba20
--- /dev/null
@@ -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<String, Object> uiConfig = new HashMap<>();
+
+        CsrfToken csrfToken = (CsrfToken) request.getAttribute(CsrfToken.class.getName());
+
+        if (csrfToken != null) {
+            Map<String, Object> 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<String, String> 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();
+    }
+}
index e5462fa7b0063104974b06033c5f9c252f49f3c1..90d73faa21e39ad3c2b58cf3d55330231203e402 100644 (file)
                <module>IntegrationTests</module>
                <module>PerformanceTests</module>
                <module>security</module>
+               <module>login</module>
+               <module>logout</module>
                <module>JaxRsServiceProvider</module>
        </modules>
 
index 99455ef5a99ff8f03c56fbf5a885cb94fec9994a..1a319e30a56e4547fafba13a99126759c4c90f98 100644 (file)
@@ -1,7 +1,7 @@
 package org.collectionspace.services.systeminfo;
 
 /**
- * Client class for Structureddate service.
+ * Client class for system info service.
  * @author remillet
  *
  */