mirror of
https://github.com/hashicorp/vault.git
synced 2025-11-29 06:31:10 +01:00
UI: Add example modal to policy form (#21583)
* created new JsonTemplate component * used JsonTemplate in modal PolicyTemplate to replace code there * renamed component and fixed when the editor content shows up * changed PolicyForm to render example modal only conditionally. added desription to policy-example.js * fixed bug in policy-example.js & edited description of that file, removed functionality from policy-template.js (it is already in policy-example.js) * changed margin on text to better match Figma design, added example modal for when editing a policy * added tests for PolicyExample in policy-example-tests * added PolicyForm tests for (1) cancelling the creation/edit of policy and (2) properly rendering the policy example modal * add changelog * clean up code by removing unnecessary comments * changed a conditional in policy-form.hbs for better readability (Kianna's comment) * fixed description in policy-example.js, changed wording for RGP example, changed wording in policy-form-test.js * added 2 more asserts in policy-form-test.js. Changed some naming for selectors in the test file * added EGP policy to PolicyExample component, moved some functionality from .hbs to .js file and vise versa * added tests to policy-exammple-test.js and policy-form-test.js to account for new EGP policy * simplified all PolicyExample tests in policy-exmaple-test.js * removed beforeEach hook in policy-exmaple-test.js
This commit is contained in:
parent
a71c174e79
commit
a4f67a6b2b
3
changelog/21583.txt
Normal file
3
changelog/21583.txt
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
```release-note:feature
|
||||||
|
ui: add example modal to policy form
|
||||||
|
```
|
||||||
@ -27,37 +27,7 @@
|
|||||||
</nav>
|
</nav>
|
||||||
{{/if}}
|
{{/if}}
|
||||||
{{#if this.showExamplePolicy}}
|
{{#if this.showExamplePolicy}}
|
||||||
<div class="has-bottom-margin-s">
|
<PolicyExample @policyType={{this.policy.policyType}} />
|
||||||
{{#if (eq this.policy.policyType "acl")}}
|
|
||||||
<p>
|
|
||||||
ACL Policies are written in Hashicorp Configuration Language (
|
|
||||||
<ExternalLink @href="https://github.com/hashicorp/hcl">HCL</ExternalLink>
|
|
||||||
) or JSON and describe which paths in Vault a user or machine is allowed to access. Here is an example policy:
|
|
||||||
</p>
|
|
||||||
{{else}}
|
|
||||||
<p class="has-bottom-margin-s">
|
|
||||||
Role Governing Policies (RGPs) are tied to client tokens or identities which is similar to
|
|
||||||
<DocLink @path="/vault/tutorials/policies/policies">ACL policies</DocLink>. They use
|
|
||||||
<DocLink @path="/vault/docs/enterprise/sentinel">Sentinel</DocLink>
|
|
||||||
as a language framework to enable fine-grained policy decisions.
|
|
||||||
</p>
|
|
||||||
<p>
|
|
||||||
Here is an example policy that uses RGP to restrict access to the
|
|
||||||
<code class="tag is-marginless is-paddingless">admin</code>
|
|
||||||
policy such that a user named James or has the
|
|
||||||
<code class="tag is-marginless is-paddingless">Team Lead</code>
|
|
||||||
role can manage the
|
|
||||||
<code class="tag is-marginless is-paddingless">admin</code>
|
|
||||||
policy:
|
|
||||||
</p>
|
|
||||||
{{/if}}
|
|
||||||
</div>
|
|
||||||
<JsonEditor
|
|
||||||
@value={{get this.policyTemplates this.policy.policyType}}
|
|
||||||
@mode="ruby"
|
|
||||||
@readOnly={{true}}
|
|
||||||
@showToolbar={{true}}
|
|
||||||
/>
|
|
||||||
{{else}}
|
{{else}}
|
||||||
<Select
|
<Select
|
||||||
@name="policyType"
|
@name="policyType"
|
||||||
|
|||||||
@ -37,39 +37,6 @@ export default class PolicyTemplate extends Component {
|
|||||||
{ label: 'Role Governing Policy', value: 'rgp', isDisabled: !this.version.hasSentinel },
|
{ label: 'Role Governing Policy', value: 'rgp', isDisabled: !this.version.hasSentinel },
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
// formatting here is purposeful so that whitespace renders correctly in JsonEditor
|
|
||||||
policyTemplates = {
|
|
||||||
acl: `
|
|
||||||
# Grant 'create', 'read' , 'update', and ‘list’ permission
|
|
||||||
# to paths prefixed by 'secret/*'
|
|
||||||
path "secret/*" {
|
|
||||||
capabilities = [ "create", "read", "update", "list" ]
|
|
||||||
}
|
|
||||||
|
|
||||||
# Even though we allowed secret/*, this line explicitly denies
|
|
||||||
# secret/super-secret. This takes precedence.
|
|
||||||
path "secret/super-secret" {
|
|
||||||
capabilities = ["deny"]
|
|
||||||
}
|
|
||||||
`,
|
|
||||||
rgp: `
|
|
||||||
# Import strings library that exposes common string operations
|
|
||||||
import "strings"
|
|
||||||
|
|
||||||
# Conditional rule (precond) checks the incoming request endpoint
|
|
||||||
# targeted to sys/policies/acl/admin
|
|
||||||
precond = rule {
|
|
||||||
strings.has_prefix(request.path, "sys/policies/admin")
|
|
||||||
}
|
|
||||||
|
|
||||||
# Vault checks to see if the request was made by an entity
|
|
||||||
# named James Thomas or Team Lead role defined as its metadata
|
|
||||||
main = rule when precond {
|
|
||||||
identity.entity.metadata.role is "Team Lead" or
|
|
||||||
identity.entity.name is "James Thomas"
|
|
||||||
}
|
|
||||||
`,
|
|
||||||
};
|
|
||||||
|
|
||||||
@action
|
@action
|
||||||
setPolicyType(type) {
|
setPolicyType(type) {
|
||||||
|
|||||||
@ -42,7 +42,6 @@
|
|||||||
{{else}}
|
{{else}}
|
||||||
<JsonEditor
|
<JsonEditor
|
||||||
@title="Policy"
|
@title="Policy"
|
||||||
@helpText="You can use Alt+Tab (Option+Tab on MacOS) in the code editor to skip to the next field"
|
|
||||||
@showToolbar={{false}}
|
@showToolbar={{false}}
|
||||||
@value={{@model.policy}}
|
@value={{@model.policy}}
|
||||||
@valueUpdated={{action (mut @model.policy)}}
|
@valueUpdated={{action (mut @model.policy)}}
|
||||||
@ -55,7 +54,6 @@
|
|||||||
{{! EDITING - no file upload toggle}}
|
{{! EDITING - no file upload toggle}}
|
||||||
<JsonEditor
|
<JsonEditor
|
||||||
@title="Policy"
|
@title="Policy"
|
||||||
@helpText="You can use Alt+Tab (Option+Tab on MacOS) in the code editor to skip to the next field"
|
|
||||||
@value={{@model.policy}}
|
@value={{@model.policy}}
|
||||||
@valueUpdated={{action (mut @model.policy)}}
|
@valueUpdated={{action (mut @model.policy)}}
|
||||||
@mode="ruby"
|
@mode="ruby"
|
||||||
@ -63,13 +61,26 @@
|
|||||||
data-test-policy-editor
|
data-test-policy-editor
|
||||||
/>
|
/>
|
||||||
{{/if}}
|
{{/if}}
|
||||||
</div>
|
<div class="has-top-margin-xs">
|
||||||
{{#each @model.additionalAttrs as |attr|}}
|
<span class="is-size-9 has-text-grey has-bottom-margin-l">
|
||||||
<FormField data-test-field={{true}} @attr={{attr}} @model={{@model}} />
|
You can use Alt+Tab (Option+Tab on MacOS) in the code editor to skip to the next field.
|
||||||
{{/each}}
|
</span>
|
||||||
</div>
|
{{! Only renders button (and modal) if not already in the "create policy" modal }}
|
||||||
<div class="has-bottom-margin-m">
|
{{#if @renderPolicyExampleModal}}
|
||||||
<p>
|
<span class="is-size-9 has-text-grey has-bottom-margin-l">
|
||||||
|
See
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="text-button has-text-info"
|
||||||
|
{{on "click" (fn (mut this.showTemplateModal))}}
|
||||||
|
data-test-policy-example-button
|
||||||
|
>
|
||||||
|
example template
|
||||||
|
</button>.
|
||||||
|
</span>
|
||||||
|
{{! Only renders more information if already in the "create policy" modal }}
|
||||||
|
{{else}}
|
||||||
|
<p class="has-top-margin-l">
|
||||||
More information about
|
More information about
|
||||||
{{uppercase @model.policyType}}
|
{{uppercase @model.policyType}}
|
||||||
policies can be found
|
policies can be found
|
||||||
@ -83,6 +94,12 @@
|
|||||||
here.
|
here.
|
||||||
</DocLink>
|
</DocLink>
|
||||||
</p>
|
</p>
|
||||||
|
{{/if}}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{{#each @model.additionalAttrs as |attr|}}
|
||||||
|
<FormField data-test-field={{true}} @attr={{attr}} @model={{@model}} />
|
||||||
|
{{/each}}
|
||||||
</div>
|
</div>
|
||||||
<div class="field is-grouped box is-fullwidth is-bottomless">
|
<div class="field is-grouped box is-fullwidth is-bottomless">
|
||||||
<div class="control">
|
<div class="control">
|
||||||
@ -106,3 +123,26 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
{{! SAMPLE POLICY MODAL. Only renders modal if not already in create policy modal }}
|
||||||
|
{{#if @renderPolicyExampleModal}}
|
||||||
|
<Modal
|
||||||
|
@title="Example {{uppercase @model.policyType}} Policy"
|
||||||
|
@onClose={{fn (mut this.showTemplateModal) false}}
|
||||||
|
@isActive={{this.showTemplateModal}}
|
||||||
|
@showCloseButton={{true}}
|
||||||
|
data-test-policy-example-modal
|
||||||
|
>
|
||||||
|
<section class="modal-card-body">
|
||||||
|
{{! code-mirror modifier does not render value initially until focus event fires }}
|
||||||
|
{{! wait until the Modal is rendered and then show the PolicyExample (contains JsonEditor) }}
|
||||||
|
{{#if this.showTemplateModal}}
|
||||||
|
<PolicyExample @policyType={{@model.policyType}} />
|
||||||
|
{{/if}}
|
||||||
|
</section>
|
||||||
|
<div class="modal-card-head has-border-top-light">
|
||||||
|
<button type="button" class="button" {{on "click" (fn (mut this.showTemplateModal) false)}} data-test-close-modal>
|
||||||
|
Close
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</Modal>
|
||||||
|
{{/if}}
|
||||||
@ -19,11 +19,13 @@ import { tracked } from '@glimmer/tracking';
|
|||||||
* @model={{this.model}}
|
* @model={{this.model}}
|
||||||
* @onSave={{transition-to "vault.cluster.policy.show" this.model.policyType this.model.name}}
|
* @onSave={{transition-to "vault.cluster.policy.show" this.model.policyType this.model.name}}
|
||||||
* @onCancel={{transition-to "vault.cluster.policies.index"}}
|
* @onCancel={{transition-to "vault.cluster.policies.index"}}
|
||||||
|
* @renderPolicyExampleModal={{true}}
|
||||||
* />
|
* />
|
||||||
* ```
|
* ```
|
||||||
* @callback onCancel - callback triggered when cancel button is clicked
|
* @callback onCancel - callback triggered when cancel button is clicked
|
||||||
* @callback onSave - callback triggered when save button is clicked. Passes saved model
|
* @callback onSave - callback triggered when save button is clicked. Passes saved model
|
||||||
* @param {object} model - ember data model from createRecord
|
* @param {object} model - ember data model from createRecord
|
||||||
|
* @param {boolean} renderPolicyExampleModal - whether or not the policy form should render the modal containing the policy example
|
||||||
*/
|
*/
|
||||||
|
|
||||||
export default class PolicyFormComponent extends Component {
|
export default class PolicyFormComponent extends Component {
|
||||||
@ -31,6 +33,7 @@ export default class PolicyFormComponent extends Component {
|
|||||||
|
|
||||||
@tracked errorBanner = '';
|
@tracked errorBanner = '';
|
||||||
@tracked showFileUpload = false;
|
@tracked showFileUpload = false;
|
||||||
|
@tracked showTemplateModal = false;
|
||||||
|
|
||||||
@task
|
@task
|
||||||
*save(event) {
|
*save(event) {
|
||||||
|
|||||||
@ -21,4 +21,5 @@
|
|||||||
@model={{this.model}}
|
@model={{this.model}}
|
||||||
@onSave={{transition-to "vault.cluster.policy.show" this.model.policyType this.model.name}}
|
@onSave={{transition-to "vault.cluster.policy.show" this.model.policyType this.model.name}}
|
||||||
@onCancel={{transition-to "vault.cluster.policies.index"}}
|
@onCancel={{transition-to "vault.cluster.policies.index"}}
|
||||||
|
@renderPolicyExampleModal={{true}}
|
||||||
/>
|
/>
|
||||||
@ -46,4 +46,5 @@
|
|||||||
@model={{this.model}}
|
@model={{this.model}}
|
||||||
@onSave={{transition-to "vault.cluster.policy.show" this.model.policyType this.model.name}}
|
@onSave={{transition-to "vault.cluster.policy.show" this.model.policyType this.model.name}}
|
||||||
@onCancel={{transition-to "vault.cluster.policy.show" this.model.policyType this.model.name}}
|
@onCancel={{transition-to "vault.cluster.policy.show" this.model.policyType this.model.name}}
|
||||||
|
@renderPolicyExampleModal={{true}}
|
||||||
/>
|
/>
|
||||||
@ -38,6 +38,7 @@
|
|||||||
}}
|
}}
|
||||||
class={{if @readOnly "readonly-codemirror"}}
|
class={{if @readOnly "readonly-codemirror"}}
|
||||||
data-test-component="code-mirror-modifier"
|
data-test-component="code-mirror-modifier"
|
||||||
|
data-test-example-modal-json-text
|
||||||
></div>
|
></div>
|
||||||
|
|
||||||
{{#if @helpText}}
|
{{#if @helpText}}
|
||||||
|
|||||||
50
ui/lib/core/addon/components/policy-example.hbs
Normal file
50
ui/lib/core/addon/components/policy-example.hbs
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
<div class="has-bottom-margin-s">
|
||||||
|
{{#if (eq @policyType "acl")}}
|
||||||
|
<p data-test-example-modal-text="acl">
|
||||||
|
ACL Policies are written in Hashicorp Configuration Language (
|
||||||
|
<ExternalLink @href="https://github.com/hashicorp/hcl">HCL</ExternalLink>
|
||||||
|
) or JSON and describe which paths in Vault a user or machine is allowed to access. Here is an example policy:
|
||||||
|
</p>
|
||||||
|
{{else if (eq @policyType "rgp")}}
|
||||||
|
<p class="has-bottom-margin-s" data-test-example-modal-text="rgp">
|
||||||
|
Role Governing Policies (RGPs) are tied to client tokens or identities which is similar to
|
||||||
|
<DocLink @path="/vault/tutorials/policies/policies">ACL policies</DocLink>. They use
|
||||||
|
<DocLink @path="/vault/docs/enterprise/sentinel">Sentinel</DocLink>
|
||||||
|
as a language framework to enable fine-grained policy decisions.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
Here is an example policy that uses RGP to restrict access to the
|
||||||
|
<code class="tag is-marginless is-paddingless">admin</code>
|
||||||
|
policy such that a user named "James Thomas" or has the
|
||||||
|
<code class="tag is-marginless is-paddingless">Team Lead</code>
|
||||||
|
role can manage the
|
||||||
|
<code class="tag is-marginless is-paddingless">admin</code>
|
||||||
|
policy:
|
||||||
|
</p>
|
||||||
|
{{else}}
|
||||||
|
<p class="has-bottom-margin-s" data-test-example-modal-text="egp">
|
||||||
|
Endpoint Governing Policies (EGPs) are tied to particular paths (e.g.
|
||||||
|
<code class="tag is-marginless is-paddingless">aws/creds/</code>
|
||||||
|
) instead of tokens. They use
|
||||||
|
<ExternalLink @href="https://docs.hashicorp.com/sentinel/language">Sentinel</ExternalLink>
|
||||||
|
as a language to access
|
||||||
|
<DocLink @path="/vault/docs/enterprise/sentinel/properties">properties</DocLink>
|
||||||
|
of the incoming requests.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
Here is an example policy that fulfills the requirement of an incoming request to be performed during the business
|
||||||
|
hours 7:00am to 6:00pm on work days:
|
||||||
|
</p>
|
||||||
|
{{/if}}
|
||||||
|
</div>
|
||||||
|
<JsonEditor @value={{get this.policyTemplates @policyType}} @mode="ruby" @readOnly={{true}} @showToolbar={{true}} />
|
||||||
|
<div class="has-bottom-margin-m has-top-padding-s">
|
||||||
|
<p>
|
||||||
|
More information about
|
||||||
|
{{uppercase @policyType}}
|
||||||
|
policies can be found
|
||||||
|
<DocLink @path={{get this.moreInformationLinks @policyType}} data-test-example-modal-information-link>
|
||||||
|
here.
|
||||||
|
</DocLink>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
98
ui/lib/core/addon/components/policy-example.js
Normal file
98
ui/lib/core/addon/components/policy-example.js
Normal file
@ -0,0 +1,98 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) HashiCorp, Inc.
|
||||||
|
* SPDX-License-Identifier: MPL-2.0
|
||||||
|
*/
|
||||||
|
|
||||||
|
import Component from '@glimmer/component';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @module PolicyExample
|
||||||
|
* The PolicyExample component receives a policy type ('acl', 'rgp', or 'egp') and renders a copyable policy example of
|
||||||
|
* that type using the <JsonEditor> component. Inside a modal, the PolicyExample component must be wrapped in a conditional
|
||||||
|
* (example below), otherwise the <JsonEditor> value won't render until it's focused.
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* <PolicyExample
|
||||||
|
* @policyType={{@model.policyType}}
|
||||||
|
* />
|
||||||
|
*
|
||||||
|
* @example (in modal)
|
||||||
|
* <Modal
|
||||||
|
* @onClose={{fn (mut this.showTemplateModal) false}}
|
||||||
|
* @isActive={{this.showTemplateModal}}
|
||||||
|
* >
|
||||||
|
* <section class="modal-card-body">
|
||||||
|
* {{! code-mirror modifier does not render value initially until focus event fires }}
|
||||||
|
* {{! wait until the Modal is rendered and then show the PolicyExample (contains JsonEditor) }}
|
||||||
|
* {{#if this.showTemplateModal}}
|
||||||
|
* <PolicyExample @policyType={{@model.policyType}}/>
|
||||||
|
* {{/if}}
|
||||||
|
* </section>
|
||||||
|
* <div class="modal-card-head has-border-top-light">
|
||||||
|
* <button type="button" class="button" {{on "click" (fn (mut this.showTemplateModal) false)}} data-test-close-modal>
|
||||||
|
* Close
|
||||||
|
* </button>
|
||||||
|
* </div>
|
||||||
|
* </Modal>
|
||||||
|
* ```
|
||||||
|
* @param {string} policyType - policy type to decide which template to render; can either be "acl" or "rgp"
|
||||||
|
*/
|
||||||
|
|
||||||
|
export default class PolicyExampleComponent extends Component {
|
||||||
|
// formatting here is purposeful so that whitespace renders correctly in JsonEditor
|
||||||
|
policyTemplates = {
|
||||||
|
acl: `
|
||||||
|
# Grant 'create', 'read' , 'update', and ‘list’ permission
|
||||||
|
# to paths prefixed by 'secret/*'
|
||||||
|
path "secret/*" {
|
||||||
|
capabilities = [ "create", "read", "update", "list" ]
|
||||||
|
}
|
||||||
|
|
||||||
|
# Even though we allowed secret/*, this line explicitly denies
|
||||||
|
# secret/super-secret. This takes precedence.
|
||||||
|
path "secret/super-secret" {
|
||||||
|
capabilities = ["deny"]
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
rgp: `
|
||||||
|
# Import strings library that exposes common string operations
|
||||||
|
import "strings"
|
||||||
|
|
||||||
|
# Conditional rule (precond) checks the incoming request endpoint
|
||||||
|
# targeted to sys/policies/acl/admin
|
||||||
|
precond = rule {
|
||||||
|
strings.has_prefix(request.path, "sys/policies/admin")
|
||||||
|
}
|
||||||
|
|
||||||
|
# Vault checks to see if the request was made by an entity
|
||||||
|
# named James Thomas or Team Lead role defined as its metadata
|
||||||
|
main = rule when precond {
|
||||||
|
identity.entity.metadata.role is "Team Lead" or
|
||||||
|
identity.entity.name is "James Thomas"
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
egp: `
|
||||||
|
import "time"
|
||||||
|
|
||||||
|
# Expect requests to only happen during work days (Monday
|
||||||
|
# through Friday) 0 for Sunday and 6 for Saturday
|
||||||
|
workdays = rule {
|
||||||
|
time.now.weekday > 0 and time.now.weekday < 6
|
||||||
|
}
|
||||||
|
|
||||||
|
# Expect requests to only happen during work hours (7:00 am -
|
||||||
|
# 6:00 pm)
|
||||||
|
workhours = rule {
|
||||||
|
time.now.hour > 7 and time.now.hour < 18
|
||||||
|
}
|
||||||
|
main = rule {
|
||||||
|
workdays and workhours
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
};
|
||||||
|
moreInformationLinks = {
|
||||||
|
acl: '/vault/docs/concepts/policies#capabilities',
|
||||||
|
rgp: '/vault/tutorials/policies/sentinel#role-governing-policies-rgps',
|
||||||
|
egp: '/vault/docs/enterprise/sentinel#endpoint-governing-policies-egps',
|
||||||
|
};
|
||||||
|
}
|
||||||
1
ui/lib/core/app/components/policy-example.js
Normal file
1
ui/lib/core/app/components/policy-example.js
Normal file
@ -0,0 +1 @@
|
|||||||
|
export { default } from 'core/components/policy-example';
|
||||||
88
ui/tests/integration/components/policy-example-test.js
Normal file
88
ui/tests/integration/components/policy-example-test.js
Normal file
@ -0,0 +1,88 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) HashiCorp, Inc.
|
||||||
|
* SPDX-License-Identifier: MPL-2.0
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { module, test } from 'qunit';
|
||||||
|
import { setupRenderingTest } from 'vault/tests/helpers';
|
||||||
|
import { render } from '@ember/test-helpers';
|
||||||
|
import { hbs } from 'ember-cli-htmlbars';
|
||||||
|
|
||||||
|
const SELECTORS = {
|
||||||
|
policyText: '[data-test-modal-title]',
|
||||||
|
policyDescription: (type) => `[data-test-example-modal-text=${type}]`,
|
||||||
|
jsonText: '[data-test-example-modal-json-text]',
|
||||||
|
informationLink: '[data-test-example-modal-information-link]',
|
||||||
|
};
|
||||||
|
|
||||||
|
module('Integration | Component | policy-example', function (hooks) {
|
||||||
|
setupRenderingTest(hooks);
|
||||||
|
|
||||||
|
test('it renders the correct paragraph for ACL policy', async function (assert) {
|
||||||
|
await render(hbs`
|
||||||
|
<PolicyExample
|
||||||
|
@policyType="acl"
|
||||||
|
/>
|
||||||
|
`);
|
||||||
|
assert
|
||||||
|
.dom(SELECTORS.policyDescription('acl'))
|
||||||
|
.hasText(
|
||||||
|
'ACL Policies are written in Hashicorp Configuration Language ( HCL ) or JSON and describe which paths in Vault a user or machine is allowed to access. Here is an example policy:'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('it renders the correct paragraph for RGP policy', async function (assert) {
|
||||||
|
await render(hbs`
|
||||||
|
<PolicyExample
|
||||||
|
@policyType="rgp"
|
||||||
|
/>
|
||||||
|
`);
|
||||||
|
assert
|
||||||
|
.dom(SELECTORS.policyDescription('rgp'))
|
||||||
|
.hasText(
|
||||||
|
'Role Governing Policies (RGPs) are tied to client tokens or identities which is similar to ACL policies . They use Sentinel as a language framework to enable fine-grained policy decisions.'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('it renders the correct paragraph for EGP policy', async function (assert) {
|
||||||
|
await render(hbs`
|
||||||
|
<PolicyExample
|
||||||
|
@policyType="egp"
|
||||||
|
/>
|
||||||
|
`);
|
||||||
|
assert
|
||||||
|
.dom(SELECTORS.policyDescription('egp'))
|
||||||
|
.hasText(
|
||||||
|
`Endpoint Governing Policies (EGPs) are tied to particular paths (e.g. aws/creds/ ) instead of tokens. They use Sentinel as a language to access properties of the incoming requests.`
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('it renders the correct JSON editor text for ACL policy', async function (assert) {
|
||||||
|
await render(hbs`
|
||||||
|
<PolicyExample
|
||||||
|
@policyType="acl"
|
||||||
|
/>
|
||||||
|
`);
|
||||||
|
assert.dom(SELECTORS.jsonText).includesText(`# Grant 'create', 'read' , 'update', and ‘list’ permission`);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('it renders the correct JSON editor text for RGP policy', async function (assert) {
|
||||||
|
await render(hbs`
|
||||||
|
<PolicyExample
|
||||||
|
@policyType="rgp"
|
||||||
|
/>
|
||||||
|
`);
|
||||||
|
assert
|
||||||
|
.dom(SELECTORS.jsonText)
|
||||||
|
.includesText(`# Import strings library that exposes common string operations`);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('it renders the correct JSON editor text for EGP policy', async function (assert) {
|
||||||
|
await render(hbs`
|
||||||
|
<PolicyExample
|
||||||
|
@policyType="egp"
|
||||||
|
/>
|
||||||
|
`);
|
||||||
|
assert.dom(SELECTORS.jsonText).includesText(`# Expect requests to only happen during work days (Monday `);
|
||||||
|
});
|
||||||
|
});
|
||||||
@ -18,6 +18,14 @@ const SELECTORS = {
|
|||||||
saveButton: '[data-test-policy-save]',
|
saveButton: '[data-test-policy-save]',
|
||||||
cancelButton: '[data-test-policy-cancel]',
|
cancelButton: '[data-test-policy-cancel]',
|
||||||
error: '[data-test-message-error]',
|
error: '[data-test-message-error]',
|
||||||
|
// For example modal:
|
||||||
|
exampleButton: '[data-test-policy-example-button]',
|
||||||
|
exampleModal: '[data-test-policy-example-modal]',
|
||||||
|
exampleModalTitle: '[data-test-modal-title]',
|
||||||
|
exampleModalClose: '[data-test-modal-close-button]',
|
||||||
|
// For additional fields for EGP policy:
|
||||||
|
fields: (name) => `[data-test-field=${name}]`,
|
||||||
|
pathsInput: (index) => `[data-test-string-list-input="${index}"]`,
|
||||||
};
|
};
|
||||||
|
|
||||||
module('Integration | Component | policy-form', function (hooks) {
|
module('Integration | Component | policy-form', function (hooks) {
|
||||||
@ -42,6 +50,9 @@ module('Integration | Component | policy-form', function (hooks) {
|
|||||||
this.put('/v1/sys/policies/rgp/**', () => {
|
this.put('/v1/sys/policies/rgp/**', () => {
|
||||||
return [204, { 'Content-Type': 'application/json' }];
|
return [204, { 'Content-Type': 'application/json' }];
|
||||||
});
|
});
|
||||||
|
this.put('/v1/sys/policies/egp/**', () => {
|
||||||
|
return [204, { 'Content-Type': 'application/json' }];
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
hooks.afterEach(function () {
|
hooks.afterEach(function () {
|
||||||
@ -102,6 +113,37 @@ module('Integration | Component | policy-form', function (hooks) {
|
|||||||
assert.ok(this.onSave.calledOnceWith(this.model));
|
assert.ok(this.onSave.calledOnceWith(this.model));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('it renders the form for new EGP policy', async function (assert) {
|
||||||
|
const model = this.store.createRecord('policy/egp');
|
||||||
|
const policy = `
|
||||||
|
path "secret/*" {
|
||||||
|
capabilities = [ "create", "read", "update", "list" ]
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
this.set('model', model);
|
||||||
|
await render(hbs`
|
||||||
|
<PolicyForm
|
||||||
|
@model={{this.model}}
|
||||||
|
@onCancel={{this.onCancel}}
|
||||||
|
@onSave={{this.onSave}}
|
||||||
|
/>
|
||||||
|
`);
|
||||||
|
assert.dom(SELECTORS.nameInput).exists({ count: 1 }, 'Name input exists');
|
||||||
|
assert.dom(SELECTORS.nameInput).hasNoText('Name field is not filled');
|
||||||
|
assert.dom(SELECTORS.uploadFileToggle).exists({ count: 1 }, 'Upload file toggle exists');
|
||||||
|
await fillIn(SELECTORS.nameInput, 'Foo');
|
||||||
|
assert.strictEqual(this.model.name, 'foo', 'Input sets name on model to lowercase input');
|
||||||
|
await fillIn(`${SELECTORS.policyEditor} textarea`, policy);
|
||||||
|
assert.strictEqual(this.model.policy, policy, 'Policy editor sets policy on model');
|
||||||
|
assert.dom(SELECTORS.fields('paths')).exists('Paths field exists');
|
||||||
|
assert.dom(SELECTORS.pathsInput('0')).exists('0 field exists');
|
||||||
|
await fillIn(SELECTORS.pathsInput('0'), 'my path');
|
||||||
|
assert.ok(this.onSave.notCalled);
|
||||||
|
assert.dom(SELECTORS.saveButton).hasText('Create policy');
|
||||||
|
await click(SELECTORS.saveButton);
|
||||||
|
assert.ok(this.onSave.calledOnceWith(this.model));
|
||||||
|
});
|
||||||
|
|
||||||
test('it toggles to upload a new policy and uploads file', async function (assert) {
|
test('it toggles to upload a new policy and uploads file', async function (assert) {
|
||||||
const policy = `
|
const policy = `
|
||||||
path "auth/token/lookup-self" {
|
path "auth/token/lookup-self" {
|
||||||
@ -156,6 +198,7 @@ module('Integration | Component | policy-form', function (hooks) {
|
|||||||
await click(SELECTORS.saveButton);
|
await click(SELECTORS.saveButton);
|
||||||
assert.ok(this.onSave.calledOnceWith(this.model));
|
assert.ok(this.onSave.calledOnceWith(this.model));
|
||||||
});
|
});
|
||||||
|
|
||||||
test('it renders the form to edit existing RGP policy', async function (assert) {
|
test('it renders the form to edit existing RGP policy', async function (assert) {
|
||||||
const model = this.store.createRecord('policy/rgp', {
|
const model = this.store.createRecord('policy/rgp', {
|
||||||
name: 'bar',
|
name: 'bar',
|
||||||
@ -185,6 +228,43 @@ module('Integration | Component | policy-form', function (hooks) {
|
|||||||
await click(SELECTORS.saveButton);
|
await click(SELECTORS.saveButton);
|
||||||
assert.ok(this.onSave.calledOnceWith(this.model));
|
assert.ok(this.onSave.calledOnceWith(this.model));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('it renders the form to edit existing EGP policy', async function (assert) {
|
||||||
|
const model = this.store.createRecord('policy/egp', {
|
||||||
|
name: 'bar',
|
||||||
|
policy: 'some policy content',
|
||||||
|
paths: ['first path'],
|
||||||
|
});
|
||||||
|
model.save();
|
||||||
|
|
||||||
|
this.set('model', model);
|
||||||
|
await render(hbs`
|
||||||
|
<PolicyForm
|
||||||
|
@model={{this.model}}
|
||||||
|
@onCancel={{this.onCancel}}
|
||||||
|
@onSave={{this.onSave}}
|
||||||
|
/>
|
||||||
|
`);
|
||||||
|
assert.dom(SELECTORS.nameInput).doesNotExist('Name input is not rendered');
|
||||||
|
assert.dom(SELECTORS.uploadFileToggle).doesNotExist('Upload file toggle does not exist');
|
||||||
|
await fillIn(`${SELECTORS.policyEditor} textarea`, 'updated-');
|
||||||
|
assert.strictEqual(
|
||||||
|
this.model.policy,
|
||||||
|
'updated-some policy content',
|
||||||
|
'Policy editor updates policy value on model'
|
||||||
|
);
|
||||||
|
await fillIn(SELECTORS.pathsInput('1'), 'second path');
|
||||||
|
assert.strictEqual(
|
||||||
|
JSON.stringify(this.model.paths),
|
||||||
|
'["first path","second path"]',
|
||||||
|
'Second path field is updated on model'
|
||||||
|
);
|
||||||
|
assert.ok(this.onSave.notCalled);
|
||||||
|
assert.dom(SELECTORS.saveButton).hasText('Save', 'Save button text is correct');
|
||||||
|
await click(SELECTORS.saveButton);
|
||||||
|
assert.ok(this.onSave.calledOnceWith(this.model));
|
||||||
|
});
|
||||||
|
|
||||||
test('it shows the error message on form when save fails', async function (assert) {
|
test('it shows the error message on form when save fails', async function (assert) {
|
||||||
const model = this.store.createRecord('policy/acl', {
|
const model = this.store.createRecord('policy/acl', {
|
||||||
name: 'bad-policy',
|
name: 'bad-policy',
|
||||||
@ -203,4 +283,138 @@ module('Integration | Component | policy-form', function (hooks) {
|
|||||||
assert.ok(this.onSave.notCalled);
|
assert.ok(this.onSave.notCalled);
|
||||||
assert.dom(SELECTORS.error).includesText('An error occurred');
|
assert.dom(SELECTORS.error).includesText('An error occurred');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('it does not create a new policy when the cancel button is clicked', async function (assert) {
|
||||||
|
const policy = `
|
||||||
|
path "secret/*" {
|
||||||
|
capabilities = [ "create", "read", "update", "list" ]
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
await render(hbs`
|
||||||
|
<PolicyForm
|
||||||
|
@model={{this.model}}
|
||||||
|
@onCancel={{this.onCancel}}
|
||||||
|
@onSave={{this.onSave}}
|
||||||
|
/>
|
||||||
|
`);
|
||||||
|
await fillIn(SELECTORS.nameInput, 'Foo');
|
||||||
|
assert.strictEqual(this.model.name, 'foo', 'Input sets name on model to lowercase input');
|
||||||
|
await fillIn(`${SELECTORS.policyEditor} textarea`, policy);
|
||||||
|
assert.strictEqual(this.model.policy, policy, 'Policy editor sets policy on model');
|
||||||
|
|
||||||
|
await click(SELECTORS.cancelButton);
|
||||||
|
assert.ok(this.onSave.notCalled);
|
||||||
|
assert.ok(this.onCancel.calledOnce, 'Form calls onCancel');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('it does not save edits when the cancel button is clicked', async function (assert) {
|
||||||
|
const model = this.store.createRecord('policy/acl', {
|
||||||
|
name: 'foo',
|
||||||
|
policy: 'some policy content',
|
||||||
|
});
|
||||||
|
model.save();
|
||||||
|
|
||||||
|
this.set('model', model);
|
||||||
|
await render(hbs`
|
||||||
|
<PolicyForm
|
||||||
|
@model={{this.model}}
|
||||||
|
@onCancel={{this.onCancel}}
|
||||||
|
@onSave={{this.onSave}}
|
||||||
|
/>
|
||||||
|
`);
|
||||||
|
await fillIn(`${SELECTORS.policyEditor} textarea`, 'updated-');
|
||||||
|
assert.strictEqual(
|
||||||
|
this.model.policy,
|
||||||
|
'updated-some policy content',
|
||||||
|
'Policy editor updates policy value on model'
|
||||||
|
);
|
||||||
|
await click(SELECTORS.cancelButton);
|
||||||
|
assert.ok(this.onSave.notCalled);
|
||||||
|
assert.ok(this.onCancel.calledOnce, 'Form calls onCancel');
|
||||||
|
|
||||||
|
await render(hbs`
|
||||||
|
<PolicyForm
|
||||||
|
@model={{this.model}}
|
||||||
|
@onCancel={{this.onCancel}}
|
||||||
|
@onSave={{this.onSave}}
|
||||||
|
/>
|
||||||
|
`);
|
||||||
|
assert.strictEqual(
|
||||||
|
this.model.policy,
|
||||||
|
'some policy content',
|
||||||
|
'Policy editor shows original policy content, meaning that onCancel worked successfully'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('it does not render the button and modal for the policy example if not specified to', async function (assert) {
|
||||||
|
await render(hbs`
|
||||||
|
<PolicyForm
|
||||||
|
@model={{this.model}}
|
||||||
|
@onCancel={{this.onCancel}}
|
||||||
|
@onSave={{this.onSave}}
|
||||||
|
/>
|
||||||
|
`);
|
||||||
|
assert.dom(SELECTORS.exampleModal).doesNotExist('Modal for the policy example does not exist');
|
||||||
|
assert.dom(SELECTORS.exampleButton).doesNotExist('Button for the policy example modal does not exist');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('it renders the button and modal for the policy example when specified to', async function (assert) {
|
||||||
|
await render(hbs`
|
||||||
|
<PolicyForm
|
||||||
|
@model={{this.model}}
|
||||||
|
@onCancel={{this.onCancel}}
|
||||||
|
@onSave={{this.onSave}}
|
||||||
|
@renderPolicyExampleModal={{true}}
|
||||||
|
/>
|
||||||
|
<div id="modal-wormhole"></div>
|
||||||
|
`);
|
||||||
|
assert.dom(SELECTORS.exampleButton).exists({ count: 1 }, 'Modal for the policy example exists');
|
||||||
|
assert.dom(SELECTORS.exampleButton).exists({ count: 1 }, 'Button for the policy example modal exists');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('it renders the correct title for ACL example for the policy example modal', async function (assert) {
|
||||||
|
await render(hbs`
|
||||||
|
<PolicyForm
|
||||||
|
@model={{this.model}}
|
||||||
|
@onCancel={{this.onCancel}}
|
||||||
|
@onSave={{this.onSave}}
|
||||||
|
@renderPolicyExampleModal={{true}}
|
||||||
|
/>
|
||||||
|
<div id="modal-wormhole"></div>
|
||||||
|
`);
|
||||||
|
await click(SELECTORS.exampleButton);
|
||||||
|
assert.dom(SELECTORS.exampleModalTitle).hasText('Example ACL Policy');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('it renders the correct title for RGP example for the policy example modal', async function (assert) {
|
||||||
|
const model = this.store.createRecord('policy/rgp');
|
||||||
|
this.set('model', model);
|
||||||
|
await render(hbs`
|
||||||
|
<PolicyForm
|
||||||
|
@model={{this.model}}
|
||||||
|
@onCancel={{this.onCancel}}
|
||||||
|
@onSave={{this.onSave}}
|
||||||
|
@renderPolicyExampleModal={{true}}
|
||||||
|
/>
|
||||||
|
<div id="modal-wormhole"></div>
|
||||||
|
`);
|
||||||
|
await click(SELECTORS.exampleButton);
|
||||||
|
assert.dom(SELECTORS.exampleModalTitle).hasText('Example RGP Policy');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('it renders the correct title for EGP example for the policy example modal', async function (assert) {
|
||||||
|
const model = this.store.createRecord('policy/egp');
|
||||||
|
this.set('model', model);
|
||||||
|
await render(hbs`
|
||||||
|
<PolicyForm
|
||||||
|
@model={{this.model}}
|
||||||
|
@onCancel={{this.onCancel}}
|
||||||
|
@onSave={{this.onSave}}
|
||||||
|
@renderPolicyExampleModal={{true}}
|
||||||
|
/>
|
||||||
|
<div id="modal-wormhole"></div>
|
||||||
|
`);
|
||||||
|
await click(SELECTORS.exampleButton);
|
||||||
|
assert.dom(SELECTORS.exampleModalTitle).hasText('Example EGP Policy');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user