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

Enhances spo folder roleassignment commands with Microsoft Entra groups, Closes #6196 #6500

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
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
18 changes: 15 additions & 3 deletions docs/docs/cmd/spo/folder/folder-roleassignment-add.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -20,13 +20,19 @@ m365 spo folder roleassignment add [options]
: The server- or site-relative decoded URL of the folder.

`--principalId [principalId]`
: The SharePoint principal id. It may be either an user id or group id for which the role assignment will be addd. Specify either `upn`, `groupName` or `principalId` but not multiple.
: The SharePoint principal id. It may be either an user id or group id for which the role assignment will be addd. Specify either `upn`, `groupName`, `principalId`, `entraGroupId` or `entraGroupName` but not multiple.

`--upn [upn]`
: The upn/email of the user. Specify either `upn`, `groupName` or `principalId` but not multiple.
: The upn/email of the user. Specify either `upn`, `groupName`, `principalId`, `entraGroupId` or `entraGroupName` but not multiple.

`--groupName [groupName]`
: The Microsoft Entra or SharePoint group name. Specify either `upn`, `groupName` or `principalId` but not multiple.
: The Microsoft Entra or SharePoint group name. Specify either `upn`, `groupName`, `principalId`, `entraGroupId` or `entraGroupName` but not multiple.

`--entraGroupId [entraGroupId]`
: ID of the Microsoft Entra group to add. Specify either `upn`, `groupName`, `principalId`, `entraGroupId` or `entraGroupName` but not multiple.

`--entraGroupName [entraGroupName]`
: Display name of the Microsoft Entra group to add. Specify either `upn`, `groupName`, `principalId`, `entraGroupId` or `entraGroupName` but not multiple.

`--roleDefinitionId [roleDefinitionId]`
: ID of the role definition. Specify either `roleDefinitionId` or `roleDefinitionName` but not both.
Expand Down Expand Up @@ -63,6 +69,12 @@ Add the role assignment to the root folder based on the upn and role definition
m365 spo folder roleassignment add --webUrl "https://contoso.sharepoint.com/sites/contoso-sales" --folderUrl "/Shared Documents" --upn "[email protected]" --roleDefinitionName "Edit"
```

Add the role assignment to the specified folder based on the Entra Group Id and role definition id.

```sh
m365 spo folder roleassignment add --webUrl "https://contoso.sharepoint.com/sites/contoso-sales" --folderUrl "/Shared Documents/FolderPermission" --entraGroupId '27ae47f1-48f1-46f3-980b-d3c1470e398d' --roleDefinitionId 1073741827
```

## Response

The command won't return a response on success.
18 changes: 15 additions & 3 deletions docs/docs/cmd/spo/folder/folder-roleassignment-remove.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -20,13 +20,19 @@ m365 spo folder roleassignment remove [options]
: The server- or site-relative decoded URL of the folder.

`--principalId [principalId]`
: The SharePoint principal id. It may be either an user id or group id for which the role assignment will be removed. Specify either `upn`, `groupName` or `principalId` but not multiple.
: The SharePoint principal id. It may be either an user id or group id for which the role assignment will be removed. Specify either `upn`, `groupName`, `principalId`, `entraGroupId` or `entraGroupName` but not multiple.

`--upn [upn]`
: The upn/email of the user. Specify either `upn`, `groupName` or `principalId` but not multiple.
: The upn/email of the user. Specify either `upn`, `groupName`, `principalId`, `entraGroupId` or `entraGroupName` but not multiple.

`--groupName [groupName]`
: The Microsoft Entra or SharePoint group name. Specify either `upn`, `groupName` or `principalId` but not multiple.
: The Microsoft Entra or SharePoint group name. Specify either `upn`, `groupName`, `principalId`, `entraGroupId` or `entraGroupName` but not multiple.

`--entraGroupId [entraGroupId]`
: ID of the Microsoft Entra group to remove. Specify either `upn`, `groupName`, `principalId`, `entraGroupId` or `entraGroupName` but not multiple.

`--entraGroupName [entraGroupName]`
: Display name of the Microsoft Entra group to remove. Specify either `upn`, `groupName`, `principalId`, `entraGroupId` or `entraGroupName` but not multiple.

`-f, --force`
: Don't prompt for confirmation when removing the role assignment.
Expand Down Expand Up @@ -60,6 +66,12 @@ Remove the role assignment from the specified folder based on the upn.
m365 spo folder roleassignment remove --webUrl "https://contoso.sharepoint.com/sites/contoso-sales" --folderUrl "/Shared Documents/FolderPermission" --upn "[email protected]"
```

