001/* 002 * (C) Copyright 2011 Nuxeo SA (http://nuxeo.com/) and contributors. 003 * 004 * All rights reserved. This program and the accompanying materials 005 * are made available under the terms of the GNU Lesser General Public License 006 * (LGPL) version 2.1 which accompanies this distribution, and is available at 007 * http://www.gnu.org/licenses/lgpl.html 008 * 009 * This library is distributed in the hope that it will be useful, 010 * but WITHOUT ANY WARRANTY; without even the implied warranty of 011 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 012 * Lesser General Public License for more details. 013 * 014 * Contributors: 015 * Wojciech Sulejman 016 */ 017package org.nuxeo.ecm.platform.signature.web.sign; 018 019import java.io.IOException; 020import java.io.OutputStream; 021import java.io.Serializable; 022 023import javax.faces.application.FacesMessage; 024import javax.faces.context.FacesContext; 025import javax.faces.validator.ValidatorException; 026import javax.servlet.http.HttpServletResponse; 027 028import org.apache.commons.logging.Log; 029import org.apache.commons.logging.LogFactory; 030import org.jboss.seam.ScopeType; 031import org.jboss.seam.annotations.In; 032import org.jboss.seam.annotations.Name; 033import org.jboss.seam.annotations.Scope; 034import org.jboss.seam.faces.FacesMessages; 035import org.jboss.seam.international.StatusMessage; 036import org.nuxeo.ecm.core.api.CoreSession; 037import org.nuxeo.ecm.core.api.DocumentModel; 038import org.nuxeo.ecm.core.api.NuxeoPrincipal; 039import org.nuxeo.ecm.directory.PasswordHelper; 040import org.nuxeo.ecm.platform.signature.api.exception.CertException; 041import org.nuxeo.ecm.platform.signature.api.pki.CertService; 042import org.nuxeo.ecm.platform.signature.api.user.CUserService; 043import org.nuxeo.ecm.platform.ui.web.api.NavigationContext; 044import org.nuxeo.ecm.platform.ui.web.api.WebActions; 045import org.nuxeo.ecm.platform.usermanager.UserManager; 046import org.nuxeo.ecm.webapp.helpers.ResourcesAccessor; 047 048/** 049 * Certificate management actions exposed as a Seam component. Used for launching certificate generation, storage and 050 * retrieving operations from low level services. Allows verifying if a user certificate is already present. 051 * 052 * @author <a href="mailto:ws@nuxeo.com">Wojciech Sulejman</a> 053 */ 054@Name("certActions") 055@Scope(ScopeType.CONVERSATION) 056public class CertActions implements Serializable { 057 058 private static final long serialVersionUID = 2L; 059 060 private static final Log LOG = LogFactory.getLog(CertActions.class); 061 062 private static final int MINIMUM_PASSWORD_LENGTH = 8; 063 064 private static final String USER_FIELD_FIRSTNAME = "user:firstName"; 065 066 private static final String USER_FIELD_LASTNAME = "user:lastName"; 067 068 private static final String USER_FIELD_EMAIL = "user:email"; 069 070 private static final String HOME_TAB = "MAIN_TABS:home"; 071 072 private static final String CERTIFICATE_TAB = "USER_CENTER:Certificate"; 073 074 @In(create = true) 075 protected transient CertService certService; 076 077 @In(create = true) 078 protected transient CUserService cUserService; 079 080 @In(create = true) 081 protected transient NavigationContext navigationContext; 082 083 @In(create = true, required = false) 084 protected FacesMessages facesMessages; 085 086 @In(create = true) 087 protected ResourcesAccessor resourcesAccessor; 088 089 @In(create = true, required = false) 090 protected transient CoreSession documentManager; 091 092 @In(create = true) 093 protected transient NuxeoPrincipal currentUser; 094 095 @In(create = true) 096 protected transient UserManager userManager; 097 098 @In(create = true, required = false) 099 protected WebActions webActions; 100 101 protected DocumentModel lastVisitedDocument; 102 103 protected DocumentModel certificate; 104 105 private static final String LOCAL_CA_CERTIFICATE_FILE_NAME = "LOCAL_CA_.crt"; 106 107 /** 108 * Retrieves a user certificate and returns a certificate's document model object 109 * 110 * @return 111 */ 112 public DocumentModel getCertificate() { 113 String userID = (String) getCurrentUserModel().getPropertyValue("user:username"); 114 return cUserService.getCertificate(userID); 115 } 116 117 /** 118 * Checks if a specified user has a certificate 119 * 120 * @param user 121 * @return 122 */ 123 public boolean hasCertificate(DocumentModel user) { 124 String userID = (String) user.getPropertyValue("user:username"); 125 return cUserService.hasCertificate(userID); 126 } 127 128 /** 129 * Checks if a specified user has a certificate 130 * 131 * @param userID 132 * @return 133 */ 134 public boolean hasCertificate(String userID) { 135 return cUserService.hasCertificate(userID); 136 } 137 138 /** 139 * Checks if a specified user has a certificate 140 * 141 * @return 142 */ 143 public boolean hasCertificate() { 144 return hasCertificate(getCurrentUserModel()); 145 } 146 147 /** 148 * Indicates whether a user has the right to generate a certificate. 149 * 150 * @param user 151 * @return 152 */ 153 public boolean canGenerateCertificate() { 154 boolean canGenerateCertificate = false; 155 // TODO currently allows generating certificates but will be used for 156 // tightening security 157 canGenerateCertificate = true; 158 return canGenerateCertificate; 159 } 160 161 /** 162 * Launches certificate generation. Requires valid passwords for certificate encryption. 163 * 164 * @param user 165 * @param firstPassword 166 * @param secondPassword 167 */ 168 public void createCertificate(String firstPassword, String secondPassword) { 169 boolean areRequirementsMet = false; 170 171 try { 172 validatePasswords(firstPassword, secondPassword); 173 validateRequiredUserFields(); 174 // passed through validations 175 areRequirementsMet = true; 176 } catch (ValidatorException v) { 177 facesMessages.add(StatusMessage.Severity.ERROR, v.getFacesMessage().getDetail()); 178 } 179 180 if (areRequirementsMet) { 181 try { 182 cUserService.createCertificate(getCurrentUserModel(), firstPassword); 183 facesMessages.add(StatusMessage.Severity.INFO, 184 resourcesAccessor.getMessages().get("label.cert.created")); 185 } catch (CertException e) { 186 LOG.error(e); 187 facesMessages.add(StatusMessage.Severity.ERROR, 188 resourcesAccessor.getMessages().get("label.cert.generate.problem") + e.getMessage()); 189 } 190 } 191 } 192 193 /** 194 * @since 5.8 - action to remove certificate. 195 */ 196 public void deleteCertificate() { 197 try { 198 cUserService.deleteCertificate((String) getCurrentUserModel().getPropertyValue("user:username")); 199 facesMessages.add(StatusMessage.Severity.INFO, resourcesAccessor.getMessages().get("label.cert.deleted")); 200 } catch (CertException e) { 201 LOG.error("Digital signature certificate deletion issue", e); 202 facesMessages.add(StatusMessage.Severity.ERROR, 203 resourcesAccessor.getMessages().get("label.cert.delete.problem") + e.getMessage()); 204 } 205 } 206 207 /** 208 * Validates that the password follows business rules. 209 * <p> 210 * The password must be typed in twice correctly, follow minimum length, and be different than the application login 211 * password. 212 * <p> 213 * The validations are performed in the following sequence cheapest validations first, then the ones requiring more 214 * system resources. 215 * 216 * @param firstPassword 217 * @param secondPassword 218 */ 219 public void validatePasswords(String firstPassword, String secondPassword) { 220 221 if (firstPassword == null || secondPassword == null) { 222 FacesMessage message = new FacesMessage(FacesMessage.SEVERITY_ERROR, resourcesAccessor.getMessages().get( 223 "label.review.added.reviewer"), null); 224 facesMessages.add(StatusMessage.Severity.ERROR, "ABC" + message.getDetail()); 225 throw new ValidatorException(message); 226 } 227 228 if (!firstPassword.equals(secondPassword)) { 229 FacesMessage message = new FacesMessage(FacesMessage.SEVERITY_ERROR, resourcesAccessor.getMessages().get( 230 "label.cert.password.mismatch"), null); 231 throw new ValidatorException(message); 232 } 233 234 // at least 8 characters 235 if (firstPassword.length() < MINIMUM_PASSWORD_LENGTH) { 236 FacesMessage message = new FacesMessage(FacesMessage.SEVERITY_ERROR, resourcesAccessor.getMessages().get( 237 "label.cert.password.too.short"), null); 238 throw new ValidatorException(message); 239 } 240 241 String hashedUserPassword = (String) getCurrentUserModel().getPropertyValue("user:password"); 242 243 /* 244 * If the certificate password matches the user login password an exception is thrown, as those passwords should 245 * not be the same to increase security and decouple one from another to allow for reuse 246 */ 247 if (hashedUserPassword != null && PasswordHelper.verifyPassword(firstPassword, hashedUserPassword)) { 248 FacesMessage message = new FacesMessage(FacesMessage.SEVERITY_ERROR, resourcesAccessor.getMessages().get( 249 "label.cert.password.is.login.password"), null); 250 throw new ValidatorException(message); 251 } 252 } 253 254 /** 255 * Validates user identity fields required for certificate generation NXP-6485 256 * <p> 257 */ 258 public void validateRequiredUserFields() { 259 260 DocumentModel user = userManager.getUserModel(currentUser.getName()); 261 // first name 262 String firstName = (String) user.getPropertyValue(USER_FIELD_FIRSTNAME); 263 if (null == firstName || firstName.length() == 0) { 264 FacesMessage message = new FacesMessage(FacesMessage.SEVERITY_ERROR, resourcesAccessor.getMessages().get( 265 "label.cert.user.firstname.missing"), null); 266 throw new ValidatorException(message); 267 } 268 // last name 269 String lastName = (String) user.getPropertyValue(USER_FIELD_LASTNAME); 270 if (null == lastName || lastName.length() == 0) { 271 FacesMessage message = new FacesMessage(FacesMessage.SEVERITY_ERROR, resourcesAccessor.getMessages().get( 272 "label.cert.user.lastname.missing"), null); 273 throw new ValidatorException(message); 274 } 275 // email - // a very forgiving check (e.g. accepts _@localhost) 276 String email = (String) user.getPropertyValue(USER_FIELD_EMAIL); 277 String emailRegex = ".+@.+"; 278 if (null == email || email.length() == 0 || !email.matches(emailRegex)) { 279 FacesMessage message = new FacesMessage(FacesMessage.SEVERITY_ERROR, resourcesAccessor.getMessages().get( 280 "label.cert.user.email.problem"), null); 281 throw new ValidatorException(message); 282 } 283 } 284 285 public void downloadRootCertificate() throws CertException { 286 try { 287 byte[] rootCertificateData = cUserService.getRootCertificateData(); 288 HttpServletResponse response = (HttpServletResponse) FacesContext.getCurrentInstance().getExternalContext().getResponse(); 289 response.setContentType("application/octet-stream"); 290 response.addHeader("Content-Disposition", "attachment;filename=" + LOCAL_CA_CERTIFICATE_FILE_NAME); 291 response.setContentLength(rootCertificateData.length); 292 OutputStream writer = response.getOutputStream(); 293 writer.write(rootCertificateData); 294 writer.flush(); 295 writer.close(); 296 FacesContext.getCurrentInstance().responseComplete(); 297 } catch (IOException e) { 298 throw new CertException(e); 299 } 300 } 301 302 public String goToCertificateManagement() { 303 lastVisitedDocument = navigationContext.getCurrentDocument(); 304 webActions.setCurrentTabIds(HOME_TAB); 305 webActions.setCurrentTabIds(CERTIFICATE_TAB); 306 return "view_home"; 307 } 308 309 public String backToDocument() { 310 if (lastVisitedDocument != null) { 311 webActions.setCurrentTabIds("sign_view"); 312 return navigationContext.navigateToDocument(lastVisitedDocument); 313 } else { 314 return navigationContext.goHome(); 315 } 316 } 317 318 protected DocumentModel getCurrentUserModel() { 319 return userManager.getUserModel(currentUser.getName()); 320 } 321}