diff --git a/README.md b/README.md index cdc468c..6c68f4a 100644 --- a/README.md +++ b/README.md @@ -184,6 +184,9 @@ None ## Release Notes +- Add the command "Ping Cluster" on sources (cf. [#61](https://github.com/yannick-beot-sp/vscode-sailpoint-identitynow/pull/61)) +- Export of roles was failing due to 1-level Membership Criteria + ### 0.0.25 - Add test connection (cf. [#58](https://github.com/yannick-beot-sp/vscode-sailpoint-identitynow/issues/58)) diff --git a/src/parser/RoleMembershipSelectorConverter.ts b/src/parser/RoleMembershipSelectorConverter.ts index 800ae8b..3a89c09 100644 --- a/src/parser/RoleMembershipSelectorConverter.ts +++ b/src/parser/RoleMembershipSelectorConverter.ts @@ -32,24 +32,19 @@ export class RoleMembershipSelectorConverter implements Visitor x.children === undefined || x.children.length === 0)) { // We have already 2 level2. Needs to create 1 level artifically this.root = { operation: this.root.operation === "AND" ? "OR" : "AND", children: [this.root] }; } + } async visitAttribute(val: Attribute, arg: RoleCriteriaLevel1): Promise { diff --git a/src/parser/parser.ts b/src/parser/parser.ts index 6b67321..14deaab 100644 --- a/src/parser/parser.ts +++ b/src/parser/parser.ts @@ -23,7 +23,7 @@ export class Parser { if (isLogicalOperation(token)) { const currentOperator = token.toLowerCase() === "and" ? "AND" : "OR"; if (operator !== undefined && operator !== currentOperator){ - throw new ParseException("All operators should be either \"sand\" or \"or\""); + throw new ParseException("All operators should be either \"and\" or \"or\""); } operator = currentOperator; diff --git a/src/parser/roleMembershipSelectorToStringConverter.ts b/src/parser/roleMembershipSelectorToStringConverter.ts index 066782f..0897ad1 100644 --- a/src/parser/roleMembershipSelectorToStringConverter.ts +++ b/src/parser/roleMembershipSelectorToStringConverter.ts @@ -22,7 +22,7 @@ export async function roleMembershipSelectorToStringConverter( return await convertRoleCriteriaLevel2(roleCriteriaLevel1.children[0], sourceIdToName); } - if (isLogicalOperation(roleCriteriaLevel1.children[0].operation)) { + if (roleCriteriaLevel1.children.some(c => isLogicalOperation(c.operation))) { return join(roleCriteriaLevel1.operation, (await Promise.all( roleCriteriaLevel1.children @@ -44,7 +44,9 @@ function join(op: RoleCriteriaOperation, expressions: string[]) { async function convertRoleCriteriaLevel2( roleCriteriaLevel2: RoleCriteriaLevel2, sourceIdToName: CacheService): Promise { - if (roleCriteriaLevel2.children === null) { + if (roleCriteriaLevel2.children === null + || roleCriteriaLevel2.children === undefined || + roleCriteriaLevel2.children.length === 0) { return await convertRoleCriteriaLevel3(roleCriteriaLevel2, sourceIdToName); } diff --git a/src/test/unit/RoleMembershipSelectorConverter.test.ts b/src/test/unit/RoleMembershipSelectorConverter.test.ts index 0b23305..e5afd38 100644 --- a/src/test/unit/RoleMembershipSelectorConverter.test.ts +++ b/src/test/unit/RoleMembershipSelectorConverter.test.ts @@ -20,24 +20,20 @@ class MockupCache extends CacheService{ suite('RoleMembershipSelectorConverter Test Suite', () => { const parser = new Parser(); + describe('1 level conversion', async () => { const expectedComparisonOperator: RoleCriteriaLevel1 = { "operation": "OR", "children": [ { - "operation": "AND", - "children": [ - { - "operation": "EQUALS", - "key": { - "type": "IDENTITY", - "property": "attribute.department", - sourceId: undefined - }, - "stringValue": "Customer Service" - } - ] + "operation": "EQUALS", + "key": { + "type": "IDENTITY", + "property": "attribute.department", + sourceId: undefined + }, + "stringValue": "Customer Service" } ] }; @@ -67,8 +63,84 @@ suite('RoleMembershipSelectorConverter Test Suite', () => { assert.deepEqual(converter.root, expectedComparisonOperator); }); }); + describe('2 level conversion', async () => { it("should parse an expression with 2 comparisons and convert it", async () => { + const expectedComparisonOperator2: RoleCriteriaLevel1 = { + "operation": "OR", + "children": [{ + "operation": "AND", + "children": [ + { + "operation": "EQUALS", + "key": { + "type": "IDENTITY", + "property": "attribute.department", + sourceId: undefined + }, + "stringValue": "Customer Service" + }, + { + "operation": "EQUALS", + "key": { + "type": "IDENTITY", + "property": "attribute.cloudLifecycleState", + "sourceId": undefined + }, + "stringValue": "active", + }, + ] + }] + }; + + const converter = new RoleMembershipSelectorConverter(new MockupCache()); + + const expression = parser.parse("identity.department eq 'Customer Service' and identity.cloudLifecycleState eq 'active'"); + + await converter.visitExpression(expression, undefined); + + assert.deepEqual(converter.root, expectedComparisonOperator2); + }); + it("should parse an expression with 2 non-identity and convert it", async () => { + const expectedComparisonOperator3: RoleCriteriaLevel1 = { + "operation": "OR", + "children": [{ + "operation": "AND", + "children": [ + { + "operation": "EQUALS", + "key": { + "type": "ENTITLEMENT", + "property": "attribute.memberOf", + "sourceId": "6ba6925ebc1a4e5d98ca6fd3fc542ea4" + }, + "stringValue": "CN=Accounting,OU=Groups,OU=Demo,DC=seri,DC=sailpointdemo,DC=com", + }, + { + "operation": "EQUALS", + "key": { + "type": "ACCOUNT", + "property": "attribute.departmentNumber", + "sourceId": "6ba6925ebc1a4e5d98ca6fd3fc542ea4" + }, + "stringValue": "1234", + }, + ] + }] + }; + + const converter = new RoleMembershipSelectorConverter(new MockupCache()); + + const expression = parser.parse("'Active Directory'.entitlement.memberOf EQ \"CN=Accounting,OU=Groups,OU=Demo,DC=seri,DC=sailpointdemo,DC=com\" and 'Active Directory'.attribute.departmentNumber eq \"1234\""); + + await converter.visitExpression(expression, undefined); + + assert.deepEqual(converter.root, expectedComparisonOperator3); + }); + }); + + describe('3 level conversion', async () => { + it("should parse an expression and convert it", async () => { const expectedComparisonOperator2: RoleCriteriaLevel1 = { "operation": "OR", "children": [ @@ -94,63 +166,58 @@ suite('RoleMembershipSelectorConverter Test Suite', () => { "stringValue": "active", }, ] - } - ] - }; - - const converter = new RoleMembershipSelectorConverter(new MockupCache()); - - const expression = parser.parse("identity.department eq 'Customer Service' and identity.cloudLifecycleState eq 'active'"); - - await converter.visitExpression(expression, undefined); - - assert.deepEqual(converter.root, expectedComparisonOperator2); - }); - it("should parse an expression with 2 non-identity and convert it", async () => { - const expectedComparisonOperator3: RoleCriteriaLevel1 = { - "operation": "OR", - "children": [ + }, { "operation": "AND", "children": [ { "operation": "EQUALS", "key": { - "type": "ENTITLEMENT", - "property": "attribute.memberOf", - "sourceId": "6ba6925ebc1a4e5d98ca6fd3fc542ea4" + "type": "IDENTITY", + "property": "attribute.cloudLifecycleState", + "sourceId": undefined }, - "stringValue": "CN=Accounting,OU=Groups,OU=Demo,DC=seri,DC=sailpointdemo,DC=com", + "stringValue": "active", }, { - "operation": "EQUALS", + "operation": "CONTAINS", "key": { - "type": "ACCOUNT", - "property": "attribute.departmentNumber", - "sourceId": "6ba6925ebc1a4e5d98ca6fd3fc542ea4" + "type": "IDENTITY", + "property": "attribute.jobTitle", + "sourceId": undefined }, - "stringValue": "1234", - }, + "stringValue": "Accounts Payable Analyst", + } ] } ] - } + }; const converter = new RoleMembershipSelectorConverter(new MockupCache()); - const expression = parser.parse("'Active Directory'.entitlement.memberOf EQ \"CN=Accounting,OU=Groups,OU=Demo,DC=seri,DC=sailpointdemo,DC=com\" and 'Active Directory'.attribute.departmentNumber eq \"1234\""); + const expression = parser.parse("(identity.department eq 'Customer Service' and identity.cloudLifecycleState eq 'active') OR (identity.cloudLifecycleState eq 'active' AND identity.jobTitle co 'Accounts Payable Analyst')"); await converter.visitExpression(expression, undefined); - assert.deepEqual(converter.root, expectedComparisonOperator3); + assert.deepEqual(converter.root, expectedComparisonOperator2); }); + }); - describe('3 level conversion', async () => { - it("should parse an expression and convert it", async () => { + describe('unbalanced level conversion', async () => { + it("should parse an expression and convert it with parenthesis", async () => { const expectedComparisonOperator2: RoleCriteriaLevel1 = { "operation": "OR", "children": [ + { + "operation": "EQUALS", + "key": { + "type": "ENTITLEMENT", + "property": "attribute.ProfileId", + "sourceId": "6ba6925ebc1a4e5d98ca6fd3fc542ea4" + }, + "stringValue": "00e1i000000eM2qAAE" + }, { "operation": "AND", "children": [ @@ -158,21 +225,46 @@ suite('RoleMembershipSelectorConverter Test Suite', () => { "operation": "EQUALS", "key": { "type": "IDENTITY", - "property": "attribute.department", - sourceId: undefined + "property": "attribute.cloudLifecycleState", + "sourceId": undefined }, - "stringValue": "Customer Service" + "stringValue": "active", }, { "operation": "EQUALS", "key": { "type": "IDENTITY", - "property": "attribute.cloudLifecycleState", + "property": "attribute.usertype", "sourceId": undefined }, - "stringValue": "active", - }, + "stringValue": "External", + } ] + } + ] + }; + + const converter = new RoleMembershipSelectorConverter(new MockupCache()); + + const expression = parser.parse("('Active Directory'.entitlement.ProfileId eq '00e1i000000eM2qAAE') or (identity.cloudLifecycleState eq 'active' and identity.usertype eq 'External')"); + + await converter.visitExpression(expression, undefined); + + assert.deepEqual(converter.root, expectedComparisonOperator2); + }); + + it("should parse an expression and convert it without parenthesis", async () => { + const expectedComparisonOperator2: RoleCriteriaLevel1 = { + "operation": "OR", + "children": [ + { + "operation": "EQUALS", + "key": { + "type": "ENTITLEMENT", + "property": "attribute.ProfileId", + "sourceId": "6ba6925ebc1a4e5d98ca6fd3fc542ea4" + }, + "stringValue": "00e1i000000eM2qAAE" }, { "operation": "AND", @@ -187,22 +279,22 @@ suite('RoleMembershipSelectorConverter Test Suite', () => { "stringValue": "active", }, { - "operation": "CONTAINS", + "operation": "EQUALS", "key": { "type": "IDENTITY", - "property": "attribute.jobTitle", + "property": "attribute.usertype", "sourceId": undefined }, - "stringValue": "Accounts Payable Analyst", + "stringValue": "External", } ] } ] - }; + };; const converter = new RoleMembershipSelectorConverter(new MockupCache()); - const expression = parser.parse("(identity.department eq 'Customer Service' and identity.cloudLifecycleState eq 'active') OR (identity.cloudLifecycleState eq 'active' AND identity.jobTitle co 'Accounts Payable Analyst')"); + const expression = parser.parse("'Active Directory'.entitlement.ProfileId eq '00e1i000000eM2qAAE' or (identity.cloudLifecycleState eq 'active' and identity.usertype eq 'External')"); await converter.visitExpression(expression, undefined); @@ -210,4 +302,5 @@ suite('RoleMembershipSelectorConverter Test Suite', () => { }); }); + }); diff --git a/src/test/unit/roleMembershipSelectorToStringConverter.test.ts b/src/test/unit/roleMembershipSelectorToStringConverter.test.ts index 8f03658..d104ba9 100644 --- a/src/test/unit/roleMembershipSelectorToStringConverter.test.ts +++ b/src/test/unit/roleMembershipSelectorToStringConverter.test.ts @@ -15,6 +15,7 @@ class MockupCache extends CacheService{ suite('await roleMembershipSelectorToStringConverter Test Suite', () => { const cache = new MockupCache(); + describe('1 level conversion', async () => { it("should parse an expression and convert it", async () => { const roleCriteria1: RoleCriteriaLevel1 = { @@ -37,11 +38,12 @@ suite('await roleMembershipSelectorToStringConverter Test Suite', () => { ] }; const result = await roleMembershipSelectorToStringConverter(roleCriteria1, cache); - const expected = "identity.department eq \"Customer Service\""; + const expected = "identity.department eq 'Customer Service'"; assert.deepEqual(result, expected); }); }); + describe('2 level conversion', async () => { it("should parse an expression with 2 comparisons and convert it", async () => { const roleCriteria2: RoleCriteriaLevel1 = { @@ -75,7 +77,7 @@ suite('await roleMembershipSelectorToStringConverter Test Suite', () => { const result = await roleMembershipSelectorToStringConverter(roleCriteria2, cache); - const expected = "identity.department eq \"Customer Service\" and identity.cloudLifecycleState eq \"active\""; + const expected = "identity.department eq 'Customer Service' and identity.cloudLifecycleState eq 'active'"; assert.deepEqual(result, expected); }); @@ -111,7 +113,7 @@ suite('await roleMembershipSelectorToStringConverter Test Suite', () => { const result = await roleMembershipSelectorToStringConverter(roleCriteria3, cache); - const expected = "\"Active Directory\".entitlement.memberOf eq \"CN=Accounting,OU=Groups,OU=Demo,DC=seri,DC=sailpointdemo,DC=com\" and \"Active Directory\".attribute.departmentNumber eq \"1234\""; + const expected = "'Active Directory'.entitlement.memberOf eq 'CN=Accounting,OU=Groups,OU=Demo,DC=seri,DC=sailpointdemo,DC=com' and 'Active Directory'.attribute.departmentNumber eq '1234'"; assert.deepEqual(result, expected); @@ -174,7 +176,61 @@ suite('await roleMembershipSelectorToStringConverter Test Suite', () => { const result = await roleMembershipSelectorToStringConverter(roleCriteria4, cache); - const expected = "(identity.department eq \"Customer Service\" and identity.cloudLifecycleState eq \"active\") or (identity.cloudLifecycleState eq \"active\" and identity.jobTitle co \"Accounts Payable Analyst\")"; + const expected = "(identity.department eq 'Customer Service' and identity.cloudLifecycleState eq 'active') or (identity.cloudLifecycleState eq 'active' and identity.jobTitle co 'Accounts Payable Analyst')"; + + assert.deepEqual(result, expected); + }); + + }); + + describe('Unbalanced criteria', async () => { + it("should parse an expression and convert it", async () => { + const roleCriteria4: RoleCriteriaLevel1 = { + "operation": "OR", + "key": null, + "stringValue": "", + "children": [ + { + "operation": "EQUALS", + "key": { + "type": "ENTITLEMENT", + "property": "attribute.ProfileId", + "sourceId": "2c9180887229516601722cabce3f0ad5" + }, + "stringValue": "00e1i000000eM2qAAE", + "children": [] + }, + { + "operation": "AND", + "key": null, + "stringValue": "", + "children": [ + { + "operation": "EQUALS", + "key": { + "type": "IDENTITY", + "property": "attribute.cloudLifecycleState", + "sourceId": "" + }, + "stringValue": "active", + }, + { + "operation": "EQUALS", + "key": { + "type": "IDENTITY", + "property": "attribute.usertype", + "sourceId": "" + }, + "stringValue": "External", + } + ] + } + ] + }; + + const result = await roleMembershipSelectorToStringConverter(roleCriteria4, cache); + + const expected = "('Active Directory'.entitlement.ProfileId eq '00e1i000000eM2qAAE') or (identity.cloudLifecycleState eq 'active' and identity.usertype eq 'External')"; assert.deepEqual(result, expected); });