summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--src/assets/styles/_form-components.scss2
-rw-r--r--src/components/AppNavigation/AppNavigation.vue2
-rw-r--r--src/locales/en-US.json31
-rw-r--r--src/main.js2
-rw-r--r--src/router/index.js8
-rw-r--r--src/store/api.js4
-rw-r--r--src/store/index.js4
-rw-r--r--src/store/modules/AccessControl/SslCertificatesStore.js158
-rw-r--r--src/views/AccessControl/SslCertificates/ModalUploadCertificate.vue164
-rw-r--r--src/views/AccessControl/SslCertificates/SslCertificates.vue209
-rw-r--r--src/views/AccessControl/SslCertificates/index.js2
11 files changed, 581 insertions, 5 deletions
diff --git a/src/assets/styles/_form-components.scss b/src/assets/styles/_form-components.scss
index d9ae9d40..e7a7b0c9 100644
--- a/src/assets/styles/_form-components.scss
+++ b/src/assets/styles/_form-components.scss
@@ -34,4 +34,4 @@
color: $gray-700!important;
}
}
-}
+} \ No newline at end of file
diff --git a/src/components/AppNavigation/AppNavigation.vue b/src/components/AppNavigation/AppNavigation.vue
index d0fee43f..f2f049b3 100644
--- a/src/components/AppNavigation/AppNavigation.vue
+++ b/src/components/AppNavigation/AppNavigation.vue
@@ -81,7 +81,7 @@
<b-nav-item to="/access-control/local-user-management">
{{ $t('appNavigation.localUserManagement') }}
</b-nav-item>
- <b-nav-item href="javascript:void(0)">
+ <b-nav-item to="/access-control/ssl-certificates">
{{ $t('appNavigation.sslCertificates') }}
</b-nav-item>
</b-collapse>
diff --git a/src/locales/en-US.json b/src/locales/en-US.json
index 19b20824..d1d5f615 100644
--- a/src/locales/en-US.json
+++ b/src/locales/en-US.json
@@ -1,11 +1,13 @@
{
"global": {
"action": {
+ "add": "Add",
"confirm": "Confirm",
"cancel": "Cancel",
"delete": "Delete",
"disable": "Disable",
"enable": "Enable",
+ "replace": "Replace",
"save": "Save",
"selected": "Selected"
},
@@ -17,6 +19,7 @@
"invalidFormat": "Invalid format",
"lengthMustBeBetween": "Length must be between %{min} – %{max} characters",
"mustBeAtLeast": "Must be at least %{value}",
+ "required": "Required",
"selectAnOption": "Select an option",
"valueMustBeBetween": "Value must be between %{min} – %{max}"
},
@@ -227,5 +230,33 @@
"errorSaveSettings": "Error saving settings.",
"successSaveSettings": "Successfully saved settings."
}
+ },
+ "pageSslCertificates": {
+ "addNewCertificate": "Add new certificate",
+ "caCertificate": "CA Certificate",
+ "deleteCertificate": "Delete certificate",
+ "httpsCertificate": "HTTPS Certificate",
+ "ldapCertificate": "LDAP Certificate",
+ "replaceCertificate": "Replace certificate",
+ "modal": {
+ "certificateType": "Certificate type",
+ "certificateFile": "Certificate file",
+ "deleteConfirmMessage": "Are you sure you want to delete '%{certificate}' issued by %{issuedBy}? This action cannot be undone."
+ },
+ "table": {
+ "certificate": "Certificate",
+ "issuedBy": "Issued by",
+ "issuedTo": "Issued to",
+ "validFrom": "Valid from",
+ "validUntil": "Valid until"
+ },
+ "toast": {
+ "errorAddCertificate": "Error adding certificate.",
+ "errorDeleteCertificate": "Error deleting certificate.",
+ "errorReplaceCertificate": "Error replacing certificate.",
+ "successAddCertificate": "Successfully added %{certificate}.",
+ "successDeleteCertificate": "Successfully deleted %{certificate}.",
+ "successReplaceCertificate": "Successfully replaced %{certificate}."
+ }
}
} \ No newline at end of file
diff --git a/src/main.js b/src/main.js
index ab1f2967..17fc50fa 100644
--- a/src/main.js
+++ b/src/main.js
@@ -10,6 +10,7 @@ import {
CollapsePlugin,
FormPlugin,
FormCheckboxPlugin,
+ FormFilePlugin,
FormGroupPlugin,
FormInputPlugin,
FormRadioPlugin,
@@ -64,6 +65,7 @@ Vue.use(BVConfigPlugin, {
Vue.use(CollapsePlugin);
Vue.use(FormPlugin);
Vue.use(FormCheckboxPlugin);
+Vue.use(FormFilePlugin);
Vue.use(FormGroupPlugin);
Vue.use(FormInputPlugin);
Vue.use(FormRadioPlugin);
diff --git a/src/router/index.js b/src/router/index.js
index cd6cf8bf..2af53ea6 100644
--- a/src/router/index.js
+++ b/src/router/index.js
@@ -39,6 +39,14 @@ const routes = [
}
},
{
+ path: '/access-control/ssl-certificates',
+ name: 'ssl-certificates',
+ component: () => import('@/views/AccessControl/SslCertificates'),
+ meta: {
+ title: 'appPageTitle.sslCertificates'
+ }
+ },
+ {
path: '/control/reboot-bmc',
name: 'reboot-bmc',
component: () => import('@/views/Control/RebootBmc'),
diff --git a/src/store/api.js b/src/store/api.js
index 8fdbdd2f..24a38e4b 100644
--- a/src/store/api.js
+++ b/src/store/api.js
@@ -29,8 +29,8 @@ export default {
delete(path, payload) {
return api.delete(path, payload);
},
- post(path, payload) {
- return api.post(path, payload);
+ post(path, payload, config) {
+ return api.post(path, payload, config);
},
patch(path, payload) {
return api.patch(path, payload);
diff --git a/src/store/index.js b/src/store/index.js
index 08ada05e..0180213d 100644
--- a/src/store/index.js
+++ b/src/store/index.js
@@ -4,6 +4,7 @@ import Vuex from 'vuex';
import GlobalStore from './modules/GlobalStore';
import AuthenticationStore from './modules/Authentication/AuthenticanStore';
import LocalUserManagementStore from './modules/AccessControl/LocalUserMangementStore';
+import SslCertificatesStore from './modules/AccessControl/SslCertificatesStore';
import OverviewStore from './modules/Overview/OverviewStore';
import FirmwareStore from './modules/Configuration/FirmwareStore';
import BootSettingsStore from './modules/Control/BootSettingsStore';
@@ -32,7 +33,8 @@ export default new Vuex.Store({
powerControl: PowerControlStore,
networkSettings: NetworkSettingStore,
eventLog: EventLogStore,
- sensors: SensorsStore
+ sensors: SensorsStore,
+ sslCertificates: SslCertificatesStore
},
plugins: [WebSocketPlugin]
});
diff --git a/src/store/modules/AccessControl/SslCertificatesStore.js b/src/store/modules/AccessControl/SslCertificatesStore.js
new file mode 100644
index 00000000..e1758d3c
--- /dev/null
+++ b/src/store/modules/AccessControl/SslCertificatesStore.js
@@ -0,0 +1,158 @@
+import api from '../../api';
+import i18n from '../../../i18n';
+
+const CERTIFICATE_TYPES = [
+ {
+ type: 'HTTPS Certificate',
+ location: '/redfish/v1/Managers/bmc/NetworkProtocol/HTTPS/Certificates/',
+ label: i18n.t('pageSslCertificates.httpsCertificate')
+ },
+ {
+ type: 'LDAP Certificate',
+ location: '/redfish/v1/AccountService/LDAP/Certificates/',
+ label: i18n.t('pageSslCertificates.ldapCertificate')
+ },
+ {
+ type: 'TrustStore Certificate',
+ location: '/redfish/v1/Managers/bmc/Truststore/Certificates/',
+ // Web UI will show 'CA Certificate' instead of
+ // 'TrustStore Certificate' after user testing revealed
+ // the term 'TrustStore Certificate' wasn't recognized/was unfamilar
+ label: i18n.t('pageSslCertificates.caCertificate')
+ }
+];
+
+const getCertificateProp = (type, prop) => {
+ const certificate = CERTIFICATE_TYPES.find(
+ certificate => certificate.type === type
+ );
+ return certificate ? certificate[prop] : null;
+};
+
+const SslCertificatesStore = {
+ namespaced: true,
+ state: {
+ allCertificates: [],
+ availableUploadTypes: []
+ },
+ getters: {
+ allCertificates: state => state.allCertificates,
+ availableUploadTypes: state => state.availableUploadTypes
+ },
+ mutations: {
+ setCertificates(state, certificates) {
+ state.allCertificates = certificates;
+ },
+ setAvailableUploadTypes(state, availableUploadTypes) {
+ state.availableUploadTypes = availableUploadTypes;
+ }
+ },
+ actions: {
+ getCertificates({ commit }) {
+ api
+ .get('/redfish/v1/CertificateService/CertificateLocations')
+ .then(({ data: { Links: { Certificates } } }) =>
+ Certificates.map(certificate => certificate['@odata.id'])
+ )
+ .then(certificateLocations => {
+ const promises = certificateLocations.map(location =>
+ api.get(location)
+ );
+ api.all(promises).then(
+ api.spread((...responses) => {
+ const certificates = responses.map(({ data }) => {
+ const {
+ Name,
+ ValidNotAfter,
+ ValidNotBefore,
+ Issuer = {},
+ Subject = {}
+ } = data;
+ return {
+ type: Name,
+ location: data['@odata.id'],
+ certificate: getCertificateProp(Name, 'label'),
+ issuedBy: Issuer.CommonName,
+ issuedTo: Subject.CommonName,
+ validFrom: new Date(ValidNotBefore),
+ validUntil: new Date(ValidNotAfter)
+ };
+ });
+ const availableUploadTypes = CERTIFICATE_TYPES.filter(
+ ({ type }) =>
+ !certificates
+ .map(certificate => certificate.type)
+ .includes(type)
+ );
+
+ commit('setCertificates', certificates);
+ commit('setAvailableUploadTypes', availableUploadTypes);
+ })
+ );
+ });
+ },
+ async addNewCertificate({ dispatch }, { file, type }) {
+ return await api
+ .post(getCertificateProp(type, 'location'), file, {
+ headers: { 'Content-Type': 'application/x-pem-file' }
+ })
+ .then(() => dispatch('getCertificates'))
+ .then(() =>
+ i18n.t('pageSslCertificates.toast.successAddCertificate', {
+ certificate: getCertificateProp(type, 'label')
+ })
+ )
+ .catch(error => {
+ console.log(error);
+ throw new Error(
+ i18n.t('pageSslCertificates.toast.errorAddCertificate')
+ );
+ });
+ },
+ async replaceCertificate(
+ { dispatch },
+ { certificateString, location, type }
+ ) {
+ const data = {};
+ data.CertificateString = certificateString;
+ data.CertificateType = 'PEM';
+ data.CertificateUri = { '@odata.id': location };
+
+ return await api
+ .post(
+ '/redfish/v1/CertificateService/Actions/CertificateService.ReplaceCertificate',
+ data
+ )
+ .then(() => dispatch('getCertificates'))
+ .then(() =>
+ i18n.t('pageSslCertificates.toast.successReplaceCertificate', {
+ certificate: getCertificateProp(type, 'label')
+ })
+ )
+ .catch(error => {
+ console.log(error);
+ throw new Error(
+ i18n.t('pageSslCertificates.toast.errorReplaceCertificate')
+ );
+ });
+ },
+ async deleteCertificate({ dispatch }, { type, location }) {
+ return await api
+ .delete(location)
+ .then(() => dispatch('getCertificates'))
+ .then(() =>
+ i18n.t('pageSslCertificates.toast.successDeleteCertificate', {
+ certificate: getCertificateProp(type, 'label')
+ })
+ )
+ .catch(error => {
+ console.log(error);
+ throw new Error(
+ i18n.t('pageSslCertificates.toast.errorDeleteCertificate')
+ );
+ });
+ }
+ }
+};
+
+export default SslCertificatesStore;
diff --git a/src/views/AccessControl/SslCertificates/ModalUploadCertificate.vue b/src/views/AccessControl/SslCertificates/ModalUploadCertificate.vue
new file mode 100644
index 00000000..653a232f
--- /dev/null
+++ b/src/views/AccessControl/SslCertificates/ModalUploadCertificate.vue
@@ -0,0 +1,164 @@
+<template>
+ <b-modal id="upload-certificate" ref="modal" @ok="onOk" @hidden="resetForm">
+ <template v-slot:modal-title>
+ <template v-if="certificate">
+ {{ $t('pageSslCertificates.replaceCertificate') }}
+ </template>
+ <template v-else>
+ {{ $t('pageSslCertificates.addNewCertificate') }}
+ </template>
+ </template>
+ <b-form>
+ <!-- Replace Certificate type -->
+ <template v-if="certificate !== null">
+ <dl class="mb-4">
+ <dt>{{ $t('pageSslCertificates.modal.certificateType') }}</dt>
+ <dd>{{ certificate.certificate }}</dd>
+ </dl>
+ </template>
+
+ <!-- Add new Certificate type -->
+ <template v-else>
+ <b-form-group
+ :label="$t('pageSslCertificates.modal.certificateType')"
+ label-for="certificate-type"
+ >
+ <b-form-select
+ id="certificate-type"
+ v-model="form.certificateType"
+ :options="certificateOptions"
+ :state="getValidationState($v.form.certificateType)"
+ @input="$v.form.certificateType.$touch()"
+ >
+ </b-form-select>
+ <b-form-invalid-feedback role="alert">
+ <template v-if="!$v.form.certificateType.required">
+ {{ $t('global.form.fieldRequired') }}
+ </template>
+ </b-form-invalid-feedback>
+ </b-form-group>
+ </template>
+
+ <b-form-group
+ :label="$t('pageSslCertificates.modal.certificateFile')"
+ label-for="certificate-file"
+ >
+ <b-form-file
+ id="certificate-file"
+ v-model="form.file"
+ accept=".pem"
+ plain
+ :state="getValidationState($v.form.file)"
+ />
+ <b-form-invalid-feedback role="alert">
+ <template v-if="!$v.form.file.required">
+ {{ $t('global.form.required') }}
+ </template>
+ </b-form-invalid-feedback>
+ </b-form-group>
+ </b-form>
+ <template v-slot:modal-ok>
+ <template v-if="certificate">
+ {{ $t('global.action.replace') }}
+ </template>
+ <template v-else>
+ {{ $t('global.action.add') }}
+ </template>
+ </template>
+ </b-modal>
+</template>
+
+<script>
+import { required, requiredIf } from 'vuelidate/lib/validators';
+import VuelidateMixin from '../../../components/Mixins/VuelidateMixin.js';
+
+export default {
+ mixins: [VuelidateMixin],
+ props: {
+ certificate: {
+ type: Object,
+ default: null,
+ validator: prop => {
+ if (prop === null) return true;
+ return (
+ prop.hasOwnProperty('type') && prop.hasOwnProperty('certificate')
+ );
+ }
+ }
+ },
+ data() {
+ return {
+ form: {
+ certificateType: null,
+ file: null
+ }
+ };
+ },
+ computed: {
+ certificateTypes() {
+ return this.$store.getters['sslCertificates/availableUploadTypes'];
+ },
+ certificateOptions() {
+ return this.certificateTypes.map(({ type, label }) => {
+ return {
+ text: label,
+ value: type
+ };
+ });
+ }
+ },
+ watch: {
+ certificateOptions: function(options) {
+ if (options.length) {
+ this.form.certificateType = options[0].value;
+ }
+ }
+ },
+ validations() {
+ return {
+ form: {
+ certificateType: {
+ required: requiredIf(function() {
+ return !this.certificate;
+ })
+ },
+ file: {
+ required
+ }
+ }
+ };
+ },
+ methods: {
+ handleSubmit() {
+ this.$v.$touch();
+ if (this.$v.$invalid) return;
+ this.$emit('ok', {
+ addNew: !this.certificate,
+ file: this.form.file,
+ location: this.certificate ? this.certificate.location : null,
+ type: this.certificate
+ ? this.certificate.type
+ : this.form.certificateType
+ });
+ this.closeModal();
+ },
+ closeModal() {
+ this.$nextTick(() => {
+ this.$refs.modal.hide();
+ });
+ },
+ resetForm() {
+ this.form.certificateType = this.certificateOptions.length
+ ? this.certificateOptions[0].value
+ : null;
+ this.form.file = null;
+ this.$v.$reset();
+ },
+ onOk(bvModalEvt) {
+ // prevent modal close
+ bvModalEvt.preventDefault();
+ this.handleSubmit();
+ }
+ }
+};
+</script>
diff --git a/src/views/AccessControl/SslCertificates/SslCertificates.vue b/src/views/AccessControl/SslCertificates/SslCertificates.vue
new file mode 100644
index 00000000..ae28271f
--- /dev/null
+++ b/src/views/AccessControl/SslCertificates/SslCertificates.vue
@@ -0,0 +1,209 @@
+<template>
+ <b-container fluid>
+ <page-title />
+ <b-row>
+ <b-col xl="9" class="text-right">
+ <b-button
+ variant="primary"
+ :disabled="certificatesForUpload.length === 0"
+ @click="initModalUploadCertificate(null)"
+ >
+ <icon-add />
+ {{ $t('pageSslCertificates.addNewCertificate') }}
+ </b-button>
+ </b-col>
+ </b-row>
+ <b-row>
+ <b-col xl="9">
+ <b-table :fields="fields" :items="tableItems">
+ <template v-slot:cell(validFrom)="{ value }">
+ {{ value | formatDate }}
+ </template>
+
+ <template v-slot:cell(validUntil)="{ value }">
+ {{ value | formatDate }}
+ </template>
+
+ <template v-slot:cell(actions)="{ value, item }">
+ <table-row-action
+ v-for="(action, index) in value"
+ :key="index"
+ :value="action.value"
+ :title="action.title"
+ :enabled="action.enabled"
+ @click:tableAction="onTableRowAction($event, item)"
+ >
+ <template v-slot:icon>
+ <icon-replace v-if="action.value === 'replace'" />
+ <icon-trashcan v-if="action.value === 'delete'" />
+ </template>
+ </table-row-action>
+ </template>
+ </b-table>
+ </b-col>
+ </b-row>
+
+ <!-- Modals -->
+ <modal-upload-certificate :certificate="modalCertificate" @ok="onModalOk" />
+ </b-container>
+</template>
+
+<script>
+import IconAdd from '@carbon/icons-vue/es/add--alt/20';
+import IconReplace from '@carbon/icons-vue/es/renew/20';
+import IconTrashcan from '@carbon/icons-vue/es/trash-can/20';
+
+import ModalUploadCertificate from './ModalUploadCertificate';
+import PageTitle from '../../../components/Global/PageTitle';
+import TableRowAction from '../../../components/Global/TableRowAction';
+
+import BVToastMixin from '../../../components/Mixins/BVToastMixin';
+
+export default {
+ name: 'SslCertificates',
+ components: {
+ IconAdd,
+ IconReplace,
+ IconTrashcan,
+ ModalUploadCertificate,
+ PageTitle,
+ TableRowAction
+ },
+ mixins: [BVToastMixin],
+ data() {
+ return {
+ modalCertificate: null,
+ fields: [
+ {
+ key: 'certificate',
+ label: this.$t('pageSslCertificates.table.certificate')
+ },
+ {
+ key: 'issuedBy',
+ label: this.$t('pageSslCertificates.table.issuedBy')
+ },
+ {
+ key: 'issuedTo',
+ label: this.$t('pageSslCertificates.table.issuedTo')
+ },
+ {
+ key: 'validFrom',
+ label: this.$t('pageSslCertificates.table.validFrom')
+ },
+ {
+ key: 'validUntil',
+ label: this.$t('pageSslCertificates.table.validUntil')
+ },
+ {
+ key: 'actions',
+ label: '',
+ tdClass: 'text-right'
+ }
+ ]
+ };
+ },
+ computed: {
+ certificates() {
+ return this.$store.getters['sslCertificates/allCertificates'];
+ },
+ tableItems() {
+ return this.certificates.map(certificate => {
+ return {
+ ...certificate,
+ actions: [
+ {
+ value: 'replace',
+ title: this.$t('pageSslCertificates.replaceCertificate')
+ },
+ {
+ value: 'delete',
+ title: this.$t('pageSslCertificates.deleteCertificate'),
+ enabled:
+ certificate.type === 'TrustStore Certificate' ? true : false
+ }
+ ]
+ };
+ });
+ },
+ certificatesForUpload() {
+ return this.$store.getters['sslCertificates/availableUploadTypes'];
+ }
+ },
+ created() {
+ this.$store.dispatch('sslCertificates/getCertificates');
+ },
+ methods: {
+ onTableRowAction(event, rowItem) {
+ switch (event) {
+ case 'replace':
+ this.initModalUploadCertificate(rowItem);
+ break;
+ case 'delete':
+ this.initModalDeleteCertificate(rowItem);
+ break;
+ default:
+ break;
+ }
+ },
+ initModalUploadCertificate(certificate = null) {
+ this.modalCertificate = certificate;
+ this.$bvModal.show('upload-certificate');
+ },
+ initModalDeleteCertificate(certificate) {
+ this.$bvModal
+ .msgBoxConfirm(
+ this.$t('pageSslCertificates.modal.deleteConfirmMessage', {
+ issuedBy: certificate.issuedBy,
+ certificate: certificate.certificate
+ }),
+ {
+ title: this.$t('pageSslCertificates.deleteCertificate'),
+ okTitle: this.$t('global.action.delete')
+ }
+ )
+ .then(deleteConfirmed => {
+ if (deleteConfirmed) this.deleteCertificate(certificate);
+ });
+ },
+ onModalOk({ addNew, file, type, location }) {
+ if (addNew) {
+ // Upload a new certificate
+ this.addNewCertificate(file, type);
+ } else {
+ // Replace an existing certificate
+ this.replaceCertificate(file, type, location);
+ }
+ },
+ addNewCertificate(file, type) {
+ this.$store
+ .dispatch('sslCertificates/addNewCertificate', { file, type })
+ .then(success => this.successToast(success))
+ .catch(({ message }) => this.errorToast(message));
+ },
+ replaceCertificate(file, type, location) {
+ const reader = new FileReader();
+ reader.readAsBinaryString(file);
+ reader.onloadend = event => {
+ const certificateString = event.target.result;
+ this.$store
+ .dispatch('sslCertificates/replaceCertificate', {
+ certificateString,
+ type,
+ location
+ })
+ .then(success => this.successToast(success))
+ .catch(({ message }) => this.errorToast(message));
+ };
+ },
+ deleteCertificate({ type, location }) {
+ this.$store
+ .dispatch('sslCertificates/deleteCertificate', {
+ type,
+ location
+ })
+ .then(success => this.successToast(success))
+ .catch(({ message }) => this.errorToast(message));
+ }
+ }
+};
+</script>
diff --git a/src/views/AccessControl/SslCertificates/index.js b/src/views/AccessControl/SslCertificates/index.js
new file mode 100644
index 00000000..03daa565
--- /dev/null
+++ b/src/views/AccessControl/SslCertificates/index.js
@@ -0,0 +1,2 @@
+import SslCertificates from './SslCertificates.vue';
+export default SslCertificates;