diff --git a/api/admin.go b/api/admin.go index 3b324c75fb..d48c8d3798 100644 --- a/api/admin.go +++ b/api/admin.go @@ -42,6 +42,7 @@ func InitAdmin() { BaseRoutes.Admin.Handle("/reset_mfa", ApiAdminSystemRequired(adminResetMfa)).Methods("POST") BaseRoutes.Admin.Handle("/reset_password", ApiAdminSystemRequired(adminResetPassword)).Methods("POST") BaseRoutes.Admin.Handle("/ldap_sync_now", ApiAdminSystemRequired(ldapSyncNow)).Methods("POST") + BaseRoutes.Admin.Handle("/ldap_test", ApiAdminSystemRequired(ldapTest)).Methods("POST") BaseRoutes.Admin.Handle("/saml_metadata", ApiAppHandler(samlMetadata)).Methods("GET") BaseRoutes.Admin.Handle("/add_certificate", ApiAdminSystemRequired(addCertificate)).Methods("POST") BaseRoutes.Admin.Handle("/remove_certificate", ApiAdminSystemRequired(removeCertificate)).Methods("POST") @@ -643,7 +644,7 @@ func ldapSyncNow(c *Context, w http.ResponseWriter, r *http.Request) { if ldapI := einterfaces.GetLdapInterface(); ldapI != nil { ldapI.SyncNow() } else { - l4g.Error("%v", model.NewLocAppError("saveComplianceReport", "ent.compliance.licence_disable.app_error", nil, "").Error()) + l4g.Error("%v", model.NewLocAppError("ldapSyncNow", "ent.ldap.disabled.app_error", nil, "").Error()) } } }() @@ -653,6 +654,24 @@ func ldapSyncNow(c *Context, w http.ResponseWriter, r *http.Request) { w.Write([]byte(model.MapToJson(rdata))) } +func ldapTest(c *Context, w http.ResponseWriter, r *http.Request) { + if ldapI := einterfaces.GetLdapInterface(); ldapI != nil && utils.IsLicensed && *utils.License.Features.LDAP && *utils.Cfg.LdapSettings.Enable { + if err := ldapI.RunTest(); err != nil { + c.Err = err + c.Err.StatusCode = 500 + } + } else { + c.Err = model.NewLocAppError("ldapTest", "ent.ldap.disabled.app_error", nil, "") + c.Err.StatusCode = http.StatusNotImplemented + } + + if c.Err == nil { + rdata := map[string]string{} + rdata["status"] = "ok" + w.Write([]byte(model.MapToJson(rdata))) + } +} + func samlMetadata(c *Context, w http.ResponseWriter, r *http.Request) { samlInterface := einterfaces.GetSamlInterface() diff --git a/api/admin_test.go b/api/admin_test.go index 967e3ceb3f..3d8a956769 100644 --- a/api/admin_test.go +++ b/api/admin_test.go @@ -161,6 +161,18 @@ func TestEmailTest(t *testing.T) { } } +func TestLdapTest(t *testing.T) { + th := Setup().InitBasic().InitSystemAdmin() + + if _, err := th.BasicClient.TestLdap(utils.Cfg); err == nil { + t.Fatal("Shouldn't have permissions") + } + + if _, err := th.SystemAdminClient.TestLdap(utils.Cfg); err == nil { + t.Fatal("should have errored") + } +} + func TestGetTeamAnalyticsStandard(t *testing.T) { th := Setup().InitBasic().InitSystemAdmin() th.CreatePrivateChannel(th.BasicClient, th.BasicTeam) diff --git a/einterfaces/ldap.go b/einterfaces/ldap.go index fb14a8f023..f50ea277e9 100644 --- a/einterfaces/ldap.go +++ b/einterfaces/ldap.go @@ -16,6 +16,7 @@ type LdapInterface interface { Syncronize() *model.AppError StartLdapSyncJob() SyncNow() + RunTest() *model.AppError GetAllLdapUsers() ([]*model.User, *model.AppError) } diff --git a/i18n/en.json b/i18n/en.json index cdb05f14bd..50e7525f13 100644 --- a/i18n/en.json +++ b/i18n/en.json @@ -2503,6 +2503,10 @@ "id": "ent.ldap.validate_filter.app_error", "translation": "Invalid LDAP Filter" }, + { + "id": "ent.ldap.disabled.app_error", + "translation": "LDAP disabled or licence does not support LDAP." + }, { "id": "ent.mfa.activate.authenticate.app_error", "translation": "Error attempting to authenticate MFA token" diff --git a/model/client.go b/model/client.go index e2e003fe88..f43e5ad799 100644 --- a/model/client.go +++ b/model/client.go @@ -887,6 +887,19 @@ func (c *Client) TestEmail(config *Config) (*Result, *AppError) { } } +// TestLdap will run a connection test on the current LDAP settings. +// It will return the standard OK response if settings work. Otherwise +// it will return an appropriate error. +func (c *Client) TestLdap(config *Config) (*Result, *AppError) { + if r, err := c.DoApiPost("/admin/ldap_test", config.ToJson()); err != nil { + return nil, err + } else { + defer closeBody(r) + return &Result{r.Header.Get(HEADER_REQUEST_ID), + r.Header.Get(HEADER_ETAG_SERVER), MapFromJson(r.Body)}, nil + } +} + func (c *Client) GetComplianceReports() (*Result, *AppError) { if r, err := c.DoApiGet("/admin/compliance_reports", "", ""); err != nil { return nil, err diff --git a/webapp/client/client.jsx b/webapp/client/client.jsx index b842d99393..a059bb38ae 100644 --- a/webapp/client/client.jsx +++ b/webapp/client/client.jsx @@ -471,6 +471,15 @@ export default class Client { end(this.handleResponse.bind(this, 'ldapSyncNow', success, error)); } + ldapTest(success, error) { + request. + post(`${this.getAdminRoute()}/ldap_test`). + set(this.defaultHeaders). + type('application/json'). + accept('application/json'). + end(this.handleResponse.bind(this, 'ldap_test', success, error)); + } + // Team Routes Section createTeamFromSignup(teamSignup, success, error) { diff --git a/webapp/components/admin_console/admin_settings.jsx b/webapp/components/admin_console/admin_settings.jsx index 8601722eb3..9975a3975a 100644 --- a/webapp/components/admin_console/admin_settings.jsx +++ b/webapp/components/admin_console/admin_settings.jsx @@ -21,6 +21,7 @@ export default class AdminSettings extends React.Component { this.handleChange = this.handleChange.bind(this); this.handleSubmit = this.handleSubmit.bind(this); + this.doSubmit = this.doSubmit.bind(this); this.state = Object.assign(this.getStateFromConfig(props.config), { saveNeeded: false, @@ -39,6 +40,10 @@ export default class AdminSettings extends React.Component { handleSubmit(e) { e.preventDefault(); + this.doSubmit(); + } + + doSubmit(callback) { this.setState({ saving: true, serverError: null @@ -59,12 +64,20 @@ export default class AdminSettings extends React.Component { saveNeeded: false, saving: false }); + + if (callback) { + callback(); + } }, (err) => { this.setState({ saving: false, serverError: err.message }); + + if (callback) { + callback(); + } } ); } diff --git a/webapp/components/admin_console/admin_sidebar.jsx b/webapp/components/admin_console/admin_sidebar.jsx index 4fcfe2731f..0b107e19a4 100644 --- a/webapp/components/admin_console/admin_sidebar.jsx +++ b/webapp/components/admin_console/admin_sidebar.jsx @@ -207,7 +207,7 @@ export default class AdminSidebar extends React.Component { title={ } /> diff --git a/webapp/components/admin_console/ldap_settings.jsx b/webapp/components/admin_console/ldap_settings.jsx index 23728870ec..3d93ae6d8e 100644 --- a/webapp/components/admin_console/ldap_settings.jsx +++ b/webapp/components/admin_console/ldap_settings.jsx @@ -8,6 +8,7 @@ import SettingsGroup from './settings_group.jsx'; import TextSetting from './text_setting.jsx'; import SyncNowButton from './sync_now_button.jsx'; +import LdapTestButton from './ldap_test_button.jsx'; import * as Utils from 'utils/utils.jsx'; @@ -76,7 +77,7 @@ export default class LdapSettings extends AdminSettings {

); @@ -150,6 +151,23 @@ export default class LdapSettings extends AdminSettings { onChange={this.handleChange} disabled={!this.state.enable} /> + + } + helpText={ + + } + value={this.state.skipCertificateVerification} + onChange={this.handleChange} + /> + + } + placeholder={Utils.localizeMessage('admin.ldap.loginNameEx', 'Ex "LDAP Username"')} + helpText={ + + } + value={this.state.loginFieldName} + onChange={this.handleChange} + disabled={!this.state.enable} + /> } helpText={ @@ -356,22 +393,24 @@ export default class LdapSettings extends AdminSettings { onChange={this.handleChange} disabled={!this.state.enable} /> - } + placeholder={Utils.localizeMessage('admin.ldap.maxPageSizeEx', 'Ex "2000"')} helpText={ } - value={this.state.skipCertificateVerification} + value={this.state.maxPageSize} onChange={this.handleChange} + disabled={!this.state.enable} /> - - } - placeholder={Utils.localizeMessage('admin.ldap.maxPageSizeEx', 'Ex "2000"')} - helpText={ - - } - value={this.state.maxPageSize} - onChange={this.handleChange} - disabled={!this.state.enable} - /> - - } - placeholder={Utils.localizeMessage('admin.ldap.loginNameEx', 'Ex "LDAP Username"')} - helpText={ - - } - value={this.state.loginFieldName} - onChange={this.handleChange} - disabled={!this.state.enable} - /> + ); } diff --git a/webapp/components/admin_console/ldap_test_button.jsx b/webapp/components/admin_console/ldap_test_button.jsx new file mode 100644 index 0000000000..fbe348158b --- /dev/null +++ b/webapp/components/admin_console/ldap_test_button.jsx @@ -0,0 +1,139 @@ +// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +import React from 'react'; + +import Client from 'client/web_client.jsx'; +import * as Utils from 'utils/utils.jsx'; + +import {FormattedMessage, FormattedHTMLMessage} from 'react-intl'; + +export default class LdapTestButton extends React.Component { + static get propTypes() { + return { + disabled: React.PropTypes.bool, + submitFunction: React.PropTypes.func, + saveNeeded: React.PropTypes.bool + }; + } + constructor(props) { + super(props); + + this.handleLdapTest = this.handleLdapTest.bind(this); + + this.state = { + buisy: false, + fail: null, + success: false + }; + } + + handleLdapTest(e) { + e.preventDefault(); + + this.setState({ + buisy: true, + fail: null, + success: false + }); + + const doRequest = () => { //eslint-disable-line func-style + Client.ldapTest( + () => { + this.setState({ + buisy: false, + success: true + }); + }, + (err) => { + this.setState({ + buisy: false, + fail: err.message + }); + } + ); + }; + + // If we need to run the save function then run it with our request function as callback + if (this.props.saveNeeded) { + this.props.submitFunction(doRequest); + } else { + doRequest(); + } + } + + render() { + let message = null; + if (this.state.fail) { + message = ( +
+ + +
+ ); + } else if (this.state.success) { + message = ( +
+ + +
+ ); + } + + let helpText = ( + + ); + + let contents = null; + if (this.state.loading) { + contents = ( + + + {Utils.localizeMessage('admin.reload.loading', ' Loading...')} + + ); + } else { + contents = ( + + ); + } + + return ( +
+
+
+ + {message} +
+
+ {helpText} +
+
+
+ ); + } +} diff --git a/webapp/i18n/en.json b/webapp/i18n/en.json index 4211ff13b8..f8d2c580e4 100644 --- a/webapp/i18n/en.json +++ b/webapp/i18n/en.json @@ -143,6 +143,7 @@ "admin.authentication.gitlab": "GitLab", "admin.authentication.oauth": "OAuth 2.0", "admin.authentication.saml": "SAML", + "admin.authentication.ldap": "AD/LDAP", "admin.banner.heading": "Note:", "admin.cluster.enableDescription": "When true, Mattermost will run in High Availability mode. Please see documentation to learn more about configuring High Availability for Mattermost.", "admin.cluster.enableTitle": "Enable High Availability Mode:", @@ -415,7 +416,7 @@ "admin.ldap.loginNameTitle": "Login Field Name:", "admin.ldap.maxPageSizeEx": "Ex \"2000\"", "admin.ldap.maxPageSizeHelpText": "The maximum number of users the Mattermost server will request from the LDAP server at one time. 0 is unlimited.", - "admin.ldap.maxPageSizeTitle": "Maximum Page Size", + "admin.ldap.maxPageSizeTitle": "Maximum Page Size:", "admin.ldap.nicknameAttrDesc": "(Optional) The attribute in the LDAP server that will be used to populate the nickname of users in Mattermost.", "admin.ldap.nicknameAttrEx": "Ex \"nickname\"", "admin.ldap.nicknameAttrTitle": "Nickname Attribute:", @@ -429,11 +430,13 @@ "admin.ldap.serverDesc": "The domain or IP address of LDAP server.", "admin.ldap.serverEx": "Ex \"10.0.0.23\"", "admin.ldap.serverTitle": "LDAP Server:", - "admin.ldap.skipCertificateVerification": "Skip Certificate Verification", + "admin.ldap.skipCertificateVerification": "Skip Certificate Verification:", "admin.ldap.skipCertificateVerificationDesc": "Skips the certificate verification step for TLS or STARTTLS connections. Not recommended for production environments where TLS is required. For testing only.", "admin.ldap.syncFailure": "Sync Failure: {error}", + "admin.ldap.testFailure": "LDAP Test Failure: {error}", + "admin.ldap.testHelpText": "Tests if the Mattermost server can connect to the LDAP server specified. See log file for more detailed error messages.", "admin.ldap.syncIntervalHelpText": "LDAP Synchronization updates Mattermost user information to reflect updates on the LDAP server. For example, when a user’s name changes on the LDAP server, the change updates in Mattermost when synchronization is performed. Accounts removed from or disabled in the LDAP server have their Mattermost accounts set to “Inactive” and have their account sessions revoked. Mattermost performs synchronization on the interval entered. For example, if 60 is entered, Mattermost synchronizes every 60 minutes.", - "admin.ldap.syncIntervalTitle": "Synchronization Interval (minutes)", + "admin.ldap.syncIntervalTitle": "Synchronization Interval (minutes):", "admin.ldap.syncNowHelpText": "Initiates an LDAP synchronization immediately.", "admin.ldap.sync_button": "LDAP Synchronize Now", "admin.ldap.uernameAttrDesc": "The attribute in the LDAP server that will be used to populate the username field in Mattermost. This may be the same as the ID Attribute.", @@ -684,7 +687,7 @@ "admin.sidebar.gitlab": "GitLab", "admin.sidebar.images": "Images", "admin.sidebar.integrations": "Integrations", - "admin.sidebar.ldap": "LDAP", + "admin.sidebar.ldap": "AD/LDAP", "admin.sidebar.legalAndSupport": "Legal and Support", "admin.sidebar.license": "Edition and License", "admin.sidebar.localization": "Localization",