Remove the role assignment from the specified folder based on the Entra group id.

```sh
m365 spo folder roleassignment remove --webUrl "https://contoso.sharepoint.com/sites/contoso-sales" --folderUrl "/Shared Documents/FolderPermission" --entraGroupId '27ae47f1-48f1-46f3-980b-d3c1470e398d'
```

## Response

The command won't return a response on success.
142 changes: 141 additions & 1 deletion src/m365/spo/commands/folder/folder-roleassignment-add.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,65 @@ import spoRoleDefinitionFolderCommand from '../roledefinition/roledefinition-lis
import spoUserGetCommand from '../user/user-get.js';
import spoFolderGetCommand from './folder-get.js';
import command from './folder-roleassignment-add.js';
import { entraGroup } from '../../../../utils/entraGroup.js';
import { spo } from '../../../../utils/spo.js';

const graphGroup = {
id: '27ae47f1-48f1-46f3-980b-d3c1470e398d',
deletedDateTime: null,
classification: null,
createdDateTime: '2024-03-22T20:18:37Z',
creationOptions: [],
description: null,
displayName: 'Marketing',
expirationDateTime: null,
groupTypes: [
'Unified'
],
isAssignableToRole: null,
mail: '[email protected]',
mailEnabled: true,
mailNickname: 'Marketing',
membershipRule: null,
membershipRuleProcessingState: null,
onPremisesDomainName: null,
onPremisesLastSyncDateTime: null,
onPremisesNetBiosName: null,
onPremisesSamAccountName: null,
onPremisesSecurityIdentifier: null,
onPremisesSyncEnabled: null,
preferredDataLocation: null,
preferredLanguage: null,
proxyAddresses: [
'SPO:SPO_de7704ba-415d-4dd0-9bbd-fa565007a87e@SPO_18c58817-3bc9-489d-ac63-f7264fb357e5',
'SMTP:[email protected]'
],
renewedDateTime: '2024-03-22T20:18:37Z',
resourceBehaviorOptions: [],
resourceProvisioningOptions: [],
securityEnabled: true,
securityIdentifier: 'S-1-12-1-665733105-1190349041-3268610968-2369326662',
theme: null,
uniqueName: null,
visibility: 'Private',
onPremisesProvisioningErrors: [],
serviceProvisioningErrors: []
};

const entraGroupResponse = {
Id: 11,
IsHiddenInUI: false,
LoginName: 'c:0o.c|federateddirectoryclaimprovider|27ae47f1-48f1-46f3-980b-d3c1470e398d',
Title: 'Marketing members',
PrincipalType: 1,
Email: '',
Expiration: '',
IsEmailAuthenticationGuestUser: false,
IsShareByEmailGuestUser: false,
IsSiteAdmin: false,
UserId: null,
UserPrincipalName: null
};

