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