Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Restructure SSH and AWS configuration screens #27831

Merged
merged 26 commits into from
Jul 30, 2024
Merged
Show file tree
Hide file tree
Changes from 25 commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
dd23306
setup the toggle to display mount configuration options
Monkeychip Jul 22, 2024
9cdb05d
whew.. getting there. aws only, borked for ssh
Monkeychip Jul 22, 2024
a993df0
another round, better than before
Monkeychip Jul 22, 2024
44746a3
masked things
Monkeychip Jul 22, 2024
da35c8a
changelog
Monkeychip Jul 22, 2024
c6be041
fix broken oss test
Monkeychip Jul 22, 2024
7fa427f
move to component
Monkeychip Jul 23, 2024
84ff4c6
handle ssh things and cleanup
Monkeychip Jul 23, 2024
22bd6a6
Merge branch 'main' into ui/VAULT-28466/restructure-aws-configuration
Monkeychip Jul 24, 2024
2b9d7ac
wip test coverage
Monkeychip Jul 24, 2024
e8c637e
test coverage for the component
Monkeychip Jul 24, 2024
7632f5c
copywrite header miss
Monkeychip Jul 24, 2024
a29f1a7
update no model error
Monkeychip Jul 24, 2024
540e229
setup configuration aws acceptance tests
Monkeychip Jul 24, 2024
2adb2d1
update CONFIURABLE_SECRET_ENGINES
Monkeychip Jul 25, 2024
1d7e19a
acceptance tests for aws
Monkeychip Jul 25, 2024
a80f310
ssh configuration
Monkeychip Jul 25, 2024
8905e53
Merge branch 'main' into ui/VAULT-28466/restructure-aws-configuration
Monkeychip Jul 26, 2024
b26adfe
clean up
Monkeychip Jul 26, 2024
0af0333
remove comment
Monkeychip Jul 26, 2024
9c18d9d
move to confirm model before destructuring
Monkeychip Jul 26, 2024
ad96509
pr comments
Monkeychip Jul 27, 2024
404fb84
Merge branch 'main' into ui/VAULT-28466/restructure-aws-configuration
Monkeychip Jul 27, 2024
d3279b2
fix check for ssh config error
Monkeychip Jul 28, 2024
f2f302c
add message check in api error test
Monkeychip Jul 29, 2024
340bc6e
pr comments
Monkeychip Jul 29, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions changelog/27831.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
```release-note:improvement
ui: For AWS and SSH secret engines hide mount configuration details in toggle and display configuration details or cta.
```
20 changes: 20 additions & 0 deletions ui/app/adapters/aws/root-config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
/**
* Copyright (c) HashiCorp, Inc.
* SPDX-License-Identifier: BUSL-1.1
*/

import ApplicationAdapter from '../application';
import { encodePath } from 'vault/utils/path-encoding-helpers';

export default class AwsRootConfig extends ApplicationAdapter {
namespace = 'v1';
// For now this is only being used on the vault.cluster.secrets.backend.configuration route. This is a read-only route.
// Eventually, this will be used to create the root config for the AWS secret backend, replacing the requests located on the secret-engine adapter.
queryRecord(store, type, query) {
const { backend } = query;
return this.ajax(`/v1/${encodePath(backend)}/config/root`, 'GET').then((resp) => {
resp.id = backend;
return resp;
});
}
}
20 changes: 20 additions & 0 deletions ui/app/adapters/ssh/ca-config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
/**
* Copyright (c) HashiCorp, Inc.
* SPDX-License-Identifier: BUSL-1.1
*/

import ApplicationAdapter from '../application';
import { encodePath } from 'vault/utils/path-encoding-helpers';

export default class SshCaConfig extends ApplicationAdapter {
namespace = 'v1';
// For now this is only being used on the vault.cluster.secrets.backend.configuration route. This is a read-only route.
// Eventually, this will be used to create the ca config for the SSH secret backend, replacing the requests located on the secret-engine adapter.
queryRecord(store, type, query) {
const { backend } = query;
return this.ajax(`/v1/${encodePath(backend)}/config/ca`, 'GET').then((resp) => {
resp.id = backend;
return resp;
});
}
}
44 changes: 44 additions & 0 deletions ui/app/components/secret-engine/configuration-details.hbs
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👏

Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
{{!
Copyright (c) HashiCorp, Inc.
SPDX-License-Identifier: BUSL-1.1
~}}

