Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adds command pa app permission list #5166

Closed
Closed
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
124 changes: 124 additions & 0 deletions docs/docs/cmd/pa/app/app-permission-list.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
import Global from '/docs/cmd/_global.mdx';
import Tabs from '@theme/Tabs';
import TabItem from '@theme/TabItem';

# pa app permission list

Lists all permissions of a Power Apps app

## Usage

```sh
m365 pa app permission list [options]
```

## Options

```md definition-list
`---appName <appName>`
MathijsVerbeeck marked this conversation as resolved.
Show resolved Hide resolved
: The name (GUID) of the Microsoft Power App.

`--asAdmin`
: Run the command as admin for apps you don't own.

`-e, --environmentName [environmentName]`
: The name of the environment. Required when using `asAdmin`.

`--roleName [roleName]`
MathijsVerbeeck marked this conversation as resolved.
Show resolved Hide resolved
: Filter the results to only a given role: `Owner`, `CanEdit`, `CanView`.
MathijsVerbeeck marked this conversation as resolved.
Show resolved Hide resolved
```

<Global />

## Examples

List all permissions for an app you own
MathijsVerbeeck marked this conversation as resolved.
Show resolved Hide resolved

```sh
m365 pa app permission list --appName bc9f0a7e-53df-46af-b669-5888bb2f63d0
```

List all permissions as admin for an app you don't own
MathijsVerbeeck marked this conversation as resolved.
Show resolved Hide resolved

```sh
m365 pa app permission list --environmentName "Default-e448e34a-40cc-441a-94c6-ec701a9a9ec2" --appName bc9f0a7e-53df-46af-b669-5888bb2f63d0 --asAdmin
```

List all co-owners for an app you own
MathijsVerbeeck marked this conversation as resolved.
Show resolved Hide resolved

```sh
m365 pa app permission list --appName bc9f0a7e-53df-46af-b669-5888bb2f63d0 --roleName CanEdit
```

## Response

<Tabs>
<TabItem value="JSON">
MathijsVerbeeck marked this conversation as resolved.
Show resolved Hide resolved

```json
[
{
"name": "fe36f75e-c103-410b-a18a-2bf6df06ac3a",
"id": "/providers/Microsoft.PowerApps/apps/37ea6004-f07b-46ca-8ef3-a256b67b4dbb/permissions/fe36f75e-c103-410b-a18a-2bf6df06ac3a",
"type": "Microsoft.PowerApps/apps/permissions",
"properties": {
"roleName": "Owner",
"principal": {
"id": "fe36f75e-c103-410b-a18a-2bf6df06ac3a",
"displayName": "John Doe",
"email": "[email protected]",
"type": "User",
"tenantId": "e1dd4023-a656-480a-8a0e-c1b1eec51e1d"
},
"scope": "/providers/Microsoft.PowerApps/environments/Default-e1dd4023-a656-480a-8a0e-c1b1eec51e1d/apps/37ea6004-f07b-46ca-8ef3-a256b67b4dbb",
"notifyShareTargetOption": "NotSpecified",
"inviteGuestToTenant": false,
"createdOn": "2022-10-25T21:28:14.2122305Z",
"createdBy": "f0db9c91-3dae-49c8-98fa-8059b8909d45"
},
"roleName": "Owner",
"principalId": "fe36f75e-c103-410b-a18a-2bf6df06ac3a",
"principalType": "User"
}
]
```

</TabItem>
<TabItem value="Text">

```text
roleName principalId principalType
-------- ------------------------------------ -------------
Owner fe36f75e-c103-410b-a18a-2bf6df06ac3a User
```

</TabItem>
<TabItem value="CSV">

```csv
name,id,type,roleName,principalId,principalType
fe36f75e-c103-410b-a18a-2bf6df06ac3a,/providers/Microsoft.PowerApps/apps/37ea6004-f07b-46ca-8ef3-a256b67b4dbb/permissions/fe36f75e-c103-410b-a18a-2bf6df06ac3a,Microsoft.PowerApps/apps/permissions,Owner,fe36f75e-c103-410b-a18a-2bf6df06ac3a,User
```

