001/*
002 * (C) Copyright 2010-2015 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 *     Thierry Delprat
018 *     Julien Carsique
019 *
020 */
021
022package org.nuxeo.ecm.admin.setup;
023
024import static org.nuxeo.common.Environment.NUXEO_DATA_DIR;
025import static org.nuxeo.common.Environment.NUXEO_LOG_DIR;
026import static org.nuxeo.common.Environment.PRODUCT_NAME;
027import static org.nuxeo.common.Environment.PRODUCT_VERSION;
028import static org.nuxeo.launcher.config.ConfigurationGenerator.NUXEO_CONF;
029import static org.nuxeo.launcher.config.ConfigurationGenerator.NUXEO_DEV_SYSTEM_PROP;
030import static org.nuxeo.launcher.config.ConfigurationGenerator.PARAM_BIND_ADDRESS;
031import static org.nuxeo.launcher.config.ConfigurationGenerator.PARAM_DB_HOST;
032import static org.nuxeo.launcher.config.ConfigurationGenerator.PARAM_DB_NAME;
033import static org.nuxeo.launcher.config.ConfigurationGenerator.PARAM_DB_PORT;
034import static org.nuxeo.launcher.config.ConfigurationGenerator.PARAM_DB_PWD;
035import static org.nuxeo.launcher.config.ConfigurationGenerator.PARAM_DB_USER;
036import static org.nuxeo.launcher.config.ConfigurationGenerator.PARAM_NUXEO_URL;
037import static org.nuxeo.launcher.config.ConfigurationGenerator.PARAM_TEMPLATE_DBNAME;
038import static org.nuxeo.launcher.config.ConfigurationGenerator.SECRET_KEYS;
039
040import java.io.IOException;
041import java.io.Serializable;
042import java.math.BigDecimal;
043import java.sql.SQLException;
044import java.util.*;
045import java.util.Map.Entry;
046
047import javax.faces.application.FacesMessage;
048import javax.faces.component.UIComponent;
049import javax.faces.component.UIInput;
050import javax.faces.component.ValueHolder;
051import javax.faces.context.FacesContext;
052import javax.faces.event.AbortProcessingException;
053import javax.faces.event.AjaxBehaviorEvent;
054import javax.faces.validator.ValidatorException;
055import javax.naming.AuthenticationException;
056import javax.naming.NamingException;
057
058import org.apache.commons.lang3.StringUtils;
059import org.apache.commons.logging.Log;
060import org.apache.commons.logging.LogFactory;
061import org.jboss.seam.ScopeType;
062import org.jboss.seam.annotations.Factory;
063import org.jboss.seam.annotations.In;
064import org.jboss.seam.annotations.Name;
065import org.jboss.seam.annotations.Scope;
066import org.jboss.seam.contexts.Contexts;
067import org.jboss.seam.faces.FacesMessages;
068import org.jboss.seam.international.StatusMessage;
069import org.nuxeo.ecm.platform.ui.web.util.ComponentUtils;
070import org.nuxeo.launcher.commons.DatabaseDriverException;
071import org.nuxeo.launcher.config.ConfigurationException;
072import org.nuxeo.launcher.config.ConfigurationGenerator;
073
074/**
075 * Serves UI for the setup screen, handling properties that can be saved on the bin/nuxeo.conf file on server.
076 * <p>
077 * Manages some important parameters to perform validation on them, and accepts custom parameters that would be present
078 * in the server nuxeo.conf file, and moves to advanced mode any property that would not be in that list.
079 *
080 * @since 5.5
081 */
082@Scope(ScopeType.SESSION)
083@Name("setupWizardAction")
084public class SetupWizardActionBean implements Serializable {
085
086    private static final long serialVersionUID = 1L;
087
088    protected static final Log log = LogFactory.getLog(SetupWizardActionBean.class);
089
090    /**
091     * The list of important parameters that need to be presented first to the user
092     */
093    private static final String[] managedKeyParameters = { PARAM_BIND_ADDRESS, PARAM_NUXEO_URL, NUXEO_DATA_DIR,
094            NUXEO_LOG_DIR, PRODUCT_NAME, PRODUCT_VERSION, NUXEO_CONF, PARAM_TEMPLATE_DBNAME, PARAM_DB_NAME,
095            PARAM_DB_USER, PARAM_DB_PWD, PARAM_DB_HOST, PARAM_DB_PORT, "nuxeo.db.min-pool-size",
096            "nuxeo.db.min-pool-size", "nuxeo.db.max-pool-size", "nuxeo.vcs.min-pool-size", "nuxeo.vcs.max-pool-size",
097            "nuxeo.notification.eMailSubjectPrefix", "mailservice.user", "mailservice.password", "mail.store.protocol",
098            "mail.transport.protocol", "mail.store.host", "mail.store.port", "mail.store.user", "mail.store.password",
099            "mail.debug", "mail.transport.host", "mail.transport.port", "mail.transport.auth", "mail.transport.user",
100            "mail.transport.password", "mail.from", "mail.user", "mail.transport.usetls", "nuxeo.http.proxy.host",
101            "nuxeo.http.proxy.port", "nuxeo.http.proxy.login", "nuxeo.http.proxy.password", NUXEO_DEV_SYSTEM_PROP,
102            "nuxeo.directory.type", "nuxeo.user.group.storage", "nuxeo.ldap.url", "nuxeo.ldap.binddn",
103            "nuxeo.ldap.bindpassword", "nuxeo.ldap.retries", "nuxeo.ldap.user.searchBaseDn",
104            "nuxeo.ldap.user.searchClass", "nuxeo.ldap.user.searchFilter", "nuxeo.ldap.user.searchScope",
105            "nuxeo.ldap.user.readonly", "nuxeo.ldap.user.mapping.rdn", "nuxeo.ldap.user.mapping.username",
106            "nuxeo.ldap.user.mapping.password", "nuxeo.ldap.user.mapping.firstname",
107            "nuxeo.ldap.user.mapping.lastname", "nuxeo.ldap.user.mapping.email", "nuxeo.ldap.user.mapping.company",
108            "nuxeo.ldap.group.searchBaseDn", "nuxeo.ldap.group.searchFilter", "nuxeo.ldap.group.searchScope",
109            "nuxeo.ldap.group.readonly", "nuxeo.ldap.group.mapping.rdn", "nuxeo.ldap.group.mapping.name",
110            "nuxeo.ldap.group.mapping.label", "nuxeo.ldap.group.mapping.members.staticAttributeId",
111            "nuxeo.ldap.group.mapping.members.dynamicAttributeId", "nuxeo.ldap.defaultAdministratorId",
112            "nuxeo.ldap.defaultMembersGroup", "nuxeo.user.anonymous.enable", "nuxeo.user.emergency.enable",
113            "nuxeo.user.emergency.username", "nuxeo.user.emergency.password", "nuxeo.user.emergency.firstname",
114            "nuxeo.user.emergency.lastname" };
115
116    protected Map<String, String> parameters;
117
118    protected Map<String, String> advancedParameters;
119
120    protected static final String PROXY_NONE = "none";
121
122    protected static final String PROXY_ANONYMOUS = "anonymous";
123
124    protected static final String PROXY_AUTHENTICATED = "authenticated";
125
126    protected static final String DIRECTORY_DEFAULT = "default";
127
128    protected static final String DIRECTORY_LDAP = "ldap";
129
130    protected static final String DIRECTORY_MULTI = "multi";
131
132    private static final String ERROR_DB_DRIVER = "error.db.driver.notfound";
133
134    private static final String ERROR_DB_CONNECTION = "error.db.connection";
135
136    private static final String ERROR_LDAP_CONNECTION = "error.ldap.connection";
137
138    private static final String ERROR_LDAP_AUTHENTICATION = "error.ldap.authentication";
139
140    private static final String ERROR_DB_FS = "error.db.fs";
141
142    protected String proxyType = PROXY_NONE;
143
144    protected String directoryType = DIRECTORY_DEFAULT;
145
146    protected boolean needsRestart = false;
147
148    @In(create = true)
149    private transient ConfigurationGenerator setupConfigGenerator;
150
151    protected Properties userConfig;
152
153    @In(create = true, required = false)
154    protected FacesMessages facesMessages;
155
156    @In(create = true)
157    protected Map<String, String> messages;
158
159    private Boolean needGroupConfiguration;
160
161    @Factory(value = "setupRequiresRestart", scope = ScopeType.EVENT)
162    public boolean isNeedsRestart() {
163        return needsRestart;
164    }
165
166    public void setNeedsRestart(boolean needsRestart) {
167        this.needsRestart = needsRestart;
168    }
169
170    @Factory(value = "setupConfigGenerator", scope = ScopeType.PAGE)
171    public ConfigurationGenerator getConfigurationGenerator() {
172        if (setupConfigGenerator == null) {
173            setupConfigGenerator = new ConfigurationGenerator();
174            if (setupConfigGenerator.init()) {
175                setParameters();
176            }
177        }
178        return setupConfigGenerator;
179    }
180
181    @Factory(value = "setupConfigurable", scope = ScopeType.APPLICATION)
182    public boolean isConfigurable() {
183        return setupConfigGenerator.isConfigurable();
184    }
185
186    @Factory(value = "advancedParams", scope = ScopeType.EVENT)
187    public Map<String, String> getAdvancedParameters() {
188        return advancedParameters;
189    }
190
191    @Factory(value = "setupParams", scope = ScopeType.EVENT)
192    public Map<String, String> getParameters() {
193        return parameters;
194    }
195
196    /**
197     * Fill {@link #parameters} and {@link #advancedParameters} with properties from #
198     * {@link ConfigurationGenerator#getUserConfig()}
199     *
200     * @since 5.6
201     */
202    protected void setParameters() {
203        userConfig = setupConfigGenerator.getUserConfig();
204        parameters = new HashMap<>();
205        advancedParameters = new TreeMap<>();
206        // will remove managed parameters later in setParameter()
207        for (String key : userConfig.stringPropertyNames()) {
208            if (System.getProperty(key) == null
209                    || key.matches("^(nuxeo|org\\.nuxeo|catalina|derby|h2|java\\.home|"
210                            + "java\\.io\\.tmpdir|tomcat|sun\\.rmi\\.dgc).*")) {
211                advancedParameters.put(key, userConfig.getProperty(key).trim());
212            }
213        }
214        for (String keyParam : managedKeyParameters) {
215            String parameter = userConfig.getProperty(keyParam);
216            setParameter(keyParam, parameter);
217        }
218
219        proxyType = PROXY_NONE;
220        if (parameters.get("nuxeo.http.proxy.host") != null) {
221            proxyType = PROXY_ANONYMOUS;
222            if (parameters.get("nuxeo.http.proxy.login") != null) {
223                proxyType = PROXY_AUTHENTICATED;
224            }
225        }
226
227        if (parameters.get("nuxeo.directory.type") != null) {
228            directoryType = parameters.get("nuxeo.directory.type");
229        }
230    }
231
232    /**
233     * Adds parameter value to the
234     *
235     * @param key parameter key such as used in templates and nuxeo.conf
236     */
237    private void setParameter(String key, String value) {
238        if (value != null) {
239            parameters.put(key, value.trim());
240            advancedParameters.remove(key);
241        }
242    }
243
244    public void save() {
245        saveParameters();
246        setNeedsRestart(true);
247        resetParameters();
248        // initialize setupConfigurator again, as it's in scope page
249        getConfigurationGenerator();
250        facesMessages.add(StatusMessage.Severity.INFO, messages.get("label.parameters.saved"));
251    }
252
253    @SuppressWarnings("unchecked")
254    protected void saveParameters() {
255
256        // Fix types (replace Long, BigDecimal and Boolean with String)
257        Iterator it = parameters.entrySet().iterator();
258        while (it.hasNext()) {
259            Map.Entry entry = (Map.Entry) it.next();
260            Object value = entry.getValue();
261            if (value instanceof Long || value instanceof Boolean || value instanceof BigDecimal) {
262                entry.setValue(value.toString());
263            }
264        }
265
266        // manage httpProxy settings (setting null is not accepted)
267        if (!PROXY_AUTHENTICATED.equals(proxyType)) {
268            parameters.put("nuxeo.http.proxy.login", "");
269            parameters.put("nuxeo.http.proxy.password", "");
270        }
271        if (PROXY_NONE.equals(proxyType)) {
272            parameters.put("nuxeo.http.proxy.host", "");
273            parameters.put("nuxeo.http.proxy.port", "");
274        }
275
276        // Remove empty values for password keys
277        for (String pwdKey : SECRET_KEYS) {
278            if (StringUtils.isEmpty(parameters.get(pwdKey))) {
279                parameters.remove(pwdKey);
280            }
281        }
282
283        Map<String, String> customParameters = new HashMap<>();
284        customParameters.putAll(parameters);
285        customParameters.putAll(advancedParameters);
286        try {
287            setupConfigGenerator.saveFilteredConfiguration(customParameters);
288        } catch (ConfigurationException e) {
289            log.error(e, e);
290        }
291    }
292
293    public void resetParameters() {
294        setupConfigGenerator = null;
295        parameters = null;
296        advancedParameters = null;
297        Contexts.getPageContext().remove("setupConfigGenerator");
298    }
299
300    /**
301     * @since 5.6
302     */
303    public void checkDatabaseParameters(FacesContext context, UIComponent component, Object value) {
304        Map<String, Object> attributes = component.getAttributes();
305        String dbNameInputId = (String) attributes.get("dbNameInputId");
306        String dbUserInputId = (String) attributes.get("dbUserInputId");
307        String dbPwdInputId = (String) attributes.get("dbPwdInputId");
308        String dbHostInputId = (String) attributes.get("dbHostInputId");
309        String dbPortInputId = (String) attributes.get("dbPortInputId");
310
311        if (dbNameInputId == null || dbUserInputId == null || dbPwdInputId == null || dbHostInputId == null
312                || dbPortInputId == null) {
313            log.error("Cannot validate database parameters: missing inputIds");
314            return;
315        }
316
317        UIInput dbNameComp = (UIInput) component.findComponent(dbNameInputId);
318        UIInput dbUserComp = (UIInput) component.findComponent(dbUserInputId);
319        UIInput dbPwdComp = (UIInput) component.findComponent(dbPwdInputId);
320        UIInput dbHostComp = (UIInput) component.findComponent(dbHostInputId);
321        UIInput dbPortComp = (UIInput) component.findComponent(dbPortInputId);
322        if (dbNameComp == null || dbUserComp == null || dbPwdComp == null || dbHostComp == null || dbPortComp == null) {
323            log.error("Cannot validate inputs: not found");
324            return;
325        }
326
327        String dbName = (String) dbNameComp.getLocalValue();
328        String dbUser = (String) dbUserComp.getLocalValue();
329        String dbPwd = (String) dbPwdComp.getLocalValue();
330        String dbHost = (String) dbHostComp.getLocalValue();
331        Long dbPortLong = ((BigDecimal) dbPortComp.getLocalValue()).longValue();
332        String dbPort = dbPortLong.toString();
333
334        if (StringUtils.isEmpty(dbPwd)) {
335            dbPwd = parameters.get("nuxeo.db.password");
336        }
337
338        String errorLabel = null;
339        Exception error = null;
340        try {
341            setupConfigGenerator.checkDatabaseConnection(parameters.get(ConfigurationGenerator.PARAM_TEMPLATE_DBNAME),
342                    dbName, dbUser, dbPwd, dbHost, dbPort);
343        } catch (IOException e) {
344            errorLabel = ERROR_DB_FS;
345            error = e;
346        } catch (DatabaseDriverException e) {
347            errorLabel = ERROR_DB_DRIVER;
348            error = e;
349        } catch (SQLException e) {
350            errorLabel = ERROR_DB_CONNECTION;
351            error = e;
352        }
353        if (error != null) {
354            log.error(error, error);
355            FacesMessage message = new FacesMessage(FacesMessage.SEVERITY_ERROR, ComponentUtils.translate(context,
356                    errorLabel), null);
357            throw new ValidatorException(message);
358        }
359
360        FacesMessage message = new FacesMessage(FacesMessage.SEVERITY_INFO, ComponentUtils.translate(context,
361                "error.db.none"), null);
362        message.setSeverity(FacesMessage.SEVERITY_INFO);
363        context.addMessage(component.getClientId(context), message);
364    }
365
366    public void templateChange(AjaxBehaviorEvent event) {
367        String dbTemplate;
368        UIComponent select = event.getComponent();
369        if (select instanceof ValueHolder) {
370            dbTemplate = (String) ((ValueHolder) select).getValue();
371        } else {
372            log.error("Bad component returned " + select);
373            throw new AbortProcessingException("Bad component returned " + select);
374        }
375        setupConfigGenerator.changeDBTemplate(dbTemplate);
376        setParameters();
377        Contexts.getEventContext().remove("setupParams");
378        Contexts.getEventContext().remove("advancedParams");
379        FacesContext context = FacesContext.getCurrentInstance();
380        context.renderResponse();
381    }
382
383    public void proxyChange(AjaxBehaviorEvent event) {
384        UIComponent select = event.getComponent();
385        if (select instanceof ValueHolder) {
386            proxyType = (String) ((ValueHolder) select).getValue();
387        } else {
388            log.error("Bad component returned " + select);
389            throw new AbortProcessingException("Bad component returned " + select);
390        }
391        Contexts.getEventContext().remove("setupParams");
392        FacesContext context = FacesContext.getCurrentInstance();
393        context.renderResponse();
394    }
395
396    /**
397     * Initialized by {@link #getParameters()}
398     */
399    public String getProxyType() {
400        return proxyType;
401    }
402
403    public void setProxyType(String proxyType) {
404        this.proxyType = proxyType;
405    }
406
407    public String getDirectoryType() {
408        return directoryType;
409    }
410
411    public void setDirectoryType(String directoryType) {
412        parameters.put("nuxeo.directory.type", directoryType);
413        this.directoryType = directoryType;
414    }
415
416    public void setDirectoryStorage(String directoryStorage) {
417        parameters.put("nuxeo.user.group.storage", directoryStorage);
418    }
419
420    public void ldapStorageChange() {
421        needGroupConfiguration = null;
422    }
423
424    public boolean getNeedGroupConfiguration() {
425        if (needGroupConfiguration == null) {
426            String storageType = parameters.get("nuxeo.user.group.storage");
427            if ("userLdapOnly".equals(storageType) || "multiUserSqlGroup".equals(storageType)) {
428                needGroupConfiguration = Boolean.FALSE;
429            } else {
430                needGroupConfiguration = Boolean.TRUE;
431            }
432        }
433        return needGroupConfiguration;
434    }
435
436    public void directoryChange(AjaxBehaviorEvent event) {
437        UIComponent select = event.getComponent();
438        if (select instanceof ValueHolder) {
439            directoryType = (String) ((ValueHolder) select).getValue();
440        } else {
441            log.error("Bad component returned " + select);
442            throw new AbortProcessingException("Bad component returned " + select);
443        }
444        if ("multi".equals(directoryType)) {
445            setDirectoryStorage("multiUserGroup");
446        } else {
447            setDirectoryStorage("default");
448        }
449        needGroupConfiguration = null;
450        Contexts.getEventContext().remove("setupParams");
451        FacesContext context = FacesContext.getCurrentInstance();
452        context.renderResponse();
453    }
454
455    public void checkLdapNetworkParameters(FacesContext context, UIComponent component, Object value) {
456        Map<String, Object> attributes = component.getAttributes();
457        String ldapUrlId = (String) attributes.get("directoryLdapUrl");
458
459        if (ldapUrlId == null) {
460            log.error("Cannot validate LDAP parameters: missing inputIds");
461            return;
462        }
463
464        UIInput ldapUrlComp = (UIInput) component.findComponent(ldapUrlId);
465        if (ldapUrlComp == null) {
466            log.error("Cannot validate LDAP inputs: not found");
467            return;
468        }
469
470        String ldapUrl = (String) ldapUrlComp.getLocalValue();
471
472        String errorLabel = null;
473        Exception error = null;
474        try {
475            setupConfigGenerator.checkLdapConnection(ldapUrl, null, null, false);
476        } catch (NamingException e) {
477            errorLabel = ERROR_LDAP_CONNECTION;
478            error = e;
479        }
480        if (error != null) {
481            log.error(error, error);
482            FacesMessage message = new FacesMessage(FacesMessage.SEVERITY_ERROR, ComponentUtils.translate(context,
483                    errorLabel), null);
484            throw new ValidatorException(message);
485        }
486
487        FacesMessage message = new FacesMessage(FacesMessage.SEVERITY_INFO, ComponentUtils.translate(context,
488                "error.ldap.network.none"), null);
489        message.setSeverity(FacesMessage.SEVERITY_INFO);
490        context.addMessage(component.getClientId(context), message);
491    }
492
493    public void checkLdapAuthenticationParameters(FacesContext context, UIComponent component, Object value) {
494
495        Map<String, Object> attributes = component.getAttributes();
496        String ldapUrlId = (String) attributes.get("ldapUrl");
497        String ldapBinddnId = (String) attributes.get("ldapBindDn");
498        String ldapBindpwdId = (String) attributes.get("ldapBindPwd");
499
500        if (ldapUrlId == null || ldapBinddnId == null || ldapBindpwdId == null) {
501            log.error("Cannot validate LDAP parameters: missing inputIds");
502            return;
503        }
504
505        UIInput ldapUrlComp = (UIInput) component.findComponent(ldapUrlId);
506        UIInput ldapBinddnComp = (UIInput) component.findComponent(ldapBinddnId);
507        UIInput ldapBindpwdComp = (UIInput) component.findComponent(ldapBindpwdId);
508        if (ldapUrlComp == null || ldapBinddnComp == null || ldapBindpwdComp == null) {
509            log.error("Cannot validate LDAP inputs: not found");
510            return;
511        }
512
513        String ldapUrl = (String) ldapUrlComp.getLocalValue();
514        String ldapBindDn = (String) ldapBinddnComp.getLocalValue();
515        String ldapBindPwd = (String) ldapBindpwdComp.getLocalValue();
516
517        String errorLabel = null;
518        Exception error = null;
519        try {
520            setupConfigGenerator.checkLdapConnection(ldapUrl, ldapBindDn, ldapBindPwd, true);
521        } catch (NamingException e) {
522            if (e instanceof AuthenticationException) {
523                errorLabel = ERROR_LDAP_AUTHENTICATION;
524            } else {
525                errorLabel = ERROR_LDAP_CONNECTION;
526            }
527            error = e;
528        }
529        if (error != null) {
530            log.error(error, error);
531            FacesMessage message = new FacesMessage(FacesMessage.SEVERITY_ERROR, ComponentUtils.translate(context,
532                    errorLabel), null);
533            throw new ValidatorException(message);
534        }
535
536        FacesMessage message = new FacesMessage(FacesMessage.SEVERITY_INFO, ComponentUtils.translate(context,
537                "error.ldap.auth.none"), null);
538        message.setSeverity(FacesMessage.SEVERITY_INFO);
539        context.addMessage(component.getClientId(context), message);
540    }
541
542}