Skip to content

Commit 094e531

Browse files
committed
Adds 'spe container permission remove' command. Closes #6161
1 parent 6d1d954 commit 094e531

5 files changed

Lines changed: 369 additions & 0 deletions

File tree

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
import Global from '../../_global.mdx';
2+
import Tabs from '@theme/Tabs';
3+
import TabItem from '@theme/TabItem';
4+
5+
# spe container permission remove
6+
7+
Removes permissions from a SharePoint Embedded Container
8+
9+
## Usage
10+
11+
```sh
12+
m365 spe container permission remove [options]
13+
```
14+
15+
## Options
16+
17+
```md definition-list
18+
`-i, --id <id>`
19+
: ID of the permission to remove.
20+
21+
`--containerId [containerId]`
22+
: ID of a SharePoint Embedded container. Specify either `containerId` or `containerName` but not both.
23+
24+
`-n, --containerName [containerName]`
25+
: Display name of the Container. Specify either `containerId` or `containerName` but not both.
26+
27+
`--containerTypeId [containerTypeId]`
28+
: The ID of the container type. Specify either `containerTypeId` or `containerTypeName` when using `containerName` but not both.
29+
30+
`--containerTypeName [containerTypeName]`
31+
: The name of the container type. Specify either `containerTypeId` or `containerTypeName` when using `containerName` but not both.
32+
33+
`-f, --force [force]`
34+
: Don't prompt for confirmation.
35+
```
36+
37+
## Permissions
38+
39+
<Tabs>
40+
<TabItem value="Delegated">
41+
42+
| Resource | Permissions |
43+
|-----------------|-------------------------------|
44+
| Microsoft Graph | FileStorageContainer.Selected |
45+
46+
</TabItem>
47+
<TabItem value="Application">
48+
49+
| Resource | Permissions |
50+
|-----------------|-------------------------------|
51+
| Microsoft Graph | FileStorageContainer.Selected |
52+
53+
</TabItem>
54+
</Tabs>
55+
56+
## Examples
57+
58+
Removes the specified permission from a container
59+
60+
```sh
61+
m365 spe container permission remove --containerId "b!ISJs1WRro0y0EWgkUYcktDa0mE8zSlFEqFzqRn70Zwp1CEtDEBZgQICPkRbil_5Z" --Id "cmVhZGVyX2k6MCMuZnxtZW1iZXJzaGlwfHJvcnlicjExMUBvdXRsb29rLmNvbQ"
62+
```
63+
64+
Removes the specified permission from a container and doesn't prompt for confirmation
65+
66+
```sh
67+
m365 spe container permission remove --containerId "b!ISJs1WRro0y0EWgkUYcktDa0mE8zSlFEqFzqRn70Zwp1CEtDEBZgQICPkRbil_5Z" --Id "cmVhZGVyX2k6MCMuZnxtZW1iZXJzaGlwfHJvcnlicjExMUBvdXRsb29rLmNvbQ" --force
68+
```
69+
70+
## Response
71+
72+
The command won't return a response on success.