</TabItem>
<TabItem value="Markdown">

```md
# pa app permission list --appName "37ea6004-f07b-46ca-8ef3-a256b67b4dbb"

Date: 25/06/2023

## fe36f75e-c103-410b-a18a-2bf6df06ac3a (/providers/Microsoft.PowerApps/apps/37ea6004-f07b-46ca-8ef3-a256b67b4dbb/permissions/fe36f75e-c103-410b-a18a-2bf6df06ac3a)

Property | Value
---------|-------
name | fe36f75e-c103-410b-a18a-2bf6df06ac3a
id | /providers/Microsoft.PowerApps/apps/37ea6004-f07b-46ca-8ef3-a256b67b4dbb/permissions/fe36f75e-c103-410b-a18a-2bf6df06ac3a
type | Microsoft.PowerApps/apps/permissions
roleName | Owner
principalId | fe36f75e-c103-410b-a18a-2bf6df06ac3a
principalType | User
```

</TabItem>
</Tabs>
5 changes: 5 additions & 0 deletions docs/src/config/sidebars.js
Original file line number Diff line number Diff line change
Expand Up @@ -1140,6 +1140,11 @@ const sidebars = {
type: 'doc',
label: 'app consent set',
id: 'cmd/pa/app/app-consent-set'
},
{
type: 'doc',
label: 'app permission list',
id: 'cmd/pa/app/app-permission-list'
}
]
},
Expand Down
1 change: 1 addition & 0 deletions src/m365/pa/commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ export default {
APP_LIST: `${prefix} app list`,
APP_REMOVE: `${prefix} app remove`,
APP_CONSENT_SET: `${prefix} app consent set`,
APP_PERMISSION_LIST: `${prefix} app permission list`,
CONNECTOR_EXPORT: `${prefix} connector export`,
CONNECTOR_LIST: `${prefix} connector list`,
ENVIRONMENT_GET: `${prefix} environment get`,
Expand Down
210 changes: 210 additions & 0 deletions src/m365/pa/commands/app/app-permission-list.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,210 @@
import * as assert from 'assert';
import * as sinon from 'sinon';
import { telemetry } from '../../../../telemetry';
import auth from '../../../../Auth';
import { Cli } from '../../../../cli/Cli';
import { CommandInfo } from '../../../../cli/CommandInfo';
import { Logger } from '../../../../cli/Logger';
import Command, { CommandError } from '../../../../Command';
import request from '../../../../request';
import { pid } from '../../../../utils/pid';
import { session } from '../../../../utils/session';
import { sinonUtil } from '../../../../utils/sinonUtil';
import commands from '../../commands';
import { formatting } from '../../../../utils/formatting';
const command: Command = require('./app-permission-list');

describe(commands.APP_PERMISSION_LIST, () => {
const environmentName = 'Default-d87a7535-dd31-4437-bfe1-95340acd55c6';
const appName = '58768250-1943-470b-9743-715020ae21f4';
const roleName = 'Owner';

const permissionsResponse = [
{
'name': 'fe36f75e-c103-410b-a18a-2bf6df06ac3a',
'id': '/providers/Microsoft.PowerApps/apps/37ea6004-f07b-46ca-8ef3-a256b67b4dbb/permissions/fe36f75e-c103-410b-a18a-2bf6df06ac3a',
'type': 'Microsoft.PowerApps/apps/permissions',
'properties': {
'roleName': 'Owner',
'principal': {
'id': 'fe36f75e-c103-410b-a18a-2bf6df06ac3a',
'displayName': 'John Doe',
'email': '[email protected]',
'type': 'User',
'tenantId': 'e1dd4023-a656-480a-8a0e-c1b1eec51e1d'
},
'scope': '/providers/Microsoft.PowerApps/environments/Default-e1dd4023-a656-480a-8a0e-c1b1eec51e1d/apps/37ea6004-f07b-46ca-8ef3-a256b67b4dbb',
'notifyShareTargetOption': 'NotSpecified',
'inviteGuestToTenant': false,
'createdOn': '2022-10-25T21:28:14.2122305Z',
'createdBy': 'f0db9c91-3dae-49c8-98fa-8059b8909d45'
}
}
];

const permissionsResponseFormatted = [
{
'name': 'fe36f75e-c103-410b-a18a-2bf6df06ac3a',
'id': '/providers/Microsoft.PowerApps/apps/37ea6004-f07b-46ca-8ef3-a256b67b4dbb/permissions/fe36f75e-c103-410b-a18a-2bf6df06ac3a',
'type': 'Microsoft.PowerApps/apps/permissions',
'roleName': 'Owner',
'principalId': 'fe36f75e-c103-410b-a18a-2bf6df06ac3a',
'principalType': 'User',
'properties': {
'roleName': 'Owner',
'principal': {
'id': 'fe36f75e-c103-410b-a18a-2bf6df06ac3a',
'displayName': 'John Doe',
'email': '[email protected]',
'type': 'User',
'tenantId': 'e1dd4023-a656-480a-8a0e-c1b1eec51e1d'
},
'scope': '/providers/Microsoft.PowerApps/environments/Default-e1dd4023-a656-480a-8a0e-c1b1eec51e1d/apps/37ea6004-f07b-46ca-8ef3-a256b67b4dbb',
'notifyShareTargetOption': 'NotSpecified',
'inviteGuestToTenant': false,
'createdOn': '2022-10-25T21:28:14.2122305Z',
'createdBy': 'f0db9c91-3dae-49c8-98fa-8059b8909d45'
}
}
];


let log: string[];
let logger: Logger;
let loggerLogSpy: sinon.SinonSpy;
let commandInfo: CommandInfo;

before(() => {
sinon.stub(auth, 'restoreAuth').resolves();
sinon.stub(telemetry, 'trackEvent').returns();
sinon.stub(pid, 'getProcessName').returns('');
sinon.stub(session, 'getId').returns('');
auth.service.connected = true;
commandInfo = Cli.getCommandInfo(command);
});

beforeEach(() => {
log = [];
logger = {
log: (msg: string) => {
log.push(msg);
},
logRaw: (msg: string) => {
log.push(msg);
},
logToStderr: (msg: string) => {
log.push(msg);
}
};
loggerLogSpy = sinon.spy(logger, 'log');
});

afterEach(() => {
sinonUtil.restore([
request.get
]);
});

after(() => {
sinon.restore();
auth.service.connected = false;
});

it('has correct name', () => {
assert.strictEqual(command.name, commands.APP_PERMISSION_LIST);
});

it('has a description', () => {
assert.notStrictEqual(command.description, null);
});

it('defines correct properties for the default output', () => {
assert.deepStrictEqual(command.defaultProperties(), ['roleName', 'principalId', 'principalType']);
});

it('correctly retrieves all permissions when asAdmin and environmentName is passed', async () => {
sinon.stub(request, 'get').callsFake(async (opts) => {
if (opts.url === `https://api.powerapps.com/providers/Microsoft.PowerApps/scopes/admin/environments/${formatting.encodeQueryParameter(environmentName)}/apps/${appName}/permissions?api-version=2022-11-01`) {
return { value: permissionsResponse };
}
throw 'Invalid request';
});

await command.action(logger, { options: { appName: appName, asAdmin: true, environmentName: environmentName, verbose: true } });
assert(loggerLogSpy.calledWith(permissionsResponseFormatted));
});

it('correctly filters permissions when roleName is passed', async () => {
const permissionsWithDifferentRoleName = { 'name': 'fe36f75e-c103-410b-a18a-2bf6df06ac3a', 'id': '/providers/Microsoft.PowerApps/apps/37ea6004-f07b-46ca-8ef3-a256b67b4dbb/permissions/fe36f75e-c103-410b-a18a-2bf6df06ac3a', 'type': 'Microsoft.PowerApps/apps/permissions', 'properties': { 'roleName': 'CanEdit', 'principal': { 'id': 'fe36f75e-c103-410b-a18a-2bf6df06ac3a', 'displayName': 'John Doe', 'email': '[email protected]', 'type': 'User', 'tenantId': 'e1dd4023-a656-480a-8a0e-c1b1eec51e1d' }, 'scope': '/providers/Microsoft.PowerApps/environments/Default-e1dd4023-a656-480a-8a0e-c1b1eec51e1d/apps/37ea6004-f07b-46ca-8ef3-a256b67b4dbb', 'notifyShareTargetOption': 'NotSpecified', 'inviteGuestToTenant': false, 'createdOn': '2022-10-25T21:28:14.2122305Z', 'createdBy': 'f0db9c91-3dae-49c8-98fa-8059b8909d45' } };
const permissionsResponseClone = [...permissionsResponse];
permissionsResponseClone.push(permissionsWithDifferentRoleName);

sinon.stub(request, 'get').callsFake(async (opts) => {
if (opts.url === `https://api.powerapps.com/providers/Microsoft.PowerApps/apps/${appName}/permissions?api-version=2022-11-01`) {
return { value: permissionsResponseClone };
}
throw 'Invalid request';
});

await command.action(logger, { options: { appName: appName, roleName: roleName, verbose: true } });
assert(loggerLogSpy.calledWith(permissionsResponseFormatted));
});

it('correctly handles no permissions found', async () => {
sinon.stub(request, 'get').resolves({ value: [] });

await command.action(logger, { options: { appName: appName, verbose: true } });
assert(loggerLogSpy.calledWith([]));
});

it('correctly handles no permissions found with specific roleName', async () => {
const roleName = 'CanEdit';
sinon.stub(request, 'get').resolves({ value: permissionsResponse });

await command.action(logger, { options: { appName: appName, roleName: roleName, verbose: true } });
assert(loggerLogSpy.calledWith([]));
});

it('correctly handles API error when app not found or no access', async () => {
const error = {
error: {
code: 'Forbidden',
message: `The user with object id 'fe36f75e-c103-410b-a18a-2bf6df06ac3a' does not have permission to access this.`
}
};
sinon.stub(request, 'get').rejects(error);

await assert.rejects(command.action(logger, { options: { appName: appName } } as any),
new CommandError(error.error.message));
});

it('passes validation if asAdmin specified with environment', async () => {
const actual = await command.validate({ options: { appName: appName, asAdmin: true, environmentName: environmentName } }, commandInfo);
assert.strictEqual(actual, true);
});

it('passes validation if roleName is a valid roleName', async () => {
const actual = await command.validate({ options: { appName: appName, roleName: roleName } }, commandInfo);
assert.strictEqual(actual, true);
});

it('fails validation if roleName is not a valid roleName', async () => {
const actual = await command.validate({ options: { appName: appName, roleName: 'invalid' } }, commandInfo);
assert.notStrictEqual(actual, true);
});

it('fails validation if appName is not a valid guid', async () => {
const actual = await command.validate({ options: { appName: 'invalid' } }, commandInfo);
assert.notStrictEqual(actual, true);
});

it('fails validation if asAdmin specified without environmentName', async () => {
const actual = await command.validate({ options: { appName: appName, asAdmin: true } }, commandInfo);
assert.notStrictEqual(actual, true);
});

it('fails validation if environmentName specified without asAdmin', async () => {
const actual = await command.validate({ options: { appName: appName, environmentName: environmentName } }, commandInfo);
assert.notStrictEqual(actual, true);
});
});
Loading