<class>org.collectionspace.services.account.AccountTenant</class>
<class>org.collectionspace.services.account.Status</class>
<class>org.collectionspace.services.authentication.User</class>
+ <class>org.collectionspace.services.authentication.Token</class>
<class>org.collectionspace.services.authorization.perms.Permission</class>
<class>org.collectionspace.services.authorization.perms.PermissionAction</class>
<class>org.collectionspace.services.authorization.PermissionRoleRel</class>
<!-- Exclude the resource path to public items' content from AuthN and AuthZ. Lets us publish resources with anonymous access. -->
<sec:http pattern="/publicitems/*/*/content" security="none" />
+ <!-- Exclude the resource path to handle an account password reset request from AuthN and AuthZ. Lets us process password resets anonymous access. -->
+ <sec:http pattern="/accounts/requestpasswordreset" security="none" />
+
+ <!-- Exclude the resource path to account process a password resets from AuthN and AuthZ. Lets us process password resets anonymous access. -->
+ <sec:http pattern="/accounts/processpasswordreset" security="none" />
+
<!-- All other paths must be authenticated. -->
<sec:http realm="org.collectionspace.services" create-session="stateless" authentication-manager-ref="userAuthenticationManager">
<sec:intercept-url pattern="/**" access="isFullyAuthenticated()" />
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 AccountClient() throws Exception {
super();
<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" />
</xs:element>
</xs:sequence>
</xs:complexType>
-
+
<!-- FIXME tenant definition could be in a separate schema -->
<xs:element name="tenant">
<xs:complexType>
*/
package org.collectionspace.services.account;
+import org.collectionspace.authentication.AuthN;
import org.collectionspace.services.account.storage.AccountStorageClient;
+import org.collectionspace.services.account.storage.csidp.TokenStorageClient;
+import org.collectionspace.services.authentication.Token;
import org.collectionspace.services.authorization.AccountPermission;
import org.collectionspace.services.authorization.AccountRole;
import org.collectionspace.services.authorization.AccountRoleRel;
import org.collectionspace.services.client.AccountClient;
import org.collectionspace.services.client.PayloadOutputPart;
import org.collectionspace.services.client.RoleClient;
+import org.collectionspace.services.common.EmailUtil;
import org.collectionspace.services.common.SecurityResourceBase;
+import org.collectionspace.services.common.ServiceMain;
import org.collectionspace.services.common.ServiceMessages;
import org.collectionspace.services.common.UriInfoWrapper;
+import org.collectionspace.services.common.authorization_mgt.AuthorizationCommon;
import org.collectionspace.services.common.context.RemoteServiceContextFactory;
import org.collectionspace.services.common.context.ServiceContext;
import org.collectionspace.services.common.context.ServiceContextFactory;
+import org.collectionspace.services.common.document.DocumentNotFoundException;
import org.collectionspace.services.common.query.UriInfoImpl;
import org.collectionspace.services.common.storage.StorageClient;
import org.collectionspace.services.common.storage.jpa.JpaStorageUtils;
+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 java.net.URI;
import java.net.URISyntaxException;
+import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import javax.ws.rs.Produces;
import javax.ws.rs.QueryParam;
import javax.ws.rs.core.Context;
+import javax.ws.rs.core.MultivaluedMap;
import javax.ws.rs.core.PathSegment;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.UriBuilder;
@Produces("application/xml")
public class AccountResource extends SecurityResourceBase {
- final Logger logger = LoggerFactory.getLogger(AccountResource.class);
+ 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() {
@GET
@Path("{csid}")
- public AccountsCommon getAccount(@PathParam("csid") String csid) {
- return (AccountsCommon)get(csid, AccountsCommon.class);
+ public AccountsCommon getAccount(@Context UriInfo ui, @PathParam("csid") String csid) {
+ return (AccountsCommon)get(ui, csid, AccountsCommon.class);
}
@GET
@PUT
@Path("{csid}")
- public AccountsCommon updateAccount(@PathParam("csid") String csid,AccountsCommon theUpdate) {
- return (AccountsCommon)update(csid, theUpdate, AccountsCommon.class);
+ public AccountsCommon updateAccount(@Context UriInfo ui, @PathParam("csid") String csid,AccountsCommon theUpdate) {
+ return (AccountsCommon)update(ui, csid, theUpdate, AccountsCommon.class);
}
+
+ /**
+ * Resets an accounts password.
+ *
+ * Requires three query params:
+ * id = CSID of the account
+ * token = the password reset token generated by the system
+ * password = the new password
+ *
+ * @param ui
+ * @return
+ */
+ @POST
+ @Path(PROCESS_PASSWORD_RESET_PATH)
+ public Response processPasswordReset(@Context UriInfo ui) {
+ Response response = null;
+ //
+ // Create a read/write copy of the UriInfo info
+ //
+ ui = new UriInfoWrapper(ui);
+ MultivaluedMap<String,String> queryParams = ui.getQueryParameters();
+
+ //
+ // Get the 'token' and 'password' params
+ //
+ String tokenId = queryParams.getFirst("token");
+ if (tokenId == null || tokenId.trim().isEmpty()) {
+ response = Response.status(Response.Status.BAD_REQUEST).entity(
+ "The query parameter 'token' is missing or contains no value.").type("text/plain").build();
+ return response;
+ }
+
+ String password = queryParams.getFirst("password");
+ if (password == null || password.trim().isEmpty()) {
+ response = Response.status(Response.Status.BAD_REQUEST).entity(
+ "The query parameter 'password' is missing or contains no value.").type("text/plain").build();
+ return response;
+ }
+
+ //
+ // Retrieve the token from the DB
+ //
+ Token token;
+ try {
+ token = TokenStorageClient.get(tokenId);
+ } catch (DocumentNotFoundException e1) {
+ String errMsg = String.format("The token '%s' is not valid or does not exist.",
+ tokenId);
+ response = Response.status(Response.Status.BAD_REQUEST).entity(errMsg).type("text/plain").build();
+ return response;
+ }
+
+ //
+ // Make sure the token is not null
+ //
+ if (token == null) {
+ String errMsg = String.format("The token '%s' is not valid.",
+ tokenId);
+ response = Response.status(Response.Status.BAD_REQUEST).entity(errMsg).type("text/plain").build();
+ return response;
+ }
+
+ //
+ // From the token, get the account to update.
+ //
+ queryParams.add(AuthN.TENANT_ID_QUERY_PARAM, token.getTenantId());
+ AccountsCommon targetAccount = getAccount(ui, token.getAccountCsid());
+ if (targetAccount == null) {
+ String errMsg = String.format("The token '%s' is not valid. The account it was created for no longer exists.",
+ tokenId);
+ response = Response.status(Response.Status.BAD_REQUEST).entity(errMsg).type("text/plain").build();
+ return response;
+ }
+
+ //
+ //
+ //
+ String tenantId = token.getTenantId();
+ TenantBindingType tenantBindingType = ServiceMain.getInstance().getTenantBindingConfigReader().getTenantBinding(tenantId);
+ EmailConfig emailConfig = tenantBindingType.getEmailConfig();
+ if (emailConfig != null) {
+ try {
+ if (AuthorizationCommon.hasTokenExpired(emailConfig, token) == false) {
+ AccountsCommon accountUpdate = new AccountsCommon();
+ accountUpdate.setUserId(targetAccount.getUserId());
+ accountUpdate.setPassword(password.getBytes());
+ updateAccount(ui, targetAccount.getCsid(), accountUpdate);
+ String msg = String.format("Successfully reset password using token ID='%s'.",
+ token.getId());
+ response = Response.status(Response.Status.OK).entity(msg).type("text/plain").build();
+ } else {
+ String errMsg = String.format("Could not reset password using token with ID='%s'. Password reset token has expired.",
+ token.getId());
+ response = Response.status(Response.Status.INTERNAL_SERVER_ERROR).entity(errMsg).type("text/plain").build();
+ }
+ } catch (NoSuchAlgorithmException e) {
+ String errMsg = String.format("Could not reset password for using token ID='%s'. Error: '%s'",
+ e.getMessage(), token.getId());
+ response = Response.status(Response.Status.BAD_REQUEST).entity(errMsg).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.",
+ tenantId);
+ response = Response.status(Response.Status.BAD_REQUEST).entity(errMsg).type("text/plain").build();
+ }
+
+ return response;
+ }
+
+ @POST
+ @Path(PASSWORD_RESET_PATH)
+ 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) {
+ response = Response.status(Response.Status.BAD_REQUEST).entity("You must specify an 'email' query paramater.").type("text/plain").build();
+ return response;
+ }
- @DELETE
+ String tenantId = queryParams.getFirst(AuthN.TENANT_ID_QUERY_PARAM);
+ if (tenantId == null) {
+ response = Response.status(Response.Status.BAD_REQUEST).entity("You must specify an 'tid' (tenant ID) query paramater.").type("text/plain").build();
+ return response;
+ }
+
+ AccountsCommonList accountList = getAccountList(ui);
+ if (accountList == null || accountList.getTotalItems() == 0) {
+ response = Response.status(Response.Status.NOT_FOUND).entity("Could not locatate an account associated with the email: " +
+ email).type("text/plain").build();
+ } else if (accountList.getTotalItems() > 1) {
+ response = Response.status(Response.Status.BAD_REQUEST).entity("Located more than one account associated with the email: " +
+ email).type("text/plain").build();
+ } else {
+ AccountListItem accountListItem = accountList.getAccountListItem().get(0);
+ try {
+ response = requestPasswordReset(ui, tenantId, accountListItem);
+ } catch (Exception e) {
+ response = Response.status(Response.Status.INTERNAL_SERVER_ERROR).entity(e.getMessage()).type("text/plain").build();
+ }
+ }
+
+ return response;
+ }
+
+ private boolean contains(String targetTenantID, List<AccountTenant> accountTenantList) {
+ boolean result = false;
+
+ for (AccountTenant accountTenant : accountTenantList) {
+ if (accountTenant.getTenantId().equalsIgnoreCase(targetTenantID)) {
+ result = true;
+ break;
+ }
+ }
+
+ return result;
+ }
+
+ /*
+ * Sends an email to a user allow them to reset their password.
+ */
+ private Response requestPasswordReset(UriInfo ui, String targetTenantID, AccountListItem accountListItem) throws Exception {
+ Response result = null;
+
+ if (contains(targetTenantID, accountListItem.getTenants()) == false) {
+ String errMsg = String.format("Could not send a password request email to user ID='%s'. That account is not associtated with the targeted tenant ID = '%s'.",
+ accountListItem.email, targetTenantID);
+ result = Response.status(Response.Status.INTERNAL_SERVER_ERROR).entity(errMsg).type("text/plain").build();
+ return result;
+ }
+
+ TenantBindingType tenantBindingType = ServiceMain.getInstance().getTenantBindingConfigReader().getTenantBinding(targetTenantID);
+ EmailConfig emailConfig = tenantBindingType.getEmailConfig();
+ if (emailConfig != null) {
+ UriBuilder baseUrlBuilder = ui.getBaseUriBuilder();
+ String deprecatedConfigBaseUrl = emailConfig.getBaseurl();
+
+ Object[] emptyValues = new String[0];
+ String baseUrl = baseUrlBuilder.replacePath(null).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.
+ //
+ if (deprecatedConfigBaseUrl != null) {
+ if (deprecatedConfigBaseUrl.equalsIgnoreCase(baseUrl) == false) {
+ String warnMsg = String.format("Ignoring deprecated 'baseurl' email config value '%s'. Using '%s' instead.",
+ deprecatedConfigBaseUrl, baseUrl);
+ logger.warn(warnMsg);
+ }
+ }
+
+ Token token = TokenStorageClient.create(accountListItem.getCsid(), targetTenantID,
+ emailConfig.getPasswordResetConfig().getTokenExpirationSeconds());
+ 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'",
+ 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 send to '%s'.", 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.",
+ targetTenantID);
+ result = Response.status(Response.Status.BAD_REQUEST).entity(errMsg).type("text/plain").build();
+ }
+
+ return result;
+ }
+
+ @DELETE
@Path("{csid}")
public Response deleteAccount(@Context UriInfo uriInfo, @PathParam("csid") String csid) {
logger.debug("deleteAccount with csid=" + csid);
AccountListItem accListItem = new AccountListItem();
accListItem.setScreenName(account.getScreenName());
accListItem.setUserid(account.getUserId());
+ accListItem.setTenantid(account.getTenants().get(0).getTenantId()); // pick the default/first tenant
+ accListItem.setTenants(account.getTenants());
accListItem.setEmail(account.getEmail());
accListItem.setStatus(account.getStatus());
String id = account.getCsid();
import java.util.HashMap;
import javax.persistence.EntityManager;
import javax.persistence.EntityManagerFactory;
+
import org.collectionspace.services.account.AccountsCommon;
import org.collectionspace.services.account.storage.csidp.UserStorageClient;
import org.collectionspace.services.authentication.User;
}
throw new DocumentException(e);
} finally {
- if (em != null) {
+ if (emf != null) {
JpaStorageUtils.releaseEntityManagerFactory(emf);
}
}
}
- @Override
+ @Override
public void get(ServiceContext ctx, String id, DocumentHandler handler)
throws DocumentNotFoundException, DocumentException {
if (ctx == null) {
if (docFilter == null) {
docFilter = handler.createDocumentFilter();
}
- EntityManagerFactory emf = null;
- EntityManager em = null;
+
try {
handler.prepare(Action.GET);
Object o = null;
o = JpaStorageUtils.getEntity(
"org.collectionspace.services.account.AccountsCommon", whereClause, params);
if (null == o) {
- if (em != null && em.getTransaction().isActive()) {
- em.getTransaction().rollback();
- }
String msg = "could not find entity with id=" + id;
throw new DocumentNotFoundException(msg);
}
logger.debug("Caught exception ", e);
}
throw new DocumentException(e);
- } finally {
- if (emf != null) {
- JpaStorageUtils.releaseEntityManagerFactory(emf);
- }
}
}
package org.collectionspace.services.account.storage;
+import org.collectionspace.services.client.AccountClient;
+
/**
* AccountStorageConstants declares query params, etc.
* @author
final public static String Q_SCREEN_NAME = "sn";
final public static String Q_USER_ID= "uid";
- final public static String Q_EMAIL = "email";
-
+ final public static String Q_EMAIL = AccountClient.EMAIL_QUERY_PARAM;
final public static String SCREEN_NAME = "screenName";
final public static String USER_ID = "userId";
--- /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 2010 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.
+ */
+/*
+ * To change this template, choose Tools | Templates
+ * and open the template in the editor.
+ */
+package org.collectionspace.services.account.storage.csidp;
+
+import java.math.BigInteger;
+import java.util.Date;
+import java.util.UUID;
+
+import javax.persistence.EntityManager;
+import javax.persistence.EntityManagerFactory;
+import javax.persistence.Query;
+
+import org.collectionspace.services.authentication.Token;
+import org.collectionspace.services.common.document.BadRequestException;
+import org.collectionspace.services.common.document.DocumentNotFoundException;
+import org.collectionspace.services.common.document.JaxbUtils;
+import org.collectionspace.services.common.security.SecurityUtils;
+import org.collectionspace.services.common.storage.jpa.JpaStorageUtils;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public class TokenStorageClient {
+
+ static private final Logger logger = LoggerFactory.getLogger(TokenStorageClient.class);
+
+ /**
+ * create user with given userId and password
+ * @param userId
+ * @param password
+ * @return user
+ */
+ static public Token create(String accountCsid, String tenantId, BigInteger expireSeconds) {
+ EntityManagerFactory emf = JpaStorageUtils.getEntityManagerFactory();
+ Token token = new Token();
+
+ try {
+ EntityManager em = emf.createEntityManager();
+
+ token.setId(UUID.randomUUID().toString());
+ token.setAccountCsid(accountCsid);
+ token.setTenantId(tenantId);
+ token.setExpireSeconds(expireSeconds);
+ token.setEnabled(true);
+ token.setCreatedAtItem(new Date());
+
+ em.getTransaction().begin();
+ em.persist(token);
+ em.getTransaction().commit();
+
+ } finally {
+ if (emf != null) {
+ JpaStorageUtils.releaseEntityManagerFactory(emf);
+ }
+ }
+
+ return token;
+ }
+
+ /**
+ * Get token for given ID
+ * @param em EntityManager
+ * @param id
+ */
+ static public Token get(String id) throws DocumentNotFoundException {
+ EntityManagerFactory emf = JpaStorageUtils.getEntityManagerFactory();
+ Token tokenFound = null;
+
+ try {
+ EntityManager em = emf.createEntityManager();
+ em.getTransaction().begin();
+ tokenFound = em.find(Token.class, id);
+ em.getTransaction().commit();
+ if (tokenFound == null) {
+ String msg = "Could not find token with ID=" + id;
+ logger.error(msg);
+ throw new DocumentNotFoundException(msg);
+ }
+ } finally {
+ if (emf != null) {
+ JpaStorageUtils.releaseEntityManagerFactory(emf);
+ }
+ }
+
+ return tokenFound;
+ }
+
+ /**
+ * Update a token for given an id
+ * @param id
+ * @param enabledFlag
+ */
+ static public void update(String id, boolean enabledFlag) throws DocumentNotFoundException {
+ EntityManagerFactory emf = JpaStorageUtils.getEntityManagerFactory();
+ Token tokenFound = null;
+
+ try {
+ EntityManager em = emf.createEntityManager();
+ tokenFound = get(id);
+ if (id != null) {
+ tokenFound.setEnabled(enabledFlag);
+ tokenFound.setUpdatedAtItem(new Date());
+ if (logger.isDebugEnabled()) {
+ logger.debug("Updated token=" + JaxbUtils.toString(tokenFound, Token.class));
+ }
+ em.persist(tokenFound);
+ }
+ } finally {
+ if (emf != null) {
+ JpaStorageUtils.releaseEntityManagerFactory(emf);
+ }
+ }
+ }
+
+ /**
+ * Deletes the token with given id
+ * @param id
+ * @throws Exception if user for given userId not found
+ */
+ static public void delete(String id) throws DocumentNotFoundException {
+ EntityManagerFactory emf = JpaStorageUtils.getEntityManagerFactory();
+
+ 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 (tokenDelCount != 1) {
+ String msg = "Could not find token with id=" + id;
+ logger.error(msg);
+ throw new DocumentNotFoundException(msg);
+ }
+ } finally {
+ if (emf != null) {
+ JpaStorageUtils.releaseEntityManagerFactory(emf);
+ }
+ }
+ }
+
+ private String getEncPassword(String userId, byte[] password) throws BadRequestException {
+ //jaxb unmarshaller already unmarshal xs:base64Binary, no need to b64 decode
+ //byte[] bpass = Base64.decodeBase64(accountReceived.getPassword());
+ try {
+ SecurityUtils.validatePassword(new String(password));
+ } catch (Exception e) {
+ throw new BadRequestException(e.getMessage());
+ }
+ String secEncPasswd = SecurityUtils.createPasswordHash(
+ userId, new String(password));
+ return secEncPasswd;
+ }
+}
</xs:sequence>
</xs:complexType>
</xs:element>
+
+ <xs:element name="token">
+ <xs:complexType>
+ <xs:annotation>
+ <xs:documentation>
+ Definition for creating password reset tockens.
+ </xs:documentation>
+ <xs:appinfo>
+ <hj:entity>
+ <orm:table name="tokens"/>
+ </hj:entity>
+ </xs:appinfo>
+ </xs:annotation>
+ <xs:sequence>
+ <xs:element name="id" type="xs:string" minOccurs="1" maxOccurs="1">
+ <xs:annotation>
+ <xs:appinfo>
+ <hj:id>
+ <orm:column name="id" length="128" nullable="false"/>
+ </hj:id>
+ </xs:appinfo>
+ </xs:annotation>
+ </xs:element>
+ <xs:element name="accountCsid" type="xs:string" minOccurs="1" maxOccurs="1">
+ <xs:annotation>
+ <xs:appinfo>
+ <hj:basic>
+ <orm:column name="account_csid" length="128" nullable="false"/>
+ </hj:basic>
+ </xs:appinfo>
+ </xs:annotation>
+ </xs:element>
+ <xs:element name="tenantId" type="xs:string" minOccurs="1" maxOccurs="1">
+ <xs:annotation>
+ <xs:appinfo>
+ <hj:basic>
+ <orm:column name="tenant_id" length="128" nullable="false"/>
+ </hj:basic>
+ </xs:appinfo>
+ </xs:annotation>
+ </xs:element>
+ <xs:element name="expireSeconds" type="xs:integer" minOccurs="1" maxOccurs="1">
+ <xs:annotation>
+ <xs:appinfo>
+ <hj:basic>
+ <orm:column name="expire_seconds" nullable="false"/>
+ </hj:basic>
+ </xs:appinfo>
+ </xs:annotation>
+ </xs:element>
+ <xs:element name="enabled" type="xs:boolean" minOccurs="1" maxOccurs="1">
+ <xs:annotation>
+ <xs:appinfo>
+ <hj:basic>
+ <orm:column name="enabled" nullable="false"/>
+ </hj:basic>
+ </xs:appinfo>
+ </xs:annotation>
+ </xs:element>
+ <xs:element name="createdAt" type="xs:dateTime">
+ <xs:annotation>
+ <xs:appinfo>
+ <hj:basic>
+ <orm:column name="created_at" nullable="false"/>
+ </hj:basic>
+ </xs:appinfo>
+ </xs:annotation>
+ </xs:element>
+ <xs:element name="updatedAt" type="xs:dateTime">
+ <xs:annotation>
+ <xs:appinfo>
+ <hj:basic>
+ <orm:column name="updated_at" />
+ </hj:basic>
+ </xs:appinfo>
+ </xs:annotation>
+ </xs:element>
+ </xs:sequence>
+ </xs:complexType>
+ </xs:element>
</xs:schema>
DROP TABLE IF EXISTS users;
create table users (username varchar(128) not null, created_at timestamp not null, passwd varchar(128) not null, updated_at timestamp, primary key (username));
+
+DROP TABLE IF EXISTS tokens;
+create table tokens (id varchar(128) not null, account_csid varchar(128) not null, tenant_id varchar(128) not null, expire_seconds integer not null, enabled boolean not null, created_at timestamp not null, updated_at timestamp, primary key (id));
*/
@Override
public CSpaceUser getUser() {
+ CSpaceUser result = null;
+
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
- Object principal = authentication.getPrincipal();
-
- CSpaceUser user = null;
- if (principal instanceof CSpaceUser ) {
- user = (CSpaceUser) principal;
- }
+ Object principal = null;
+ if (authentication != null) {
+ principal = authentication.getPrincipal();
+ if (principal instanceof CSpaceUser ) {
+ result = (CSpaceUser) principal;
+ }
+ }
- return user;
+ return result;
}
/**
--- /dev/null
+package org.collectionspace.services.common;
+
+import java.util.Date;
+import java.util.Properties;
+
+import javax.mail.Message;
+import javax.mail.MessagingException;
+import javax.mail.Session;
+import javax.mail.Transport;
+import javax.mail.internet.MimeMessage;
+
+import org.collectionspace.services.config.tenant.EmailConfig;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public class EmailUtil {
+ final static Logger logger = LoggerFactory.getLogger(EmailUtil.class);
+
+ private static final String MAIL_SMTP_HOST = "mail.smtp.host";
+ private static final String MAIL_SMTP_PORT = "mail.smtp.port";
+ private static final String MAIL_SMTP_TLS = "mail.smtp.starttls.enable";
+
+ private static final String MAIL_FROM = "mail.from";
+ private static final String MAIL_DEBUG = "mail.debug";
+
+ public static void main(String [] args) {
+ String username = "collectionspace.lyrasis@gmail.com";
+ String password = "CSpace11-GG";
+ String recipient = "remillet@gmail.com";
+
+ Properties props = new Properties();
+
+// props.setProperty("mail.smtp.host", "smtp.gmail.com");
+// props.setProperty("mail.from", "collectionspace.lyrasis@gmail.com");
+// props.setProperty("mail.smtp.starttls.enable", "true");
+// props.setProperty("mail.smtp.port", "587");
+// props.setProperty("mail.debug", "true");
+
+ props.setProperty(MAIL_SMTP_HOST, "smtp.gmail.com");
+ props.setProperty(MAIL_SMTP_PORT, "587");
+ props.setProperty(MAIL_SMTP_TLS, "true");
+ props.setProperty(MAIL_FROM, "collectionspace.lyrasis@gmail.com");
+ props.setProperty(MAIL_DEBUG, "true");
+
+ Session session = Session.getInstance(props, null);
+ MimeMessage msg = new MimeMessage(session);
+
+ try {
+ msg.setRecipients(Message.RecipientType.TO, recipient);
+ msg.setSubject("JavaMail hello world example");
+ msg.setSentDate(new Date());
+ msg.setText("Hello, world!\n");
+
+ Transport transport = session.getTransport("smtp");
+
+ transport.connect(username, password);
+ transport.sendMessage(msg, msg.getAllRecipients());
+ transport.close();
+ } catch (Exception e) {
+ System.err.println(e.getMessage());
+ }
+ }
+
+ /*
+ * recipients - Is a comma separated sequence of addresses
+ */
+ public static String sendMessage(EmailConfig emailConfig, String recipients, String message) {
+ String result = null;
+
+ Properties props = new Properties();
+
+ props.setProperty(MAIL_SMTP_HOST, emailConfig.getSmtpConfig().getHost());
+ props.setProperty(MAIL_SMTP_PORT, emailConfig.getSmtpConfig().getPort().toString());
+ props.setProperty(MAIL_SMTP_TLS,
+ new Boolean(emailConfig.getSmtpConfig().getSmtpAuth().isEnabled()).toString());
+
+ props.setProperty(MAIL_FROM, emailConfig.getFrom());
+ props.setProperty(MAIL_DEBUG, new Boolean(emailConfig.getSmtpConfig().isDebug()).toString());
+
+ Session session = Session.getInstance(props, null);
+ MimeMessage msg = new MimeMessage(session);
+
+ try {
+ msg.setRecipients(Message.RecipientType.TO, recipients);
+ msg.setSubject(emailConfig.getPasswordResetConfig().getSubject());
+ msg.setSentDate(new Date());
+ msg.setText(message);
+
+ Transport transport = session.getTransport("smtp");
+ if (emailConfig.getSmtpConfig().getSmtpAuth().isEnabled()) {
+ String username = emailConfig.getSmtpConfig().getSmtpAuth().getUsername();
+ String password = emailConfig.getSmtpConfig().getSmtpAuth().getPassword();
+ transport.connect(username, password);
+ } else {
+ transport.connect();
+ }
+
+ transport.sendMessage(msg, msg.getAllRecipients());
+ transport.close();
+ } catch (MessagingException e) {
+ logger.error(e.getMessage(), e);
+ result = e.getMessage();
+ }
+
+ return result;
+ }
+
+}
import org.slf4j.LoggerFactory;
import javax.ws.rs.PathParam;
+import javax.ws.rs.core.Context;
import javax.ws.rs.core.MultivaluedMap;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.UriBuilder;
}
public Object get(String csid, Class objectClass) {
+ return get((UriInfo)null, csid, objectClass);
+ }
+ public Object get(UriInfo ui, String csid, Class objectClass) {
logger.debug("get with csid=" + csid);
ensureCSID(csid, ServiceMessages.GET_FAILED + "csid");
Object result = null;
try {
- ServiceContext ctx = createServiceContext((Object) null, objectClass);
+ ServiceContext ctx = createServiceContext((Object) null, objectClass, ui);
DocumentHandler handler = createDocumentHandler(ctx);
getStorageClient(ctx).get(ctx, csid, handler);
result = ctx.getOutput();
public Object getList(UriInfo ui, Class objectClass) {
try {
- ServiceContext ctx = createServiceContext((Object) null, objectClass);
+ ServiceContext ctx = createServiceContext((Object) null, objectClass, ui);
DocumentHandler handler = createDocumentHandler(ctx);
MultivaluedMap<String, String> queryParams = (ui != null ? ui.getQueryParameters() : null);
DocumentFilter myFilter = handler.createDocumentFilter();
}
}
- public Object update(@PathParam("csid") String csid, Object theUpdate, Class objectClass) {
+ public Object update(String csid, Object theUpdate, Class objectClass) {
+ return update((UriInfo)null, csid, theUpdate, objectClass);
+ }
+
+ public Object update(UriInfo ui, String csid, Object theUpdate, Class objectClass) {
if (logger.isDebugEnabled()) {
logger.debug("updateRole with csid=" + csid);
}
ensureCSID(csid, ServiceMessages.PUT_FAILED + this.getClass().getName());
try {
- ServiceContext ctx = createServiceContext(theUpdate, objectClass);
+ ServiceContext ctx = createServiceContext(theUpdate, objectClass, ui);
DocumentHandler handler = createDocumentHandler(ctx);
getStorageClient(ctx).update(ctx, csid, handler);
return ctx.getOutput();
public static final String SEARCH_FAILED = "Search request" + FAILED;
public static final String AUTH_REFS_FAILED = "Authority references request" + FAILED;
+ public static final String PASSWORD_RESET_REQUEST_FAILED = "Password reset request" + FAILED;
public static final String UNKNOWN_ERROR_MSG = "Unknown error ";
public static final String VALIDATION_FAILURE = "Validation failure ";
public static final String MISSING_CSID = "missing csid";
package org.collectionspace.services.common.authorization_mgt;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import javax.persistence.EntityManagerFactory;
import org.collectionspace.authentication.AuthN;
+import org.collectionspace.services.account.AccountListItem;
+
+import org.collectionspace.services.authentication.Token;
import org.collectionspace.services.authorization.AuthZ;
import org.collectionspace.services.authorization.CSpaceAction;
import org.collectionspace.services.authorization.PermissionException;
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.Profiler;
import org.collectionspace.services.client.RoleClient;
import org.collectionspace.services.client.workflow.WorkflowClient;
import org.collectionspace.services.common.storage.JDBCTools;
import org.collectionspace.services.common.storage.jpa.JpaStorageUtils;
import org.collectionspace.services.config.service.ServiceBindingType;
+import org.collectionspace.services.config.tenant.EmailConfig;
+import org.collectionspace.services.config.tenant.PasswordResetConfig;
import org.collectionspace.services.config.tenant.TenantBindingType;
import org.collectionspace.services.lifecycle.Lifecycle;
import org.collectionspace.services.lifecycle.TransitionDef;
public class AuthorizationCommon {
final public static String REFRESH_AUTZ_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.";
+ final private static String tokensalt = "74102328UserDetailsReset";
+ final private static int TIME_SCALAR = 100000;
+ private static final String DEFAULT_PASSWORD_RESET_EMAIL_SUBJECT = "Password reset for CollectionSpace account";
+
//
// ActionGroup labels/constants
//
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 Role getRole(String tenantId, String displayName) {
Role role = null;
}
}
+
+ 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 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());
+ logger.warn(msg);
+ }
+ //
+ // Note: the current tenant bindings config for expiration takes precedence over the config used to create the token.
+ //
+ 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);
+ }
+ }
+
+ 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();
+ 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;
+ }
}
// If anonymous access is being attempted, then a tenant ID needs to be set as a query param
//
if (uriInfo == null) {
- String errMsg = "Anonymous access attempted without a valid tenant ID query paramter. A null 'UriInfo' instance was passed into the service context constructor.";
+ String errMsg = "Anonymous access attempted without a valid tenant ID query or path paramter. Error: A null 'UriInfo' instance was passed into the service context constructor.";
logger.error(errMsg);
throw new UnauthorizedException(errMsg);
}
-// String tenantId = uriInfo.getQueryParameters().getFirst(AuthNContext.TENANT_ID_QUERY_PARAM);
- String tenantId = uriInfo.getPathParameters().getFirst(AuthN.TENANT_ID_PATH_PARAM);
- if (tenantId == null) {
- String errMsg = String.format("Anonymous access to '%s' attempted without a valid tenant ID query paramter.",
+ String tenantIdQueryParam = uriInfo.getQueryParameters().getFirst(AuthN.TENANT_ID_QUERY_PARAM);
+ String tenantPathParam = uriInfo.getPathParameters().getFirst(AuthN.TENANT_ID_PATH_PARAM);
+ if (tenantIdQueryParam == null && tenantPathParam == null) {
+ String errMsg = String.format("Anonymous access to '%s' attempted without a valid tenant ID query or path paramter.",
uriInfo.getPath());
logger.error(errMsg);
throw new UnauthorizedException(errMsg);
}
- result = tenantId;
+
+ result = tenantIdQueryParam != null ? tenantIdQueryParam : tenantPathParam; // If both have value, user the query param (not path) value
}
return result;
/** The Constant logger. */
private static final Logger logger = LoggerFactory.getLogger(SecurityInterceptor.class);
private static final String ACCOUNT_PERMISSIONS = "accounts/*/accountperms";
+ private static final String PASSWORD_RESET = "accounts/requestpasswordreset";
+ private static final String PROCESS_PASSWORD_RESET = "accounts/processpasswordreset";
private static final String NUXEO_ADMIN = null;
//
// Use this thread specific member instance to hold our login context with Nuxeo
private boolean isAnonymousRequest(HttpRequest request, ResourceMethodInvoker resourceMethodInvoker) {
boolean result = false;
+ String resName = SecurityUtils.getResourceName(request.getUri());
+ if (resName.equalsIgnoreCase(PASSWORD_RESET) || resName.equals(PROCESS_PASSWORD_RESET)) {
+ return true;
+ }
+
Class<?> resourceClass = resourceMethodInvoker.getResourceClass();
try {
CollectionSpaceResource resourceInstance = (CollectionSpaceResource)resourceClass.newInstance();
return result;
}
+ /*
+ * Check to see if the resource required authorization to access
+ *
+ */
+ private boolean requiresAuthorization(String resName) {
+ boolean result = true;
+ //
+ // All active users are allowed to see the *their* (we enforce this) current list of permissions. If this is not
+ // the request, then we'll do a full AuthZ check.
+ //
+ if (resName.equalsIgnoreCase(ACCOUNT_PERMISSIONS) == true) {
+ result = false;
+ }
+
+ return result;
+ }
+
/* (non-Javadoc)
* @see org.jboss.resteasy.spi.interception.PreProcessInterceptor#preProcess(org.jboss.resteasy.spi.HttpRequest, org.jboss.resteasy.core.ResourceMethod)
*/
//
checkActive();
- //
- // All active users are allowed to see the *their* (we enforce this) current list of permissions. If this is not
- // the request, then we'll do a full AuthZ check.
- //
- if (resName.equalsIgnoreCase(ACCOUNT_PERMISSIONS) != true) { //see comment immediately above
+ if (requiresAuthorization(resName) == true) { //see comment immediately above
AuthZ authZ = AuthZ.get();
CSpaceResource res = new URIResourceImpl(AuthN.get().getCurrentTenantId(), resName, httpMethod);
if (authZ.isAccessAllowed(res) == false) {
+++ /dev/null
-<?xml version="1.0" encoding="UTF-8"?>
-<service:cow xmlns:types="http://collectionspace.org/services/config/types"
- xmlns:service="http://collectionspace.org/services/config/service"
- xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
- xsi:schemaLocation="http://collectionspace.org/services/config/service file:/C:/dev/src/cspace/services/services/config/src/main/resources/service.xsd" name="name0" type="type0" version="0.1" supportsReplicating="false" remoteClientConfigName="remoteClientConfigName0" requiresUniqueShortId="false">
- <service:uriPath>123</service:uriPath>
- <service:uriPath>123</service:uriPath>
- <service:uriPath>123</service:uriPath>
- <service:object name="name1" version="0.1">
- <service:property>
- <types:item>
- <types:key>123</types:key>
- <types:value>123</types:value>
- </types:item>
- <types:item>
- <types:key>123</types:key>
- <types:value>123</types:value>
- </types:item>
- <types:item>
- <types:key>123</types:key>
- <types:value>123</types:value>
- </types:item>
- </service:property>
- <service:property>
- <types:item>
- <types:key>123</types:key>
- <types:value>123</types:value>
- </types:item>
- <types:item>
- <types:key>123</types:key>
- <types:value>123</types:value>
- </types:item>
- <types:item>
- <types:key>123</types:key>
- <types:value>123</types:value>
- </types:item>
- </service:property>
- <service:property>
- <types:item>
- <types:key>123</types:key>
- <types:value>123</types:value>
- </types:item>
- <types:item>
- <types:key>123</types:key>
- <types:value>123</types:value>
- </types:item>
- <types:item>
- <types:key>123</types:key>
- <types:value>123</types:value>
- </types:item>
- </service:property>
- <service:part id="ID000" control_group="External" versionable="false" auditable="false" label="label0" updated="2006-05-04T18:13:51.0" order="0">
- <service:properties>
- <types:item>
- <types:key>123</types:key>
- <types:value>123</types:value>
- </types:item>
- <types:item>
- <types:key>123</types:key>
- <types:value>123</types:value>
- </types:item>
- <types:item>
- <types:key>123</types:key>
- <types:value>123</types:value>
- </types:item>
- </service:properties>
- <service:properties>
- <types:item>
- <types:key>123</types:key>
- <types:value>123</types:value>
- </types:item>
- <types:item>
- <types:key>123</types:key>
- <types:value>123</types:value>
- </types:item>
- <types:item>
- <types:key>123</types:key>
- <types:value>123</types:value>
- </types:item>
- </service:properties>
- <service:properties>
- <types:item>
- <types:key>123</types:key>
- <types:value>123</types:value>
- </types:item>
- <types:item>
- <types:key>123</types:key>
- <types:value>123</types:value>
- </types:item>
- <types:item>
- <types:key>123</types:key>
- <types:value>123</types:value>
- </types:item>
- </service:properties>
- <service:content contentType="contentType0">
- <service:contentDigest algorithm="MD5" value="value0"/>
- <service:contentLocation type="internalId" ref="http://www.oxygenxml.com/"/>
- <service:partHandler>123</service:partHandler>
- </service:content>
- </service:part>
- <service:part id="ID001" control_group="External" versionable="false" auditable="false" label="label1" updated="2006-05-04T18:13:51.0" order="0">
- <service:properties>
- <types:item>
- <types:key>123</types:key>
- <types:value>123</types:value>
- </types:item>
- <types:item>
- <types:key>123</types:key>
- <types:value>123</types:value>
- </types:item>
- <types:item>
- <types:key>123</types:key>
- <types:value>123</types:value>
- </types:item>
- </service:properties>
- <service:properties>
- <types:item>
- <types:key>123</types:key>
- <types:value>123</types:value>
- </types:item>
- <types:item>
- <types:key>123</types:key>
- <types:value>123</types:value>
- </types:item>
- <types:item>
- <types:key>123</types:key>
- <types:value>123</types:value>
- </types:item>
- </service:properties>
- <service:properties>
- <types:item>
- <types:key>123</types:key>
- <types:value>123</types:value>
- </types:item>
- <types:item>
- <types:key>123</types:key>
- <types:value>123</types:value>
- </types:item>
- <types:item>
- <types:key>123</types:key>
- <types:value>123</types:value>
- </types:item>
- </service:properties>
- <service:content contentType="contentType1">
- <service:contentDigest algorithm="MD5" value="value1"/>
- <service:xmlContent schemaLocation="schemaLocation0" namespaceURI="namespaceURI0">
- </service:xmlContent>
- <service:partHandler>123</service:partHandler>
- </service:content>
- </service:part>
- <service:part id="ID002" control_group="External" versionable="false" auditable="false" label="label2" updated="2006-05-04T18:13:51.0" order="0">
- <service:properties>
- <types:item>
- <types:key>123</types:key>
- <types:value>123</types:value>
- </types:item>
- <types:item>
- <types:key>123</types:key>
- <types:value>123</types:value>
- </types:item>
- <types:item>
- <types:key>123</types:key>
- <types:value>123</types:value>
- </types:item>
- </service:properties>
- <service:properties>
- <types:item>
- <types:key>123</types:key>
- <types:value>123</types:value>
- </types:item>
- <types:item>
- <types:key>123</types:key>
- <types:value>123</types:value>
- </types:item>
- <types:item>
- <types:key>123</types:key>
- <types:value>123</types:value>
- </types:item>
- </service:properties>
- <service:properties>
- <types:item>
- <types:key>123</types:key>
- <types:value>123</types:value>
- </types:item>
- <types:item>
- <types:key>123</types:key>
- <types:value>123</types:value>
- </types:item>
- <types:item>
- <types:key>123</types:key>
- <types:value>123</types:value>
- </types:item>
- </service:properties>
- <service:content contentType="contentType2">
- <service:contentDigest algorithm="MD5" value="value2"/>
- <service:xmlContent schemaLocation="schemaLocation1" namespaceURI="namespaceURI1">
- </service:xmlContent>
- <service:partHandler>123</service:partHandler>
- </service:content>
- </service:part>
- <service:serviceHandler>123</service:serviceHandler>
- </service:object>
- <service:documentHandler>123</service:documentHandler>
- <service:DocHandlerParams>
- <service:classname>123</service:classname>
- <service:params>
- <service:CacheControlConfigElement>
- <service:CacheControlConfigList>
- <service:key>123</service:key>
- <service:private>123</service:private>
- <service:public>123</service:public>
- <service:noCache>123</service:noCache>
- <service:mustRevalidate>123</service:mustRevalidate>
- <service:proxyRevalidate>123</service:proxyRevalidate>
- <service:noStore>123</service:noStore>
- <service:noTransform>123</service:noTransform>
- <service:maxAge>123</service:maxAge>
- <service:sMaxAge>123</service:sMaxAge>
- </service:CacheControlConfigList>
- <service:CacheControlConfigList>
- <service:key>123</service:key>
- <service:private>123</service:private>
- <service:public>123</service:public>
- <service:noCache>123</service:noCache>
- <service:mustRevalidate>123</service:mustRevalidate>
- <service:proxyRevalidate>123</service:proxyRevalidate>
- <service:noStore>123</service:noStore>
- <service:noTransform>123</service:noTransform>
- <service:maxAge>123</service:maxAge>
- <service:sMaxAge>123</service:sMaxAge>
- </service:CacheControlConfigList>
- <service:CacheControlConfigList>
- <service:key>123</service:key>
- <service:private>123</service:private>
- <service:public>123</service:public>
- <service:noCache>123</service:noCache>
- <service:mustRevalidate>123</service:mustRevalidate>
- <service:proxyRevalidate>123</service:proxyRevalidate>
- <service:noStore>123</service:noStore>
- <service:noTransform>123</service:noTransform>
- <service:maxAge>123</service:maxAge>
- <service:sMaxAge>123</service:sMaxAge>
- </service:CacheControlConfigList>
- </service:CacheControlConfigElement>
- <service:SchemaName>123</service:SchemaName>
- <service:RefnameDisplayNameField>
- <service:setter>123</service:setter>
- <service:element>123</service:element>
- <service:schema>123</service:schema>
- <service:xpath>123</service:xpath>
- </service:RefnameDisplayNameField>
- <service:SupportsHierarchy>123</service:SupportsHierarchy>
- <service:SupportsVersioning>123</service:SupportsVersioning>
- <service:DublinCoreTitle>123</service:DublinCoreTitle>
- <service:SummaryFields>123</service:SummaryFields>
- <service:AbstractCommonListClassname>123</service:AbstractCommonListClassname>
- <service:CommonListItemClassname>123</service:CommonListItemClassname>
- <service:ListResultsItemMethodName>123</service:ListResultsItemMethodName>
- <service:ListResultsFields>
- <service:Extended>123</service:Extended>
- <service:ListResultField>
- <service:setter>123</service:setter>
- <service:element>123</service:element>
- <service:schema>123</service:schema>
- <service:xpath>123</service:xpath>
- </service:ListResultField>
- <service:ListResultField>
- <service:setter>123</service:setter>
- <service:element>123</service:element>
- <service:schema>123</service:schema>
- <service:xpath>123</service:xpath>
- </service:ListResultField>
- <service:ListResultField>
- <service:setter>123</service:setter>
- <service:element>123</service:element>
- <service:schema>123</service:schema>
- <service:xpath>123</service:xpath>
- </service:ListResultField>
- </service:ListResultsFields>
- </service:params>
- </service:DocHandlerParams>
- <service:AuthorityInstanceList>
- <service:AuthorityInstance>
- <service:web-url>123</service:web-url>
- <service:title-ref>123</service:title-ref>
- <service:title>123</service:title>
- </service:AuthorityInstance>
- <service:AuthorityInstance>
- <service:web-url>123</service:web-url>
- <service:title-ref>123</service:title-ref>
- <service:title>123</service:title>
- </service:AuthorityInstance>
- <service:AuthorityInstance>
- <service:web-url>123</service:web-url>
- <service:title-ref>123</service:title-ref>
- <service:title>123</service:title>
- </service:AuthorityInstance>
- </service:AuthorityInstanceList>
- <service:validatorHandler>123</service:validatorHandler>
- <service:validatorHandler>123</service:validatorHandler>
- <service:validatorHandler>123</service:validatorHandler>
- <service:clientHandler>123</service:clientHandler>
- <service:disableAsserts>123</service:disableAsserts>
- <service:initHandler>
- <service:classname>123</service:classname>
- <service:params>
- <service:field>
- <service:table>123</service:table>
- <service:col>123</service:col>
- <service:type>123</service:type>
- <service:param>123</service:param>
- </service:field>
- <service:field>
- <service:table>123</service:table>
- <service:col>123</service:col>
- <service:type>123</service:type>
- <service:param>123</service:param>
- </service:field>
- <service:field>
- <service:table>123</service:table>
- <service:col>123</service:col>
- <service:type>123</service:type>
- <service:param>123</service:param>
- </service:field>
- <service:property>
- <service:key>123</service:key>
- <service:value>123</service:value>
- </service:property>
- <service:property>
- <service:key>123</service:key>
- <service:value>123</service:value>
- </service:property>
- <service:property>
- <service:key>123</service:key>
- <service:value>123</service:value>
- </service:property>
- </service:params>
- </service:initHandler>
- <service:initHandler>
- <service:classname>123</service:classname>
- <service:params>
- <service:field>
- <service:table>123</service:table>
- <service:col>123</service:col>
- <service:type>123</service:type>
- <service:param>123</service:param>
- </service:field>
- <service:field>
- <service:table>123</service:table>
- <service:col>123</service:col>
- <service:type>123</service:type>
- <service:param>123</service:param>
- </service:field>
- <service:field>
- <service:table>123</service:table>
- <service:col>123</service:col>
- <service:type>123</service:type>
- <service:param>123</service:param>
- </service:field>
- <service:property>
- <service:key>123</service:key>
- <service:value>123</service:value>
- </service:property>
- <service:property>
- <service:key>123</service:key>
- <service:value>123</service:value>
- </service:property>
- <service:property>
- <service:key>123</service:key>
- <service:value>123</service:value>
- </service:property>
- </service:params>
- </service:initHandler>
- <service:initHandler>
- <service:classname>123</service:classname>
- <service:params>
- <service:field>
- <service:table>123</service:table>
- <service:col>123</service:col>
- <service:type>123</service:type>
- <service:param>123</service:param>
- </service:field>
- <service:field>
- <service:table>123</service:table>
- <service:col>123</service:col>
- <service:type>123</service:type>
- <service:param>123</service:param>
- </service:field>
- <service:field>
- <service:table>123</service:table>
- <service:col>123</service:col>
- <service:type>123</service:type>
- <service:param>123</service:param>
- </service:field>
- <service:property>
- <service:key>123</service:key>
- <service:value>123</service:value>
- </service:property>
- <service:property>
- <service:key>123</service:key>
- <service:value>123</service:value>
- </service:property>
- <service:property>
- <service:key>123</service:key>
- <service:value>123</service:value>
- </service:property>
- </service:params>
- </service:initHandler>
- <service:repositoryDomain>123</service:repositoryDomain>
- <service:repositoryWorkspaceId>123</service:repositoryWorkspaceId>
- <service:properties>
- <types:item>
- <types:key>123</types:key>
- <types:value>123</types:value>
- </types:item>
- <types:item>
- <types:key>123</types:key>
- <types:value>123</types:value>
- </types:item>
- <types:item>
- <types:key>123</types:key>
- <types:value>123</types:value>
- </types:item>
- </service:properties>
- <service:properties>
- <types:item>
- <types:key>123</types:key>
- <types:value>123</types:value>
- </types:item>
- <types:item>
- <types:key>123</types:key>
- <types:value>123</types:value>
- </types:item>
- <types:item>
- <types:key>123</types:key>
- <types:value>123</types:value>
- </types:item>
- </service:properties>
- <service:properties>
- <types:item>
- <types:key>123</types:key>
- <types:value>123</types:value>
- </types:item>
- <types:item>
- <types:key>123</types:key>
- <types:value>123</types:value>
- </types:item>
- <types:item>
- <types:key>123</types:key>
- <types:value>123</types:value>
- </types:item>
- </service:properties>
-</service:cow>
<xs:element name="binaryStorePath" type="xs:string" minOccurs="0" maxOccurs="1"/>
<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="serviceBindings" type="service:ServiceBindingType" minOccurs="0" maxOccurs="unbounded"/>
</xs:sequence>
<!-- tenant id, a UUID -->
</xs:sequence>
</xs:complexType>
+ <xs:complexType name="EmailConfig">
+ <xs:annotation>
+ <xs:documentation>Configuration for how a tenant sends email notifications</xs:documentation>
+ </xs:annotation>
+ <xs:sequence>
+ <xs:element name="baseurl" type="xs:string" minOccurs="1" maxOccurs="1"/>
+ <xs:element name="from" type="xs:string" minOccurs="1" maxOccurs="1"/>
+ <xs:element name="smtpConfig" type="SMTPConfig" minOccurs="1" maxOccurs="1"/>
+ <xs:element name="passwordResetConfig" type="PasswordResetConfig" minOccurs="1" maxOccurs="1"/>
+ </xs:sequence>
+ </xs:complexType>
+
+ <xs:complexType name="SMTPConfig">
+ <xs:annotation>
+ <xs:documentation>SMTP config for sending emails.</xs:documentation>
+ </xs:annotation>
+ <xs:sequence>
+ <xs:element name="host" type="xs:string" minOccurs="1" maxOccurs="1"/>
+ <xs:element name="port" type="xs:integer" minOccurs="1" maxOccurs="1"/>
+ <xs:element name="debug" type="xs:boolean" minOccurs="1" maxOccurs="1" default="true"/>
+ <xs:element name="smtpAuth" type="SMTPAuthConfig" minOccurs="1" maxOccurs="1"/>
+ </xs:sequence>
+ </xs:complexType>
+
+ <xs:complexType name="SMTPAuthConfig">
+ <xs:annotation>
+ <xs:documentation>SMTP authentication config for sending emails.</xs:documentation>
+ </xs:annotation>
+ <xs:sequence>
+ <xs:element name="username" type="xs:string" minOccurs="1" maxOccurs="1"/>
+ <xs:element name="password" type="xs:string" minOccurs="1" maxOccurs="1"/>
+ </xs:sequence>
+ <xs:attribute name="enabled" type="xs:boolean" use="optional" default="false"/>
+ </xs:complexType>
+
+ <xs:complexType name="PasswordResetConfig">
+ <xs:annotation>
+ <xs:documentation>Config for password resets</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>
+ </xs:complexType>
+
<xs:complexType name="EventListenerConfigurations">
<xs:sequence>
<xs:element name="eventListenerConfig" type="EventListenerConfig" minOccurs="0" maxOccurs="unbounded"/>