UI: Revert api service use for requests to sys/internal/ui/mounts (#31094)

* revert api service use for sys/internal/ui/mounts

* add changelog

* replace this.api.sys.internalUiListEnabledVisibleMounts with ajax request to sys/internal/ui/mounts
This commit is contained in:
claire bontempo 2025-06-25 11:22:15 -07:00 committed by GitHub
parent 71cc2df947
commit b97bdc4cff
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
14 changed files with 68 additions and 28 deletions

3
changelog/31094.txt Normal file
View file

@ -0,0 +1,3 @@
```release-note:bug
ui: Revert camelizing of parameters returned from `sys/internal/ui/mounts` so mount paths match serve value
```

View file

@ -23,6 +23,10 @@ export default class AuthRoute extends ClusterRouteBase {
@service store;
@service version;
get adapter() {
return this.store.adapterFor('application');
}
beforeModel() {
return super.beforeModel().then(() => {
return this.version.fetchFeatures();
@ -89,10 +93,9 @@ export default class AuthRoute extends ClusterRouteBase {
}
async fetchLoginSettings() {
const adapter = this.store.adapterFor('application');
try {
// TODO update with api service when api-client is updated
const response = await adapter.ajax(
const response = await this.adapter.ajax(
'/v1/sys/internal/ui/default-auth-methods',
'GET',
this.api.buildHeaders({ token: '' })
@ -114,11 +117,13 @@ export default class AuthRoute extends ClusterRouteBase {
async fetchMounts() {
try {
const resp = await this.api.sys.internalUiListEnabledVisibleMounts(
const { data } = await this.adapter.ajax(
'/v1/sys/internal/ui/mounts',
'GET',
this.api.buildHeaders({ token: '' })
);
// return a falsy value if the object is empty
return isEmptyValue(resp.auth) ? null : resp.auth;
return isEmptyValue(data.auth) ? null : data.auth;
} catch {
// catch error if there's a problem fetching mount data (i.e. invalid namespace)
return null;

View file

@ -30,6 +30,7 @@ export default class VaultClusterDashboardRoute extends Route.extend(ClusterRout
async model() {
const clusterModel = this.modelFor('vault.cluster');
const adapter = this.store.adapterFor('application');
const hasChroot = clusterModel?.hasChrootNamespace;
const replication =
hasChroot || clusterModel.replicationRedacted
@ -40,9 +41,10 @@ export default class VaultClusterDashboardRoute extends Route.extend(ClusterRout
};
const requests = [
this.getVaultConfiguration(hasChroot),
this.api.sys.internalUiListEnabledVisibleMounts().catch(() => ({})),
adapter.ajax('/v1/sys/internal/ui/mounts', 'GET').catch(() => ({})),
];
const [vaultConfiguration, { secret }] = await Promise.all(requests);
const [vaultConfiguration, { data }] = await Promise.all(requests);
const secret = data.secret;
const secretsEngines = this.api
.responseObjectToArray(secret, 'path')
.map((engine) => new SecretsEngineResource(engine));

View file

@ -8,12 +8,16 @@ import { service } from '@ember/service';
import SecretsEngineResource from 'vault/resources/secrets/engine';
import type ApiService from 'vault/services/api';
import type Store from '@ember-data/store';
export default class SecretsBackends extends Route {
@service declare readonly api: ApiService;
@service declare readonly store: Store;
async model() {
const { secret } = await this.api.sys.internalUiListEnabledVisibleMounts();
const adapter = this.store.adapterFor('application');
const { data } = await adapter.ajax('/v1/sys/internal/ui/mounts', 'GET');
const secret = data.secret;
return this.api.responseObjectToArray(secret, 'path').map((engine) => new SecretsEngineResource(engine));
}
}

View file

@ -15,6 +15,7 @@ import type RouterService from '@ember/routing/router-service';
import type ApiService from 'vault/services/api';
import type PaginationService from 'vault/services/pagination';
import type FlashMessageService from 'vault/services/flash-messages';
import type Store from '@ember-data/store';
import type { SearchSelectOption } from 'vault/app-types';
interface Args {
@ -26,6 +27,7 @@ export default class DestinationSyncPageComponent extends Component<Args> {
@service declare readonly api: ApiService;
@service declare readonly flashMessages: FlashMessageService;
@service declare readonly pagination: PaginationService;
@service declare readonly store: Store;
constructor(owner: unknown, args: Args) {
super(owner, args);
@ -48,9 +50,11 @@ export default class DestinationSyncPageComponent extends Component<Args> {
// unable to use built-in fetch functionality of SearchSelect since we need to filter by kv type
async fetchMounts() {
const adapter = this.store.adapterFor('application');
const mounts = [];
try {
const { secret } = await this.api.sys.internalUiListEnabledVisibleMounts();
const { data } = await adapter.ajax('/v1/sys/internal/ui/mounts', 'GET');
const secret = data.secret;
if (secret) {
for (const path in secret) {
const { type, options } = secret[path as keyof typeof secret];

View file

@ -130,13 +130,15 @@ module('Acceptance | auth login form', function (hooks) {
});
test('it renders preferred mount view if "with" query param is a mount path with listing_visibility="unauth"', async function (assert) {
await visit('/vault/auth?with=my-oidc%2F');
await visit('/vault/auth?with=my_oidc%2F');
await waitFor(AUTH_FORM.tabBtn('oidc'));
assert.dom(AUTH_FORM.authForm('oidc')).exists();
assert.dom(AUTH_FORM.tabBtn('oidc')).exists();
assert.dom(GENERAL.inputByAttr('role')).exists();
assert.dom(GENERAL.inputByAttr('path')).hasAttribute('type', 'hidden');
assert.dom(GENERAL.inputByAttr('path')).hasValue('my-oidc/');
assert
.dom(GENERAL.inputByAttr('path'))
.hasValue('my_oidc/', 'mount path matches server value and is not camelized');
assert.dom(GENERAL.button('Sign in with other methods')).exists('"Sign in with other methods" renders');
assert.dom(GENERAL.selectByAttr('auth type')).doesNotExist('dropdown does not render');
@ -151,7 +153,7 @@ module('Acceptance | auth login form', function (hooks) {
.dom(AUTH_FORM.tabBtn('oidc'))
.hasAttribute('aria-selected', 'true', 'it selects tab matching query param');
assert.dom(GENERAL.inputByAttr('path')).hasAttribute('type', 'hidden');
assert.dom(GENERAL.inputByAttr('path')).hasValue('my-oidc/');
assert.dom(GENERAL.inputByAttr('path')).hasValue('my_oidc/');
assert.dom(GENERAL.button('Sign in with other methods')).exists('"Sign in with other methods" renders');
assert.dom(GENERAL.backButton).doesNotExist();
});
@ -390,7 +392,7 @@ module('Acceptance | auth login form', function (hooks) {
await visit('/vault/auth');
this.server.get('/sys/internal/ui/mounts', (_, req) => {
assert.strictEqual(req.requestHeaders['x-vault-namespace'], 'admin', 'header contains namespace');
assert.strictEqual(req.requestHeaders['X-Vault-Namespace'], 'admin', 'header contains namespace');
req.passthrough();
});
await typeIn(GENERAL.inputByAttr('namespace'), 'admin');

View file

@ -23,8 +23,8 @@ module('Acceptance | Enterprise | auth form custom login settings', function (ho
`write test-ns/sys/namespaces/child -force`,
`write sys/config/ui/login/default-auth/root-rule backup_auth_types=token default_auth_type=okta disable_inheritance=false namespace_path=""`,
`write sys/config/ui/login/default-auth/ns-rule default_auth_type=ldap disable_inheritance=true namespace_path=test-ns`,
`write sys/auth/my-oidc type=oidc`,
`write sys/auth/my-oidc/tune listing_visibility="unauth"`,
`write sys/auth/my_oidc type=oidc`,
`write sys/auth/my_oidc/tune listing_visibility="unauth"`,
]);
return await logout();
});
@ -37,7 +37,7 @@ module('Acceptance | Enterprise | auth form custom login settings', function (ho
await runCmd([
'delete sys/config/ui/login/default-auth/root-rule',
'delete sys/config/ui/login/default-auth/ns-rule',
'delete sys/auth/my-oidc',
'delete sys/auth/my_oidc',
'delete test-ns/sys/namespaces/child',
'delete sys/namespaces/test-ns',
]);
@ -73,7 +73,7 @@ module('Acceptance | Enterprise | auth form custom login settings', function (ho
});
test('it ignores login settings if query param references a visible mount path', async function (assert) {
await visit('/vault/auth?with=my-oidc%2F');
await visit('/vault/auth?with=my_oidc%2F');
await waitFor(AUTH_FORM.tabBtn('oidc'));
assert
.dom(AUTH_FORM.tabBtn('oidc'))

View file

@ -33,6 +33,24 @@ module('Acceptance | secret-engine list view', function (hooks) {
return login();
});
// the new API service camelizes response keys, so this tests is to assert that does NOT happen when we re-implement it
test('it does not camelize the secret mount path', async function (assert) {
await visit('/vault/secrets');
await page.enableEngine();
await click(MOUNT_BACKEND_FORM.mountType('aws'));
await fillIn(GENERAL.inputByAttr('path'), 'aws_engine');
await click(GENERAL.submitButton);
await click(GENERAL.breadcrumbLink('Secrets'));
assert.strictEqual(
currentRouteName(),
'vault.cluster.secrets.backends',
'breadcrumb navigates to the list page'
);
assert.dom(SES.secretsBackendLink('aws_engine')).hasTextContaining('aws_engine/');
// cleanup
await runCmd(deleteEngineCmd('aws_engine'));
});
test('after enabling an unsupported engine it takes you to list page', async function (assert) {
await visit('/vault/secrets');
await page.enableEngine();

View file

@ -102,7 +102,9 @@ export const SYS_INTERNAL_UI_MOUNTS = {
options: {},
type: 'userpass',
},
'my-oidc/': {
// there was a problem with the API service camel-casing mounts that were snake cased
// so including a snake cased mount for testing
'my_oidc/': {
description: '',
options: {},
type: 'oidc',

View file

@ -158,7 +158,7 @@ export const RESPONSE_STUBS = {
},
num_uses: 0,
orphan: true,
path: 'auth/my-oidc/oidc/callback',
path: 'auth/my_oidc/oidc/callback',
policies: ['default'],
renewable: true,
ttl: 2764799,

View file

@ -110,7 +110,7 @@ module('Integration | Component | auth | form template', function (hooks) {
],
oidc: [
{
path: 'my-oidc/',
path: 'my_oidc/',
description: '',
options: {},
type: 'oidc',

View file

@ -74,7 +74,7 @@ module('Integration | Component | auth | page | listing visibility', function (h
module('with a direct link', function (hooks) {
hooks.beforeEach(function () {
// if path exists, the mount has listing_visibility="unauth"
this.directLinkIsVisibleMount = { path: 'my-oidc/', type: 'oidc' };
this.directLinkIsVisibleMount = { path: 'my_oidc/', type: 'oidc' };
this.directLinkIsJustType = { type: 'okta' };
});
@ -106,7 +106,7 @@ module('Integration | Component | auth | page | listing visibility', function (h
assert.dom(AUTH_FORM.tabs).exists({ count: 1 }, 'only one tab renders');
assert.dom(GENERAL.inputByAttr('role')).exists();
assert.dom(GENERAL.inputByAttr('path')).hasAttribute('type', 'hidden');
assert.dom(GENERAL.inputByAttr('path')).hasValue('my-oidc/');
assert.dom(GENERAL.inputByAttr('path')).hasValue('my_oidc/');
assert.dom(GENERAL.button('Sign in with other methods')).exists('"Sign in with other methods" renders');
assert.dom(GENERAL.selectByAttr('auth type')).doesNotExist();
assert.dom(AUTH_FORM.advancedSettings).doesNotExist();

View file

@ -118,7 +118,7 @@ module('Integration | Component | auth | page | ent login settings', function (h
await this.renderComponent();
assert.dom(AUTH_FORM.tabBtn('oidc')).hasText('OIDC', 'it renders default method');
assert.dom(AUTH_FORM.tabs).exists({ count: 1 }, 'only one tab renders');
this.assertPathInput(assert, { isHidden: true, value: 'my-oidc/' });
this.assertPathInput(assert, { isHidden: true, value: 'my_oidc/' });
await click(GENERAL.button('Sign in with other methods'));
assert.dom(AUTH_FORM.tabs).exists({ count: 2 }, 'it renders 2 backup type tabs');
assert
@ -137,7 +137,7 @@ module('Integration | Component | auth | page | ent login settings', function (h
assert.dom(AUTH_FORM.tabBtn('oidc')).hasText('OIDC', 'it renders default method');
assert.dom(AUTH_FORM.tabs).exists({ count: 1 }, 'only one tab renders');
assert.dom(AUTH_FORM.authForm('oidc')).exists();
this.assertPathInput(assert, { isHidden: true, value: 'my-oidc/' });
this.assertPathInput(assert, { isHidden: true, value: 'my_oidc/' });
assert.dom(GENERAL.backButton).doesNotExist();
assert.dom(GENERAL.button('Sign in with other methods')).doesNotExist();
});
@ -165,9 +165,9 @@ module('Integration | Component | auth | page | ent login settings', function (h
});
test('(default+backups): it hides advanced settings for default with visible mount but it renders for backups', async function (assert) {
this.visibleAuthMounts = { ...this.mountData('my-oidc/') };
this.visibleAuthMounts = { ...this.mountData('my_oidc/') };
await this.renderComponent();
this.assertPathInput(assert, { isHidden: true, value: 'my-oidc/' });
this.assertPathInput(assert, { isHidden: true, value: 'my_oidc/' });
await click(GENERAL.button('Sign in with other methods'));
assert.dom(AUTH_FORM.tabBtn('userpass')).hasAttribute('aria-selected', 'true');
await this.assertPathInput(assert);
@ -178,7 +178,7 @@ module('Integration | Component | auth | page | ent login settings', function (h
test('(default+backups): it only renders advanced settings for method without mounts', async function (assert) {
// default and only one backup method have visible mounts
this.visibleAuthMounts = {
...this.mountData('my-oidc/'),
...this.mountData('my_oidc/'),
...this.mountData('userpass/'),
...this.mountData('userpass2/'),
};

View file

@ -32,7 +32,7 @@ module('Integration | Component | auth | tabs', function (hooks) {
],
oidc: [
{
path: 'my-oidc/',
path: 'my_oidc/',
description: '',
options: {},
type: 'oidc',
@ -98,7 +98,7 @@ module('Integration | Component | auth | tabs', function (hooks) {
this.selectedAuthMethod = 'oidc';
await this.renderComponent();
assert.dom(GENERAL.inputByAttr('path')).hasAttribute('type', 'hidden');
assert.dom(GENERAL.inputByAttr('path')).hasValue('my-oidc/');
assert.dom(GENERAL.inputByAttr('path')).hasValue('my_oidc/');
});
test('it calls handleTabClick with tab method type', async function (assert) {