diff options
author | Yoshie Muranaka <yoshiemuranaka@gmail.com> | 2020-04-09 22:41:27 +0300 |
---|---|---|
committer | Yoshie Muranaka <yoshiemuranaka@gmail.com> | 2020-04-25 01:48:35 +0300 |
commit | c4e38abf5c31d77474d5287620d1ddc8089b6dae (patch) | |
tree | 9f606da745a8f8f30db41f9794c925cc9590fb37 | |
parent | 9e36f522b94511c1d77335493c09e68975db031c (diff) | |
download | webui-vue-c4e38abf5c31d77474d5287620d1ddc8089b6dae.tar.xz |
Add LDAP page
Adds ability to enable LDAP service and modify LDAP and
ActiveDirectory properties.
Signed-off-by: Yoshie Muranaka <yoshiemuranaka@gmail.com>
Change-Id: I59d65bba7f6fe321af395227ce2f7188d9c006b7
-rw-r--r-- | src/assets/styles/_form-components.scss | 4 | ||||
-rw-r--r-- | src/components/AppNavigation/AppNavigation.vue | 2 | ||||
-rw-r--r-- | src/locales/en-US.json | 31 | ||||
-rw-r--r-- | src/main.js | 2 | ||||
-rw-r--r-- | src/router/index.js | 8 | ||||
-rw-r--r-- | src/store/index.js | 2 | ||||
-rw-r--r-- | src/store/modules/AccessControl/LdapStore.js | 156 | ||||
-rw-r--r-- | src/views/AccessControl/Ldap/Ldap.vue | 399 | ||||
-rw-r--r-- | src/views/AccessControl/Ldap/index.js | 2 |
9 files changed, 605 insertions, 1 deletions
diff --git a/src/assets/styles/_form-components.scss b/src/assets/styles/_form-components.scss index d1fe7854..8d3ed9e4 100644 --- a/src/assets/styles/_form-components.scss +++ b/src/assets/styles/_form-components.scss @@ -49,4 +49,8 @@ color: $primary; fill: currentColor; } +} + +.form-background { + background-color: $container-bgd; }
\ No newline at end of file diff --git a/src/components/AppNavigation/AppNavigation.vue b/src/components/AppNavigation/AppNavigation.vue index 94076de3..2f56c28f 100644 --- a/src/components/AppNavigation/AppNavigation.vue +++ b/src/components/AppNavigation/AppNavigation.vue @@ -75,7 +75,7 @@ <icon-expand class="icon-expand" /> </b-button> <b-collapse id="access-control-menu" tag="ul" class="nav-item__nav"> - <b-nav-item href="javascript:void(0)"> + <b-nav-item to="/access-control/ldap"> {{ $t('appNavigation.ldap') }} </b-nav-item> <b-nav-item to="/access-control/local-user-management"> diff --git a/src/locales/en-US.json b/src/locales/en-US.json index 022eefbf..d44cc072 100644 --- a/src/locales/en-US.json +++ b/src/locales/en-US.json @@ -13,6 +13,7 @@ "filter": "Filter", "replace": "Replace", "save": "Save", + "saveSettings": "Save settings", "selected": "Selected" }, "ariaLabel": { @@ -89,6 +90,36 @@ "sslCertificates": "SSL Certificates", "unauthorized": "Unauthorized" }, + "pageLdap": { + "pageDescription": "Configure LDAP settings and manage role groups", + "settings": "Settings", + "ariaLabel": { + "ldapSettings": "LDAP settings" + }, + "form": { + "baseDn": "Base DN", + "bindDn": "Bind DN", + "bindPassword": "Bind password", + "caCertificateValidUntil": "CA Certificate valid until", + "groupIdAttribute": "Group ID attribute", + "ldapAuthentication": "LDAP authentication", + "ldapAuthenticationHelper": "Must be enabled to modify role groups", + "ldapCertificateValidUntil": "LDAP Certificate valid until", + "manageSslCertificates": "Manage SSL certificates", + "secureLdapHelper": "A CA certificate and an LDAP certificate are required to enable secure LDAP", + "secureLdapUsingSsl": "Secure LDAP using SSL", + "serverUri": "Server URI", + "serverUriTooltip": "Enabling Secure LDAP changes URI scheme to ldaps", + "serviceType": "Service type", + "userIdAttribute": "User ID attribute" + }, + "toast": { + "errorSaveActiveDirectorySettings": "Error saving Active Directory settings.", + "errorSaveLdapSettings": "Error saving Open LDAP settings.", + "successSaveActiveDirectorySettings": "Successfully saved Active Directory settings.", + "successSaveLdapSettings": "Successfully saved Open LDAP settings." + } + }, "pageLocalUserManagement": { "accountPolicySettings": "Account policy settings", "addUser": "Add user", diff --git a/src/main.js b/src/main.js index 5adc5efc..b59afd9a 100644 --- a/src/main.js +++ b/src/main.js @@ -17,6 +17,7 @@ import { FormRadioPlugin, FormSelectPlugin, FormTagsPlugin, + InputGroupPlugin, LayoutPlugin, LinkPlugin, ListGroupPlugin, @@ -82,6 +83,7 @@ Vue.use(FormInputPlugin); Vue.use(FormRadioPlugin); Vue.use(FormSelectPlugin); Vue.use(FormTagsPlugin); +Vue.use(InputGroupPlugin); Vue.use(LayoutPlugin); Vue.use(LayoutPlugin); Vue.use(LinkPlugin); diff --git a/src/router/index.js b/src/router/index.js index 2af53ea6..6e958342 100644 --- a/src/router/index.js +++ b/src/router/index.js @@ -31,6 +31,14 @@ const routes = [ } }, { + path: '/access-control/ldap', + name: 'ldap', + component: () => import('@/views/AccessControl/Ldap'), + meta: { + title: 'appPageTitle.ldap' + } + }, + { path: '/access-control/local-user-management', name: 'local-users', component: () => import('@/views/AccessControl/LocalUserManagement'), diff --git a/src/store/index.js b/src/store/index.js index 0180213d..364e16c0 100644 --- a/src/store/index.js +++ b/src/store/index.js @@ -3,6 +3,7 @@ import Vuex from 'vuex'; import GlobalStore from './modules/GlobalStore'; import AuthenticationStore from './modules/Authentication/AuthenticanStore'; +import LdapStore from './modules/AccessControl/LdapStore'; import LocalUserManagementStore from './modules/AccessControl/LocalUserMangementStore'; import SslCertificatesStore from './modules/AccessControl/SslCertificatesStore'; import OverviewStore from './modules/Overview/OverviewStore'; @@ -25,6 +26,7 @@ export default new Vuex.Store({ modules: { global: GlobalStore, authentication: AuthenticationStore, + ldap: LdapStore, localUsers: LocalUserManagementStore, overview: OverviewStore, firmware: FirmwareStore, diff --git a/src/store/modules/AccessControl/LdapStore.js b/src/store/modules/AccessControl/LdapStore.js new file mode 100644 index 00000000..54fbbccd --- /dev/null +++ b/src/store/modules/AccessControl/LdapStore.js @@ -0,0 +1,156 @@ +import api from '@/store/api'; +import i18n from '@/i18n'; + +const LdapStore = { + namespaced: true, + state: { + isServiceEnabled: null, + ldap: { + serviceEnabled: null, + serviceAddress: null, + bindDn: null, + baseDn: null, + userAttribute: null, + groupsAttribute: null + }, + activeDirectory: { + serviceEnabled: null, + serviceAddress: null, + bindDn: null, + baseDn: null, + userAttribute: null, + groupsAttribute: null + } + }, + getters: { + isServiceEnabled: state => state.isServiceEnabled, + ldap: state => state.ldap, + activeDirectory: state => state.activeDirectory + }, + mutations: { + setServiceEnabled: (state, serviceEnabled) => + (state.isServiceEnabled = serviceEnabled), + setLdapProperties: ( + state, + { + ServiceEnabled, + ServiceAddresses, + Authentication = {}, + LDAPService: { SearchSettings = {} } = {} + } + ) => { + state.ldap.serviceAddress = ServiceAddresses[0]; + state.ldap.serviceEnabled = ServiceEnabled; + state.ldap.baseDn = SearchSettings.BaseDistinguishedNames[0]; + state.ldap.bindDn = Authentication.Username; + state.ldap.userAttribute = SearchSettings.UsernameAttribute; + state.ldap.groupsAttribute = SearchSettings.GroupsAttribute; + }, + setActiveDirectoryProperties: ( + state, + { + ServiceEnabled, + ServiceAddresses, + Authentication = {}, + LDAPService: { SearchSettings = {} } = {} + } + ) => { + state.activeDirectory.serviceEnabled = ServiceEnabled; + state.activeDirectory.serviceAddress = ServiceAddresses[0]; + state.activeDirectory.bindDn = Authentication.Username; + state.activeDirectory.baseDn = SearchSettings.BaseDistinguishedNames[0]; + state.activeDirectory.userAttribute = SearchSettings.UsernameAttribute; + state.activeDirectory.groupsAttribute = SearchSettings.GroupsAttribute; + } + }, + actions: { + getAccountSettings({ commit }) { + api + .get('/redfish/v1/AccountService') + .then(({ data: { LDAP = {}, ActiveDirectory = {} } }) => { + const ldapEnabled = LDAP.ServiceEnabled; + const activeDirectoryEnabled = ActiveDirectory.ServiceEnabled; + + commit('setServiceEnabled', ldapEnabled || activeDirectoryEnabled); + commit('setLdapProperties', LDAP); + commit('setActiveDirectoryProperties', ActiveDirectory); + }) + .catch(error => console.log(error)); + }, + async saveLdapSettings({ state, dispatch }, properties) { + const data = { LDAP: properties }; + if (state.activeDirectory.serviceEnabled) { + // Disable Active Directory service if enabled + await api.patch('/redfish/v1/AccountService', { + ActiveDirectory: { ServiceEnabled: false } + }); + } + return await api + .patch('/redfish/v1/AccountService', data) + .then(() => dispatch('getAccountSettings')) + .then(() => i18n.t('pageLdap.toast.successSaveLdapSettings')) + .catch(error => { + console.log(error); + throw new Error(i18n.t('pageLdap.toast.errorSaveLdapSettings')); + }); + }, + async saveActiveDirectorySettings({ state, dispatch }, properties) { + const data = { ActiveDirectory: properties }; + if (state.ldap.serviceEnabled) { + // Disable LDAP service if enabled + await api.patch('/redfish/v1/AccountService', { + LDAP: { ServiceEnabled: false } + }); + } + return await api + .patch('/redfish/v1/AccountService', data) + .then(() => dispatch('getAccountSettings')) + .then(() => i18n.t('pageLdap.toast.successSaveActiveDirectorySettings')) + .catch(error => { + console.log(error); + throw new Error( + i18n.t('pageLdap.toast.errorSaveActiveDirectorySettings') + ); + }); + }, + async saveAccountSettings( + { dispatch }, + { + serviceEnabled, + serviceAddress, + activeDirectoryEnabled, + bindDn, + bindPassword, + baseDn, + userIdAttribute, + groupIdAttribute + } + ) { + const data = { + ServiceEnabled: serviceEnabled, + ServiceAddresses: [serviceAddress], + Authentication: { + Username: bindDn, + Password: bindPassword + }, + LDAPService: { + SearchSettings: { + BaseDistinguishedNames: [baseDn] + } + } + }; + if (groupIdAttribute) + data.LDAPService.SearchSettings.GroupsAttribute = groupIdAttribute; + if (userIdAttribute) + data.LDAPService.SearchSettings.UsernameAttribute = userIdAttribute; + + if (activeDirectoryEnabled) { + return await dispatch('saveActiveDirectorySettings', data); + } else { + return await dispatch('saveLdapSettings', data); + } + } + } +}; + +export default LdapStore; diff --git a/src/views/AccessControl/Ldap/Ldap.vue b/src/views/AccessControl/Ldap/Ldap.vue new file mode 100644 index 00000000..c2d0e347 --- /dev/null +++ b/src/views/AccessControl/Ldap/Ldap.vue @@ -0,0 +1,399 @@ +<template> + <b-container fluid="xl"> + <page-title :description="$t('pageLdap.pageDescription')" /> + <page-section :section-title="$t('pageLdap.settings')"> + <b-form novalidate @submit.prevent="handleSubmit"> + <b-row> + <b-col> + <b-form-group + class="mb-3" + :label="$t('pageLdap.form.ldapAuthentication')" + > + <b-form-text id="enable-ldap-auth-help-block"> + {{ $t('pageLdap.form.ldapAuthenticationHelper') }} + </b-form-text> + <b-form-checkbox + id="enable-ldap-auth" + v-model="form.ldapAuthenticationEnabled" + aria-describedby="enable-ldap-auth-help-block" + @change="onChangeldapAuthenticationEnabled" + > + {{ $t('global.action.enable') }} + </b-form-checkbox> + </b-form-group> + </b-col> + </b-row> + <div class="form-background p-3"> + <b-form-group + class="m-0" + :label="$t('pageLdap.ariaLabel.ldapSettings')" + label-class="sr-only" + :disabled="!form.ldapAuthenticationEnabled" + > + <b-row> + <b-col md="3" lg="4" xl="3"> + <b-form-group + class="mb-4" + :label="$t('pageLdap.form.secureLdapUsingSsl')" + > + <b-form-text id="enable-secure-help-block"> + {{ $t('pageLdap.form.secureLdapHelper') }} + </b-form-text> + <b-form-checkbox + id="enable-secure-ldap" + v-model="form.secureLdapEnabled" + aria-describedby="enable-secure-help-block" + :disabled=" + !caCertificateExpiration || !ldapCertificateExpiration + " + @change="$v.form.secureLdapEnabled.$touch()" + > + {{ $t('global.action.enable') }} + </b-form-checkbox> + </b-form-group> + <dl> + <dt>{{ $t('pageLdap.form.caCertificateValidUntil') }}</dt> + <dd v-if="caCertificateExpiration"> + {{ caCertificateExpiration | formatDate }} + </dd> + <dd v-else>--</dd> + <dt>{{ $t('pageLdap.form.ldapCertificateValidUntil') }}</dt> + <dd v-if="ldapCertificateExpiration"> + {{ ldapCertificateExpiration | formatDate }} + </dd> + <dd v-else>--</dd> + </dl> + <b-link + class="d-inline-block mb-4 m-md-0" + to="/access-control/ssl-certificates" + > + {{ $t('pageLdap.form.manageSslCertificates') }} + </b-link> + </b-col> + <b-col md="9" lg="8" xl="9"> + <b-row> + <b-col> + <b-form-group :label="$t('pageLdap.form.serviceType')"> + <b-form-radio + v-model="form.activeDirectoryEnabled" + :value="false" + @change="onChangeServiceType" + > + OpenLDAP + </b-form-radio> + <b-form-radio + v-model="form.activeDirectoryEnabled" + :value="true" + @change="onChangeServiceType" + > + Active Directory + </b-form-radio> + </b-form-group> + </b-col> + </b-row> + <b-row> + <b-col sm="6" xl="4"> + <b-form-group label-for="server-uri"> + <template v-slot:label> + {{ $t('pageLdap.form.serverUri') }} + <info-tooltip + :title="$t('pageLdap.form.serverUriTooltip')" + /> + </template> + <b-input-group :prepend="ldapProtocol"> + <b-form-input + id="server-uri" + v-model="form.serverUri" + :state="getValidationState($v.form.serverUri)" + @change="$v.form.serverUri.$touch()" + /> + <b-form-invalid-feedback role="alert"> + {{ $t('global.form.fieldRequired') }} + </b-form-invalid-feedback> + </b-input-group> + </b-form-group> + </b-col> + <b-col sm="6" xl="4"> + <b-form-group + :label="$t('pageLdap.form.bindDn')" + label-for="bind-dn" + > + <b-form-input + id="bind-dn" + v-model="form.bindDn" + :state="getValidationState($v.form.bindDn)" + @change="$v.form.bindDn.$touch()" + /> + <b-form-invalid-feedback role="alert"> + {{ $t('global.form.fieldRequired') }} + </b-form-invalid-feedback> + </b-form-group> + </b-col> + <b-col sm="6" xl="4"> + <b-form-group + :label="$t('pageLdap.form.bindPassword')" + label-for="bind-password" + > + <input-password-toggle> + <b-form-input + id="bind-password" + v-model="form.bindPassword" + type="password" + :state="getValidationState($v.form.bindPassword)" + @change="$v.form.bindPassword.$touch()" + /> + <b-form-invalid-feedback role="alert"> + {{ $t('global.form.fieldRequired') }} + </b-form-invalid-feedback> + </input-password-toggle> + </b-form-group> + </b-col> + <b-col sm="6" xl="4"> + <b-form-group + :label="$t('pageLdap.form.baseDn')" + label-for="base-dn" + > + <b-form-input + id="base-dn" + v-model="form.baseDn" + :state="getValidationState($v.form.baseDn)" + @change="$v.form.baseDn.$touch()" + /> + <b-form-invalid-feedback role="alert"> + {{ $t('global.form.fieldRequired') }} + </b-form-invalid-feedback> + </b-form-group> + </b-col> + <b-col sm="6" xl="4"> + <b-form-group + :label="$t('pageLdap.form.userIdAttribute')" + label-for="user-id-attribute" + > + <b-form-input + id="user-id-attribute" + v-model="form.userIdAttribute" + @change="$v.form.userIdAttribute.$touch()" + /> + </b-form-group> + </b-col> + <b-col sm="6" xl="4"> + <b-form-group + :label="$t('pageLdap.form.groupIdAttribute')" + label-for="group-id-attribute" + > + <b-form-input + id="group-id-attribute" + v-model="form.groupIdAttribute" + @change="$v.form.groupIdAttribute.$touch()" + /> + </b-form-group> + </b-col> + </b-row> + </b-col> + </b-row> + </b-form-group> + </div> + <b-row class="mt-4"> + <b-col> + <b-btn + variant="primary" + type="submit" + :disabled="!$v.form.$anyDirty" + > + {{ $t('global.action.saveSettings') }} + </b-btn> + </b-col> + </b-row> + </b-form> + </page-section> + </b-container> +</template> + +<script> +import { mapGetters } from 'vuex'; +import { find } from 'lodash'; +import { requiredIf } from 'vuelidate/lib/validators'; + +import BVToastMixin from '@/components/Mixins/BVToastMixin.js'; +import VuelidateMixin from '@/components/Mixins/VuelidateMixin.js'; +import PageTitle from '@/components/Global/PageTitle'; +import PageSection from '@/components/Global/PageSection'; +import InfoTooltip from '@/components/Global/InfoTooltip'; +import InputPasswordToggle from '@/components/Global/InputPasswordToggle'; + +export default { + name: 'Ldap', + components: { InfoTooltip, InputPasswordToggle, PageTitle, PageSection }, + mixins: [BVToastMixin, VuelidateMixin], + data() { + return { + form: { + ldapAuthenticationEnabled: false, + secureLdapEnabled: false, + activeDirectoryEnabled: false, + serverUri: '', + bindDn: '', + bindPassword: '', + baseDn: '', + userIdAttribute: '', + groupIdAttribute: '' + } + }; + }, + computed: { + ...mapGetters('ldap', ['isServiceEnabled', 'ldap', 'activeDirectory']), + sslCertificates() { + return this.$store.getters['sslCertificates/allCertificates']; + }, + caCertificateExpiration() { + const caCertificate = find(this.sslCertificates, { + type: 'TrustStore Certificate' + }); + if (caCertificate === undefined) return null; + return caCertificate.validUntil; + }, + ldapCertificateExpiration() { + const ldapCertificate = find(this.sslCertificates, { + type: 'LDAP Certificate' + }); + if (ldapCertificate === undefined) return null; + return ldapCertificate.validUntil; + }, + ldapProtocol() { + return this.form.secureLdapEnabled ? 'ldaps://' : 'ldap://'; + } + }, + watch: { + isServiceEnabled: function(value) { + this.form.ldapAuthenticationEnabled = value; + }, + ldap: { + handler: function(value) { + if (value.serviceEnabled || !this.form.activeDirectoryEnabled) { + this.setFormValues(value); + } + }, + deep: true + }, + activeDirectory: { + handler: function(value) { + if (value.serviceEnabled) { + this.form.activeDirectoryEnabled = true; + this.setFormValues(value); + } + }, + deep: true + } + }, + validations: { + form: { + ldapAuthenticationEnabled: {}, + secureLdapEnabled: {}, + activeDirectoryEnabled: { + required: requiredIf(function() { + return this.form.ldapAuthenticationEnabled; + }) + }, + serverUri: { + required: requiredIf(function() { + return this.form.ldapAuthenticationEnabled; + }) + }, + bindDn: { + required: requiredIf(function() { + return this.form.ldapAuthenticationEnabled; + }) + }, + bindPassword: { + required: requiredIf(function() { + return this.form.ldapAuthenticationEnabled; + }) + }, + baseDn: { + required: requiredIf(function() { + return this.form.ldapAuthenticationEnabled; + }) + }, + userIdAttribute: {}, + groupIdAttribute: {} + } + }, + created() { + this.$store.dispatch('ldap/getAccountSettings'); + this.$store.dispatch('sslCertificates/getCertificates'); + if (this.form.activeDirectoryEnabled) { + this.setFormValues(this.activeDirectory); + } else { + this.setFormValues(this.ldap); + } + }, + methods: { + setFormValues({ + serviceAddress = '', + bindDn = '', + baseDn = '', + userAttribute = '', + groupsAttribute = '' + }) { + const secureLdap = + serviceAddress && serviceAddress.includes('ldaps://') ? true : false; + const serverUri = serviceAddress + ? serviceAddress.replace(/ldaps?:\/\//, '') + : ''; + this.form.secureLdapEnabled = secureLdap; + this.form.serverUri = serverUri; + this.form.bindDn = bindDn; + this.form.bindPassword = ''; + this.form.baseDn = baseDn; + this.form.userIdAttribute = userAttribute; + this.form.groupIdAttribute = groupsAttribute; + }, + handleSubmit() { + this.$v.$touch(); + if (this.$v.$invalid) return; + const data = { + serviceEnabled: this.form.ldapAuthenticationEnabled, + activeDirectoryEnabled: this.form.activeDirectoryEnabled, + serviceAddress: `${this.ldapProtocol}${this.form.serverUri}`, + bindDn: this.form.bindDn, + bindPassword: this.form.bindPassword, + baseDn: this.form.baseDn, + userIdAttribute: this.form.userIdAttribute, + groupIdAttribute: this.form.groupIdAttribute + }; + this.$store + .dispatch('ldap/saveAccountSettings', data) + .then(success => { + this.successToast(success); + this.$v.form.$reset(); + }) + .catch(({ message }) => this.errorToast(message)) + .finally(() => { + this.form.bindPassword = ''; + }); + }, + onChangeServiceType(isActiveDirectoryEnabled) { + this.$v.form.activeDirectoryEnabled.$touch(); + const serviceType = isActiveDirectoryEnabled + ? this.activeDirectory + : this.ldap; + this.setFormValues(serviceType); + }, + onChangeldapAuthenticationEnabled(isServiceEnabled) { + this.$v.form.ldapAuthenticationEnabled.$touch(); + if (!isServiceEnabled) { + // Request will fail if sent with empty values. + // The frontend only checks for required fields + // when the service is enabled. This is to prevent + // an error if a user clears any properties then + // disables the service. + if (this.form.activeDirectoryEnabled) { + this.setFormValues(this.activeDirectory); + } else { + this.setFormValues(this.ldap); + } + } + } + } +}; +</script> diff --git a/src/views/AccessControl/Ldap/index.js b/src/views/AccessControl/Ldap/index.js new file mode 100644 index 00000000..6ae3abfc --- /dev/null +++ b/src/views/AccessControl/Ldap/index.js @@ -0,0 +1,2 @@ +import Ldap from './Ldap.vue'; +export default Ldap; |