{{#if this.configError}}
{{! Surface API errors not associated with empty configuration details }}
<Page::Error @error={{this.configError}} />
{{else if this.configModel}}
{{#each this.configModel.attrs as |attr|}}
{{#if attr.options.sensitive}}
<InfoTableRow
alwaysRender={{not (is-empty-value (get @model attr.name))}}
@label={{or attr.options.label (to-label attr.name)}}
@value={{get this.configModel (or attr.options.fieldValue attr.name)}}
>
{{#if attr.options.sensitive}}
<MaskedInput @value={{get @model attr.name}} @name={{attr.name}} @displayOnly={{true}} @allowCopy={{true}} />
{{/if}}
</InfoTableRow>
{{else}}
<InfoTableRow
@alwaysRender={{not (is-empty-value (get @model attr.name))}}
@label={{or attr.options.label (to-label attr.name)}}
@value={{get this.configModel (or attr.options.fieldValue attr.name)}}
/>
{{/if}}
{{/each}}
{{else}}
{{! Prompt for a user to configure the secret engine }}
<EmptyState
data-test-config-cta
@title="{{this.typeDisplay}} not configured"
@message="Get started by configuring your {{this.typeDisplay}} engine."
>
<Hds::Link::Standalone
@icon="chevron-right"
@iconPosition="trailing"
@text="Configure {{this.typeDisplay}}"
@route="vault.cluster.settings.configure-secret-backend"
@model={{@model.id}}
/>
</EmptyState>
{{/if}}
97 changes: 97 additions & 0 deletions ui/app/components/secret-engine/configuration-details.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
/**
* Copyright (c) HashiCorp, Inc.
* SPDX-License-Identifier: BUSL-1.1
*/

import { service } from '@ember/service';
import Component from '@glimmer/component';
import { tracked } from '@glimmer/tracking';
import { allEngines } from 'vault/helpers/mountable-secret-engines';

import type Store from '@ember-data/store';
import type SecretEngineModel from 'vault/models/secret-engine';
import type AdapterError from '@ember-data/adapter';
import type Model from '@ember-data/model';

/**
* @module ConfigurationDetails
* `ConfigurationDetails` is used by configurable secret engines (AWS, SSH) to show either an API error, configuration details, or a prompt to configure the engine. Which of these is shown is determined by the engine type and whether the user has configured the engine yet.
*
* @example
* ```js
* <SecretEngine::ConfigurationDetails @model={{this.model}} />
* ```
*
* @param {object} model - The secret-engine model to be configured.
*/

interface Args {
model: SecretEngineModel;
}

interface ConfigError {
httpStatus: number | null;
message: string | null;
errors: object | null;
}

export default class ConfigurationDetails extends Component<Args> {
@service declare readonly store: Store;
@tracked configError: ConfigError | null = null;
@tracked configModel: Model | null = null;

constructor(owner: unknown, args: Args) {
super(owner, args);
const { model } = this.args;
// Should not be able to get here without a model, but in case an upstream change allows it, catching the failure.
if (!model) {
this.configError = {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would think that if we do not expect this component to handle the error screen (it's handled upstream) then no model would default to the empty state. In that case, we could just return here without setting a tracked configError value

message:
'We are unable to access the mount information for this engine. Ask you administrator if you think you should have access to this secret engine.',
};
return;
}
const { id, type } = model;
// Fetch the config for the engine type.
switch (type) {
case 'aws':
this.fetchAwsRootConfig(id);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder if the fetchConfig method could be passed from the parent, and then this component would be extensible without needing to know about all the types of secret engines

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I had a similar question and still go back and forth.

If you're okay with it, I'll make a separate ticket for this change. There are some changes to the parent that I'd also liked to make (it currently does some kvv2 magic that I'd want to integrate).

break;
case 'ssh':
this.fetchSshCaConfig(id);
break;
}
}

async fetchAwsRootConfig(backend: string) {
try {
this.configModel = await this.store.queryRecord('aws/root-config', { backend });
} catch (e: AdapterError) {
// If the error is something other than 404 "not found" then an API error has come back and this will be displayed to the user as an error.
// If it's 404 then configError is not set nor is the configModel and a prompt to configure will be shown.
if (e.httpStatus !== 404) {
this.configError = e;
}
return;
}
}

async fetchSshCaConfig(backend: string) {
try {
this.configModel = await this.store.queryRecord('ssh/ca-config', { backend });
} catch (e: AdapterError) {
// The SSH api does not return a 404 not found but a 400 error after first mounting the engine with the
// message that keys have not been configured yet.
// We need to check the message of the 400 error and if it's the keys message, return a prompt instead of a configError.
if (e.httpStatus !== 404 && e.errors[0] !== `keys haven't been configured yet`) {
this.configError = e;
}
return;
}
}

get typeDisplay() {
const { type } = this.args.model;
return allEngines().find((engine) => engine.type === type)?.displayName;
}
}
32 changes: 32 additions & 0 deletions ui/app/models/aws/root-config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
/**
* Copyright (c) HashiCorp, Inc.
* SPDX-License-Identifier: BUSL-1.1
*/

import Model, { attr } from '@ember-data/model';
import { expandAttributeMeta } from 'vault/utils/field-to-attrs';

export default class AwsRootConfig extends Model {
@attr('string') backend; // dynamic path of secret -- set on response from value passed to queryRecord
@attr('string') accessKey;
@attr('string') region;
@attr('string', {
label: 'IAM endpoint',
})
iamEndpoint;
@attr('string', {
label: 'STS endpoint',
})
stsEndpoint;
@attr('number', {
defaultValue: -1,
label: 'Maximum retries',
subText: 'Number of max retries the client should use for recoverable errors. Default is -1.',
})
maxRetries;
// there are more options available on the API, but the UI does not support them yet.
get attrs() {
const keys = ['accessKey', 'region', 'iamEndpoint', 'stsEndpoint', 'maxRetries'];
return expandAttributeMeta(this, keys);
}
}
20 changes: 20 additions & 0 deletions ui/app/models/ssh/ca-config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
/**
* Copyright (c) HashiCorp, Inc.
* SPDX-License-Identifier: BUSL-1.1
*/

import Model, { attr } from '@ember-data/model';
import { expandAttributeMeta } from 'vault/utils/field-to-attrs';

export default class SshCaConfig extends Model {
@attr('string') backend; // dynamic path of secret -- set on response from value passed to queryRecord
@attr('string', { sensitive: true }) privateKey; // obfuscated, never returned by API
@attr('string', { sensitive: true }) publicKey;
@attr('boolean', { defaultValue: true })
generateSigningKey;
// there are more options available on the API, but the UI does not support them yet.
get attrs() {
const keys = ['publicKey', 'generateSigningKey'];
return expandAttributeMeta(this, keys);
}
}
43 changes: 24 additions & 19 deletions ui/app/templates/vault/cluster/secrets/backend/configuration.hbs
Copy link
Contributor

@hellobontempo hellobontempo Jul 26, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I noticed that this breadcrumb says Configure but I think it should be Configuration since Configure is actually a separate route 🤔 I realize this is not quite what you were addressing here, just mentioning in case you wanted to update!

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yep, I noticed that as well. I've got a ticket for breadcrumbs specifically, so holding off on any breadcrumb work for that one pr.

Original file line number Diff line number Diff line change
Expand Up @@ -26,23 +26,28 @@
</ToolbarLink>
</ToolbarActions>
</Toolbar>
{{/if}}

<div class="box is-fullwidth is-sideless is-paddingless is-marginless">
{{#each this.model.attrs as |attr|}}
{{#if (eq attr.type "object")}}
<InfoTableRow
@alwaysRender={{not (is-empty-value (get this.model attr.name))}}
@label={{or attr.options.label (to-label attr.name)}}
@value={{stringify (get this.model (or attr.options.fieldValue attr.name))}}
/>
{{else}}
<InfoTableRow
@alwaysRender={{and (not (is-empty-value (get this.model attr.name))) (not-eq attr.name "version")}}
@formatTtl={{eq attr.options.editType "ttl"}}
@label={{or attr.options.label (to-label attr.name)}}
@value={{get this.model (or attr.options.fieldValue attr.name)}}
/>
{{/if}}
{{/each}}
</div>
<SecretEngine::ConfigurationDetails @model={{this.model}} />

<SecretsEngineMountConfig @model={{this.model}} class="has-top-margin-xl has-bottom-margin-xl" data-test-mount-config />

{{else}}
<div class="box is-fullwidth is-sideless is-paddingless is-marginless">
{{#each this.model.attrs as |attr|}}
{{#if (eq attr.type "object")}}
<InfoTableRow
@alwaysRender={{not (is-empty-value (get this.model attr.name))}}
@label={{or attr.options.label (to-label attr.name)}}
@value={{stringify (get this.model (or attr.options.fieldValue attr.name))}}
/>
{{else}}
<InfoTableRow
@alwaysRender={{and (not (is-empty-value (get this.model attr.name))) (not-eq attr.name "version")}}
@formatTtl={{eq attr.options.editType "ttl"}}
@label={{or attr.options.label (to-label attr.name)}}
@value={{get this.model (or attr.options.fieldValue attr.name)}}
/>
{{/if}}
{{/each}}
</div>
{{/if}}
6 changes: 3 additions & 3 deletions ui/lib/core/addon/components/page/error.hbs
Original file line number Diff line number Diff line change
Expand Up @@ -8,17 +8,17 @@
data-test-page-error
>
{{#if (eq @error.httpStatus 404)}}
<h1 class="title is-3 has-text-grey-light">
<h1 class="title is-3 has-text-grey-light" data-test-page-error-title={{@error.httpStatus}}>
404 Not Found
</h1>
<p>Sorry, we were unable to find any content at <code>{{@error.path}}</code>.</p>
{{else if (eq @error.httpStatus 403)}}
<h1 class="title is-3 has-text-grey-light">
<h1 class="title is-3 has-text-grey-light" data-test-page-error-title={{@error.httpStatus}}>
Not authorized
</h1>
<p>You are not authorized to access content at <code>{{@error.path}}</code>.</p>
{{else}}
<h1 class="title is-3 has-text-grey-light">
<h1 class="title is-3 has-text-grey-light" data-test-page-error-title={{@error.httpStatus}}>
Error
</h1>
<p>
Expand Down
Loading
Loading