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}