describe(commands.FOLDER_ROLEASSIGNMENT_ADD, () => {
let log: any[];
Expand Down Expand Up @@ -49,7 +108,10 @@ describe(commands.FOLDER_ROLEASSIGNMENT_ADD, () => {
afterEach(() => {
sinonUtil.restore([
request.post,
cli.executeCommandWithOutput
cli.executeCommandWithOutput,
entraGroup.getGroupById,
entraGroup.getGroupByDisplayName,
spo.ensureEntraGroup
]);
});

Expand Down Expand Up @@ -126,6 +188,16 @@ describe(commands.FOLDER_ROLEASSIGNMENT_ADD, () => {
assert.notStrictEqual(actual, true);
});

it('fails validation if the entraGroupId option is not a valid GUID', async () => {
const actual = await command.validate({ options: { webUrl: 'https://contoso.sharepoint.com', folderUrl: '/Shared Documents/FolderPermission', entraGroupId: 'invalid', roleDefinitionId: 1073741827 } }, commandInfo);
assert.notStrictEqual(actual, true);
});

it('passes validation if the entraGroupId option is a valid GUID', async () => {
const actual = await command.validate({ options: { webUrl: 'https://contoso.sharepoint.com', folderUrl: '/Shared Documents/FolderPermission', entraGroupId: '37455d5c-e466-4e49-8eba-808b5acec21b', roleDefinitionId: 1073741827 } }, commandInfo);
assert.strictEqual(actual, true);
});

it('add the role assignment to the specified folder based on the upn and role definition id', async () => {
sinon.stub(request, 'post').callsFake(async (opts) => {
if (opts.url === 'https://contoso.sharepoint.com/_api/web/GetFolderByServerRelativePath(DecodedUrl=\'%2FShared%20Documents%2FFolderPermission\')/ListItemAllFields/breakroleinheritance(true)') {
Expand Down Expand Up @@ -384,4 +456,72 @@ describe(commands.FOLDER_ROLEASSIGNMENT_ADD, () => {
}
} as any), new CommandError(error));
});

it('adds the role assignment to the specified root folder based on the Entra group id and role definition id', async () => {
sinon.stub(entraGroup, 'getGroupById').withArgs(graphGroup.id).resolves(graphGroup);
sinon.stub(spo, 'ensureEntraGroup').withArgs('https://contoso.sharepoint.com', graphGroup).resolves(entraGroupResponse);

sinon.stub(request, 'post').callsFake(async (opts) => {
if (opts.url === 'https://contoso.sharepoint.com/_api/web/GetList(\'%2FShared%20Documents\')/breakroleinheritance(true)') {
return;
}

if (opts.url === 'https://contoso.sharepoint.com/_api/web/GetList(\'%2FShared%20Documents\')/roleassignments/addroleassignment(principalid=\'11\',roledefid=\'1073741827\')') {
return;
}

throw 'Invalid request';
});

sinon.stub(cli, 'executeCommandWithOutput').callsFake(async (command): Promise<any> => {
if (command === spoFolderGetCommand) {
return { "Exists": true, "IsWOPIEnabled": false, "ItemCount": 0, "Name": "test1", "ProgID": null, "ServerRelativeUrl": "/Shared Documents/FolderPermission", "TimeCreated": "2018-05-02T23:21:45Z", "TimeLastModified": "2018-05-02T23:21:45Z", "UniqueId": "0ac3da45-cacf-4c31-9b38-9ef3697d5a66", "WelcomePage": "" };
}
throw new CommandError('Unknown case');
});

await command.action(logger, {
options: {
debug: true,
webUrl: 'https://contoso.sharepoint.com',
folderUrl: '/Shared Documents',
entraGroupId: '27ae47f1-48f1-46f3-980b-d3c1470e398d',
roleDefinitionId: 1073741827
}
});
});

it('adds the role assignment to the specified root folder based on the Entra group name and role definition id', async () => {
sinon.stub(entraGroup, 'getGroupByDisplayName').withArgs(graphGroup.displayName).resolves(graphGroup);
sinon.stub(spo, 'ensureEntraGroup').withArgs('https://contoso.sharepoint.com', graphGroup).resolves(entraGroupResponse);

sinon.stub(request, 'post').callsFake(async (opts) => {
if (opts.url === 'https://contoso.sharepoint.com/_api/web/GetList(\'%2FShared%20Documents\')/breakroleinheritance(true)') {
return;
}

if (opts.url === 'https://contoso.sharepoint.com/_api/web/GetList(\'%2FShared%20Documents\')/roleassignments/addroleassignment(principalid=\'11\',roledefid=\'1073741827\')') {
return;
}

throw 'Invalid request';
});

sinon.stub(cli, 'executeCommandWithOutput').callsFake(async (command): Promise<any> => {
if (command === spoFolderGetCommand) {
return { "Exists": true, "IsWOPIEnabled": false, "ItemCount": 0, "Name": "test1", "ProgID": null, "ServerRelativeUrl": "/Shared Documents/FolderPermission", "TimeCreated": "2018-05-02T23:21:45Z", "TimeLastModified": "2018-05-02T23:21:45Z", "UniqueId": "0ac3da45-cacf-4c31-9b38-9ef3697d5a66", "WelcomePage": "" };
}
throw new CommandError('Unknown case');
});

await command.action(logger, {
options: {
debug: true,
webUrl: 'https://contoso.sharepoint.com',
folderUrl: '/Shared Documents',
entraGroupName: 'Marketing',
roleDefinitionId: 1073741827
}
});
});
});
53 changes: 41 additions & 12 deletions src/m365/spo/commands/folder/folder-roleassignment-add.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { Group } from '@microsoft/microsoft-graph-types';
import { cli } from '../../../../cli/cli.js';
import { Logger } from '../../../../cli/Logger.js';
import Command from '../../../../Command.js';
Expand All @@ -12,6 +13,8 @@ import spoGroupGetCommand, { Options as SpoGroupGetCommandOptions } from '../gro
import spoRoleDefinitionFolderCommand, { Options as SpoRoleDefinitionFolderCommandOptions } from '../roledefinition/roledefinition-list.js';
import { RoleDefinition } from '../roledefinition/RoleDefinition.js';
import spoUserGetCommand, { Options as SpoUserGetCommandOptions } from '../user/user-get.js';
import { entraGroup } from '../../../../utils/entraGroup.js';
import { spo } from '../../../../utils/spo.js';

interface CommandArgs {
options: Options;
Expand All @@ -23,6 +26,8 @@ interface Options extends GlobalOptions {
principalId?: number;
upn?: string;
groupName?: string;
entraGroupId?: string;
entraGroupName?: string;
roleDefinitionId?: number;
roleDefinitionName?: string;
}
Expand Down Expand Up @@ -51,6 +56,8 @@ class SpoFolderRoleAssignmentAddCommand extends SpoCommand {
principalId: typeof args.options.principalId !== 'undefined',
upn: typeof args.options.upn !== 'undefined',
groupName: typeof args.options.groupName !== 'undefined',
entraGroupId: typeof args.options.entraGroupId !== 'undefined',
entraGroupName: typeof args.options.entraGroupName !== 'undefined',
roleDefinitionId: typeof args.options.roleDefinitionId !== 'undefined',
roleDefinitionName: typeof args.options.roleDefinitionName !== 'undefined'
});
Expand All @@ -74,6 +81,12 @@ class SpoFolderRoleAssignmentAddCommand extends SpoCommand {
{
option: '--groupName [groupName]'
},
{
option: '--entraGroupId [entraGroupId]'
},
{
option: '--entraGroupName [entraGroupName]'
},
{
option: '--roleDefinitionId [roleDefinitionId]'
},
Expand All @@ -95,17 +108,21 @@ class SpoFolderRoleAssignmentAddCommand extends SpoCommand {
return `Specified principalId ${args.options.principalId} is not a number`;
}

if (args.options.entraGroupId && !validation.isValidGuid(args.options.entraGroupId)) {
return `'${args.options.entraGroupId}' is not a valid GUID for option entraGroupId.`;
}

if (args.options.roleDefinitionId && isNaN(args.options.roleDefinitionId)) {
return `Specified roleDefinitionId ${args.options.roleDefinitionId} is not a number`;
}

const principalOptions: any[] = [args.options.principalId, args.options.upn, args.options.groupName];
const principalOptions: any[] = [args.options.principalId, args.options.upn, args.options.groupName, args.options.entraGroupId, args.options.entraGroupName];
if (!principalOptions.some(item => item !== undefined)) {
return `Specify either principalId, upn or groupName`;
return `Specify either principalId, upn, groupName, entraGroupId or entraGroupName`;
}

if (principalOptions.filter(item => item !== undefined).length > 1) {
return `Specify either principalId, upn or groupName but not multiple`;
return `Specify either principalId, upn, groupName, entraGroupId or entraGroupName but not multiple`;
}

const roleDefinitionOptions: any[] = [args.options.roleDefinitionId, args.options.roleDefinitionName];
Expand Down Expand Up @@ -145,20 +162,32 @@ class SpoFolderRoleAssignmentAddCommand extends SpoCommand {
}

const roleDefinitionId = await this.getRoleDefinitionId(args.options);
let principalId: number | undefined = args.options.principalId;
if (args.options.upn) {
const upnPrincipalId = await this.getUserPrincipalId(args.options);
await this.breakRoleAssignment(requestUrl);
await this.addRoleAssignment(requestUrl, upnPrincipalId, roleDefinitionId);
principalId = await this.getUserPrincipalId(args.options);
}
else if (args.options.groupName) {
const groupPrincipalId = await this.getGroupPrincipalId(args.options);
await this.breakRoleAssignment(requestUrl);
await this.addRoleAssignment(requestUrl, groupPrincipalId, roleDefinitionId);
principalId = await this.getGroupPrincipalId(args.options);
}
else {
await this.breakRoleAssignment(requestUrl);
await this.addRoleAssignment(requestUrl, args.options.principalId!, roleDefinitionId);
else if (args.options.entraGroupId || args.options.entraGroupName) {
if (this.verbose) {
await logger.logToStderr('Retrieving group information...');
}

let group: Group;
if (args.options.entraGroupId) {
group = await entraGroup.getGroupById(args.options.entraGroupId);
}
else {
group = await entraGroup.getGroupByDisplayName(args.options.entraGroupName!);
}

const siteUser = await spo.ensureEntraGroup(args.options.webUrl, group);
principalId = siteUser.Id;
}

await this.breakRoleAssignment(requestUrl);
await this.addRoleAssignment(requestUrl, principalId!, roleDefinitionId);
}
catch (err: any) {
this.handleRejectedODataJsonPromise(err);
Expand Down
Loading