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