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'); + }); +});