ui: Create auth-method show page with General Info Tab (#9845)

* Update list items to be linkable to auth-methods show

* Add general, namespace, and binding sub-routes

* Remove namespace and binding tabs to be done separately

* Update auth-method byId endpoint

* Style the show auth-method kubernetes type

* Finish Kubernetes auth-method type styling

* OIDC and JWT auth-method styling

* Create consul-auth-method-view component

* Add navigation test for auth-methods

* Create Certificate component
This commit is contained in:
Kenia 2021-03-17 10:40:56 -04:00 committed by GitHub
parent d38917b12b
commit eab741eab8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
22 changed files with 562 additions and 18 deletions

View File

@ -0,0 +1,14 @@
<div class="certificate">
<CopyButton @value={{@item}} @name={{@name}} />
<button
type="button"
class={{concat "visibility" (if this.show " hide" " show")}}
{{on "click" this.setVisibility}}
>
</button>
{{#if this.show}}
<div class="key">{{@item}}</div>
{{else}}
<hr />
{{/if}}
</div>

View File

@ -0,0 +1,14 @@
import Component from '@glimmer/component';
import { tracked } from '@glimmer/tracking';
import { action } from '@ember/object';
export default class Certificate extends Component {
// =attributes
@tracked show = false;
// =actions
@action
setVisibility() {
this.show = !this.show;
}
}

View File

@ -0,0 +1,26 @@
.certificate {
display: flex;
button.visibility {
height: fit-content;
padding-top: 4px;
margin-right: 4px;
cursor: pointer;
}
button.hide::before {
@extend %with-visibility-hide-icon, %as-pseudo;
}
button.show::before {
@extend %with-visibility-show-icon, %as-pseudo;
}
div.key {
background-color: var(--gray-050);
overflow-wrap: anywhere;
}
hr {
border: 3px dashed var(--gray-300);
background-color: $white;
width: 150px;
margin: auto;
margin-top: 9px;
}
}

View File

@ -1,9 +1,74 @@
.consul-auth-method-list ul {
.consul-auth-method-type {
@extend %pill-200, %frame-gray-600;
}
.consul-consul-auth-method-view-list ul {
.locality::before {
@extend %with-public-default-mask, %as-pseudo;
margin-right: 4px;
}
}
.consul-auth-method-view {
margin-bottom: 32px;
> hr {
background-color: var(--gray-200);
}
section {
@extend %p1;
width: 100%;
position: relative;
overflow-y: auto;
h2 {
@extend %h200;
padding-bottom: 12px;
}
table {
thead td {
color: var(--gray-500);
font-weight: $typo-weight-semibold;
font-size: $typo-size-700;
}
tbody td {
font-size: $typo-size-600;
color: $black;
}
}
}
dl,
section dl {
display: flex;
flex-wrap: wrap;
> dt:last-of-type,
> dd:last-of-type {
border-bottom: 1px solid var(--gray-300) !important;
}
dt, dd {
padding: 12px 0;
margin: 0;
border-top: 1px solid var(--gray-300) !important;
color: $black !important;
}
dt {
width: 20%;
font-weight: $typo-weight-bold;
}
dd {
margin-left: auto;
width: 80%;
display: flex;
}
dd > ul li {
display: flex;
}
dd > ul li:not(:last-of-type) {
padding-bottom: 12px;
}
dd .copy-button button {
padding: 0 !important;
margin: 0 4px 0 0 !important;
}
dd .copy-button button::before {
background-color: $black;
}
dt.check + dd {
padding-top: 16px;
}
}
}

View File

@ -1,12 +1,17 @@
<ListCollection
class="consul-auth-method-list"
@items={{@items}}
as |item|>
as |item|
>
<BlockSlot @name="header">
{{#if (not-eq item.DisplayName '')}}
<p data-test-auth-method>{{item.DisplayName}}</p>
<a data-test-auth-method href={{href-to "dc.acls.auth-methods.show" item.Name}}>
{{item.DisplayName}}
</a>
{{else}}
<p data-test-auth-method>{{item.Name}}</p>
<a data-test-auth-method href={{href-to "dc.acls.auth-methods.show" item.Name}}>
{{item.Name}}
</a>
{{/if}}
</BlockSlot>
<BlockSlot @name="details">

View File

@ -1,5 +1,6 @@
export default (collection, text) => () => {
export default (collection, clickable, text) => () => {
return collection('.consul-auth-method-list [data-test-list-row]', {
authMethod: clickable('a'),
name: text('[data-test-auth-method]'),
displayName: text('[data-test-display-name]'),
type: text('[data-test-type]'),

View File

@ -0,0 +1,217 @@
<div class="consul-auth-method-view">
{{#if (eq @item.Type 'kubernetes')}}
<dl>
<dt>{{t 'models.auth-method.Type'}}</dt>
<dd><Consul::AuthMethod::Type @item={{@item}} /></dd>
{{#each (array "MaxTokenTTL" "TokenLocality" "DisplayName" "Description") as |value|}}
{{#if (get @item value)}}
<dt>{{t (concat "models.auth-method." value)}}</dt>
<dd>{{get @item value}}</dd>
{{/if}}
{{/each}}
{{#if @item.Config.Host}}
<dt>{{t 'models.auth-method.Config.Host'}}</dt>
<dd>
<CopyButton @value={{@item.Config.Host}} @name={{t 'models.auth-method.Config.Host'}}/>
<span>{{@item.Config.Host}}</span>
</dd>
{{/if}}
{{#if @item.Config.CACert}}
<dt>{{t 'models.auth-method.Config.CACert'}}</dt>
<dd>
<Certificate @item={{@item.Config.CACert}} @name={{t 'models.auth-method.Config.CACert'}} />
</dd>
{{/if}}
{{#if @item.Config.ServiceAccountJWT}}
<dt>{{t 'models.auth-method.Config.ServiceAccountJWT'}}</dt>
<dd>
<CopyButton @value={{@item.Config.ServiceAccountJWT}} @name={{t 'models.auth-method.Config.ServiceAccountJWT'}} />
<span>
{{@item.Config.ServiceAccountJWT}}
</span>
</dd>
{{/if}}
</dl>
{{else}}
<section class="meta">
<dl>
<dt>Type</dt>
<dd><Consul::AuthMethod::Type @item={{@item}} /></dd>
{{#each (array "MaxTokenTTL" "TokenLocality" "DisplayName" "Description") as |value|}}
{{#if (get @item value)}}
<dt>{{t (concat "models.auth-method." value)}}</dt>
<dd>{{get @item value}}</dd>
{{/if}}
{{/each}}
{{#if (eq @item.Type 'jwt')}}
{{#if @item.Config.JWKSURL}}
<dt>{{t 'models.auth-method.Config.JWKSURL'}}</dt>
<dd>
<CopyButton @value={{@item.Config.JWKSURL}} @name={{t 'models.auth-method.Config.JWKSURL'}} />
<span>{{@item.Config.JWKSURL}}</span>
</dd>
<dt>{{t 'models.auth-method.Config.JWKSCACert'}}</dt>
<dd>
<Certificate @item={{@item.Config.JWKSCACert}} @name={{t 'models.auth-method.Config.JWKSCACert'}} />
</dd>
{{/if}}
{{#if @item.Config.JWTValidationPubKeys}}
<dt>{{t 'models.auth-method.Config.JWTValidationPubKeys'}}</dt>
<dd>
<Certificate @item={{@item.Config.JWTValidationPubKeys}} @name={{t 'models.auth-method.Config.JWTValidationPubKeys'}} />
</dd>
{{/if}}
{{#if @item.Config.OIDCDiscoveryURL}}
<dt>{{t 'models.auth-method.Config.OIDCDiscoveryURL'}}</dt>
<dd>
<CopyButton @value={{@item.Config.OIDCDiscoveryURL}} @name={{t 'models.auth-method.Config.OIDCDiscoveryURL'}} />
<span>{{@item.Config.OIDCDiscoveryURL}}</span>
</dd>
{{/if}}
{{#if @item.Config.JWTSupportedAlgs}}
<dt>{{t 'models.auth-method.Config.JWTSupportedAlgs'}}</dt>
<dd>{{join ', ' @item.Config.JWTSupportedAlgs}}</dd>
{{/if}}
{{#if @item.Config.BoundAudiences}}
<dt>{{t 'models.auth-method.Config.BoundAudiences'}}</dt>
<dd>
<ul>
{{#each @item.Config.BoundAudiences as |bond|}}
<li>
<span>{{bond}}</span>
</li>
{{/each}}
</ul>
</dd>
{{/if}}
{{#each (array "BoundIssuer" "ExpirationLeeway" "NotBeforeLeeway" "ClockSkewLeeway") as |value|}}
{{#if (get @item.Config value)}}
<dt>{{t (concat "models.auth-method.Config." value)}}</dt>
<dd>{{get @item.Config value}}</dd>
{{/if}}
{{/each}}
{{else if (eq @item.Type 'oidc')}}
{{#if @item.Config.OIDCDiscoveryURL}}
<dt>{{t 'models.auth-method.Config.OIDCDiscoveryURL'}}</dt>
<dd>
<CopyButton @value={{@item.Config.OIDCDiscoveryURL}} @name={{t 'models.auth-method.Config.OIDCDiscoveryURL'}} />
<span>{{@item.Config.OIDCDiscoveryURL}}</span>
</dd>
{{/if}}
{{#if @item.Config.OIDCDiscoveryCACert}}
<dt>{{t 'models.auth-method.Config.OIDCDiscoveryCACert'}}</dt>
<dd>
<Certificate @item={{@item.Config.OIDCDiscoveryCACert}} @name={{t 'models.auth-method.Config.OIDCDiscoveryCACert'}} />
</dd>
{{/if}}
{{#if @item.Config.OIDCClientID}}
<dt>{{t 'models.auth-method.Config.OIDCClientID'}}</dt>
<dd>{{@item.Config.OIDCClientID}}</dd>
{{/if}}
{{#if @item.Config.OIDCClientSecret}}
<dt>{{t 'models.auth-method.Config.OIDCClientSecret'}}</dt>
<dd>{{@item.Config.OIDCClientSecret}}</dd>
{{/if}}
{{#if @item.Config.AllowedRedirectURIs}}
<dt>{{t 'models.auth-method.Config.AllowedRedirectURIs'}}</dt>
<dd>
<ul>
{{#each @item.Config.AllowedRedirectURIs as |uri|}}
<li>
<CopyButton @value={{uri}} @name="Redirect URI" />
<span>{{uri}}</span>
</li>
{{/each}}
</ul>
</dd>
{{/if}}
{{#if @item.Config.BoundAudiences}}
<dt>{{t 'models.auth-method.Config.BoundAudiences'}}</dt>
<dd>
<ul>
{{#each @item.Config.BoundAudiences as |bond|}}
<li>
<span>{{bond}}</span>
</li>
{{/each}}
</ul>
</dd>
{{/if}}
{{#if @item.Config.OIDCScopes}}
<dt>{{t 'models.auth-method.Config.OIDCScopes'}}</dt>
<dd>
<ul>
{{#each @item.Config.OIDCScopes as |scope|}}
<li>
<span>{{scope}}</span>
</li>
{{/each}}
</ul>
</dd>
{{/if}}
{{#if @item.Config.JWTSupportedAlgs}}
<dt>{{t 'models.auth-method.Config.JWTSupportedAlgs'}}</dt>
<dd>{{join ', ' @item.Config.JWTSupportedAlgs}}</dd>
{{/if}}
{{#if @item.Config.VerboseOIDCLogging}}
<dt class="check">{{t 'models.auth-method.Config.VerboseOIDCLogging'}}</dt>
<dd><input type="checkbox" disabled="disabled" checked={{@item.Config.VerboseOIDCLogging}}></dd>
{{/if}}
{{/if}}
</dl>
</section>
<hr />
{{#if @item.Config.ClaimMappings}}
<section class="claim-mappings">
<h2>Claim Mappings</h2>
<p>Use this if the claim you are capturing is singular. When mapped, the values can be any of a number, string, or boolean and will all be stringified when returned.</p>
<table>
<thead>
<tr>
<td>Key</td>
<td>Value</td>
</tr>
</thead>
<tbody>
{{#each (entries @item.Config.ClaimMappings) as |entry|}}
<tr>
<td>{{get entry 0}}</td>
<td>{{get entry 1}}</td>
</tr>
{{/each}}
</tbody>
</table>
</section>
{{/if}}
<hr />
{{#if @item.Config.ListClaimMappings}}
<section class="list-claim-mappings">
<h2>List Claim Mappings</h2>
<p>Use this if the claim you are capturing is list-like (such as groups). When mapped, the values can be any of a number, string, or boolean and will all be stringified when returned.</p>
<table>
<thead>
<tr>
<td>Key</td>
<td>Value</td>
</tr>
</thead>
<tbody>
{{#each (entries @item.Config.ListClaimMappings) as |entry|}}
<tr>
<td>{{get entry 0}}</td>
<td>{{get entry 1}}</td>
</tr>
{{/each}}
</tbody>
</table>
</section>
{{/if}}
{{/if}}
</div>

View File

@ -192,7 +192,10 @@ export const routes = {
abilities: ['read auth-methods'],
},
show: {
_options: { path: '/show' },
_options: { path: '/:id' },
'auth-method': {
_options: { path: '/auth-method' },
},
},
},
},

View File

@ -0,0 +1,27 @@
import { inject as service } from '@ember/service';
import SingleRoute from 'consul-ui/routing/single';
import { hash } from 'rsvp';
export default class ShowRoute extends SingleRoute {
@service('repository/auth-method') repo;
model(params) {
return super.model(...arguments).then(model => {
return hash({
...model,
...{
item: this.repo.findBySlug({
id: params.id,
dc: this.modelFor('dc').dc.Name,
ns: this.modelFor('nspace').nspace.substr(1),
}),
},
});
});
}
setupController(controller, model) {
super.setupController(...arguments);
controller.setProperties(model);
}
}

View File

@ -0,0 +1,16 @@
import Route from 'consul-ui/routing/route';
export default class AuthMethodRoute extends Route {
model() {
const parent = this.routeName
.split('.')
.slice(0, -1)
.join('.');
return this.modelFor(parent);
}
setupController(controller, model) {
super.setupController(...arguments);
controller.setProperties(model);
}
}

View File

@ -0,0 +1,6 @@
import Route from 'consul-ui/routing/route';
import to from 'consul-ui/utils/routing/redirect-to';
export default Route.extend({
redirect: to('auth-method'),
});

View File

@ -63,6 +63,7 @@
@import 'consul-ui/components/informed-action';
@import 'consul-ui/components/tab-nav';
@import 'consul-ui/components/search-bar';
@import 'consul-ui/components/certificate';
@import 'consul-ui/components/consul/tomography/graph';
@import 'consul-ui/components/consul/discovery-chain';

View File

@ -9,7 +9,8 @@
.consul-node-list > ul > li:not(:first-child),
.consul-token-list > ul > li:not(:first-child),
.consul-policy-list > ul > li:not(:first-child),
.consul-role-list > ul > li:not(:first-child) {
.consul-role-list > ul > li:not(:first-child),
.consul-auth-method-list > ul > li:not(:first-child) {
@extend %with-composite-row-intent;
}
.consul-lock-session-list ul > li:not(:first-child) {

View File

@ -1,6 +1,7 @@
span.policy-service-identity,
span.policy-node-identity,
.leader {
.leader,
.consul-auth-method-type {
@extend %pill-200, %frame-gray-600;
}
span.policy-service-identity::before,

View File

@ -0,0 +1,40 @@
{{#if isAuthorized }}
{{page-title item.Name}}
{{else}}
{{page-title 'Access Controls'}}
{{/if}}
<AppView
@authorized={{isAuthorized}}
@enabled={{isEnabled}}
>
<BlockSlot @name="breadcrumbs">
<ol>
<li><a data-test-back href={{href-to 'dc.acls.auth-methods'}}>All Auth Methods</a></li>
</ol>
</BlockSlot>
<BlockSlot @name="header">
<h1>
{{#if isAuthorized }}
{{item.Name}}
{{else}}
Access Controls
{{/if}}
</h1>
<Consul::AuthMethod::Type @item={{item}} />
</BlockSlot>
<BlockSlot @name="nav">
<TabNav @items={{
compact
(array
(hash label="General info" href=(href-to "dc.acls.auth-methods.show.auth-method") selected=(is-href "dc.acls.auth-methods.show.auth-method"))
)
}}/>
</BlockSlot>
<BlockSlot @name="content">
<Outlet
@name={{routeName}}
as |o|>
{{outlet}}
</Outlet>
</BlockSlot>
</AppView>

View File

@ -0,0 +1,4 @@
<div class="tab-section">
<Consul::AuthMethod::View @item={{item}} />
</div>

View File

@ -1,7 +1,20 @@
${
[1].map(() => {
[1].map(i => {
const type = `${fake.helpers.randomize(['kubernetes', 'jwt', 'oidc'])}`;
const fakeIP = `${fake.internet.ip()}`;
let sourceType;
if (type !== 'kubernetes') {
sourceType = `${fake.helpers.randomize(['JWTValidationPubKeys', 'JWKSURL', 'OIDCDiscoveryURL'])}`;
}
const claimMappings = {
"http://example.com/example-1": `${fake.hacker.noun()}`,
"http://example.com/example-2": `${fake.hacker.noun()}`
}
const listClaimMappings = {
"http://example.com/example-1": `${fake.hacker.noun()}`
}
let config = {};
switch(type) {
case 'kubernetes':
@ -14,17 +27,45 @@ ${
case 'oidc':
config = {
OIDCDiscoveryURL: `https://${fake.internet.ip()}:8443`,
OIDCDiscoveryCACert: `-----BEGIN CERTIFICATE-----${fake.internet.password(1357)}-----END CERTIFICATE-----`,
OIDCClientID: `${fake.hacker.noun()}-ID`,
OIDCClientSecret: `${fake.hacker.noun()}-secret`,
BoundAudiences: ["aud_example_0", "aud_example_1"],
OIDCScopes: ["scope_01", "scope_02", "scope_03"],
JWTSupportedAlgs: ["RS256", "RS257"],
VerboseOIDCLogging: true,
AllowedRedirectURIs: ["http://example.com/example-1", "http://example.com/example-2", "http://example.com/example-3"],
ClaimMappings: claimMappings,
ListClaimMappings: listClaimMappings
};
break;
case 'jwt':
config = {
JWTValidationPubKeys: `-----BEGIN CERTIFICATE-----${fake.internet.password(1357)}-----END CERTIFICATE-----`,
JWKSURL: `https://${fake.internet.ip()}:8443`,
OIDCDiscoveryURL: `https://${fake.internet.ip()}:8443`,
JWTSupportedAlgs: ["RS256", "RS257"],
BoundAudiences: ["aud_example_0", "aud_example_1"],
BoundIssuer: `${fake.hacker.noun()}-issuer`,
ExpirationLeeway: `${fake.random.number({min: 0, max: 60})}`,
NotBeforeLeeway: `${fake.random.number({min: 0, max: 60})}`,
ClockSkewLeeway: `${fake.random.number({min: 0, max: 60})}`,
ClaimMappings: claimMappings,
ListClaimMappings: listClaimMappings
};
break;
}
switch(sourceType) {
case 'JWTValidationPubKeys':
config.JWTValidationPubKeys = `-----BEGIN CERTIFICATE-----${fake.internet.password(1357)}-----END CERTIFICATE-----`;
break;
case 'JWKSURL':
config.JWKSURL = `https://${fake.internet.ip()}:8443`;
config.JWKSCACert = `-----BEGIN CERTIFICATE-----${fake.internet.password(1357)}-----END CERTIFICATE-----`;
break;
case 'OIDCDiscoveryURL':
config.OIDCDiscoveryURL = `https://${fake.internet.ip()}:8443`;
break;
}
return `{
"Name": "${location.pathname.get(3)}",
"Namespace": "${

View File

@ -17,7 +17,11 @@
${typeof location.search.ns !== 'undefined' ? `
"Namespace": "${location.search.ns}",
` : ``}
${env('CONSUL_NSPACES_ENABLE', false) ? `
"Type": "${fake.helpers.randomize(['kubernetes', 'jwt', 'oidc'])}",
` : `
"Type": "${fake.helpers.randomize(['kubernetes', 'jwt'])}",
`}
"Description": "${fake.lorem.sentence()}",
${i%2 ? `
"DisplayName": "${fake.hacker.noun()}-${i}",

View File

@ -0,0 +1,16 @@
@setupApplicationTest
Feature: dc / acls / auth-methods / navigation
Scenario: Clicking a auth-method in the listing and back again
Given 1 datacenter model with the value "dc-1"
And 3 authMethod models
When I visit the authMethods page for yaml
---
dc: dc-1
---
Then the url should be /dc-1/acls/auth-methods
And the title should be "Auth Methods - Consul"
Then I see 3 authMethod models
When I click authMethod on the authMethods
And I click "[data-test-back]"
Then the url should be /dc-1/acls/auth-methods

View File

@ -0,0 +1,10 @@
import steps from '../../../steps';
// step definitions that are shared between features should be moved to the
// tests/acceptance/steps/steps.js file
export default function(assert) {
return steps(assert).then('I should find a file', function() {
assert.ok(true, this.step);
});
}

View File

@ -93,7 +93,7 @@ const emptyState = emptyStateFactory(isPresent);
const consulHealthCheckList = consulHealthCheckListFactory(collection, text);
const consulUpstreamInstanceList = consulUpstreamInstanceListFactory(collection, text);
const consulAuthMethodList = consulAuthMethodListFactory(collection, text);
const consulAuthMethodList = consulAuthMethodListFactory(collection, clickable, text);
const consulIntentionList = consulIntentionListFactory(
collection,
clickable,

View File

@ -180,3 +180,35 @@ components:
asc: Ascending
desc: Descending
models:
auth-method:
Description: Description
DisplayName: Display name
TokenLocality: Token locality
Type: Type
MaxTokenTTL: Maximum token TTL
Config:
Host: Host
CACert: CA Cert
ServiceAccountJWT: Service account JSON Web Token
JWKSURL: JWKS URL
JWKSCACert: JWKS CA Cert
JWTValidationPubKeys: JWT validation pub keys
OIDCDiscoveryURL: Discovery URL
JWTSupportedAlgs: JWT supported algorithms
BoundAudiences: Bound audiences
BoundIssuer: Bound issuer
ExpirationLeeway: Expiration leeway
NotBeforeLeeway: Not before leeway
ClockSkewLeeway: Clock skew leeway
OIDCDiscoveryCACert: OIDC discovery CA cert
OIDCClientID: Client ID
OIDCClientSecret: Client secret
AllowedRedirectURIs: Allowed redirect URIs
OIDCScopes: OIDC scopes
VerboseOIDCLogging: Verbose OIDC logging
ClaimMappings: Claim Mappings
ListClaimMappings: List Claim Mappings