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