]> git.aero2k.de Git - tmp/jakarta-migration.git/commitdiff
DRYD-169: Support for new password reset mechanism. Password reset is now negotiated...
authorremillet <remillet@yahoo.com>
Tue, 21 Nov 2017 05:55:28 +0000 (21:55 -0800)
committerremillet <remillet@yahoo.com>
Tue, 21 Nov 2017 05:55:28 +0000 (21:55 -0800)
20 files changed:
services/JaxRsServiceProvider/src/main/resources/META-INF/persistence.xml
services/JaxRsServiceProvider/src/main/webapp/WEB-INF/applicationContext-security.xml
services/account/client/src/main/java/org/collectionspace/services/client/AccountClient.java
services/account/jaxb/src/main/resources/accounts_common.xsd
services/account/service/src/main/java/org/collectionspace/services/account/AccountResource.java
services/account/service/src/main/java/org/collectionspace/services/account/storage/AccountDocumentHandler.java
services/account/service/src/main/java/org/collectionspace/services/account/storage/AccountStorageClient.java
services/account/service/src/main/java/org/collectionspace/services/account/storage/AccountStorageConstants.java
services/account/service/src/main/java/org/collectionspace/services/account/storage/csidp/TokenStorageClient.java [new file with mode: 0644]
services/authentication/jaxb/src/main/resources/authentication_identity_provider.xsd
services/authentication/pstore/src/main/resources/db/postgresql/authentication.sql
services/authentication/service/src/main/java/org/collectionspace/authentication/spring/SpringAuthNContext.java
services/common/src/main/java/org/collectionspace/services/common/EmailUtil.java [new file with mode: 0644]
services/common/src/main/java/org/collectionspace/services/common/SecurityResourceBase.java
services/common/src/main/java/org/collectionspace/services/common/ServiceMessages.java
services/common/src/main/java/org/collectionspace/services/common/authorization_mgt/AuthorizationCommon.java
services/common/src/main/java/org/collectionspace/services/common/security/SecurityContextImpl.java
services/common/src/main/java/org/collectionspace/services/common/security/SecurityInterceptor.java
services/config/src/main/resources/instance1.xml [deleted file]
services/config/src/main/resources/tenant.xsd

index 254aff90073f47761ac163f05ad62fd132238bf7..a01685418cd1aeaf129eac4667fb74630da7eae1 100644 (file)
@@ -14,6 +14,7 @@
         <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>
index a1f23a938139b1997089164f0e775be6eb0cddbd..48f9d8e87159e1a5764210549dbc107689805b7e 100644 (file)
     <!-- 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()" />
index d97cf2a53d39ff3077863e9185e703e45b6899c1..6b4d8f1ae412edf4ff4ab8cb4635579a96609f8f 100644 (file)
@@ -45,6 +45,7 @@ public class AccountClient extends AbstractServiceClientImpl<AccountsCommonList,
     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();
index af8e80ce02b68428806a6a4e59a5eb0f27208a8d..e4c5daa5eae67455107750670aaaf121b450858d 100644 (file)
                                        <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>
index 9884685094e8e75fbef47d96553ebf6b4ee3e879..090f8ab558eafbe56d2659e920021079ae2a399b 100644 (file)
  */
 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;
@@ -32,21 +35,28 @@ import org.collectionspace.services.authorization.SubjectType;
 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;
@@ -61,6 +71,7 @@ import javax.ws.rs.PathParam;
 import javax.ws.rs.Produces;
 import javax.ws.rs.QueryParam;
 import javax.ws.rs.core.Context;
+import javax.ws.rs.core.MultivaluedMap;
 import javax.ws.rs.core.PathSegment;
 import javax.ws.rs.core.Response;
 import javax.ws.rs.core.UriBuilder;
