Skip to content

Commit a150865

Browse files
feat: add permissions virtual flag. fixes #175 (#478)
1 parent bce8797 commit a150865

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

48 files changed

+556
-4
lines changed

.eslintrc

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,18 @@
2727
"rules" : {
2828
"aio-cli-plugin-cloudmanager-error-usage": "off"
2929
}
30+
},
31+
{
32+
"files" : ["src/commands/**/*.js"],
33+
"rules" : {
34+
"aio-cli-plugin-cloudmanager-command-permissions": "error"
35+
}
36+
},
37+
{
38+
"files" : ["src/commands/cloudmanager/commerce/**/*.js"],
39+
"rules" : {
40+
"aio-cli-plugin-cloudmanager-command-permissions": "off"
41+
}
3042
}
3143
]
3244
}

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,3 +14,4 @@ junit.xml
1414
.vscode/
1515
/package-lock.json
1616
.aio
17+
permissions.json

CONTRIBUTING.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,13 @@ In order to ensure proper error handling, individual commands should generally *
2424

2525
Hooks should work in a similar fashion -- see the prerun `check-ims-context-config` hook as a point of reference.
2626

27+
## Custom Command Class Properties
28+
29+
In addition to the standard [oclif command properties](https://oclif.io/docs/commands), commands in this plugin may define these extra properties:
30+
31+
* `skipOrgIdCheck` -- This should be set to `true` if the command does *not* require an IMS Organization ID to be configured in the current IMS context. This is primarily used when using browser-based authentication as service account authentication will always have an organization ID.
32+
* `permissionInfo` -- This is used to output information to the CLI user about the permissions required to execute a particular command. This must be an object. Currently a single key is supported `operation` which must correspond to one of the operations defined in https://raw.githubusercontent.com/AdobeDocs/cloudmanager-api-docs/main/src/data/permissions.json. If a command doesn't require specific permissions (e.g. is a read-only operation), this should be set to an empty object.
33+
2734
## Commits and Releasing
2835

2936
Commits (generally via merged pull requests) to the `main` branch of this repository will automatically generate [semantically versioned releases](https://github.com/semantic-release). To accomplish this, commit messages must follow the [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/) syntax, specifically:

README.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1272,6 +1272,16 @@ ALIASES
12721272
_See code: [src/commands/cloudmanager/program/list-pipelines.js](https://github.com/adobe/aio-cli-plugin-cloudmanager/blob/2.13.0/src/commands/cloudmanager/program/list-pipelines.js)_
12731273
<!-- commandsstop -->
12741274

1275+
# Permissions
1276+
1277+
Information about Cloud Manager API permissions can be found on https://www.adobe.io/experience-cloud/cloud-manager/guides/getting-started/permissions/. To see the
1278+
permissions required for a specific command, you can also run any command with the flag `--permissions`, e.g.
1279+
1280+
```
1281+
$ aio cloudmanager:current-execution:advance --permissions
1282+
To execute cloudmanager:current-execution:advance, one of the following product profiles is required: Business Owner, Deployment Manager, Program Manager
1283+
```
1284+
12751285
# Variables From Standard Input
12761286

12771287
The `environment:set-variables` and `pipeline:set-variables` commands allow for variables to be passed both as flags to the command and as a JSON array provided as standard input or as a file. The objects in this array are expected to have a `name`, `value`, and `type` keys following the same syntax as the Cloud Manager API. Deleting a variable can be done by passing an empty `value`. For example, given a file named `variables.json` that contains this:
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
/*
2+
Copyright 2021 Adobe. All rights reserved.
3+
This file is licensed to you under the Apache License, Version 2.0 (the "License");
4+
you may not use this file except in compliance with the License. You may obtain a copy
5+
of the License at http://www.apache.org/licenses/LICENSE-2.0
6+
Unless required by applicable law or agreed to in writing, software distributed under
7+
the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
8+
OF ANY KIND, either express or implied. See the License for the specific language
9+
governing permissions and limitations under the License.
10+
*/
11+
12+
const availableOperations = require('../permissions.json').map(perm => perm.operation)
13+
14+
module.exports = {
15+
create: function (context) {
16+
return {
17+
Program: (node) => {
18+
const classDeclarations = node.body.filter(bodyNode => bodyNode.type === 'ClassDeclaration')
19+
if (classDeclarations.length === 0) {
20+
context.report({
21+
node: node,
22+
message: "Command file didn't contain command",
23+
})
24+
}
25+
classDeclarations.forEach(classDeclaration => {
26+
const className = classDeclaration.id.name
27+
const propertiesAssignedToClass = node.body.map(bodyNode => {
28+
if (bodyNode.type === 'ExpressionStatement' && bodyNode.expression.type === 'AssignmentExpression' &&
29+
bodyNode.expression.left.type === 'MemberExpression' && bodyNode.expression.left.object && bodyNode.expression.left.object.name === className &&
30+
bodyNode.expression.left.property) {
31+
return {
32+
name: bodyNode.expression.left.property.name,
33+
node: bodyNode.expression.right,
34+
}
35+
}
36+
return {}
37+
}).filter(property => property.name)
38+
39+
// if skipOrgIdCheck is defined, we don't need permissions
40+
if (!propertiesAssignedToClass.find(prop => prop.name === 'skipOrgIdCheck')) {
41+
const permissionInfo = propertiesAssignedToClass.find(prop => prop.name === 'permissionInfo')
42+
if (!permissionInfo) {
43+
context.report({
44+
node: classDeclaration,
45+
message: 'Class {{ className }} does not define permissionInfo.',
46+
data: {
47+
className,
48+
},
49+
})
50+
} else if (permissionInfo.node.type !== 'ObjectExpression') {
51+
context.report({
52+
node: permissionInfo.node,
53+
message: 'Class {{ className }} has permissionInfo but it is not an object.',
54+
data: {
55+
className,
56+
},
57+
})
58+
} else {
59+
const operation = permissionInfo.node.properties.find(prop => prop.key.name === 'operation')
60+
if (operation && operation.value) {
61+
if (operation.value.type !== 'Literal') {
62+
context.report({
63+
node: permissionInfo.node,
64+
message: 'Class {{ className }} has permissionInfo with operation {{ operation }} but it is not a literal.',
65+
data: {
66+
className,
67+
operation,
68+
},
69+
})
70+
} else if (!availableOperations.includes(operation.value.value)) {
71+
context.report({
72+
node: permissionInfo.node,
73+
message: 'Class {{ className }} has permissionInfo with operation {{ operation }} but that operation is not defined.',
74+
data: {
75+
className,
76+
operation: operation.value.value,
77+
},
78+
})
79+
}
80+
}
81+
}
82+
}
83+
})
84+
},
85+
}
86+
},
87+
}

package.json

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,8 @@
2020
"halfred": "^2.0.0",
2121
"inquirer": "^8.1.0",
2222
"lodash": "^4.17.15",
23-
"moment": "^2.29.0"
23+
"moment": "^2.29.0",
24+
"node-fetch": "^2.6.2"
2425
},
2526
"devDependencies": {
2627
"@adobe/eslint-config-aio-lib-config": "1.3.0",
@@ -40,10 +41,12 @@
4041
"eslint-plugin-promise": "5.1.0",
4142
"eslint-plugin-standard": "4.1.0",
4243
"execa": "5.1.1",
44+
"fetch-mock": "9.11.0",
4345
"husky": "5.2.0",
4446
"jest": "27.2.0",
4547
"jest-extended": "0.11.5",
4648
"jest-junit": "12.2.0",
49+
"node-wget": "0.4.3",
4750
"pinst": "2.1.6",
4851
"semantic-release": "17.4.7",
4952
"stdout-stderr": "0.1.13",
@@ -74,7 +77,10 @@
7477
"repositoryPrefix": "<%- repo %>/blob/<%- version %>/<%- commandPath %>",
7578
"hooks": {
7679
"prerun": "./src/hooks/prerun/prerun-all.js",
77-
"init": "./src/hooks/init/migrate-jwt-context-hook.js"
80+
"init": [
81+
"./src/hooks/init/migrate-jwt-context-hook.js",
82+
"./src/hooks/init/load-permission-info.js"
83+
]
7884
},
7985
"topics": {
8086
"cloudmanager": {
@@ -125,7 +131,8 @@
125131
},
126132
"scripts": {
127133
"posttest": "npm run lint",
128-
"lint": "eslint src test e2e --rulesdir eslint_rules",
134+
"lint:download-permissions": "wget https://raw.githubusercontent.com/AdobeDocs/cloudmanager-api-docs/main/src/data/permissions.json",
135+
"lint": "npm run lint:download-permissions && eslint src test e2e --rulesdir eslint_rules",
129136
"lint-fix": "eslint src test e2e --fix --rulesdir eslint_rules",
130137
"test": "npm run unit-tests",
131138
"unit-tests": "jest --ci",

src/cloudmanager-hook-helpers.js

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,18 @@ governing permissions and limitations under the License.
1111
*/
1212

1313
function isThisPlugin (hookOptions) {
14-
return hookOptions.Command.plugin.name === '@adobe/aio-cli-plugin-cloudmanager'
14+
if (hookOptions && hookOptions.Command) {
15+
return hookOptions.Command.plugin.name === '@adobe/aio-cli-plugin-cloudmanager'
16+
} else if (hookOptions && hookOptions.id) {
17+
return hookOptions.id.startsWith('cloudmanager:')
18+
}
19+
}
20+
21+
function isPermissionsRequest (hookOptions) {
22+
return hookOptions.argv && hookOptions.argv.length === 1 && hookOptions.argv[0] === '--permissions'
1523
}
1624

1725
module.exports = {
1826
isThisPlugin,
27+
isPermissionsRequest,
1928
}

src/commands/cloudmanager/current-execution/advance.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,4 +51,8 @@ AdvanceCurrentExecutionCommand.aliases = [
5151
'cloudmanager:advance-current-execution',
5252
]
5353

54+
AdvanceCurrentExecutionCommand.permissionInfo = {
55+
operation: 'advancePipelineExecution',
56+
}
57+
5458
module.exports = AdvanceCurrentExecutionCommand

src/commands/cloudmanager/current-execution/cancel.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,4 +51,8 @@ CancelCurrentExecutionCommand.aliases = [
5151
'cloudmanager:cancel-current-execution',
5252
]
5353

54+
CancelCurrentExecutionCommand.permissionInfo = {
55+
operation: 'cancelPipelineExecutionStep',
56+
}
57+
5458
module.exports = CancelCurrentExecutionCommand

src/commands/cloudmanager/current-execution/get.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,4 +49,6 @@ GetCurrentExecutionCommand.aliases = [
4949
'cloudmanager:get-current-execution',
5050
]
5151

52+
GetCurrentExecutionCommand.permissionInfo = {}
53+
5254
module.exports = GetCurrentExecutionCommand

0 commit comments

Comments
 (0)