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