.factorypath
m2-settings.xml
*.log
+*.log.*
cspace-app-perflog.csv
.flattened-pom.xml
# 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
<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" />
<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>
<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>
CHECKOUT_DIR=$3
OUTPUT_DIR=$4
LIBRARY_NAME=$5
+SERVICE_LIBRARY_NAME=$6
CODE_DIR=$CHECKOUT_DIR/$PACKAGE_NAME.js
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
--- /dev/null
+<#--
+ 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>
<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>
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;
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");
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())
.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);
}
}
}
- 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());
}
}
<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" />
<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>
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;
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());
<!-- 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
<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>
+++ /dev/null
-<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>
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>
- 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.
-->
<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>
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();
<?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"
</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>
-
<?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"
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>
</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>
-
<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>
*/
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;
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;
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;
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;
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() {
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
*/
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.
*
* @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;
}
} 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();
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) {
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.
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();
}
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;
/**
*
- * @author
+ * @author
*/
public class AccountDocumentHandler
extends JpaDocumentHandler<AccountsCommon, AccountsCommonList, AccountsCommon, List<AccountsCommon>> {
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);
//
// 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
//
// 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);
//
if (from.getPersonRefName() != null) {
to.setPersonRefName(from.getPersonRefName());
}
+
// Note that we do not allow update of locks
//fixme update for tenant association
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
@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) {
SubjectType.ROLE);
account.setRoleList(AccountRoleFactory.convert(accountRole.getRole()));
}
-
+
return wrapDoc.getWrappedObject();
}
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)
/**
*
- * @author
+ * @author
*/
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");
* @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();
token.setExpireSeconds(expireSeconds);
token.setEnabled(true);
token.setCreatedAtItem(new Date());
-
+
em.getTransaction().begin();
em.persist(token);
em.getTransaction().commit();
JpaStorageUtils.releaseEntityManagerFactory(emf);
}
}
-
+
return token;
}
* 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);
* 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);
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);
JpaStorageUtils.releaseEntityManagerFactory(emf);
}
}
-
+
return tokenFound;
- }
+ }
/**
* Deletes the token with given id
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();
if (emf != null) {
JpaStorageUtils.releaseEntityManagerFactory(emf);
}
- }
+ }
}
private String getEncPassword(String userId, byte[] password) throws BadRequestException {
} 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;
}
}
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);
} finally {
ctx.closeConnection();
}
-
+
return userFound;
- }
+ }
/**
* updateUser for given userId
} 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;
}
}
);
-- 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,
<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>
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);
+++ /dev/null
-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);
- }
-
-}
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"
*/
// 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();
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();
append("name", name).
toString();
}
-
+
public String getId() {
return id;
}
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
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
public String getSalt() {
return salt != null ? salt : "";
}
-
}
--- /dev/null
+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();
+ }
+}
--- /dev/null
+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();
+ }
+}
* Interface for the CollectionSpace realm.
*/
public interface CSpaceRealm {
-
+
/**
* Retrieves the "salt" used to encrypt the user's password
* @param username
/**
* Retrieves the hashed password used to authenticate a user.
- *
+ *
* @param username
* @return the password
* @throws AccountNotFoundException if the user is not found
/**
* Retrieves the roles for a user.
- *
+ *
* @param username
* @return a collection of roles
* @throws AccountException if the roles could not be retrieved
/**
* Retrieves the enabled tenants associated with a user.
- *
+ *
* @param username
* @return a collection of tenants
* @throws AccountException if the tenants could not be retrieved
/**
* 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;
-
}
/**
* 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;
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) {
}
}
}
-
+
protected long getMaxRetrySeconds() {
return this.maxRetrySeconds;
}
-
+
protected void setDelayBetweenAttemptsMillis(Map<String, ?> options) {
Object optionsObj = options.get(DELAY_BETWEEN_ATTEMPTS_MILLISECONDS_STR);
if (optionsObj != null) {
}
}
}
-
+
protected long getDelayBetweenAttemptsMillis() {
return this.delayBetweenAttemptsMillis;
}
-
+
public CSpaceDbRealm() {
datasourceName = DEFAULT_DATASOURCE_NAME;
}
-
+
/**
* CSpace Database Realm
* @param datasourceName datasource name
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);
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");
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;
}
return accountIsTenantManager;
}
-
+
/**
* Execute the tenantsQuery against the datasourceName to obtain the tenants for
* the authenticated user.
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;
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()) {
// empty Tenants set.
// FIXME should this be allowed?
}
-
+
return tenants;
}
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());
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.
// 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.
*/
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);
this.tenantsQueryNoDisabled = tenantQuery;
}
*/
-
+
/*
* This method crawls the exception chain looking for network related exceptions and
* returns 'true' if it finds one.
result = true;
break;
}
-
+
cause = cause.getCause();
}
return result;
}
-
+
/*
* Return 'true' if the exception is in the "java.net" package.
*/
}
}
}
-
+
return salt;
}
-
}
--- /dev/null
+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;
+ }
+}
--- /dev/null
+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);
+ }
+}
--- /dev/null
+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;
+ }
+}
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;
}
}
+++ /dev/null
-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;
- }
- }
-}
String salt = null;
Set<CSpaceTenant> tenants = null;
Set<GrantedAuthority> grantedAuthorities = null;
-
+
try {
password = realm.getPassword(username);
salt = realm.getSalt(username);
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;
}
}
/**
* 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
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
/**
* 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();
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;
}
}
<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>
-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
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
--
-- 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)
);
);
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)
+);
<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>
+++ /dev/null
-/**
- * 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;
- }
-}
* @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){
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);
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);
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";
}
return dir + file;
}
-
+
public static String getFilenameExtension(String filename) {
int dot = filename.lastIndexOf(FILE_EXTENSION_SEPARATOR);
return (dot>=0)?filename.substring(dot + 1):null;
public static String getStackTrace(Throwable e){
return getStackTrace(e, -1);
}
-
+
public static String implode(String strings[], String sep) {
String implodedString;
if (strings.length == 0) {
}
return implodedString;
}
-
+
/**
* Return a set of properties from a properties file.
- *
+ *
* @param clientPropertiesFilename
* @return
*/
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) {
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
//
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);
}
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 {
}
}
}
-
+
return result;
}
-
+
/**
* Test to see if 'propertyValue' is actually a property variable
* @param propertyValue
*/
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);
result = true;
}
}
-
+
return result;
}
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;
}
}
<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>
<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>
--- /dev/null
+<?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>
<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 -->
</tenant:tenantBinding>
</tenant:TenantBindingConfig>
-
<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>
</service:params>
</service:initHandler>
</tenant:serviceBindings>
-
+
</tenant:tenantBinding>
</tenant:TenantBindingConfig>
<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 -->
+++ /dev/null
-<?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>
<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 -->
<!-- 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>
<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>
<?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>
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;
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.
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
//
//
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
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.
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.
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;
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;
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
//
//
// 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};
// 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
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=?";
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
*/
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);
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
+ " 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)) {
} 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,
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);
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,
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()
+ 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()
+ ":" + "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
//
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);
//
+ " 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"
}
return tenantInfo;
}
-
+
private static ArrayList<String> compileExistingTenants(Connection conn, Hashtable<String, String> tenantInfo)
throws SQLException, Exception {
Statement stmt = null;
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;
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);
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);
}
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
+" 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)) {
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.
}
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);
}
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
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
* @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;
}
rs.close();
rs = null;
-
+
//
// Look for and save each tenants default Admin and Reader roles
//
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
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.
} 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));
}
}
}
-
+
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 {
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
}
pstmt.executeUpdate();
*/
-
+
pstmt.close();
} catch(Exception e) {
throw e;
pstmt.close();
}
}
-
+
/*
* Using the tenant bindings, ensure there are corresponding Tenant records (db columns).
*/
}
}
}
-
+
/**
- *
+ *
* @param tenantBindingConfigReader
* @param databaseProductType
* @param cspaceDatabaseName
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,
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) {
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();
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();
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);
} 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: "
+ " 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: "
+ " 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
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()) {
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)) {
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();
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 {
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;
} finally {
if (stmt != null) stmt.close();
}
-
+
return result;
}
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
*/
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());
+ ":" + 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());
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;
}
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);
--- /dev/null
+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));
+ }
+}
*/
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;
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;
/** 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;
//
switch (resName) {
case AuthZ.PASSWORD_RESET:
case AuthZ.PROCESS_PASSWORD_RESET:
+ case LOGIN:
+ case LOGOUT:
case SYSTEM_INFO:
+ case "":
return true;
}
@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();
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();
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;
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
/**
* 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);
}
/**
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);
- }
-
-
}
--- /dev/null
+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);
+ }
+}
@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();
package org.collectionspace.services.common.config;
import java.io.File;
+import java.util.List;
+
import org.collectionspace.services.common.api.JEEServerDeployment;
/**
*/
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
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");
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>();
result.add(repoDomain.getRepositoryName());
}
}
-
+
return result;
}
-
+
/*
* Returns 'true' if the tenant declares the default repository.
*/
}
}
}
-
+
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) {
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;
+ }
}
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
*
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;
@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);
}
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;
+ }
}
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>
<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"/>
<!-- 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>
--- /dev/null
+<?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>
--- /dev/null
+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;
+}
--- /dev/null
+<?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>
--- /dev/null
+<?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>
--- /dev/null
+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";
+ }
+}
--- /dev/null
+<?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>
--- /dev/null
+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;
+}
--- /dev/null
+<?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>
--- /dev/null
+<?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>
--- /dev/null
+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();
+ }
+}
<module>IntegrationTests</module>
<module>PerformanceTests</module>
<module>security</module>
+ <module>login</module>
+ <module>logout</module>
<module>JaxRsServiceProvider</module>
</modules>
package org.collectionspace.services.systeminfo;
/**
- * Client class for Structureddate service.
+ * Client class for system info service.
* @author remillet
*
*/