mirror of
https://github.com/optim-enterprises-bv/vault.git
synced 2025-11-03 03:58:01 +00: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:
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>
|
||||
{{/if}}
|
||||
{{#if this.showExamplePolicy}}
|
||||
<div class="has-bottom-margin-s">
|
||||
{{#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}}
|
||||
/>
|
||||
<PolicyExample @policyType={{this.policy.policyType}} />
|
||||
{{else}}
|
||||
<Select
|
||||
@name="policyType"
|
||||
|
||||
@@ -37,39 +37,6 @@ export default class PolicyTemplate extends Component {
|
||||
{ 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
|
||||
setPolicyType(type) {
|
||||
|
||||
@@ -42,7 +42,6 @@
|
||||
{{else}}
|
||||
<JsonEditor
|
||||
@title="Policy"
|
||||
@helpText="You can use Alt+Tab (Option+Tab on MacOS) in the code editor to skip to the next field"
|
||||
@showToolbar={{false}}
|
||||
@value={{@model.policy}}
|
||||
@valueUpdated={{action (mut @model.policy)}}
|
||||
@@ -55,7 +54,6 @@
|
||||
{{! EDITING - no file upload toggle}}
|
||||
<JsonEditor
|
||||
@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}}
|
||||
@valueUpdated={{action (mut @model.policy)}}
|
||||
@mode="ruby"
|
||||
@@ -63,27 +61,46 @@
|
||||
data-test-policy-editor
|
||||
/>
|
||||
{{/if}}
|
||||
<div class="has-top-margin-xs">
|
||||
<span class="is-size-9 has-text-grey has-bottom-margin-l">
|
||||
You can use Alt+Tab (Option+Tab on MacOS) in the code editor to skip to the next field.
|
||||
</span>
|
||||
{{! Only renders button (and modal) if not already in the "create policy" modal }}
|
||||
{{#if @renderPolicyExampleModal}}
|
||||
<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
|
||||
{{uppercase @model.policyType}}
|
||||
policies can be found
|
||||
<DocLink
|
||||
@path={{if
|
||||
(eq @model.policyType "acl")
|
||||
"/vault/docs/concepts/policies#capabilities"
|
||||
"/vault/tutorials/policies/sentinel#role-governing-policies-rgps"
|
||||
}}
|
||||
>
|
||||
here.
|
||||
</DocLink>
|
||||
</p>
|
||||
{{/if}}
|
||||
</div>
|
||||
</div>
|
||||
{{#each @model.additionalAttrs as |attr|}}
|
||||
<FormField data-test-field={{true}} @attr={{attr}} @model={{@model}} />
|
||||
{{/each}}
|
||||
</div>
|
||||
<div class="has-bottom-margin-m">
|
||||
<p>
|
||||
More information about
|
||||
{{uppercase @model.policyType}}
|
||||
policies can be found
|
||||
<DocLink
|
||||
@path={{if
|
||||
(eq @model.policyType "acl")
|
||||
"/vault/docs/concepts/policies#capabilities"
|
||||
"/vault/tutorials/policies/sentinel#role-governing-policies-rgps"
|
||||
}}
|
||||
>
|
||||
here.
|
||||
</DocLink>
|
||||
</p>
|
||||
</div>
|
||||
<div class="field is-grouped box is-fullwidth is-bottomless">
|
||||
<div class="control">
|
||||
<button
|
||||
@@ -105,4 +122,27 @@
|
||||
</button>
|
||||
</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}}
|
||||
* @onSave={{transition-to "vault.cluster.policy.show" this.model.policyType this.model.name}}
|
||||
* @onCancel={{transition-to "vault.cluster.policies.index"}}
|
||||
* @renderPolicyExampleModal={{true}}
|
||||
* />
|
||||
* ```
|
||||
* @callback onCancel - callback triggered when cancel button is clicked
|
||||
* @callback onSave - callback triggered when save button is clicked. Passes saved model
|
||||
* @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 {
|
||||
@@ -31,6 +33,7 @@ export default class PolicyFormComponent extends Component {
|
||||
|
||||
@tracked errorBanner = '';
|
||||
@tracked showFileUpload = false;
|
||||
@tracked showTemplateModal = false;
|
||||
|
||||
@task
|
||||
*save(event) {
|
||||
|
||||
@@ -21,4 +21,5 @@
|
||||
@model={{this.model}}
|
||||
@onSave={{transition-to "vault.cluster.policy.show" this.model.policyType this.model.name}}
|
||||
@onCancel={{transition-to "vault.cluster.policies.index"}}
|
||||
@renderPolicyExampleModal={{true}}
|
||||
/>
|
||||
@@ -46,4 +46,5 @@
|
||||
@model={{this.model}}
|
||||
@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}}
|
||||
@renderPolicyExampleModal={{true}}
|
||||
/>
|
||||
@@ -38,6 +38,7 @@
|
||||
}}
|
||||
class={{if @readOnly "readonly-codemirror"}}
|
||||
data-test-component="code-mirror-modifier"
|
||||
data-test-example-modal-json-text
|
||||
></div>
|
||||
|
||||
{{#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]',
|
||||
cancelButton: '[data-test-policy-cancel]',
|
||||
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) {
|
||||
@@ -42,6 +50,9 @@ module('Integration | Component | policy-form', function (hooks) {
|
||||
this.put('/v1/sys/policies/rgp/**', () => {
|
||||
return [204, { 'Content-Type': 'application/json' }];
|
||||
});
|
||||
this.put('/v1/sys/policies/egp/**', () => {
|
||||
return [204, { 'Content-Type': 'application/json' }];
|
||||
});
|
||||
});
|
||||
});
|
||||
hooks.afterEach(function () {
|
||||
@@ -102,6 +113,37 @@ module('Integration | Component | policy-form', function (hooks) {
|
||||
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) {
|
||||
const policy = `
|
||||
path "auth/token/lookup-self" {
|
||||
@@ -156,6 +198,7 @@ module('Integration | Component | policy-form', function (hooks) {
|
||||
await click(SELECTORS.saveButton);
|
||||
assert.ok(this.onSave.calledOnceWith(this.model));
|
||||
});
|
||||
|
||||
test('it renders the form to edit existing RGP policy', async function (assert) {
|
||||
const model = this.store.createRecord('policy/rgp', {
|
||||
name: 'bar',
|
||||
@@ -185,6 +228,43 @@ module('Integration | Component | policy-form', function (hooks) {
|
||||
await click(SELECTORS.saveButton);
|
||||
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) {
|
||||
const model = this.store.createRecord('policy/acl', {
|
||||
name: 'bad-policy',
|
||||
@@ -203,4 +283,138 @@ module('Integration | Component | policy-form', function (hooks) {
|
||||
assert.ok(this.onSave.notCalled);
|
||||
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');
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user