Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
72 changes: 72 additions & 0 deletions docs/docs/cmd/spe/container/container-permission-remove.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import Global from '../../_global.mdx';
import Tabs from '@theme/Tabs';
import TabItem from '@theme/TabItem';

# spe container permission remove

Removes permissions from a SharePoint Embedded Container

## Usage

```sh
m365 spe container permission remove [options]
```

## Options

```md definition-list
`-i, --id <id>`
: ID of the permission to remove.

`--containerId [containerId]`
: ID of a SharePoint Embedded container. Specify either `containerId` or `containerName` but not both.

`-n, --containerName [containerName]`
: Display name of the Container. Specify either `containerId` or `containerName` but not both.

`--containerTypeId [containerTypeId]`
: The ID of the container type. Specify either `containerTypeId` or `containerTypeName` when using `containerName` but not both.

`--containerTypeName [containerTypeName]`
: The name of the container type. Specify either `containerTypeId` or `containerTypeName` when using `containerName` but not both.

`-f, --force [force]`
: Don't prompt for confirmation.
```

## Permissions

<Tabs>
<TabItem value="Delegated">

| Resource | Permissions |
|-----------------|-------------------------------|
| Microsoft Graph | FileStorageContainer.Selected |

</TabItem>
<TabItem value="Application">

| Resource | Permissions |
|-----------------|-------------------------------|
| Microsoft Graph | FileStorageContainer.Selected |

</TabItem>
</Tabs>

## Examples

Removes the specified permission from a container

```sh
m365 spe container permission remove --containerId "b!ISJs1WRro0y0EWgkUYcktDa0mE8zSlFEqFzqRn70Zwp1CEtDEBZgQICPkRbil_5Z" --Id "cmVhZGVyX2k6MCMuZnxtZW1iZXJzaGlwfHJvcnlicjExMUBvdXRsb29rLmNvbQ"
```

Removes the specified permission from a container and doesn't prompt for confirmation

```sh
m365 spe container permission remove --containerId "b!ISJs1WRro0y0EWgkUYcktDa0mE8zSlFEqFzqRn70Zwp1CEtDEBZgQICPkRbil_5Z" --Id "cmVhZGVyX2k6MCMuZnxtZW1iZXJzaGlwfHJvcnlicjExMUBvdXRsb29rLmNvbQ" --force
```

## Response

