ui: Support for Node Identities (#8137)

* Add all the new data required for NodeIdentities

* Add potential NodeIdentity to the token list component

* Amend the policy-form/selector to allow node identity creation

* Fix up CSS for radio buttons and select label

* Add node-identity policy template component

* Fix up and add acceptance tests for NodeIndentities

* Make sure policy previews take node identities into account

* Only show certain policy markup if those we have those policies

* Potentially temporarily hide dt's that don't have icons yet
This commit is contained in:
John Cowen 2020-06-23 09:59:43 +01:00 committed by hashicorp-ci
parent 55edf71c81
commit baecab54cb
25 changed files with 237 additions and 75 deletions

View File

@ -50,6 +50,7 @@ export default Adapter.extend({
Description: serialized.Description,
Policies: serialized.Policies,
ServiceIdentities: serialized.ServiceIdentities,
NodeIdentities: serialized.NodeIdentities,
...Namespace(serialized.Namespace),
}}
`;
@ -66,6 +67,7 @@ export default Adapter.extend({
Description: serialized.Description,
Policies: serialized.Policies,
ServiceIdentities: serialized.ServiceIdentities,
NodeIdentities: serialized.NodeIdentities,
...Namespace(serialized.Namespace),
}}
`;

View File

@ -53,6 +53,7 @@ export default Adapter.extend({
Policies: serialized.Policies,
Roles: serialized.Roles,
ServiceIdentities: serialized.ServiceIdentities,
NodeIdentities: serialized.NodeIdentities,
Local: serialized.Local,
...Namespace(serialized.Namespace),
}}
@ -84,6 +85,7 @@ export default Adapter.extend({
Policies: serialized.Policies,
Roles: serialized.Roles,
ServiceIdentities: serialized.ServiceIdentities,
NodeIdentities: serialized.NodeIdentities,
Local: serialized.Local,
...Namespace(serialized.Namespace),
}}

View File

@ -3,7 +3,7 @@
<div role="group">
<fieldset>
<h2>Source</h2>
<label data-test-source-element class="type-text{{if _item.error.SourceName ' has-error'}}">
<label data-test-source-element class="type-select{{if _item.error.SourceName ' has-error'}}">
<span>Source Service</span>
<PowerSelectWithCreate
@options={{_services}}
@ -23,7 +23,7 @@
<em>Search for an existing service, or enter any Service name.</em>
</label>
{{#if (env 'CONSUL_NSPACES_ENABLED')}}
<label data-test-source-nspace class="type-text{{if _item.error.SourceNS ' has-error'}}">
<label data-test-source-nspace class="type-select{{if _item.error.SourceNS ' has-error'}}">
<span>Source Namespace</span>
<PowerSelectWithCreate
@options={{_nspaces}}
@ -46,7 +46,7 @@
</fieldset>
<fieldset>
<h2>Destination</h2>
<label data-test-destination-element class="type-text{{if _item.error.DestinationName ' has-error'}}">
<label data-test-destination-element class="type-select{{if _item.error.DestinationName ' has-error'}}">
<span>Destination Service</span>
<PowerSelectWithCreate
@options={{_services}}
@ -66,7 +66,7 @@
<em>Search for an existing service, or enter any Service name.</em>
</label>
{{#if (env 'CONSUL_NSPACES_ENABLED')}}
<label data-test-destination-nspace class="type-text{{if _item.error.DestinationNS ' has-error'}}">
<label data-test-destination-nspace class="type-select{{if _item.error.DestinationNS ' has-error'}}">
<span>Destination Namespace</span>
<PowerSelectWithCreate
@options={{_nspaces}}

View File

@ -10,7 +10,7 @@
</dd>
</dl>
{{/if}}
<a href={{href-to 'dc.acls.tokens.edit' item.AccessorID}}>{{substr item.AccessorID -8}}</a>
<a href={{href-to 'dc.acls.tokens.edit' item.AccessorID}}>{{substr item.AccessorID -8}}</a>
</BlockSlot>
<BlockSlot @name="details">
<dl>
@ -23,7 +23,9 @@
{{#let (get policies 'management') as |management|}}
{{#if (gt management.length 0)}}
<dl>
<dt>Management</dt>
<dt>
Management
</dt>
<dd>
{{#each (get policies 'management') as |item|}}
<span data-test-policy class={{policy/typeof item}}>{{item.Name}}</span>
@ -32,26 +34,34 @@
</dl>
{{/if}}
{{/let}}
{{#let (get policies 'identities') as |identities|}}
{{#if (gt identities.length 0)}}
<dl>
<dt>Identities</dt>
<dd>
{{#each (append (get policies 'identities')) as |item|}}
<span data-test-policy class={{policy/typeof item}}>Service Identity: {{item.Name}}</span>
{{#each identities as |item|}}
<span data-test-policy class={{policy/typeof item}}>{{if (eq item.template 'service-identity') 'Service' 'Node'}} Identity: {{item.Name}}</span>
{{/each}}
</dd>
</dl>
{{/if}}
{{/let}}
{{#let (append (get policies 'policies') item.Roles) as |policies|}}
{{#if (gt policies.length 0)}}
<dl>
<dt>Rules</dt>
<dd>
{{#if (token/is-legacy item) }}
Legacy tokens have embedded rules.
{{ else }}
{{#each (append (get policies 'policies') item.Roles) as |item|}}
{{#each policies as |item|}}
<span data-test-policy class={{policy/typeof item}}>{{item.Name}}</span>
{{/each}}
{{/if}}
</dd>
</dl>
{{/if}}
{{/let}}
{{/let}}
<dl>
<dt>Description</dt>

View File

@ -0,0 +1,6 @@
node "{{name}}" {
policy = "write"
}
service_prefix "" {
policy = "read"
}

View File

@ -0,0 +1,5 @@
import Component from '@ember/component';
export default Component.extend({
tagName: '',
});

View File

@ -3,11 +3,11 @@
{{#yield-slot name='template'}}
{{else}}
<header>
Policy{{if allowServiceIdentity ' or service identity?' ''}}
Policy{{if allowIdentity ' or identity?' ''}}
</header>
{{#if allowServiceIdentity}}
{{#if allowIdentity}}
<p>
A Service Identity is default policy with a configurable service name. This saves you some time and effort you're using Consul for Connect features.
Identities are default policies with configurable names. They save you some time and effort you're using Consul for Connect features.
</p>
{{! this should use radio-group }}
<div role="radiogroup" class={{if item.error.Type ' has-error'}}>
@ -34,17 +34,38 @@
</label>
<label class="type-text" data-test-rules>
<span>Rules <a href="{{env 'CONSUL_DOCS_URL'}}/guides/acl.html#rule-specification" rel="help noopener noreferrer" target="_blank">(HCL Format)</a></span>
{{#if (eq item.template '') }}
{{#if (eq item.template 'service-identity')}}
<CodeEditor @readonly={{true}} @name={{concat name "[Rules]"}} @syntax="hcl" @oninput={{action "change" (concat name "[Rules]")}}>
{{~component 'service-identity' name=item.Name~}}
</CodeEditor>
{{else if (eq item.template 'node-identity')}}
<CodeEditor @readonly={{true}} @name={{concat name "[Rules]"}} @syntax="hcl" @oninput={{action "change" (concat name "[Rules]")}}>
{{~component 'node-identity' name=item.Name~}}
</CodeEditor>
{{else}}
<CodeEditor @syntax="hcl" @class={{if item.error.Rules "error"}} @name={{concat name "[Rules]"}} @value={{item.Rules}} @onkeyup={{action "change" (concat name "[Rules]")}} />
{{#if item.error.Rules}}
<strong>{{item.error.Rules.validation}}</strong>
{{/if}}
{{else}}
<CodeEditor @readonly={{true}} @name={{concat name "[Rules]"}} @syntax="hcl" @oninput={{action "change" (concat name "[Rules]")}}>
{{~component 'service-identity' name=item.Name~}}
</CodeEditor>
{{/if}}
</label>
{{#if (eq item.template 'node-identity')}}
<DataSource @src="/*/*/datacenters"
@onchange={{action (mut datacenters) value="data"}}
/>
<label class="type-select" data-test-datacenter>
<span>Datacenter</span>
<PowerSelect
@options={{map-by 'Name' datacenters}}
@searchField="Name"
@selected={{or item.Datacenter dc}}
@searchPlaceholder="Type a datacenter name"
@onChange={{action "change" "Datacenter"}} as |Name|>
{{Name}}
</PowerSelect>
</label>
{{else}}
<div class="type-toggle">
<span>Valid datacenters</span>
<label>
@ -52,10 +73,11 @@
<span>All</span>
</label>
</div>
{{#if isScoped }}
{{#if isScoped }}
<DataSource @src="/*/*/datacenters"
@onchange={{action (mut datacenters) value="data"}}
/>
<div class="checkbox-group" role="group">
{{#each datacenters as |dc| }}
<label class="type-checkbox">
@ -72,6 +94,9 @@
{{/if}}
{{/each}}
</div>
{{/if}}
{{/if}}
{{#if (eq item.template '') }}
<label class="type-text">

View File

@ -4,7 +4,7 @@ import { get, set } from '@ember/object';
export default FormComponent.extend({
type: 'policy',
name: 'policy',
allowServiceIdentity: true,
allowIdentity: true,
classNames: ['policy-form'],
isScoped: false,
@ -20,6 +20,10 @@ export default FormComponent.extend({
name: 'Service Identity',
template: 'service-identity',
},
{
name: 'Node Identity',
template: 'node-identity',
},
];
},
actions: {

View File

@ -8,7 +8,7 @@ export default (submitable, cancelable, radiogroup, text) => (
prefix: 'policy',
...submitable(),
...cancelable(),
...radiogroup('template', ['', 'service-identity'], 'policy'),
...radiogroup('template', ['', 'service-identity', 'node-identity'], 'policy'),
rules: {
error: text('[data-test-rules] strong'),
},

View File

@ -50,35 +50,48 @@
<BlockSlot @name="row">
<td class={{policy/typeof item}}>
{{#if item.ID }}
<a href={{href-to 'dc.acls.policies.edit' item.ID}}>{{item.Name}}</a>
<a href={{href-to 'dc.acls.policies.edit' item.ID}}>{{item.Name}}</a>
{{else}}
<a name={{item.Name}}>{{item.Name}}</a>
{{/if}}
</td>
</BlockSlot>
<BlockSlot @name="details">
{{#if (eq item.template '')}}
<DataSource
@src={{concat '/' item.Namespace '/' dc '/policy/' item.ID}}
@onchange={{action (mut loadedItem) value="data"}}
@loading="lazy"
/>
{{/if}}
{{#if (eq item.template '')}}
<DataSource
@src={{concat '/' item.Namespace '/' dc '/policy/' item.ID}}
@onchange={{action (mut loadedItem) value="data"}}
@loading="lazy"
/>
{{/if}}
{{#if (eq item.template 'node-identity')}}
<dl>
<dt>Datacenter:</dt>
<dd>
{{item.Datacenter}}
</dd>
</dl>
{{else}}
<dl>
<dt>Datacenters:</dt>
<dd>
{{join ', ' (policy/datacenters (or loadedItem item))}}
</dd>
</dl>
{{/if}}
<label class="type-text">
<span>Rules <a href="{{env 'CONSUL_DOCS_URL'}}/guides/acl.html#rule-specification" rel="help noopener noreferrer" target="_blank">(HCL Format)</a></span>
{{#if (eq item.template '')}}
<CodeEditor @syntax="hcl" @readonly={{true}} @value={{or loadedItem.Rules item.Rules}} />
{{else}}
<CodeEditor @syntax="hcl" @readonly={{true}}>
{{~component 'service-identity' name=item.Name~}}
</CodeEditor>
{{/if}}
{{#if (eq item.template 'service-identity')}}
<CodeEditor @syntax="hcl" @readonly={{true}}>
{{~component 'service-identity' name=item.Name~}}
</CodeEditor>
{{else if (eq item.template 'node-identity')}}
<CodeEditor @syntax="hcl" @readonly={{true}}>
{{~component 'node-identity' name=item.Name~}}
</CodeEditor>
{{else}}
<CodeEditor @syntax="hcl" @readonly={{true}} @value={{or loadedItem.Rules item.Rules}} />
{{/if}}
</label>
<div>
<ConfirmationDialog @message="Are you sure you want to remove this policy from this token?">

View File

@ -10,7 +10,7 @@ export default ChildSelectorComponent.extend({
repo: service('repository/policy/component'),
name: 'policy',
type: 'policy',
allowServiceIdentity: true,
allowIdentity: true,
classNames: ['policy-selector'],
init: function() {
this._super(...arguments);

View File

@ -3,14 +3,14 @@ import { get } from '@ember/object';
import minimizeModel from 'consul-ui/utils/minimizeModel';
const normalizeServiceIdentities = function(items) {
const normalizeIdentities = function(items, template, name, dc) {
return (items || []).map(function(item) {
const policy = {
template: 'service-identity',
Name: item.ServiceName,
template: template,
Name: item[name],
};
if (typeof item.Datacenters !== 'undefined') {
policy.Datacenters = item.Datacenters;
if (typeof item[dc] !== 'undefined') {
policy[dc] = item[dc];
}
return policy;
});
@ -25,17 +25,17 @@ const normalizePolicies = function(items) {
};
});
};
const serializeServiceIdentities = function(items) {
const serializeIdentities = function(items, template, name, dc) {
return items
.filter(function(item) {
return get(item, 'template') === 'service-identity';
return get(item, 'template') === template;
})
.map(function(item) {
const identity = {
ServiceName: get(item, 'Name'),
[name]: get(item, 'Name'),
};
if (get(item, 'Datacenters')) {
identity.Datacenters = get(item, 'Datacenters');
if (typeof get(item, dc) !== 'undefined') {
identity[dc] = get(item, dc);
}
return identity;
});
@ -51,9 +51,18 @@ export default Mixin.create({
respondForQueryRecord: function(respond, query) {
return this._super(function(cb) {
return respond((headers, body) => {
body.Policies = normalizePolicies(body.Policies).concat(
normalizeServiceIdentities(body.ServiceIdentities)
);
body.Policies = normalizePolicies(body.Policies)
.concat(
normalizeIdentities(
body.ServiceIdentities,
'service-identity',
'ServiceName',
'Datacenters'
)
)
.concat(
normalizeIdentities(body.NodeIdentities, 'node-identity', 'NodeName', 'Datacenter')
);
return cb(headers, body);
});
}, query);
@ -64,9 +73,18 @@ export default Mixin.create({
return cb(
headers,
body.map(function(item) {
item.Policies = normalizePolicies(item.Policies).concat(
normalizeServiceIdentities(item.ServiceIdentities)
);
item.Policies = normalizePolicies(item.Policies)
.concat(
normalizeIdentities(
item.ServiceIdentities,
'service-identity',
'ServiceName',
'Datacenters'
)
)
.concat(
normalizeIdentities(item.NodeIdentities, 'node-identity', 'NodeName', 'Datacenter')
);
return item;
})
);
@ -75,7 +93,18 @@ export default Mixin.create({
},
serialize: function(snapshot, options) {
const data = this._super(...arguments);
data.ServiceIdentities = serializeServiceIdentities(data.Policies);
data.ServiceIdentities = serializeIdentities(
data.Policies,
'service-identity',
'ServiceName',
'Datacenters'
);
data.NodeIdentities = serializeIdentities(
data.Policies,
'node-identity',
'NodeName',
'Datacenter'
);
data.Policies = minimizeModel(serializePolicies(data.Policies));
return data;
},

View File

@ -22,6 +22,11 @@ export default Model.extend({
return [];
},
}),
NodeIdentities: attr({
defaultValue: function() {
return [];
},
}),
// frontend only for ordering where CreateIndex can't be used
CreateTime: attr('date'),
//

View File

@ -39,6 +39,11 @@ export default Model.extend({
return [];
},
}),
NodeIdentities: attr({
defaultValue: function() {
return [];
},
}),
CreateTime: attr('date'),
Hash: attr('string'),
CreateIndex: attr('number'),

View File

@ -15,7 +15,6 @@
}
%radio-group label > span {
margin-left: 1em;
margin-top: 0.2em;
}
%radio-group label,
%radio-group label > span {

View File

@ -13,6 +13,11 @@
.consul-token-list > ul > li:not(:first-child) {
@extend %with-composite-row-intent;
}
/*TODO: This hides the icons-less dt's in the below lists as */
/* they don't have tooltips */
.consul-token-list > ul > li:not(:first-child) dt {
display: none;
}
/* TODO: the service list has a 1px offset */
.consul-service-list li > div:first-child > dl:first-child dd {
margin-top: 1px;

View File

@ -20,6 +20,7 @@ label span {
@extend %form-row;
}
%radio-group label,
%main-content .type-select,
%main-content .type-password,
%main-content .type-text {
@extend %form-element;

View File

@ -30,7 +30,7 @@
<p>
By adding policies to this namespaces, you will apply them to all tokens created within this namespace.
</p>
<PolicySelector @dc={{dc}} @nspace="default" @allowServiceIdentity={{false}} @items={{item.ACLs.PolicyDefaults}} />
<PolicySelector @dc={{dc}} @nspace="default" @allowIdentity={{false}} @items={{item.ACLs.PolicyDefaults}} />
</fieldset>
{{/if}}
<div>

View File

@ -6,6 +6,7 @@ Feature: dc / acls / policies / as many / add existing: Add existing policy
---
Policies: ~
ServiceIdentities: ~
NodeIdentities: ~
---
And 2 policy models from yaml
---

View File

@ -6,6 +6,7 @@ Feature: dc / acls / policies / as many / add new: Add new policy
---
Policies: ~
ServiceIdentities: ~
NodeIdentities: ~
---
When I visit the [Model] page for yaml
---
@ -52,7 +53,6 @@ Feature: dc / acls / policies / as many / add new: Add new policy
Then I fill in the policies.form with yaml
---
Name: New-Service-Identity
Description: New Service Identity Description
---
And I click serviceIdentity on the policies.form
And I click submit on the policies.form
@ -73,6 +73,31 @@ Feature: dc / acls / policies / as many / add new: Add new policy
| token |
| role |
-------------
Scenario: Adding a new node identity as a child of [Model]
Then I fill in the policies.form with yaml
---
Name: New-Node-Identity
---
And I click nodeIdentity on the policies.form
And I click submit on the policies.form
And I submit
Then a PUT request was made to "/v1/acl/[Model]/key?dc=datacenter" from yaml
---
body:
Namespace: @namespace
NodeIdentities:
- NodeName: New-Node-Identity
Datacenter: datacenter
---
Then the url should be /datacenter/acls/[Model]s
And "[data-notification]" has the "notification-update" class
And "[data-notification]" has the "success" class
Where:
-------------
| Model |
| token |
| role |
-------------
Scenario: Adding a new policy as a child of [Model] and getting an error
Given the url "/v1/acl/policy" responds with from yaml
---
@ -113,3 +138,15 @@ Feature: dc / acls / policies / as many / add new: Add new policy
| token |
| role |
-------------
Scenario: Try to edit the Node Identity using the code editor
And I click nodeIdentity on the policies.form
Then I can't fill in the policies.form with yaml
---
Rules: key {}
---
Where:
-------------
| Model |
| token |
| role |
-------------

View File

@ -6,8 +6,8 @@ Feature: dc / acls / policies / as many / list: List
---
ServiceIdentities:
- ServiceName: Service-Identity
- ServiceName: Service-Identity 2
- ServiceName: Service-Identity 3
NodeIdentities:
- NodeName: Node-Identity
Policies:
- Name: Policy
ID: 0000
@ -22,8 +22,8 @@ Feature: dc / acls / policies / as many / list: List
[Model]: key
---
Then the url should be /datacenter/acls/[Model]s/key
# ServiceIdentities turn into policies
Then I see 6 policy models on the policies component
# Identities turn into policies
Then I see 5 policy models on the policies component
Where:
-------------
| Model |

View File

@ -5,6 +5,7 @@ Feature: dc / acls / policies / as many / remove: Remove
And 1 [Model] model from yaml
---
ServiceIdentities: ~
NodeIdentities: ~
Policies:
- Name: Policy
ID: 00000000-0000-0000-0000-000000000001

View File

@ -6,6 +6,7 @@ Feature: dc / acls / tokens / clone: Cloning an ACL token
---
AccessorID: token
SecretID: ee52203d-989f-4f7a-ab5a-2bef004164ca
Legacy: ~
---
Scenario: Cloning an ACL token from the listing page
When I visit the tokens page for yaml

View File

@ -4,16 +4,27 @@ export const createPolicies = function(item) {
template: '',
...item,
};
}).concat(
item.ServiceIdentities.map(function(item) {
const policy = {
template: 'service-identity',
Name: item.ServiceName,
};
if (typeof item.Datacenters !== 'undefined') {
policy.Datacenters = item.Datacenters;
}
return policy;
})
);
})
.concat(
item.ServiceIdentities.map(function(item) {
const policy = {
template: 'service-identity',
Name: item.ServiceName,
};
if (typeof item.Datacenters !== 'undefined') {
policy.Datacenters = item.Datacenters;
}
return policy;
})
)
.concat(
item.NodeIdentities.map(function(item) {
const policy = {
template: 'node-identity',
Name: item.NodeName,
Datacenter: item.Datacenter,
};
return policy;
})
);
};

View File

@ -1211,9 +1211,9 @@
js-yaml "^3.13.1"
"@hashicorp/consul-api-double@^3.0.0":
version "3.0.0"
resolved "https://registry.yarnpkg.com/@hashicorp/consul-api-double/-/consul-api-double-3.0.0.tgz#a1ad94f33e006d01cebf2b22d1f87efef1756ece"
integrity sha512-wB1YgDBN3eOvNWRzSOc2k0eVE7C8YHFC5rJEdNWWTzjOokj6zDr40g2yLiaDMXl21XQs5LE8kNstEe8pf2+JUQ==
version "3.1.0"
resolved "https://registry.yarnpkg.com/@hashicorp/consul-api-double/-/consul-api-double-3.1.0.tgz#6f0cfc32d99b6f0c876d473ff8fdc489b7403904"
integrity sha512-TsRvkBJTzMaXlSyaFT4HU+Phhk+7K2kWPmnuM41cqkHsCLfoTllQ37avQyLYGM/57yfd0ajbI4M6IKoYJnQUWg==
"@hashicorp/ember-cli-api-double@^3.1.0":
version "3.1.0"