diff --git a/frontend/src/metabase/admin/settings/components/SettingsLdapForm.jsx b/frontend/src/metabase/admin/settings/components/SettingsLdapForm.jsx
new file mode 100644
index 0000000000000000000000000000000000000000..4791c2010f8155ee59dfd891107b32869c14fd1b
--- /dev/null
+++ b/frontend/src/metabase/admin/settings/components/SettingsLdapForm.jsx
@@ -0,0 +1,183 @@
+import React, { Component, PropTypes } from "react";
+import cx from "classnames";
+import _ from "underscore";
+
+import MetabaseUtils from "metabase/lib/utils";
+import SettingsSetting from "./SettingsSetting.jsx";
+
+export default class SettingsLdapForm extends Component {
+
+    constructor(props, context) {
+        super(props, context);
+
+        this.state = {
+            dirty: false,
+            formData: {},
+            submitting: "default",
+            valid: false,
+            validationErrors: {}
+        }
+    }
+
+    static propTypes = {
+        elements: PropTypes.array.isRequired,
+        formErrors: PropTypes.object,
+        updateLdapSettings: PropTypes.func.isRequired
+    };
+
+    componentWillMount() {
+        // this gives us an opportunity to load up our formData with any existing values for elements
+        let formData = {};
+        this.props.elements.forEach(function(element) {
+            formData[element.key] = element.value;
+        });
+
+        this.setState({formData});
+    }
+
+    componentDidMount() {
+        this.validateForm();
+    }
+
+    componentDidUpdate() {
+        this.validateForm();
+    }
+
+    setSubmitting(submitting) {
+        this.setState({submitting});
+    }
+
+    setFormErrors(formErrors) {
+        this.setState({formErrors});
+    }
+
+    // return null if element passes validation, otherwise return an error message
+    validateElement([validationType, validationMessage], value, element) {
+        if (MetabaseUtils.isEmpty(value)) return;
+
+        switch (validationType) {
+            case "integer":
+                return isNaN(parseInt(value)) ? (validationMessage || "That's not a valid integer") : null;
+        }
+    }
+
+    validateForm() {
+        let { elements } = this.props;
+        let { formData } = this.state;
+
+        let valid = true,
+            validationErrors = {};
+
+        elements.forEach(function(element) {
+            // test for required elements
+            if (element.required && MetabaseUtils.isEmpty(formData[element.key])) {
+                valid = false;
+            }
+
+            if (element.validations) {
+                element.validations.forEach(function(validation) {
+                    validationErrors[element.key] = this.validateElement(validation, formData[element.key], element);
+                    if (validationErrors[element.key]) valid = false;
+                }, this);
+            }
+        }, this);
+
+        if (this.state.valid !== valid || !_.isEqual(this.state.validationErrors, validationErrors)) {
+            this.setState({ valid, validationErrors });
+        }
+    }
+
+    handleChangeEvent(element, value, event) {
+        this.setState({
+            dirty: true,
+            formData: { ...this.state.formData, [element.key]: (MetabaseUtils.isEmpty(value)) ? null : value }
+        });
+    }
+
+    handleFormErrors(error) {
+        // parse and format
+        let formErrors = {};
+        if (error.data && error.data.message) {
+            formErrors.message = error.data.message;
+        } else {
+            formErrors.message = "Looks like we ran into some problems";
+        }
+
+        if (error.data && error.data.errors) {
+            formErrors.elements = error.data.errors;
+        }
+
+        return formErrors;
+    }
+
+    updateLdapSettings(e) {
+        e.preventDefault();
+
+        this.setState({
+            formErrors: null,
+            submitting: "working"
+        });
+
+        let { formData, valid } = this.state;
+
+        if (valid) {
+            this.props.updateLdapSettings(formData).then(() => {
+                this.setState({
+                    dirty: false,
+                    submitting: "success"
+                });
+
+                // show a confirmation for 3 seconds, then return to normal
+                setTimeout(() => this.setState({ submitting: "default" }), 3000);
+            }, (error) => {
+                this.setState({
+                    submitting: "default",
+                    formErrors: this.handleFormErrors(error)
+                });
+            });
+        }
+    }
+
+    render() {
+        let { elements } = this.props;
+        let { formData, formErrors, submitting, valid, validationErrors } = this.state;
+
+        let settings = elements.map((element, index) => {
+            // merge together data from a couple places to provide a complete view of the Element state
+            let errorMessage = (formErrors && formErrors.elements) ? formErrors.elements[element.key] : validationErrors[element.key];
+            let value = formData[element.key] == null ? element.defaultValue : formData[element.key];
+
+            return (
+                <SettingsSetting
+                    key={element.key}
+                    setting={{ ...element, value }}
+                    updateSetting={this.handleChangeEvent.bind(this, element)}
+                    errorMessage={errorMessage}
+                />
+            );
+        });
+
+        let saveSettingsButtonStates = {
+            default: "Save changes",
+            working: "Saving...",
+            success: "Changes saved!"
+        };
+
+        let disabled = (!valid || submitting !== "default"),
+            saveButtonText = saveSettingsButtonStates[submitting];
+
+        return (
+            <form noValidate>
+                <ul>
+                    {settings}
+                    <li className="m2 mb4">
+                        <button className={cx("Button mr2", {"Button--primary": !disabled}, {"Button--success-new": submitting === "success"})} disabled={disabled} onClick={this.updateLdapSettings.bind(this)}>
+                            {saveButtonText}
+                        </button>
+                        { formErrors && formErrors.message ? <span className="pl2 text-error text-bold">{formErrors.message}</span> : null }
+                    </li>
+                </ul>
+            </form>
+        );
+    }
+}
diff --git a/frontend/src/metabase/admin/settings/containers/SettingsEditorApp.jsx b/frontend/src/metabase/admin/settings/containers/SettingsEditorApp.jsx
index 83e121977c4634a7a59b361963bfa73e21636f57..08b1f8deb083688194929d24a6dbf28cca39670d 100644
--- a/frontend/src/metabase/admin/settings/containers/SettingsEditorApp.jsx
+++ b/frontend/src/metabase/admin/settings/containers/SettingsEditorApp.jsx
@@ -11,6 +11,7 @@ import AdminLayout from "metabase/components/AdminLayout.jsx";
 import SettingsSetting from "../components/SettingsSetting.jsx";
 import SettingsEmailForm from "../components/SettingsEmailForm.jsx";
 import SettingsSlackForm from "../components/SettingsSlackForm.jsx";