The command won't return a response on success.
5 changes: 5 additions & 0 deletions docs/src/config/sidebars.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2221,6 +2221,11 @@ const sidebars: SidebarsConfig = {
label: 'container permission list',
id: 'cmd/spe/container/container-permission-list'
},
{
type: 'doc',
label: 'container permission remove',
id: 'cmd/spe/container/container-permission-remove'
},
{
type: 'doc',
label: 'container recyclebinitem list',
Expand Down
1 change: 1 addition & 0 deletions src/m365/spe/commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ export default {
CONTAINER_LIST: `${prefix} container list`,
CONTAINER_REMOVE: `${prefix} container remove`,
CONTAINER_PERMISSION_LIST: `${prefix} container permission list`,
CONTAINER_PERMISSION_REMOVE: `${prefix} container permission remove`,
CONTAINER_RECYCLEBINITEM_LIST: `${prefix} container recyclebinitem list`,
CONTAINER_RECYCLEBINITEM_REMOVE: `${prefix} container recyclebinitem remove`,
CONTAINER_RECYCLEBINITEM_RESTORE: `${prefix} container recyclebinitem restore`,
Expand Down
181 changes: 181 additions & 0 deletions src/m365/spe/commands/container/container-permission-remove.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,181 @@
import assert from 'assert';
import sinon from 'sinon';
import auth from '../../../../Auth.js';
import { Logger } from '../../../../cli/Logger.js';
import { CommandInfo } from "../../../../cli/CommandInfo.js";
import { CommandError } from '../../../../Command.js';
import request from '../../../../request.js';
import { telemetry } from '../../../../telemetry.js';
import { pid } from '../../../../utils/pid.js';
import { sinonUtil } from '../../../../utils/sinonUtil.js';
import commands from '../../commands.js';
import command, { options } from './container-permission-remove.js';
import { formatting } from '../../../../utils/formatting.js';
import { session } from '../../../../utils/session.js';
import { spe } from '../../../../utils/spe.js';
import { cli } from '../../../../cli/cli.js';

describe(commands.CONTAINER_PERMISSION_REMOVE, () => {
let log: string[];
let logger: Logger;
let commandInfo: CommandInfo;
let commandOptionsSchema: typeof options;
let promptIssued: boolean;

const permissionId = 'cmVhZGVyX2k6MCMuZnxtZW1iZXJzaGlwfHJvcnlicjExMUBvdXRsb29rLmNvbQ';
const containerTypeId = 'c6f08d91-77fa-485f-9369-f246ec0fc19c';
const containerTypeName = 'Container type name';
const containerId = 'b!McTeU0-dW0GxKwECWdW04TIvEK-Js9xJib_RFqF-CqZxNe3OHVAIT4SqBxGm4fND';
const containerName = 'Container name';

before(() => {
sinon.stub(auth, 'restoreAuth').resolves();
sinon.stub(telemetry, 'trackEvent').resolves();
sinon.stub(pid, 'getProcessName').returns('');
sinon.stub(session, 'getId').returns('');

sinon.stub(spe, 'getContainerTypeIdByName').withArgs(containerTypeName).resolves(containerTypeId);
sinon.stub(spe, 'getContainerIdByName').withArgs(containerTypeId, containerName).resolves(containerId);

auth.connection.active = true;
commandInfo = cli.getCommandInfo(command);
commandOptionsSchema = commandInfo.command.getSchemaToParse() as typeof options;
});

beforeEach(() => {
log = [];
logger = {
log: async (msg: string) => {
log.push(msg);
},
logRaw: async (msg: string) => {
log.push(msg);
},
logToStderr: async (msg: string) => {
log.push(msg);
}
};
sinon.stub(cli, 'promptForConfirmation').callsFake(() => {
promptIssued = true;
return Promise.resolve(false);
});
});

afterEach(() => {
sinonUtil.restore([
request.delete,
cli.promptForConfirmation
]);
});

after(() => {
sinon.restore();
auth.connection.active = false;
});

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

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

it('fails validation if permission id is not passed', async () => {
const actual = commandOptionsSchema.safeParse({ containerId: containerId });
assert.strictEqual(actual.success, false);
});

it('fails validation if both containerId and containerName options are passed', async () => {
const actual = commandOptionsSchema.safeParse({ id: permissionId, containerId: containerId, containerName: containerName });
assert.strictEqual(actual.success, false);
});

it('fails validation if neither containerId nor containerName options are passed', async () => {
const actual = commandOptionsSchema.safeParse({ id: permissionId });
assert.strictEqual(actual.success, false);
});

it('fails validation if containerId and containerTypeId options are passed', async () => {
const actual = commandOptionsSchema.safeParse({ id: permissionId, containerId: containerId, containerTypeId: containerTypeId });
assert.strictEqual(actual.success, false);
});

it('fails validation if containerId and containerTypeName options are passed', async () => {
const actual = commandOptionsSchema.safeParse({ id: permissionId, containerId: containerId, containerTypeName: containerTypeName });
assert.strictEqual(actual.success, false);
});

it('fails validation if containerName and both containerTypeId and containerTypeName options are passed', async () => {
const actual = commandOptionsSchema.safeParse({ id: permissionId, containerName: containerName, containerTypeId: containerTypeId, containerTypeName: containerTypeName });
assert.strictEqual(actual.success, false);
});

it('correctly removes permissions for a container by id', async () => {
const deleteStub = sinon.stub(request, 'delete').callsFake(async (opts) => {
if (opts.url === `https://graph.microsoft.com/v1.0/storage/fileStorage/containers/${formatting.encodeQueryParameter(containerId)}/permissions/${permissionId}`) {
return;
}

throw 'Invalid DELETE request: ' + opts.url;
});

await command.action(logger, { options: commandOptionsSchema.parse({ id: permissionId, containerId: containerId, verbose: true, force: true }) });
assert(deleteStub.calledOnce);
});

it('correctly removes permissions for a container by name and container type by id and prompts for confirmation', async () => {
sinonUtil.restore(cli.promptForConfirmation);
sinon.stub(cli, 'promptForConfirmation').resolves(true);

const deleteStub = sinon.stub(request, 'delete').callsFake(async (opts) => {
if (opts.url === `https://graph.microsoft.com/v1.0/storage/fileStorage/containers/${formatting.encodeQueryParameter(containerId)}/permissions/${permissionId}`) {
return;
}

throw 'Invalid DELETE request: ' + opts.url;
});

await command.action(logger, { options: commandOptionsSchema.parse({ id: permissionId, containerName: containerName, containerTypeId: containerTypeId, verbose: true }) });
assert(deleteStub.calledOnce);
});

it('correctly removes permissions for a container by name and container type by name', async () => {
const deleteStub = sinon.stub(request, 'delete').callsFake(async (opts) => {
if (opts.url === `https://graph.microsoft.com/v1.0/storage/fileStorage/containers/${formatting.encodeQueryParameter(containerId)}/permissions/${permissionId}`) {
return;
}

throw 'Invalid PATCH request: ' + opts.url;
});

await command.action(logger, { options: commandOptionsSchema.parse({ id: permissionId, containerName: containerName, containerTypeName: containerTypeName, verbose: true, force: true }) });
assert(deleteStub.calledOnce);
});

it('prompts before removing permissions when confirm option not passed', async () => {
await command.action(logger, { options: commandOptionsSchema.parse({ id: permissionId, containerId: containerId }) });

assert(promptIssued);
});

it('aborts removing permissions when prompt not confirmed', async () => {
const deleteSpy = sinon.stub(request, 'delete').resolves();

await command.action(logger, { options: commandOptionsSchema.parse({ id: permissionId, containerId: containerId }) });
assert(deleteSpy.notCalled);
});

it('correctly handles unexpected error', async () => {
const errorMessage = 'Access denied';
sinon.stub(request, 'delete').rejects({
error: {
code: 'accessDenied',
message: errorMessage
}
});

await assert.rejects(command.action(logger, { options: commandOptionsSchema.parse({ id: permissionId, containerId: containerId, force: true }) }),
new CommandError(errorMessage));
});
});
110 changes: 110 additions & 0 deletions src/m365/spe/commands/container/container-permission-remove.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
import { z } from 'zod';
import { Logger } from '../../../../cli/Logger.js';
import { globalOptionsZod } from '../../../../Command.js';
import commands from '../../commands.js';
import GraphCommand from '../../../base/GraphCommand.js';
import { spe } from '../../../../utils/spe.js';
import request, { CliRequestOptions } from '../../../../request.js';
import { formatting } from '../../../../utils/formatting.js';
import { cli } from '../../../../cli/cli.js';

export const options = z.strictObject({
...globalOptionsZod.shape,
id: z.string().alias('i'),
containerId: z.string().optional(),
containerName: z.string().alias('n').optional(),
containerTypeId: z.uuid().optional(),
containerTypeName: z.string().optional(),
force: z.boolean().alias('f').optional()
});
declare type Options = z.infer<typeof options>;

interface CommandArgs {
options: Options;
}

class SpeContainerPermissionRemoveCommand extends GraphCommand {
public get name(): string {
return commands.CONTAINER_PERMISSION_REMOVE;
}

public get description(): string {
return 'Removes SharePoint Embedded Container permission';
}

public get schema(): z.ZodType {
return options;
}

public getRefinedSchema(schema: typeof options): z.ZodObject<any> | undefined {
return schema
.refine((options: Options) => [options.containerId, options.containerName].filter(o => o !== undefined).length === 1, {
error: 'Use one of the following options: containerId or containerName.'
})
.refine((options: Options) => !options.containerName || [options.containerTypeId, options.containerTypeName].filter(o => o !== undefined).length === 1, {
error: 'Use one of the following options when specifying the container name: containerTypeId or containerTypeName.'
})
.refine((options: Options) => options.containerName || [options.containerTypeId, options.containerTypeName].filter(o => o !== undefined).length === 0, {
error: 'Options containerTypeId and containerTypeName are only required when removing permissions from a container by name.'
});
}

public async commandAction(logger: Logger, args: CommandArgs): Promise<void> {
if (!args.options.force) {
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}'?` });

if (!result) {
return;
}
}

try {
const containerId = await this.getContainerId(args.options, logger);

if (this.verbose) {
await logger.logToStderr(`Removing permissions from container with ID '${containerId}'...`);
}

const requestOptions: CliRequestOptions = {
url: `${this.resource}/v1.0/storage/fileStorage/containers/${formatting.encodeQueryParameter(containerId)}/permissions/${args.options.id}`,
headers: {
accept: 'application/json;odata.metadata=none'
},
responseType: 'json'
};

await request.delete(requestOptions);
}
catch (err: any) {
this.handleRejectedODataJsonPromise(err);
}
}

private async getContainerId(options: Options, logger: Logger): Promise<string> {
if (options.containerId) {
return options.containerId;
}

const containerTypeId = await this.getContainerTypeId(options, logger);

if (this.verbose) {
await logger.logToStderr(`Getting container ID for container with name '${options.containerName}'...`);
}

return spe.getContainerIdByName(containerTypeId, options.containerName!);
}

private async getContainerTypeId(options: Options, logger: Logger): Promise<string> {
if (options.containerTypeId) {
return options.containerTypeId;
}

if (this.verbose) {
await logger.logToStderr(`Getting container type with name '${options.containerTypeName}'...`);
}

return spe.getContainerTypeIdByName(options.containerTypeName!);
}
}

export default new SpeContainerPermissionRemoveCommand();
Loading