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