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}