* refactor: 💡 initial refactor for utils to share components * refactor: 💡 actions logic * Add missing copyright headers * refactor: 💡 simplify actions getter logic * refactor: 💡 convert output fields logic into a separate getter * test: 💍 fix unit test * fix: 🐛 bug with unknown fields lint error * refactor: 💡 pr feedback - simplify template check * refactor: 💡 remove unnecessary unit test file
316 lines
13 KiB
JavaScript
316 lines
13 KiB
JavaScript
/**
|
|
* Copyright IBM Corp. 2021, 2026
|
|
* SPDX-License-Identifier: BUSL-1.1
|
|
*/
|
|
|
|
import { module, test } from 'qunit';
|
|
import { setupTest } from 'ember-qunit';
|
|
import { setupMirage } from 'admin/tests/helpers/mirage';
|
|
import { setupIntl } from 'ember-intl/test-support';
|
|
import { createGrantLinter } from 'admin/utils/grant-linter';
|
|
|
|
const createContext = (lineText) => ({
|
|
pos: lineText.length,
|
|
state: {
|
|
doc: {
|
|
toString() {
|
|
return lineText;
|
|
},
|
|
},
|
|
},
|
|
});
|
|
|
|
module('Unit | Utility | grant-linter', function (hooks) {
|
|
setupTest(hooks);
|
|
setupMirage(hooks);
|
|
setupIntl(hooks, 'en-us');
|
|
|
|
hooks.beforeEach(async function () {
|
|
const response = await fetch('/grants-schema.json');
|
|
|
|
if (!response.ok) {
|
|
throw new Error(`Failed to fetch grants schema: ${response.status}`);
|
|
}
|
|
this.intl = this.owner.lookup('service:intl');
|
|
const grantsSchemaData = await response.json();
|
|
const translateLintingError = (key, options = {}) =>
|
|
this.intl.t(`resources.role.edit-grants.linting-errors.${key}`, options);
|
|
this.grantLinter = createGrantLinter(
|
|
grantsSchemaData,
|
|
translateLintingError,
|
|
);
|
|
});
|
|
|
|
test.each(
|
|
'diagnostics for syntax and formatting errors',
|
|
[
|
|
['ids=*; type=*; actions=*', 'Whitespace is not allowed'], // whitespace around semicolons
|
|
['ids=*;type= *;actions=*', 'Whitespace is not allowed'], // whitespace around equal sign
|
|
['ids=*;type=*;actions=read, write', 'Whitespace is not allowed'], // whitespace around commas
|
|
['ids=*;type=*;actions=read,write;', 'Trailing semicolon is not allowed'], // trailing semicolon
|
|
[';ids=*;type=*;actions=read,write', 'Leading semicolon is not allowed'], // leading semicolon
|
|
['ids={{.Account.Id}};type=+;actions=read', 'Invalid character "+"'], // invalid character +
|
|
['ids={{.Account.Id}};type=();actions=read', 'Invalid character "("'], // invalid characters ()
|
|
['type=target', 'Missing "actions" or "output_fields" fields'], // missing actions/output_fields field
|
|
['actions=read,write', 'Missing "ids" or "type" fields'], // missing type/ids field
|
|
['ids=hc_123', 'Missing "actions" field'], // missing actions field
|
|
[
|
|
'type=credential-store;ids=;actions=read',
|
|
'"ids" value cannot be empty',
|
|
], // empty ids value
|
|
['type=;actions=read', '"type" value cannot be empty'], // empty type field
|
|
['ids=csst_1234;actions=', '"actions" value cannot be empty'], // empty actions value
|
|
[
|
|
'type=credential;ids=csst_1234;actions=read,write;ids=cs_5678',
|
|
'Duplicate field "ids"',
|
|
], // duplicate ids field
|
|
[
|
|
'type=credential;actions=read,write;type=host',
|
|
'Duplicate field "type"',
|
|
], // duplicate type field
|
|
[
|
|
'type=credential;ids=csst_1234;actions=read,write;actions=delete',
|
|
'Duplicate field "actions"',
|
|
], // duplicate actions field
|
|
[
|
|
'type=credential;ids=cs_1234;actions=read;random=',
|
|
'Unknown field "random"',
|
|
], // unknown field "random"
|
|
[
|
|
'type=credential;ids=cs_1234;actions=read;random=name',
|
|
'Unknown field "random"',
|
|
], // unknown field "random"
|
|
[
|
|
'=credential;actions=;ids=cs_1234',
|
|
'Invalid format: expected key=value',
|
|
], // missing type key
|
|
['type;actions=;ids=cs_1234', 'Invalid format: expected key=value'], // missing type value and equal sign
|
|
],
|
|
function (assert, [grantString, errorMsg]) {
|
|
const diagnostics = this.grantLinter(createContext(grantString));
|
|
|
|
assert.strictEqual(diagnostics[0].message, errorMsg);
|
|
},
|
|
);
|
|
|
|
test.each(
|
|
'diagnostics for type field values',
|
|
[
|
|
['actions=read;type=random', 'Invalid resource type "random"'], // invalid resource type
|
|
['actions=read;type=account', 'Type "account" is not a top-level type'], // invalid resource type
|
|
[
|
|
'type=credential,random;actions=read',
|
|
'Invalid resource type "credential,random"',
|
|
], // invalid resource type
|
|
['ids=*;actions=read', 'Missing "type" field for wildcard ids'], // missing type field with wildcard id
|
|
['type=credential-store;actions=create'], // valid grant string
|
|
],
|
|
function (assert, [grantString, errorMsg]) {
|
|
const diagnostics = this.grantLinter(createContext(grantString));
|
|
|
|
if (errorMsg) {
|
|
assert.strictEqual(diagnostics[0].message, errorMsg);
|
|
} else {
|
|
assert.strictEqual(diagnostics.length, 0);
|
|
}
|
|
},
|
|
);
|
|
|
|
test.each(
|
|
'diagnostics for actions field values',
|
|
[
|
|
['type=credential-store;actions=,', 'Invalid syntax'],
|
|
[
|
|
'type=session;actions=read,*,write',
|
|
'Wildcard action "*" cannot be combined with other actions',
|
|
], // wildcard action cannot be combined with other actions
|
|
[
|
|
'type=policy;actions=execute',
|
|
'Only collection actions are allowed. Invalid action "execute"',
|
|
], // invalid action for resource type
|
|
[
|
|
'type=target;actions=list,no-op',
|
|
'Only collection actions are allowed. Invalid action "no-op"',
|
|
], // invalid action when type is not specified
|
|
['ids=ampw_1234;type=*;actions=read,read', 'Duplicate action "read"'], // duplicate action
|
|
[
|
|
'ids=cs_1234;actions=create',
|
|
'Only id actions for "credential-store" type are allowed. Invalid action "create"',
|
|
], // invalid action for specific resource
|
|
[
|
|
'ids=*;type=auth-method;actions=no-op',
|
|
'"no-op" action should only be used with "list" action',
|
|
],
|
|
[
|
|
'ids=ampw_098;type=account;actions=list',
|
|
'"list" action should be used with "no-op" action to have an effect',
|
|
],
|
|
[
|
|
'ids=*;type=account;actions=list',
|
|
'"list" action should be used with "no-op" action to have an effect',
|
|
],
|
|
[
|
|
'ids=ampw_098;type=account;actions=list,read,no-op',
|
|
'"no-op" action is unnecessary when other actions are specified',
|
|
],
|
|
['ids=hc_1234;type=*;actions=add-hosts,read'], // valid actions for resource type
|
|
['type=worker;actions=create:controller-led'], // valid actions for resource type
|
|
['type=auth-method;actions=list'], // valid actions for resource type
|
|
],
|
|
function (assert, [grantString, errorMsg]) {
|
|
const diagnostics = this.grantLinter(createContext(grantString));
|
|
|
|
if (errorMsg) {
|
|
assert.strictEqual(diagnostics[0].message, errorMsg);
|
|
} else {
|
|
assert.strictEqual(diagnostics.length, 0);
|
|
}
|
|
},
|
|
);
|
|
|
|
test.each(
|
|
'diagnostics for ids field values',
|
|
[
|
|
['type=credential;ids=,,,;actions=read', 'Invalid syntax'],
|
|
['ids=cs_1234,cs_1234;actions=read', 'Duplicate id "cs_1234"'], // duplicate ids
|
|
[
|
|
'ids={{.Account.Id}},{{.Account.Id}};actions=read',
|
|
'Duplicate id "{{.Account.Id}}"',
|
|
], // duplicate template ids
|
|
[
|
|
'ids={{.Account.Id}},{{account.id}};actions=read',
|
|
'Duplicate id "{{.Account.Id}}" and "{{account.id}}"',
|
|
], // duplicate template ids
|
|
[
|
|
'ids={{.User.Id}},{{user.id}};actions=read',
|
|
'Duplicate id "{{.User.Id}}" and "{{user.id}}"',
|
|
], // duplicate template ids
|
|
[
|
|
'actions=read;type=credential;ids=cs_1234,*,cs_5678',
|
|
'Wildcard id "*" cannot be combined with other ids',
|
|
], // wildcard id cannot be combined with other ids
|
|
[
|
|
'ids={{.Account.Id}};type=credential;actions=read',
|
|
'Pinned IDs must be "credential-store" ids. Invalid id "{{.Account.Id}}"',
|
|
], // template ids not valid with type specified
|
|
[
|
|
'ids=hsst_1234;type=*;actions=read',
|
|
'Pinned IDs must support child types. Invalid id "hsst_1234"',
|
|
], // template ids not valid with type specified
|
|
['ids={{.Random.Id}};actions=read', 'Unknown template "{{.Random.Id}}"'], // invalid template id
|
|
[
|
|
'type=host-catalog;ids=hcst_1234;actions=list,delete',
|
|
'Type "host-catalog" is not a child type of the pinned ID',
|
|
], // only non-top level resource types can be pinned by id
|
|
[
|
|
'type=host-set;ids=hcst_1234,g_1234;actions=read',
|
|
'Ids do not have the same type. Invalid id "g_1234"',
|
|
], // all ids must be valid for the resource type
|
|
[
|
|
'type=credential;ids=csst_1234,random;actions=delete',
|
|
'Invalid id "random"',
|
|
], // all ids must be valid for the resource type
|
|
[
|
|
'ids=cs_1234,g_1234;actions=read',
|
|
'Ids do not have the same type. Invalid id "g_1234"',
|
|
], // all ids must be same type when type is not specified
|
|
['ids=none_1234,csvlt_123;actions=list', 'Invalid id "none_1234"'], // valid ids prefixes only
|
|
[
|
|
'ids=g_123,amoidc_456;type=*;actions=read,list',
|
|
'Pinned IDs must support child types. Invalid id "g_123"',
|
|
], // all ids must support child types when type is wildcard
|
|
[
|
|
'ids=amoidc_456,hcst_123;type=*;actions=read,list',
|
|
'Ids do not have the same type. Invalid id "hcst_123"',
|
|
], // all ids must be the same type when type is wildcard
|
|
['ids=cs_1234,csst_5678;actions=read'], // valid ids when they are the same type and type is not specified
|
|
['ids=*;type=*;actions=read'], // valid wildcard id
|
|
['ids=hcst_123,hcplg_456;type=host-set;actions=read'], // valid pinned ids for host-set resource type
|
|
['ids={{.Account.Id}};actions=read'], // valid template id when type is not specified
|
|
[
|
|
'ids={{.Account.Id}},{{.User.Id}};actions=read',
|
|
'Template ids cannot be combined with each other',
|
|
], // mixed template ids is not allowed
|
|
['ids=csst_1234,csvlt_5678;actions=*'], // valid id prefixes with same type
|
|
['ids=amldap_123,amoidc_456;type=*;actions=read,list'], // valid ids with wildcard type
|
|
],
|
|
function (assert, [grantString, errorMsg]) {
|
|
const diagnostics = this.grantLinter(createContext(grantString));
|
|
|
|
if (errorMsg) {
|
|
assert.strictEqual(diagnostics[0].message, errorMsg);
|
|
} else {
|
|
assert.strictEqual(diagnostics.length, 0);
|
|
}
|
|
},
|
|
);
|
|
|
|
test.each(
|
|
'diagnostics for output_fields field values',
|
|
[
|
|
['type=target;output_fields=,', 'Invalid syntax'],
|
|
[
|
|
'type=target;output_fields=*,id',
|
|
'Wildcard output field "*" cannot be combined with other fields',
|
|
],
|
|
['type=target;output_fields=id,id', 'Duplicate output field "id"'],
|
|
[
|
|
'type=target;output_fields=invalid_field',
|
|
'Invalid output field "invalid_field" for type "target"',
|
|
],
|
|
[
|
|
'ids=ttcp_123;output_fields=member_ids',
|
|
'Invalid output field "member_ids" for type "target"',
|
|
],
|
|
['ids=hcst_123;type=*;actions=read;output_fields=host_catalog_id'], // valid: child type (host) output field
|
|
['ids=hcst_123;type=*;actions=read;output_fields=host_set_ids'], // valid: child type (host) output field
|
|
[
|
|
'ids=hcst_123;type=*;actions=read;output_fields=session_max_seconds',
|
|
'Invalid output field "session_max_seconds"',
|
|
], // invalid: not a host or host-set field
|
|
[
|
|
'ids=*;type=*;actions=read;output_fields=id',
|
|
'Invalid output field "id"',
|
|
], // no specific type context, specific field is invalid
|
|
['type=target;output_fields=id,name'], // valid output fields
|
|
['type=target;output_fields=*'], // valid wildcard
|
|
['ids=ttcp_123;output_fields=scope_id'], // valid inferred type
|
|
['ids={{.User.Id}};output_fields=email'], // valid user template output field
|
|
['ids={{user.id}};output_fields=email'], // valid user template alias output field
|
|
['ids={{.Account.Id}};output_fields=auth_method_id'], // valid account template output field
|
|
['ids={{account.id}};output_fields=auth_method_id'], // valid account template alias output field
|
|
[
|
|
'ids={{.User.Id}};output_fields=auth_method_id',
|
|
'Invalid output field "auth_method_id" for type "user"',
|
|
], // account field is invalid for user template
|
|
[
|
|
'ids={{.Account.Id}};output_fields=email',
|
|
'Invalid output field "email" for type "account"',
|
|
], // user field is invalid for account template
|
|
[
|
|
'ids={{.User.Id}},{{.Account.Id}};output_fields=email',
|
|
'Template ids cannot be combined with each other',
|
|
], // ids error fires first; mixed templates are disallowed
|
|
[
|
|
'ids={{.User.Id}},{{.Account.Id}};output_fields=auth_method_id',
|
|
'Template ids cannot be combined with each other',
|
|
], // ids error fires first; mixed templates are disallowed
|
|
[
|
|
'ids={{.User.Id}},{{.Account.Id}};output_fields=session_max_seconds',
|
|
'Template ids cannot be combined with each other',
|
|
], // ids error fires first; mixed templates are disallowed
|
|
['type=target;output_fields=id,scope_id,name'], // valid multiple output fields
|
|
['ids=*;type=*;actions=read;output_fields=*'], // wildcard output fields with wildcard type is valid
|
|
],
|
|
function (assert, [grantString, errorMsg]) {
|
|
const diagnostics = this.grantLinter(createContext(grantString));
|
|
|
|
if (errorMsg) {
|
|
assert.strictEqual(diagnostics[0].message, errorMsg);
|
|
} else {
|
|
assert.strictEqual(diagnostics.length, 0);
|
|
}
|
|
},
|
|
);
|
|
});
|