ui: Auth Methods - Create Binding Rules tab (#9914)
* Create BindingRule adapter and tests * Create BindingRule serializer and test * Create BindingRule model and repository * Add binding-rules mock data * Create binding-rules router and call endpoint * Create Binding rules tab * Create and use BindingView component * Create empty state for BindingView * Remove binding rule requestForQueryRecord endpoint and tests * Update binding rules selector to be monospaced * Add bind type tooltip * Create and Tabular-dl styling component * Update hr tag global styling * Rename BindingView to BindingList and refactor * Add translations for bind types tooltip info * Remove unused endpoint * Refactor based on review notes
This commit is contained in:
parent
0398833f54
commit
5ce88774b8
|
@ -0,0 +1,14 @@
|
|||
import Adapter from './application';
|
||||
|
||||
export default class BindingRuleAdapter extends Adapter {
|
||||
requestForQuery(request, { dc, ns, authmethod, index, id }) {
|
||||
return request`
|
||||
GET /v1/acl/binding-rules?${{ dc, authmethod }}
|
||||
|
||||
${{
|
||||
...this.formatNspace(ns),
|
||||
index,
|
||||
}}
|
||||
`;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,28 @@
|
|||
<div class="consul-auth-method-binding-list">
|
||||
<h2>{{@item.BindName}}</h2>
|
||||
<dl>
|
||||
<dt class="type">{{t "models.binding-rule.BindType"}}</dt>
|
||||
<dd>
|
||||
{{@item.BindType}}
|
||||
<span>
|
||||
{{#if (eq @item.BindType 'service')}}
|
||||
<Tooltip>
|
||||
{{t "components.consul.auth-method.binding-list.bind-type.service"}}
|
||||
</Tooltip>
|
||||
{{else if (eq @item.BindType 'node')}}
|
||||
<Tooltip>
|
||||
{{t "components.consul.auth-method.binding-list.bind-type.node"}}
|
||||
</Tooltip>
|
||||
{{else if (eq @item.BindType 'role')}}
|
||||
<Tooltip>
|
||||
{{t "components.consul.auth-method.binding-list.bind-type.role"}}
|
||||
</Tooltip>
|
||||
{{/if}}
|
||||
</span>
|
||||
</dd>
|
||||
<dt>{{t "models.binding-rule.Selector"}}</dt>
|
||||
<dd><code>{{@item.Selector}}</code></dd>
|
||||
<dt>{{t "models.binding-rule.Description"}}</dt>
|
||||
<dd>{{@item.Description}}</dd>
|
||||
</dl>
|
||||
</div>
|
|
@ -1,14 +1,14 @@
|
|||
.consul-consul-auth-method-view-list ul {
|
||||
// List
|
||||
.consul-auth-method-list ul {
|
||||
.locality::before {
|
||||
@extend %with-public-default-mask, %as-pseudo;
|
||||
margin-right: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
// View
|
||||
.consul-auth-method-view {
|
||||
margin-bottom: 32px;
|
||||
> hr {
|
||||
background-color: var(--gray-200);
|
||||
}
|
||||
section {
|
||||
@extend %p1;
|
||||
width: 100%;
|
||||
|
@ -32,43 +32,26 @@
|
|||
}
|
||||
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;
|
||||
}
|
||||
@extend %tabular-dl;
|
||||
}
|
||||
}
|
||||
|
||||
// Binding List
|
||||
.consul-auth-method-binding-list {
|
||||
p {
|
||||
margin-bottom: 4px !important;
|
||||
}
|
||||
h2 {
|
||||
@extend %h200;
|
||||
padding-bottom: 12px;
|
||||
}
|
||||
dl {
|
||||
@extend %tabular-dl;
|
||||
}
|
||||
code {
|
||||
background-color: var(--gray-050);
|
||||
padding: 0 12px;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -24,7 +24,7 @@
|
|||
%tooltip-content {
|
||||
@extend %p3;
|
||||
padding: 12px;
|
||||
max-width: 192px;
|
||||
max-width: 224px;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
|
|
|
@ -0,0 +1,19 @@
|
|||
import Model, { attr } from '@ember-data/model';
|
||||
|
||||
export const PRIMARY_KEY = 'uid';
|
||||
export const SLUG_KEY = 'ID';
|
||||
|
||||
export default class BindingRule extends Model {
|
||||
@attr('string') uid;
|
||||
@attr('string') ID;
|
||||
|
||||
@attr('string') Datacenter;
|
||||
@attr('string') Namespace;
|
||||
@attr('string', { defaultValue: () => '' }) Description;
|
||||
@attr('string') AuthMethod;
|
||||
@attr('string', { defaultValue: () => '' }) Selector;
|
||||
@attr('string') BindType;
|
||||
@attr('string') BindName;
|
||||
@attr('number') CreateIndex;
|
||||
@attr('number') ModifyIndex;
|
||||
}
|
|
@ -196,6 +196,9 @@ export const routes = {
|
|||
'auth-method': {
|
||||
_options: { path: '/auth-method' },
|
||||
},
|
||||
'binding-rules': {
|
||||
_options: { path: '/binding-rules' },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
|
|
@ -4,16 +4,25 @@ import { hash } from 'rsvp';
|
|||
|
||||
export default class ShowRoute extends SingleRoute {
|
||||
@service('repository/auth-method') repo;
|
||||
@service('repository/binding-rule') bindingRuleRepo;
|
||||
|
||||
model(params) {
|
||||
const dc = this.modelFor('dc').dc;
|
||||
const nspace = this.modelFor('nspace').nspace.substr(1);
|
||||
|
||||
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),
|
||||
dc: dc.Name,
|
||||
ns: nspace,
|
||||
}),
|
||||
bindingRules: this.bindingRuleRepo.findAllByDatacenter({
|
||||
ns: nspace,
|
||||
dc: dc.Name,
|
||||
authmethod: params.id,
|
||||
}),
|
||||
},
|
||||
});
|
||||
|
|
|
@ -0,0 +1,16 @@
|
|||
import Route from 'consul-ui/routing/route';
|
||||
|
||||
export default class BindingRulesRoute 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);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
import Serializer from './application';
|
||||
import { PRIMARY_KEY, SLUG_KEY } from 'consul-ui/models/binding-rule';
|
||||
|
||||
export default class BindingRuleSerializer extends Serializer {
|
||||
primaryKey = PRIMARY_KEY;
|
||||
slugKey = SLUG_KEY;
|
||||
}
|
|
@ -0,0 +1,32 @@
|
|||
import RepositoryService from 'consul-ui/services/repository';
|
||||
import statusFactory from 'consul-ui/utils/acls-status';
|
||||
import isValidServerErrorFactory from 'consul-ui/utils/http/acl/is-valid-server-error';
|
||||
import { PRIMARY_KEY, SLUG_KEY } from 'consul-ui/models/binding-rule';
|
||||
import dataSource from 'consul-ui/decorators/data-source';
|
||||
|
||||
const isValidServerError = isValidServerErrorFactory();
|
||||
const status = statusFactory(isValidServerError, Promise);
|
||||
const MODEL_NAME = 'binding-rule';
|
||||
|
||||
export default class BindingRuleService extends RepositoryService {
|
||||
getModelName() {
|
||||
return MODEL_NAME;
|
||||
}
|
||||
|
||||
getPrimaryKey() {
|
||||
return PRIMARY_KEY;
|
||||
}
|
||||
|
||||
getSlugKey() {
|
||||
return SLUG_KEY;
|
||||
}
|
||||
|
||||
@dataSource('/:ns/:dc/binding-rules')
|
||||
async findAllByDatacenter() {
|
||||
return super.findAllByDatacenter(...arguments);
|
||||
}
|
||||
|
||||
status(obj) {
|
||||
return status(obj);
|
||||
}
|
||||
}
|
|
@ -104,4 +104,5 @@ html {
|
|||
hr {
|
||||
height: 1px;
|
||||
margin: 1.5rem 0;
|
||||
background-color: var(--gray-200);
|
||||
}
|
||||
|
|
|
@ -33,6 +33,7 @@
|
|||
@import './components/more-popover-menu';
|
||||
@import './components/definition-table';
|
||||
@import './components/radio-card';
|
||||
@import './components/tabular-dl';
|
||||
|
||||
/**/
|
||||
|
||||
|
|
|
@ -0,0 +1,2 @@
|
|||
@import './layout';
|
||||
@import './skin';
|
|
@ -0,0 +1,38 @@
|
|||
%tabular-dl {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
> dt:last-of-type,
|
||||
> dd:last-of-type {
|
||||
border-bottom: 1px solid !important;
|
||||
}
|
||||
dt,
|
||||
dd {
|
||||
padding: 12px 0;
|
||||
margin: 0;
|
||||
border-top: 1px solid !important;
|
||||
}
|
||||
dt {
|
||||
width: 20%;
|
||||
}
|
||||
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;
|
||||
}
|
||||
dt.check + dd {
|
||||
padding-top: 16px;
|
||||
}
|
||||
dt.type + dd span::before {
|
||||
margin-left: 4px;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,21 @@
|
|||
%tabular-dl {
|
||||
> dt:last-of-type,
|
||||
> dd:last-of-type {
|
||||
border-color: var(--gray-300) !important;
|
||||
}
|
||||
dt,
|
||||
dd {
|
||||
border-color: var(--gray-300) !important;
|
||||
color: $black !important;
|
||||
}
|
||||
dt {
|
||||
font-weight: $typo-weight-bold;
|
||||
}
|
||||
dd .copy-button button::before {
|
||||
background-color: $black;
|
||||
}
|
||||
dt.type + dd span::before {
|
||||
@extend %with-info-circle-outline-mask, %as-pseudo;
|
||||
background-color: var(--gray-500);
|
||||
}
|
||||
}
|
|
@ -27,6 +27,7 @@
|
|||
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"))
|
||||
(hash label="Binding rules" href=(href-to "dc.acls.auth-methods.show.binding-rules") selected=(is-href "dc.acls.auth-methods.show.binding-rules"))
|
||||
)
|
||||
}}/>
|
||||
</BlockSlot>
|
||||
|
|
|
@ -0,0 +1,28 @@
|
|||
|
||||
<div class="tab-section">
|
||||
{{#if (gt bindingRules.length 0)}}
|
||||
<p>Binding rules allow an operator to express a systematic way of automatically linking roles and service identities to newly created tokens without operator intervention.
|
||||
</p>
|
||||
<p>Successful authentication with an auth method returns a set of trusted identity attributes corresponding to the authenticated identity. Those attributes are matched against all configured binding rules for that auth method to determine what privileges to grant the Consul ACL token it will ultimately create.
|
||||
</p>
|
||||
<hr />
|
||||
{{#each bindingRules as |item|}}
|
||||
<Consul::AuthMethod::BindingList @item={{item}} />
|
||||
<hr />
|
||||
{{/each}}
|
||||
{{else}}
|
||||
<EmptyState>
|
||||
<BlockSlot @name="header">
|
||||
<h2>No binding rules</h2>
|
||||
</BlockSlot>
|
||||
<BlockSlot @name="body">
|
||||
<p>Binding rules allow an operator to express a systematic way of automatically linking roles and service identities to newly created tokens without operator intervention.</p>
|
||||
</BlockSlot>
|
||||
<BlockSlot @name="actions">
|
||||
<li class="docs-link">
|
||||
<a href="{{env 'CONSUL_DOCS_API_URL'}}/acl/binding-rules" rel="noopener noreferrer" target="_blank">Read the documentation</a>
|
||||
</li>
|
||||
</BlockSlot>
|
||||
</EmptyState>
|
||||
{{/if}}
|
||||
</div>
|
|
@ -0,0 +1,30 @@
|
|||
[
|
||||
${
|
||||
range(
|
||||
env(
|
||||
'CONSUL_BINDING_RULE_COUNT',
|
||||
Math.floor(
|
||||
(
|
||||
Math.random() * env('CONSUL_BINDING_RULE_MAX', 10)
|
||||
) + parseInt(env('CONSUL_BINDING_RULE_MIN', 1))
|
||||
)
|
||||
)
|
||||
).map(
|
||||
function(item, i) {
|
||||
return `
|
||||
{
|
||||
"ID": "${fake.random.uuid()}",
|
||||
"Description": "${fake.lorem.sentence()}",
|
||||
"AuthMethod": "${fake.hacker.noun()}-${i}",
|
||||
"Selector": "serviceaccount.namespace==${fake.hacker.noun()} and serviceaccount.name!=${fake.hacker.noun()}",
|
||||
"BindType": "${fake.helpers.randomize(['service', 'node', 'role'])}",
|
||||
"BindName": "${fake.hacker.noun()}-${i}",
|
||||
"Namespace": "${location.search.ns}",
|
||||
"CreateIndex": ${fake.random.number()},
|
||||
"ModifyIndex": 10
|
||||
}
|
||||
`
|
||||
}
|
||||
)
|
||||
}
|
||||
]
|
|
@ -0,0 +1,38 @@
|
|||
import { module, test } from 'qunit';
|
||||
import { setupTest } from 'ember-qunit';
|
||||
import getNspaceRunner from 'consul-ui/tests/helpers/get-nspace-runner';
|
||||
|
||||
const nspaceRunner = getNspaceRunner('binding-rule');
|
||||
module('Integration | Adapter | binding-rule', function(hooks) {
|
||||
setupTest(hooks);
|
||||
const dc = 'dc-1';
|
||||
test('requestForQuery returns the correct url/method', function(assert) {
|
||||
const adapter = this.owner.lookup('adapter:binding-rule');
|
||||
const client = this.owner.lookup('service:client/http');
|
||||
const expected = `GET /v1/acl/binding-rules?dc=${dc}`;
|
||||
const actual = adapter.requestForQuery(client.requestParams.bind(client), {
|
||||
dc: dc,
|
||||
});
|
||||
assert.equal(`${actual.method} ${actual.url}`, expected);
|
||||
});
|
||||
test('requestForQuery returns the correct body', function(assert) {
|
||||
return nspaceRunner(
|
||||
(adapter, serializer, client) => {
|
||||
return adapter.requestForQuery(client.body, {
|
||||
dc: dc,
|
||||
ns: 'team-1',
|
||||
index: 1,
|
||||
});
|
||||
},
|
||||
{
|
||||
index: 1,
|
||||
ns: 'team-1',
|
||||
},
|
||||
{
|
||||
index: 1,
|
||||
},
|
||||
this,
|
||||
assert
|
||||
);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,39 @@
|
|||
import { module, test } from 'qunit';
|
||||
import { setupTest } from 'ember-qunit';
|
||||
import { get } from 'consul-ui/tests/helpers/api';
|
||||
module('Integration | Serializer | binding-rule', function(hooks) {
|
||||
setupTest(hooks);
|
||||
const dc = 'dc-1';
|
||||
const undefinedNspace = 'default';
|
||||
[undefinedNspace, 'team-1', undefined].forEach(nspace => {
|
||||
test(`respondForQuery returns the correct data for list endpoint when nspace is ${nspace}`, function(assert) {
|
||||
const serializer = this.owner.lookup('serializer:binding-rule');
|
||||
const request = {
|
||||
url: `/v1/acl/binding-rules?dc=${dc}${
|
||||
typeof nspace !== 'undefined' ? `&ns=${nspace}` : ``
|
||||
}`,
|
||||
};
|
||||
return get(request.url).then(function(payload) {
|
||||
const expected = payload.map(item =>
|
||||
Object.assign({}, item, {
|
||||
Datacenter: dc,
|
||||
Namespace: item.Namespace || undefinedNspace,
|
||||
uid: `["${item.Namespace || undefinedNspace}","${dc}","${item.ID}"]`,
|
||||
})
|
||||
);
|
||||
const actual = serializer.respondForQuery(
|
||||
function(cb) {
|
||||
const headers = {};
|
||||
const body = payload;
|
||||
return cb(headers, body);
|
||||
},
|
||||
{
|
||||
dc: dc,
|
||||
ns: nspace,
|
||||
}
|
||||
);
|
||||
assert.deepEqual(actual, expected);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,12 @@
|
|||
import { module, test } from 'qunit';
|
||||
import { setupTest } from 'ember-qunit';
|
||||
|
||||
module('Unit | Adapter | binding-rule', function(hooks) {
|
||||
setupTest(hooks);
|
||||
|
||||
// Replace this with your real tests.
|
||||
test('it exists', function(assert) {
|
||||
let adapter = this.owner.lookup('adapter:binding-rule');
|
||||
assert.ok(adapter);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,23 @@
|
|||
import { module, test } from 'qunit';
|
||||
import { setupTest } from 'ember-qunit';
|
||||
|
||||
module('Unit | Serializer | binding-rule', function(hooks) {
|
||||
setupTest(hooks);
|
||||
|
||||
// Replace this with your real tests.
|
||||
test('it exists', function(assert) {
|
||||
let store = this.owner.lookup('service:store');
|
||||
let serializer = store.serializerFor('binding-rule');
|
||||
|
||||
assert.ok(serializer);
|
||||
});
|
||||
|
||||
test('it serializes records', function(assert) {
|
||||
let store = this.owner.lookup('service:store');
|
||||
let record = store.createRecord('binding-rule', {});
|
||||
|
||||
let serializedRecord = record.serialize();
|
||||
|
||||
assert.ok(serializedRecord);
|
||||
});
|
||||
});
|
|
@ -143,6 +143,11 @@ components:
|
|||
options:
|
||||
local: Creates local tokens
|
||||
global: Creates global tokens
|
||||
binding-list:
|
||||
bind-type:
|
||||
service: The bind name value is used as an ACLServiceIdentity.ServiceName field in the token that is created.
|
||||
node: The bind name value is used as an ACLNodeIdentity.NodeName field in the token that is created.
|
||||
role: The bind name value is used as an RoleLink.Name field in the token that is created.
|
||||
kv:
|
||||
search-bar:
|
||||
kind:
|
||||
|
@ -209,6 +214,11 @@ models:
|
|||
VerboseOIDCLogging: Verbose OIDC logging
|
||||
ClaimMappings: Claim Mappings
|
||||
ListClaimMappings: List Claim Mappings
|
||||
binding-rule:
|
||||
BindType: Type
|
||||
Description: Description
|
||||
Selector: Selector
|
||||
|
||||
|
||||
|
||||
|
||||
|
|
Loading…
Reference in New Issue