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-set.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 set

Updates SharePoint Embedded Container permission

## Usage

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

## Options

```md definition-list
`-i, --id <id>`
: ID of the permission which will be modified.

`--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.

`-r, --roles <roles>`:
: Comma separated list of permissions. Possible values are `reader`, `writer`, `manager`, `owner`.
```

## Permissions

<Tabs>
<TabItem value="Delegated">

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

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

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

</TabItem>
</Tabs>

## Examples

Updates role for the specified container

```sh
m365 spe container permission set --containerId "b!ISJs1WRro0y0EWgkUYcktDa0mE8zSlFEqFzqRn70Zwp1CEtDEBZgQICPkRbil_5Z" --id "cmVhZGVyX2k6MCMuZnxtZW1iZXJzaGlwfHJvcnlicjExMUBvdXRsb29rLmNvbQ" --roles "reader"
```

Updates multiple roles for the specified container

```sh
m365 spe container permission set --containerId "b!ISJs1WRro0y0EWgkUYcktDa0mE8zSlFEqFzqRn70Zwp1CEtDEBZgQICPkRbil_5Z" --id "cmVhZGVyX2k6MCMuZnxtZW1iZXJzaGlwfHJvcnlicjExMUBvdXRsb29rLmNvbQ" --roles "reader,writer"
```

## 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 set',
id: 'cmd/spe/container/container-permission-set'
},
{
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_SET: `${prefix} container permission set`,
CONTAINER_RECYCLEBINITEM_LIST: `${prefix} container recyclebinitem list`,
CONTAINER_RECYCLEBINITEM_REMOVE: `${prefix} container recyclebinitem remove`,
CONTAINER_RECYCLEBINITEM_RESTORE: `${prefix} container recyclebinitem restore`,
Expand Down
190 changes: 190 additions & 0 deletions src/m365/spe/commands/container/container-permission-set.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,190 @@
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-set.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_SET, () => {
let log: string[];
let logger: Logger;
let commandInfo: CommandInfo;
let commandOptionsSchema: typeof options;

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);
}
};
});

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

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

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

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, roles: 'reader' });
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, roles: 'reader' });
assert.strictEqual(actual.success, false);
});

it('fails validation if neither containerId nor containerName options are passed', async () => {
const actual = commandOptionsSchema.safeParse({ id: permissionId, roles: 'reader' });
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, roles: 'reader' });
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, roles: 'reader' });
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, roles: 'reader' });
assert.strictEqual(actual.success, false);
});

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

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

it('correctly updates permissions for a container by id', async () => {
const patchStub = sinon.stub(request, 'patch').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, containerId: containerId, roles: 'reader', verbose: true }) });
assert.deepStrictEqual(patchStub.lastCall.args[0].data, {
roles: ['reader']
});
});

it('correctly updates permissions for a container by name and container type by id', async () => {
const patchStub = sinon.stub(request, 'patch').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, containerTypeId: containerTypeId, roles: 'reader', verbose: true }) });
assert.deepStrictEqual(patchStub.lastCall.args[0].data, {
roles: ['reader']
});
});

it('correctly updates permissions for a container by name and container type by name', async () => {
const patchStub = sinon.stub(request, 'patch').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, roles: 'reader', verbose: true }) });
assert.deepStrictEqual(patchStub.lastCall.args[0].data, {
roles: ['reader']
});
});

it('correctly updates multiple permissions for a container by id', async () => {
const patchStub = sinon.stub(request, 'patch').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, containerId: containerId, roles: 'reader,writer', verbose: true }) });
assert.deepStrictEqual(patchStub.lastCall.args[0].data, {
roles: ['reader', 'writer']
});
});

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

await assert.rejects(command.action(logger, { options: commandOptionsSchema.parse({ id: permissionId, containerId: containerId, roles: 'reader' }) }),
new CommandError(errorMessage));
});
});
104 changes: 104 additions & 0 deletions src/m365/spe/commands/container/container-permission-set.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
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';

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(),
roles: z.string().alias('r').transform((value) => value.split(',')).pipe(z.enum(['reader', 'writer', 'manager', 'owner']).array())
});
declare type Options = z.infer<typeof options>;

interface CommandArgs {
options: Options;
}

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

public get description(): string {
return 'Updates 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 adding permissions to a container by name.'
});
}

public async commandAction(logger: Logger, args: CommandArgs): Promise<void> {
try {
const containerId = await this.getContainerId(args.options, logger);

if (this.verbose) {
await logger.logToStderr(`Adding permissions to 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',
data: {
roles: args.options.roles
}
};

await request.patch(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 SpeContainerPermissionSetCommand();
Loading