@@ -73,8 +84,10 @@ import javax.ws.rs.core.UriInfo;
 @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() {
@@ -109,8 +122,8 @@ public class AccountResource extends SecurityResourceBase {
 
     @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
@@ -195,12 +208,224 @@ public class AccountResource extends SecurityResourceBase {
 
     @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);
index 09a3444830ff6bd55efebd6a3ae2d27934718cd1..1c7d796f7c8e944c5ca58485853be723575fa86f 100644 (file)
@@ -164,6 +164,8 @@ public class AccountDocumentHandler
             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();
index 2bf5b401eb373c2f9ca8e78ad5dd2ce7fea1424a..ea517d1a2433f28f0e1ba6cb400a981e44e8cdea 100644 (file)
@@ -27,6 +27,7 @@ import java.util.Date;
 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;
@@ -125,13 +126,13 @@ public class AccountStorageClient extends JpaStorageClientImpl {
             }
             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) {
@@ -146,8 +147,7 @@ public class AccountStorageClient extends JpaStorageClientImpl {
         if (docFilter == null) {
             docFilter = handler.createDocumentFilter();
         }
-        EntityManagerFactory emf = null;
-        EntityManager em = null;
+
         try {
             handler.prepare(Action.GET);
             Object o = null;
@@ -159,9 +159,6 @@ public class AccountStorageClient extends JpaStorageClientImpl {
             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);
             }
@@ -175,10 +172,6 @@ public class AccountStorageClient extends JpaStorageClientImpl {
                 logger.debug("Caught exception ", e);
             }
             throw new DocumentException(e);
-        } finally {
-            if (emf != null) {
-                JpaStorageUtils.releaseEntityManagerFactory(emf);
-            }
         }
     }
 
index 35b8e250a8b1a52b5d30b593f88e051077d0dfeb..88b3428990611c55ecf4f0315b3c4ab97f383cad 100644 (file)
@@ -50,6 +50,8 @@
 
 package org.collectionspace.services.account.storage;
 
+import org.collectionspace.services.client.AccountClient;
+
 /**
  * AccountStorageConstants declares query params, etc.
  * @author
@@ -58,8 +60,7 @@ public class AccountStorageConstants {
 
     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";
diff --git a/services/account/service/src/main/java/org/collectionspace/services/account/storage/csidp/TokenStorageClient.java b/services/account/service/src/main/java/org/collectionspace/services/account/storage/csidp/TokenStorageClient.java
new file mode 100644 (file)
index 0000000..9d585da
--- /dev/null
@@ -0,0 +1,182 @@
+/**
+ *  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;
+    }
+}
index 5a62a55c8e4fc815820237b216ad22e74838874b..d8e0347198961da1f234aebf7faebe6fa4e28385 100644 (file)
             </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>
 
index 7aff0f078d86366783104406318e1e1706333714..d424d2ae69900397c6fb2c9e91993c59389bf1cd 100644 (file)
@@ -1,2 +1,5 @@
 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));
index 1529343ac0581a24a889ae9f50a331c7829b7e73..c1dc8dd681dec25d0f8b1dae1282bb15981d2ea8 100644 (file)
@@ -58,15 +58,18 @@ public class SpringAuthNContext implements AuthNContext {
      */
     @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;
     }
 
     /**
diff --git a/services/common/src/main/java/org/collectionspace/services/common/EmailUtil.java b/services/common/src/main/java/org/collectionspace/services/common/EmailUtil.java
new file mode 100644 (file)
index 0000000..103154f
--- /dev/null
@@ -0,0 +1,109 @@
+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;
+       }
+               
+}
index 226a6091ad02848e47023cb22dc4016c0860ed11..9bf20714f3cd1410632300140a7e99045854025d 100644 (file)
@@ -7,6 +7,7 @@ import org.slf4j.Logger;
 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;
@@ -31,12 +32,15 @@ public abstract class SecurityResourceBase extends AbstractCollectionSpaceResour
         }
 
     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();
@@ -49,7 +53,7 @@ public abstract class SecurityResourceBase extends AbstractCollectionSpaceResour
 
     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();
@@ -65,13 +69,17 @@ public abstract class SecurityResourceBase extends AbstractCollectionSpaceResour
         }
     }
 
-    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();
index 630bff3c4b9058bedc710891d1b42c23f1d30d4a..86770ebdf4a0b88584c8bc36028916b1a30ee4f1 100644 (file)
@@ -56,6 +56,7 @@ public class ServiceMessages {
     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";
index 638125f74abcce725e236e0d0166eaae74263285..c9376c2685b5e0bfcfd400069d9dfa49340da63c 100644 (file)
@@ -1,5 +1,7 @@
 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;
@@ -18,6 +20,9 @@ import javax.persistence.EntityManager;
 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;
@@ -32,6 +37,7 @@ import org.collectionspace.services.authorization.perms.ActionType;
 import org.collectionspace.services.authorization.perms.EffectType;
 import org.collectionspace.services.authorization.perms.Permission;
 import org.collectionspace.services.authorization.perms.PermissionAction;
+
 import org.collectionspace.services.client.Profiler;
 import org.collectionspace.services.client.RoleClient;
 import org.collectionspace.services.client.workflow.WorkflowClient;
@@ -44,6 +50,8 @@ import org.collectionspace.services.common.storage.DatabaseProductType;
 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;
@@ -58,6 +66,15 @@ import org.springframework.security.acls.model.AlreadyExistsException;
 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
     //
@@ -126,6 +143,7 @@ public class AuthorizationCommon {
        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;
        
@@ -1162,5 +1180,88 @@ public class AuthorizationCommon {
                }
         
     }
+       
+       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;
+       }
 }
index 057b28bb061019021ecba0b8041bae5c2577e1da..0d8b81ce720347aa4206ce6679ee2c530db3243a 100644 (file)
@@ -54,20 +54,21 @@ public class SecurityContextImpl implements SecurityContext {
             // 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;
index 30182024f70a3f6c85ada472b0162d5b1df54e92..c996233d85bd506a19628f2395c3870887651a9d 100644 (file)
@@ -84,6 +84,8 @@ public class SecurityInterceptor implements PreProcessInterceptor, PostProcessIn
        /** 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
@@ -99,6 +101,11 @@ public class SecurityInterceptor implements PreProcessInterceptor, PostProcessIn
     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();
@@ -112,6 +119,23 @@ public class SecurityInterceptor implements PreProcessInterceptor, PostProcessIn
        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)
         */
@@ -158,11 +182,7 @@ public class SecurityInterceptor implements PreProcessInterceptor, PostProcessIn
                        //
                        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) {
diff --git a/services/config/src/main/resources/instance1.xml b/services/config/src/main/resources/instance1.xml
deleted file mode 100644 (file)
index 8878129..0000000
+++ /dev/null
@@ -1,453 +0,0 @@
-<?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>
index 177dbaed4085c24c69d87251b1772bb775ada501..7e81de2e22db421d0a0f5a4eeb0131c55fb02f81 100644 (file)
@@ -50,6 +50,7 @@
             <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"/>