+import SettingsLdapForm from "../components/SettingsLdapForm.jsx";
 import SettingsSetupList from "../components/SettingsSetupList.jsx";
 import SettingsUpdatesForm from "../components/SettingsUpdatesForm.jsx";
 import SettingsSingleSignOnForm from "../components/SettingsSingleSignOnForm.jsx";
@@ -51,6 +52,7 @@ export default class SettingsEditorApp extends Component {
         updateSetting: PropTypes.func.isRequired,
         updateEmailSettings: PropTypes.func.isRequired,
         updateSlackSettings: PropTypes.func.isRequired,
+        updateLdapSettings: PropTypes.func.isRequired,
         sendTestEmail: PropTypes.func.isRequired
     };
 
@@ -136,8 +138,15 @@ export default class SettingsEditorApp extends Component {
                     updateSetting={this.updateSetting}
                 />
             );
+        } else if (activeSection.name === "LDAP") {
+            return (
+                <SettingsLdapForm
+                    ref="ldapForm"
+                    elements={activeSection.settings}
+                    updateLdapSettings={this.props.updateLdapSettings}
+                />
+            );
         } else {
-
             return (
                 <ul>
                     {activeSection.settings
diff --git a/frontend/src/metabase/admin/settings/selectors.js b/frontend/src/metabase/admin/settings/selectors.js
index c0bf672f6e863d624d7222e4c0aeebad2ba2d04c..8f6f5287565d1b8f51042b958e624136b4b55e2c 100644
--- a/frontend/src/metabase/admin/settings/selectors.js
+++ b/frontend/src/metabase/admin/settings/selectors.js
@@ -178,7 +178,6 @@ const SECTIONS = [
                 display_name: "LDAP Port",
                 placeholder: "389",
                 type: "string",
-                required: true,
                 validations: [["integer", "That's not a valid port number"]]
             },
             {
@@ -187,7 +186,7 @@ const SECTIONS = [
                 description: null,
                 type: "radio",
                 options: { none: "None", ssl: "SSL", starttls: "StartTLS" },
-                defaultValue: 'none'
+                defaultValue: "none"
             },
             {
                 key: "ldap-bind-dn",
@@ -210,26 +209,22 @@ const SECTIONS = [
             {
                 key: "ldap-user-filter",
                 display_name: "User filter",
-                type: "string",
-                required: true
+                type: "string"
             },
             {
                 key: "ldap-attribute-email",
                 display_name: "User email attribute",
-                type: "string",
-                required: true
+                type: "string"
             },
             {
                 key: "ldap-attribute-firstname",
                 display_name: "User first name attribute",
-                type: "string",
-                required: true
+                type: "string"
             },
             {
                 key: "ldap-attribute-lastname",
                 display_name: "User last name attribute",
-                type: "string",
-                required: true
+                type: "string"
             }
         ]
     },
diff --git a/frontend/src/metabase/admin/settings/settings.js b/frontend/src/metabase/admin/settings/settings.js
index cc63d4829c5fc1a0545b1fe181c2c951900b4cc2..9a27de73fa8314b552fb38663658d93981497ea5 100644
--- a/frontend/src/metabase/admin/settings/settings.js
+++ b/frontend/src/metabase/admin/settings/settings.js
@@ -1,7 +1,7 @@
 
 import { createThunkAction, handleActions, combineReducers } from "metabase/lib/redux";
 
-import { SettingsApi, EmailApi, SlackApi } from "metabase/services";
+import { SettingsApi, EmailApi, SlackApi, LdapApi } from "metabase/services";
 
 import { refreshSiteSettings } from "metabase/redux/settings";
 
@@ -71,6 +71,20 @@ export const updateSlackSettings = createThunkAction(UPDATE_SLACK_SETTINGS, func
     };
 }, {});
 
+// updateEmailSettings
+export const UPDATE_LDAP_SETTINGS = "metabase/admin/settings/UPDATE_LDAP_SETTINGS";
+export const updateLdapSettings = createThunkAction(UPDATE_LDAP_SETTINGS, function(settings) {
+    return async function(dispatch, getState) {
+        try {
+            await LdapApi.updateSettings(settings);
+            await dispatch(refreshSiteSettings());
+        } catch(error) {
+            console.log("error updating LDAP settings", settings, error);
+            throw error;
+        }
+    };
+});
+
 export const RELOAD_SETTINGS = "metabase/admin/settings/RELOAD_SETTINGS";
 export const reloadSettings = createThunkAction(RELOAD_SETTINGS, function() {
     return async function(dispatch, getState) {
diff --git a/frontend/src/metabase/services.js b/frontend/src/metabase/services.js
index 7705b6c16e9de928820cfaf621fab1616a4b815b..08b90259c62106126814fe5a1e61ace5657a736c 100644
--- a/frontend/src/metabase/services.js
+++ b/frontend/src/metabase/services.js
@@ -85,6 +85,10 @@ export const SlackApi = {
     updateSettings:              PUT("/api/slack/settings"),
 };
 
+export const LdapApi = {
+    updateSettings:              PUT("/api/ldap/settings")
+};
+
 export const MetabaseApi = {
     db_list:                     GET("/api/database"),
     db_list_with_tables:         GET("/api/database?include_tables=true"),