From 1622f09e747b2e993942404536181d42379cc6de Mon Sep 17 00:00:00 2001
From: Kianna <30884335+kiannaquach@users.noreply.github.com>
Date: Fri, 5 Jan 2024 12:47:49 -0800
Subject: [PATCH] UI: VAULT-21538 unauth endpoint message display (#24665)
* WIP unauth display
* Add modal custom message
* Close multiple modals
* Update todo with ticket number
* On init make custom message request
* Use serializer
* Update fetchMessages
* Add copyright headers
* Add services and serializers
* Send null instead of empty strings
* Fix tests!
* Add copywrite headers
* Add some acceptance tests
* Test cleanup
* Put tests back
* pass hooks to module
* Move module out
* Seperate tests
* Copywrite
* Add aria-prohibited-attr runList options
* Code cleanup
* Add date-time-local transform
* Add copyright headers
* Remove comments
* Remove date transform stuff for now!
* Put getISODateFormat back into the serailize function
---
ui/app/controllers/vault/cluster/auth.js | 2 +
ui/app/serializers/config-ui/message.js | 15 ++-
ui/app/services/custom-messages.js | 47 +++++++
ui/app/templates/components/auth-form.hbs | 1 -
ui/app/templates/vault/cluster/auth.hbs | 30 +++++
.../page/create-and-edit-message-form.hbs | 2 +-
ui/mirage/handlers/custom-messages.js | 46 ++++---
.../acceptance/custom-messages-auth-test.js | 115 ++++++++++++++++++
.../helpers/config-ui/message-selectors.js | 21 ++++
.../page/create-and-edit-message-test.js | 18 +--
.../mfa-login-enforcement-form-test.js | 1 +
.../serializers/config-ui/message-test.js | 72 +++++++++++
12 files changed, 331 insertions(+), 39 deletions(-)
create mode 100644 ui/app/services/custom-messages.js
create mode 100644 ui/tests/acceptance/custom-messages-auth-test.js
create mode 100644 ui/tests/helpers/config-ui/message-selectors.js
create mode 100644 ui/tests/unit/serializers/config-ui/message-test.js
diff --git a/ui/app/controllers/vault/cluster/auth.js b/ui/app/controllers/vault/cluster/auth.js
index dd35d2b63e7f..d6585f1455fd 100644
--- a/ui/app/controllers/vault/cluster/auth.js
+++ b/ui/app/controllers/vault/cluster/auth.js
@@ -17,6 +17,7 @@ export default Controller.extend({
version: service(),
auth: service(),
router: service(),
+ customMessages: service(),
queryParams: [{ authMethod: 'with', oidcProvider: 'o' }],
namespaceQueryParam: alias('clusterController.namespaceQueryParam'),
wrappedToken: alias('vaultController.wrappedToken'),
@@ -52,6 +53,7 @@ export default Controller.extend({
yield timeout(500);
const ns = this.fullNamespaceFromInput(value);
this.namespaceService.setNamespace(ns, true);
+ this.customMessages.fetchMessages(ns);
this.set('namespaceQueryParam', ns);
}).restartable(),
diff --git a/ui/app/serializers/config-ui/message.js b/ui/app/serializers/config-ui/message.js
index c6c1cc0db35d..7633cd7bd2d4 100644
--- a/ui/app/serializers/config-ui/message.js
+++ b/ui/app/serializers/config-ui/message.js
@@ -27,7 +27,6 @@ export default class MessageSerializer extends ApplicationSerializer {
return snapshotDateTime;
}
-
normalizeResponse(store, primaryModelClass, payload, id, requestType) {
if (requestType === 'queryRecord') {
const transformed = {
@@ -55,22 +54,26 @@ export default class MessageSerializer extends ApplicationSerializer {
// if this date is not an object and isn’t a local date string, then return the snapshot date, which is set by default
// values defined on the model.
json.start_time = this.getISODateFormat(snapshot.record.startTime, json.start_time);
- json.end_time = this.getISODateFormat(snapshot.record.endTime, json.end_time);
+ json.end_time = snapshot.record.endTime
+ ? this.getISODateFormat(snapshot.record.endTime, json.end_time)
+ : null;
delete json?.link_title;
delete json?.link_href;
return json;
}
- extractLazyPaginatedData(payload) {
+ mapPayload(payload) {
if (payload.data) {
if (payload.data?.keys && Array.isArray(payload.data.keys)) {
return payload.data.keys.map((key) => {
- return {
+ const data = {
id: key,
linkTitle: payload.data.key_info.link?.title,
linkHref: payload.data.key_info.link?.href,
...payload.data.key_info[key],
};
+ if (data.message) data.message = decodeString(data.message);
+ return data;
});
}
Object.assign(payload, payload.data);
@@ -78,4 +81,8 @@ export default class MessageSerializer extends ApplicationSerializer {
}
return payload;
}
+
+ extractLazyPaginatedData(payload) {
+ return this.mapPayload(payload);
+ }
}
diff --git a/ui/app/services/custom-messages.js b/ui/app/services/custom-messages.js
new file mode 100644
index 000000000000..07be4ef84130
--- /dev/null
+++ b/ui/app/services/custom-messages.js
@@ -0,0 +1,47 @@
+/**
+ * Copyright (c) HashiCorp, Inc.
+ * SPDX-License-Identifier: BUSL-1.1
+ */
+
+import Service, { inject as service } from '@ember/service';
+import { tracked } from '@glimmer/tracking';
+
+export default class CustomMessageService extends Service {
+ @service store;
+ @service namespace;
+ @tracked messages = [];
+ @tracked showMessageModal = true;
+
+ constructor() {
+ super(...arguments);
+ this.fetchMessages(this.namespace.path);
+ }
+
+ get bannerMessages() {
+ return this.messages?.filter((message) => message?.type === 'banner');
+ }
+
+ get modalMessages() {
+ return this.messages?.filter((message) => message?.type === 'modal');
+ }
+
+ async fetchMessages(ns) {
+ try {
+ const url = '/v1/sys/internal/ui/unauthenticated-messages';
+ const opts = {
+ method: 'GET',
+ headers: {},
+ };
+ if (ns) {
+ opts.headers['X-Vault-Namespace'] = ns;
+ }
+ const result = await fetch(url, opts);
+ const body = await result.json();
+ if (body.errors) return (this.messages = []);
+ const serializer = this.store.serializerFor('config-ui/message');
+ this.messages = serializer.mapPayload(body);
+ } catch (e) {
+ return e;
+ }
+ }
+}
diff --git a/ui/app/templates/components/auth-form.hbs b/ui/app/templates/components/auth-form.hbs
index 01ea4f760f57..d7c1c35e7ca8 100644
--- a/ui/app/templates/components/auth-form.hbs
+++ b/ui/app/templates/components/auth-form.hbs
@@ -2,7 +2,6 @@
Copyright (c) HashiCorp, Inc.
SPDX-License-Identifier: BUSL-1.1
~}}
-
{{#if (and this.waitingForOktaNumberChallenge (not this.cancelAuthForOktaNumberChallenge))}}
{{else}}
+ {{#if this.customMessages.bannerMessages.length}}
+ {{#each this.customMessages.bannerMessages as |bannerMessage|}}
+
+ {{bannerMessage.title}}
+
+ {{bannerMessage.message}}
+
+
+ {{! TODO: VAULT-22908 display links when api is updated to { link: { 'learn': 'www.learn.com'} } }}
+
+
+ {{/each}}
+ {{/if}}
+ {{#if this.customMessages.modalMessages.length}}
+ {{#each this.customMessages.modalMessages as |modalMessage|}}
+
+
+ {{modalMessage.title}}
+
+
+ {{modalMessage.message}}
+ {{! TODO: VAULT-22908 display links when api is updated to { link: { 'learn': 'www.learn.com'} } }}
+
+
+
+
+
+ {{/each}}
+ {{/if}}
+
<:header>
{{#if this.oidcProvider}}
diff --git a/ui/lib/config-ui/addon/components/messages/page/create-and-edit-message-form.hbs b/ui/lib/config-ui/addon/components/messages/page/create-and-edit-message-form.hbs
index 1a734e5af5aa..084af1489b36 100644
--- a/ui/lib/config-ui/addon/components/messages/page/create-and-edit-message-form.hbs
+++ b/ui/lib/config-ui/addon/components/messages/page/create-and-edit-message-form.hbs
@@ -97,7 +97,7 @@
{{@message.title}}
-
+
{{@message.message}}
{{#if @message.linkHref}}
diff --git a/ui/mirage/handlers/custom-messages.js b/ui/mirage/handlers/custom-messages.js
index 2591cb6f1896..3e273e770db7 100644
--- a/ui/mirage/handlers/custom-messages.js
+++ b/ui/mirage/handlers/custom-messages.js
@@ -92,31 +92,43 @@ export default function (server) {
server.get('/sys/internal/ui/unauthenticated-messages', () => {
return {
+ request_id: '664fbad0-fcd8-9023-4c5b-81a7962e9f4b',
+ lease_id: '',
+ renewable: false,
+ lease_duration: 0,
data: {
key_info: {
- '01234567-89ab-cdef-0123-456789abcdef': {
- title: 'Unauthenticated Title One',
- message:
- 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Curabitur nulla augue, placerat quis risus blandit, molestie imperdiet massa. Sed blandit rutrum odio quis varius. Fusce purus orci, maximus ac libero.',
- type: 'modal',
+ '02180e3f-bd5b-a851-bcc9-6f7983806df0': {
authenticated: false,
- start_time: '2023-10-15T02:36:43.986212308Z',
- end_time: '2024-10-15T02:36:43.986212308Z',
- options: {},
- },
- '76543210-89ab-cdef-0123-456789abcdef': {
- title: 'Unauthenticated Title Two',
- message:
- 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Curabitur nulla augue, placerat quis risus blandit, molestie imperdiet massa. Sed blandit rutrum odio quis varius. Fusce purus orci, maximus ac libero.',
+ end_time: null,
+ link: {
+ title: '',
+ },
+ message: 'aGVsbG8gd29ybGQgaGVsbG8gd29scmQ=',
+ options: null,
+ start_time: '2024-01-04T08:00:00Z',
+ title: 'Banner title',
type: 'banner',
+ },
+ 'a7d7d9b1-a1ca-800c-17c5-0783be88e29c': {
authenticated: false,
- start_time: '2021-10-15T02:36:43.986212308Z',
- end_time: '2031-10-15T02:36:43.986212308Z',
- options: {},
+ end_time: null,
+ link: {
+ title: '',
+ },
+ message: 'aGVyZSBpcyBhIGNvb2wgbWVzc2FnZQ==',
+ options: null,
+ start_time: '2024-01-01T08:00:00Z',
+ title: 'Modal title',
+ type: 'modal',
},
},
- keys: ['01234567-89ab-cdef-0123-456789abcdef', '76543210-89ab-cdef-0123-456789abcdef'],
+ keys: ['02180e3f-bd5b-a851-bcc9-6f7983806df0', 'a7d7d9b1-a1ca-800c-17c5-0783be88e29c'],
},
+ wrap_info: null,
+ warnings: null,
+ auth: null,
+ mount_type: '',
};
});
diff --git a/ui/tests/acceptance/custom-messages-auth-test.js b/ui/tests/acceptance/custom-messages-auth-test.js
new file mode 100644
index 000000000000..269225e7cc61
--- /dev/null
+++ b/ui/tests/acceptance/custom-messages-auth-test.js
@@ -0,0 +1,115 @@
+/**
+ * Copyright (c) HashiCorp, Inc.
+ * SPDX-License-Identifier: BUSL-1.1
+ */
+
+import { module, test } from 'qunit';
+import { setupApplicationTest } from 'ember-qunit';
+import { click, visit } from '@ember/test-helpers';
+import { PAGE } from 'vault/tests/helpers/config-ui/message-selectors';
+import { setupMirage } from 'ember-cli-mirage/test-support';
+
+const unauthenticatedMessageResponse = {
+ request_id: '664fbad0-fcd8-9023-4c5b-81a7962e9f4b',
+ lease_id: '',
+ renewable: false,
+ lease_duration: 0,
+ data: {
+ key_info: {
+ 'some-awesome-id-2': {
+ authenticated: false,
+ end_time: null,
+ link: {
+ title: '',
+ },
+ message: 'aGVsbG8gd29ybGQgaGVsbG8gd29scmQ=',
+ options: null,
+ start_time: '2024-01-04T08:00:00Z',
+ title: 'Banner title',
+ type: 'banner',
+ },
+ 'some-awesome-id-1': {
+ authenticated: false,
+ end_time: null,
+ link: {
+ title: '',
+ },
+ message: 'aGVyZSBpcyBhIGNvb2wgbWVzc2FnZQ==',
+ options: null,
+ start_time: '2024-01-01T08:00:00Z',
+ title: 'Modal title',
+ type: 'modal',
+ },
+ },
+ keys: ['some-awesome-id-2', 'some-awesome-id-1'],
+ },
+ wrap_info: null,
+ warnings: null,
+ auth: null,
+ mount_type: '',
+};
+
+module('Acceptance | auth custom messages auth tests', function (hooks) {
+ setupApplicationTest(hooks);
+ setupMirage(hooks);
+
+ hooks.beforeEach(function () {
+ return this.server.get('/sys/internal/ui/mounts', () => ({}));
+ });
+
+ test('it shows the alert banner and modal message', async function (assert) {
+ this.server.get('/sys/internal/ui/unauthenticated-messages', function () {
+ return unauthenticatedMessageResponse;
+ });
+ await visit('/vault/auth');
+ const modalId = 'some-awesome-id-1';
+ const alertId = 'some-awesome-id-2';
+ assert.dom(PAGE.modal(modalId)).exists();
+ assert.dom(PAGE.modalTitle(modalId)).hasText('Modal title');
+ assert.dom(PAGE.modalBody(modalId)).exists();
+ assert.dom(PAGE.modalBody(modalId)).hasText('here is a cool message');
+ await click(PAGE.modalButton(modalId));
+ assert.dom(PAGE.alertTitle(alertId)).hasText('Banner title');
+ assert.dom(PAGE.alertDescription(alertId)).hasText('hello world hello wolrd');
+ });
+ test('it shows the multiple modal messages', async function (assert) {
+ const modalIdOne = 'some-awesome-id-2';
+ const modalIdTwo = 'some-awesome-id-1';
+
+ this.server.get('/sys/internal/ui/unauthenticated-messages', function () {
+ unauthenticatedMessageResponse.data.key_info[modalIdOne].type = 'modal';
+ unauthenticatedMessageResponse.data.key_info[modalIdOne].title = 'Modal title 1';
+ unauthenticatedMessageResponse.data.key_info[modalIdTwo].type = 'modal';
+ unauthenticatedMessageResponse.data.key_info[modalIdTwo].title = 'Modal title 2';
+ return unauthenticatedMessageResponse;
+ });
+ await visit('/vault/auth');
+ assert.dom(PAGE.modal(modalIdOne)).exists();
+ assert.dom(PAGE.modalTitle(modalIdOne)).hasText('Modal title 1');
+ assert.dom(PAGE.modalBody(modalIdOne)).exists();
+ assert.dom(PAGE.modalBody(modalIdOne)).hasText('hello world hello wolrd');
+ await click(PAGE.modalButton(modalIdOne));
+ assert.dom(PAGE.modal(modalIdTwo)).exists();
+ assert.dom(PAGE.modalTitle(modalIdTwo)).hasText('Modal title 2');
+ assert.dom(PAGE.modalBody(modalIdTwo)).exists();
+ assert.dom(PAGE.modalBody(modalIdTwo)).hasText('here is a cool message');
+ await click(PAGE.modalButton(modalIdTwo));
+ });
+ test('it shows the multiple banner messages', async function (assert) {
+ const bannerIdOne = 'some-awesome-id-2';
+ const bannerIdTwo = 'some-awesome-id-1';
+
+ this.server.get('/sys/internal/ui/unauthenticated-messages', function () {
+ unauthenticatedMessageResponse.data.key_info[bannerIdOne].type = 'banner';
+ unauthenticatedMessageResponse.data.key_info[bannerIdOne].title = 'Banner title 1';
+ unauthenticatedMessageResponse.data.key_info[bannerIdTwo].type = 'banner';
+ unauthenticatedMessageResponse.data.key_info[bannerIdTwo].title = 'Banner title 2';
+ return unauthenticatedMessageResponse;
+ });
+ await visit('/vault/auth');
+ assert.dom(PAGE.alertTitle(bannerIdOne)).hasText('Banner title 1');
+ assert.dom(PAGE.alertDescription(bannerIdOne)).hasText('hello world hello wolrd');
+ assert.dom(PAGE.alertTitle(bannerIdTwo)).hasText('Banner title 2');
+ assert.dom(PAGE.alertDescription(bannerIdTwo)).hasText('here is a cool message');
+ });
+});
diff --git a/ui/tests/helpers/config-ui/message-selectors.js b/ui/tests/helpers/config-ui/message-selectors.js
new file mode 100644
index 000000000000..c4f966af2604
--- /dev/null
+++ b/ui/tests/helpers/config-ui/message-selectors.js
@@ -0,0 +1,21 @@
+/**
+ * Copyright (c) HashiCorp, Inc.
+ * SPDX-License-Identifier: MPL-2.0
+ */
+
+export const PAGE = {
+ // General selectors that are common between pages
+ radio: (radioName) => `[data-test-radio="${radioName}"]`,
+ field: (fieldName) => `[data-test-field="${fieldName}"]`,
+ input: (input) => `[data-test-input="${input}"]`,
+ button: (buttonName) => `[data-test-button="${buttonName}"]`,
+ inlineErrorMessage: `[data-test-inline-error-message]`,
+ fieldVaildation: (fieldName) => `[data-test-field-validation="${fieldName}"]`,
+ modal: (name) => `[data-test-modal="${name}"]`,
+ modalTitle: (title) => `[data-test-modal-title="${title}"]`,
+ modalBody: (name) => `[data-test-modal-body="${name}"]`,
+ modalButton: (name) => `[data-test-modal-button="${name}"]`,
+ alert: (name) => `data-test-alert=${name}`,
+ alertTitle: (name) => `[data-test-alert-title="${name}"]`,
+ alertDescription: (name) => `[data-test-alert-description="${name}"]`,
+};
diff --git a/ui/tests/integration/components/config-ui/messages/page/create-and-edit-message-test.js b/ui/tests/integration/components/config-ui/messages/page/create-and-edit-message-test.js
index e45173bab06d..4c63d8b9ff5b 100644
--- a/ui/tests/integration/components/config-ui/messages/page/create-and-edit-message-test.js
+++ b/ui/tests/integration/components/config-ui/messages/page/create-and-edit-message-test.js
@@ -11,21 +11,7 @@ import { render, click, fillIn } from '@ember/test-helpers';
import { hbs } from 'ember-cli-htmlbars';
import { datetimeLocalStringFormat } from 'core/utils/date-formatters';
import { format, addDays, startOfDay } from 'date-fns';
-
-const PAGE = {
- radio: (radioName) => `[data-test-radio="${radioName}"]`,
- field: (fieldName) => `[data-test-field="${fieldName}"]`,
- input: (input) => `[data-test-input="${input}"]`,
- button: (buttonName) => `[data-test-button="${buttonName}"]`,
- inlineErrorMessage: `[data-test-inline-error-message]`,
- fieldVaildation: (fieldName) => `[data-test-field-validation="${fieldName}"]`,
- modal: (name) => `[data-test-modal="${name}"]`,
- modalTitle: (title) => `[data-test-modal-title="${title}"]`,
- modalBody: '[data-test-modal-body]',
- modalButton: (name) => `[data-test-modal-button="${name}"]`,
- alertTitle: (name) => `[data-test-alert-title="${name}"]`,
- alertDescription: (name) => `[data-test-alert-description="${name}"]`,
-};
+import { PAGE } from 'vault/tests/helpers/config-ui/message-selectors';
module('Integration | Component | messages/page/create-and-edit-message', function (hooks) {
setupRenderingTest(hooks);
@@ -185,6 +171,6 @@ module('Integration | Component | messages/page/create-and-edit-message', functi
assert.dom(PAGE.modal('preview modal')).exists();
assert.dom(PAGE.modal('preview image')).doesNotExist();
assert.dom(PAGE.modalTitle('Preview modal title')).hasText('Preview modal title');
- assert.dom(PAGE.modalBody).hasText('Some preview modal message thats super long.');
+ assert.dom(PAGE.modalBody('Preview modal title')).hasText('Some preview modal message thats super long.');
});
});
diff --git a/ui/tests/integration/components/mfa-login-enforcement-form-test.js b/ui/tests/integration/components/mfa-login-enforcement-form-test.js
index 779ca1e44338..dfe1e3ee64e8 100644
--- a/ui/tests/integration/components/mfa-login-enforcement-form-test.js
+++ b/ui/tests/integration/components/mfa-login-enforcement-form-test.js
@@ -35,6 +35,7 @@ module('Integration | Component | mfa-login-enforcement-form', function (hooks)
label: { enabled: false },
// TODO: add labels to enforcement targets key/value style inputs
'select-name': { enabled: false },
+ 'aria-prohibited-attr': { enabled: false },
},
});
});
diff --git a/ui/tests/unit/serializers/config-ui/message-test.js b/ui/tests/unit/serializers/config-ui/message-test.js
new file mode 100644
index 000000000000..40e5c83d7578
--- /dev/null
+++ b/ui/tests/unit/serializers/config-ui/message-test.js
@@ -0,0 +1,72 @@
+/**
+ * Copyright (c) HashiCorp, Inc.
+ * SPDX-License-Identifier: MPL-2.0
+ */
+
+import { module, test } from 'qunit';
+import { setupTest } from 'ember-qunit';
+
+module('Unit | Serializer | config-ui/message', function (hooks) {
+ setupTest(hooks);
+
+ test('it should always encode message when creating/updating a message', function (assert) {
+ const store = this.owner.lookup('service:store');
+ const record = store.createRecord('config-ui/message', {
+ id: '01234567-89ab-cdef-0123-456789abcdef',
+ active: true,
+ type: 'banner',
+ authenticated: true,
+ title: 'Message title 1',
+ message: 'Some long long long message',
+ link: { title: '', href: '' },
+ startTime: '2024-01-03T20:54:29.802Z',
+ endTime: '',
+ });
+ const expectedResult = {
+ authenticated: true,
+ end_time: null,
+ link: {
+ href: '',
+ title: '',
+ },
+ message: 'U29tZSBsb25nIGxvbmcgbG9uZyBtZXNzYWdl',
+ start_time: '2024-01-03T20:54:29.802Z',
+ title: 'Message title 1',
+ type: 'banner',
+ };
+
+ const serializedRecord = record.serialize();
+ assert.deepEqual(serializedRecord, expectedResult, 'encode the message string');
+ });
+
+ test('it should always use ISO date format when creating/updating a message', function (assert) {
+ const store = this.owner.lookup('service:store');
+ const date = new Date();
+ const record = store.createRecord('config-ui/message', {
+ id: '01234567-89ab-cdef-0123-456789abcdef',
+ active: true,
+ type: 'banner',
+ authenticated: true,
+ title: 'Message title 1',
+ message: 'Some long long long message',
+ link: { title: '', href: '' },
+ startTime: date,
+ endTime: '',
+ });
+ const expectedResult = {
+ authenticated: true,
+ end_time: null,
+ link: {
+ href: '',
+ title: '',
+ },
+ message: 'U29tZSBsb25nIGxvbmcgbG9uZyBtZXNzYWdl',
+ start_time: date.toISOString(),
+ title: 'Message title 1',
+ type: 'banner',
+ };
+
+ const serializedRecord = record.serialize();
+ assert.deepEqual(serializedRecord, expectedResult, 'uses ISO date string');
+ });
+});