docs/src/config/sidebars.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2221,6 +2221,11 @@ const sidebars: SidebarsConfig = {
22212221
label: 'container permission list',
22222222
id: 'cmd/spe/container/container-permission-list'
22232223
},
2224+
{
2225+
type: 'doc',
2226+
label: 'container permission remove',
2227+
id: 'cmd/spe/container/container-permission-remove'
2228+
},
22242229
{
22252230
type: 'doc',
22262231
label: 'container recyclebinitem list',

src/m365/spe/commands.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ export default {
77
CONTAINER_LIST: `${prefix} container list`,
88
CONTAINER_REMOVE: `${prefix} container remove`,
99
CONTAINER_PERMISSION_LIST: `${prefix} container permission list`,
10+
CONTAINER_PERMISSION_REMOVE: `${prefix} container permission remove`,
1011
CONTAINER_RECYCLEBINITEM_LIST: `${prefix} container recyclebinitem list`,
1112
CONTAINER_RECYCLEBINITEM_REMOVE: `${prefix} container recyclebinitem remove`,
1213
CONTAINER_RECYCLEBINITEM_RESTORE: `${prefix} container recyclebinitem restore`,
Lines changed: 181 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,181 @@
1+
import assert from 'assert';
2+
import sinon from 'sinon';
3+
import auth from '../../../../Auth.js';
4+
import { Logger } from '../../../../cli/Logger.js';
5+
import { CommandInfo } from "../../../../cli/CommandInfo.js";
6+
import { CommandError } from '../../../../Command.js';
7+
import request from '../../../../request.js';
8+
import { telemetry } from '../../../../telemetry.js';
9+
import { pid } from '../../../../utils/pid.js';
10+
import { sinonUtil } from '../../../../utils/sinonUtil.js';
11+
import commands from '../../commands.js';
12+
import command, { options } from './container-permission-remove.js';
13+
import { formatting } from '../../../../utils/formatting.js';
14+
import { session } from '../../../../utils/session.js';
15+
import { spe } from '../../../../utils/spe.js';
16+
import { cli } from '../../../../cli/cli.js';
17+
18+
describe(commands.CONTAINER_PERMISSION_REMOVE, () => {
19+
let log: string[];
20+
let logger: Logger;
21+
let commandInfo: CommandInfo;
22+
let commandOptionsSchema: typeof options;
23+
let promptIssued: boolean;
24+
25+
const permissionId = 'cmVhZGVyX2k6MCMuZnxtZW1iZXJzaGlwfHJvcnlicjExMUBvdXRsb29rLmNvbQ';
26+
const containerTypeId = 'c6f08d91-77fa-485f-9369-f246ec0fc19c';
27+
const containerTypeName = 'Container type name';
28+
const containerId = 'b!McTeU0-dW0GxKwECWdW04TIvEK-Js9xJib_RFqF-CqZxNe3OHVAIT4SqBxGm4fND';
29+
const containerName = 'Container name';
30+
31+
before(() => {
32+
sinon.stub(auth, 'restoreAuth').resolves();
33+
sinon.stub(telemetry, 'trackEvent').resolves();
34+
sinon.stub(pid, 'getProcessName').returns('');
35+
sinon.stub(session, 'getId').returns('');
36+
37+
sinon.stub(spe, 'getContainerTypeIdByName').withArgs(containerTypeName).resolves(containerTypeId);
38+
sinon.stub(spe, 'getContainerIdByName').withArgs(containerTypeId, containerName).resolves(containerId);
39+
40+
auth.connection.active = true;
41+
commandInfo = cli.getCommandInfo(command);
42+
commandOptionsSchema = commandInfo.command.getSchemaToParse() as typeof options;
43+
});
44+
45+
beforeEach(() => {
46+
log = [];
47+
logger = {
48+
log: async (msg: string) => {
49+
log.push(msg);
50+
},
51+
logRaw: async (msg: string) => {
52+
log.push(msg);
53+
},
54+
logToStderr: async (msg: string) => {
55+
log.push(msg);
56+
}
57+
};
58+
sinon.stub(cli, 'promptForConfirmation').callsFake(() => {
59+
promptIssued = true;
60+
return Promise.resolve(false);
61+
});
62+
});
63+
64+
afterEach(() => {
65+
sinonUtil.restore([
66+
request.delete,
67+
cli.promptForConfirmation
68+
]);
69+
});
70+
71+
after(() => {
72+
sinon.restore();
73+
auth.connection.active = false;
74+
});
75+
76+
it('has correct name', () => {
77+
assert.strictEqual(command.name, commands.CONTAINER_PERMISSION_REMOVE);
78+
});
79+
80+
it('has a description', () => {
81+
assert.notStrictEqual(command.description, null);
82+
});
83+
84+
it('fails validation if permission id is not passed', async () => {
85+
const actual = commandOptionsSchema.safeParse({ containerId: containerId });
86+
assert.strictEqual(actual.success, false);
87+
});
88+
89+
it('fails validation if both containerId and containerName options are passed', async () => {
90+
const actual = commandOptionsSchema.safeParse({ id: permissionId, containerId: containerId, containerName: containerName });
91+
assert.strictEqual(actual.success, false);
92+
});
93+
94+
it('fails validation if neither containerId nor containerName options are passed', async () => {
95+
const actual = commandOptionsSchema.safeParse({ id: permissionId });
96+
assert.strictEqual(actual.success, false);
97+
});
98+
99+
it('fails validation if containerId and containerTypeId options are passed', async () => {
100+
const actual = commandOptionsSchema.safeParse({ id: permissionId, containerId: containerId, containerTypeId: containerTypeId });
101+
assert.strictEqual(actual.success, false);
102+
});
103+
104+
it('fails validation if containerId and containerTypeName options are passed', async () => {
105+
const actual = commandOptionsSchema.safeParse({ id: permissionId, containerId: containerId, containerTypeName: containerTypeName });
106+
assert.strictEqual(actual.success, false);
107+
});
108+
109+
it('fails validation if containerName and both containerTypeId and containerTypeName options are passed', async () => {
110+
const actual = commandOptionsSchema.safeParse({ id: permissionId, containerName: containerName, containerTypeId: containerTypeId, containerTypeName: containerTypeName });
111+
assert.strictEqual(actual.success, false);
112+
});
113+
114+
it('correctly removes permissions for a container by id', async () => {
115+
const deleteStub = sinon.stub(request, 'delete').callsFake(async (opts) => {
116+
if (opts.url === `https://graph.microsoft.com/v1.0/storage/fileStorage/containers/${formatting.encodeQueryParameter(containerId)}/permissions/${permissionId}`) {
117+
return;
118+
}
119+
120+
throw 'Invalid DELETE request: ' + opts.url;
121+
});
122+
123+
await command.action(logger, { options: commandOptionsSchema.parse({ id: permissionId, containerId: containerId, verbose: true, force: true }) });
124+
assert(deleteStub.calledOnce);
125+
});
126+
127+
it('correctly removes permissions for a container by name and container type by id and prompts for confirmation', async () => {
128+
sinonUtil.restore(cli.promptForConfirmation);
129+
sinon.stub(cli, 'promptForConfirmation').resolves(true);
130+
131+
const deleteStub = sinon.stub(request, 'delete').callsFake(async (opts) => {
132+
if (opts.url === `https://graph.microsoft.com/v1.0/storage/fileStorage/containers/${formatting.encodeQueryParameter(containerId)}/permissions/${permissionId}`) {
133+
return;
134+
}
135+
136+
throw 'Invalid DELETE request: ' + opts.url;
137+
});
138+
139+
await command.action(logger, { options: commandOptionsSchema.parse({ id: permissionId, containerName: containerName, containerTypeId: containerTypeId, verbose: true }) });
140+
assert(deleteStub.calledOnce);
141+
});
142+
143+
it('correctly removes permissions for a container by name and container type by name', async () => {
144+
const deleteStub = sinon.stub(request, 'delete').callsFake(async (opts) => {
145+
if (opts.url === `https://graph.microsoft.com/v1.0/storage/fileStorage/containers/${formatting.encodeQueryParameter(containerId)}/permissions/${permissionId}`) {
146+
return;
147+
}
148+
149+
throw 'Invalid PATCH request: ' + opts.url;
150+
});
151+
152+
await command.action(logger, { options: commandOptionsSchema.parse({ id: permissionId, containerName: containerName, containerTypeName: containerTypeName, verbose: true, force: true }) });
153+
assert(deleteStub.calledOnce);
154+
});
155+
156+
it('prompts before removing permissions when confirm option not passed', async () => {
157+
await command.action(logger, { options: commandOptionsSchema.parse({ id: permissionId, containerId: containerId }) });
158+
159+
assert(promptIssued);
160+
});
161+
162+
it('aborts removing permissions when prompt not confirmed', async () => {
163+
const deleteSpy = sinon.stub(request, 'delete').resolves();
164+
165+
await command.action(logger, { options: commandOptionsSchema.parse({ id: permissionId, containerId: containerId }) });
166+
assert(deleteSpy.notCalled);
167+
});
168+
169+
it('correctly handles unexpected error', async () => {
170+
const errorMessage = 'Access denied';
171+
sinon.stub(request, 'delete').rejects({
172+
error: {
173+
code: 'accessDenied',
174+
message: errorMessage
175+
}
176+
});
177+
178+
await assert.rejects(command.action(logger, { options: commandOptionsSchema.parse({ id: permissionId, containerId: containerId, force: true }) }),
179+
new CommandError(errorMessage));
180+
});
181+
});
Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
import { z } from 'zod';
2+
import { Logger } from '../../../../cli/Logger.js';
3+
import { globalOptionsZod } from '../../../../Command.js';
4+
import commands from '../../commands.js';
5+
import GraphCommand from '../../../base/GraphCommand.js';
6+
import { spe } from '../../../../utils/spe.js';
7+
import request, { CliRequestOptions } from '../../../../request.js';
8+
import { formatting } from '../../../../utils/formatting.js';
9+
import { cli } from '../../../../cli/cli.js';
10+
11+
export const options = z.strictObject({
12+
...globalOptionsZod.shape,
13+
id: z.string().alias('i'),
14+
containerId: z.string().optional(),
15+
containerName: z.string().alias('n').optional(),
16+
containerTypeId: z.uuid().optional(),
17+
containerTypeName: z.string().optional(),
18+
force: z.boolean().alias('f').optional()
19+
});
20+
declare type Options = z.infer<typeof options>;
21+
22+
interface CommandArgs {
23+
options: Options;
24+
}
25+
26+
class SpeContainerPermissionRemoveCommand extends GraphCommand {
27+
public get name(): string {
28+
return commands.CONTAINER_PERMISSION_REMOVE;
29+
}
30+
31+
public get description(): string {
32+
return 'Removes SharePoint Embedded Container permission';
33+
}
34+
35+
public get schema(): z.ZodType {
36+
return options;
37+
}
38+
39+
public getRefinedSchema(schema: typeof options): z.ZodObject<any> | undefined {
40+
return schema
41+
.refine((options: Options) => [options.containerId, options.containerName].filter(o => o !== undefined).length === 1, {
42+
error: 'Use one of the following options: containerId or containerName.'
43+
})
44+
.refine((options: Options) => !options.containerName || [options.containerTypeId, options.containerTypeName].filter(o => o !== undefined).length === 1, {
45+
error: 'Use one of the following options when specifying the container name: containerTypeId or containerTypeName.'
46+
})
47+
.refine((options: Options) => options.containerName || [options.containerTypeId, options.containerTypeName].filter(o => o !== undefined).length === 0, {
48+
error: 'Options containerTypeId and containerTypeName are only required when removing permissions from a container by name.'
49+
});
50+
}
51+
52+
public async commandAction(logger: Logger, args: CommandArgs): Promise<void> {
53+
if (!args.options.force) {
54+
const result = await cli.promptForConfirmation({ message: `Are you sure you want to remove permission '${args.options.id}' from container '${args.options.containerId || args.options.containerName}'?` });
55+
56+
if (!result) {
57+
return;
58+
}
59+
}
60+
61+
try {
62+
const containerId = await this.getContainerId(args.options, logger);
63+
64+
if (this.verbose) {
65+
await logger.logToStderr(`Removing permissions from container with ID '${containerId}'...`);
66+
}
67+
68+
const requestOptions: CliRequestOptions = {
69+
url: `${this.resource}/v1.0/storage/fileStorage/containers/${formatting.encodeQueryParameter(containerId)}/permissions/${args.options.id}`,
70+
headers: {
71+
accept: 'application/json;odata.metadata=none'
72+
},
73+
responseType: 'json'
74+
};
75+
76+
await request.delete(requestOptions);
77+
}
78+
catch (err: any) {
79+
this.handleRejectedODataJsonPromise(err);
80+
}
81+
}
82+
83+
private async getContainerId(options: Options, logger: Logger): Promise<string> {
84+
if (options.containerId) {
85+
return options.containerId;
86+
}
87+
88+
const containerTypeId = await this.getContainerTypeId(options, logger);
89+
90+
if (this.verbose) {
91+
await logger.logToStderr(`Getting container ID for container with name '${options.containerName}'...`);
92+
}
93+
94+
return spe.getContainerIdByName(containerTypeId, options.containerName!);
95+
}
96+
97+
private async getContainerTypeId(options: Options, logger: Logger): Promise<string> {
98+
if (options.containerTypeId) {
99+
return options.containerTypeId;
100+
}
101+
102+
if (this.verbose) {
103+
await logger.logToStderr(`Getting container type with name '${options.containerTypeName}'...`);
104+
}
105+
106+
return spe.getContainerTypeIdByName(options.containerTypeName!);
107+
}
108+
}
109+
110+
export default new SpeContainerPermissionRemoveCommand();

0 commit comments

Comments
 (0)