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

Aligned Entra group member commands. Closes #6546 #6585

Closed
wants to merge 11 commits into from
Closed
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
19 changes: 17 additions & 2 deletions docs/docs/cmd/entra/group/group-member-add.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,13 @@ m365 entra group member add [options]

```md definition-list
`-i, --groupId [groupId]`
: The ID of the Microsoft Entra group. Specify `groupId` or `groupDisplayName` but not both.
: The ID of the Microsoft Entra group. Specify `groupId`, `groupDisplayName` or `groupName` but not multiple.

`-n, --groupDisplayName [groupDisplayName]`
: The display name of the Microsoft Entra group. Specify `groupId` or `groupDisplayName` but not both.
: (deprecated. Use option `groupName` instead) The display name of the Microsoft Entra group. Specify `groupId`, `groupDisplayName` or `groupName` but not multiple.

`--groupName [groupName]`
: The display name of the Microsoft Entra group. Specify `groupId`, `groupDisplayName` or `groupName` but not multiple.

`--ids [ids]`
: Microsoft Entra IDs of users. You can also pass a comma-separated list of IDs. Specify either `ids` or `userNames` but not both.
Expand All @@ -39,6 +42,12 @@ Add a single member specified by ID as a member to a group specified by display
m365 entra group member add --groupDisplayName Developers --ids 098b9f52-f48c-4401-819f-29c33794c3f5 --role Member
```

Add a single member specified by ID as a member to a group specified by group name.

```sh
m365 entra group member add --groupName Developers --ids 098b9f52-f48c-4401-819f-29c33794c3f5 --role Member
```

Add multiple members specified by ID as members to a group specified by ID.

```sh
Expand All @@ -51,6 +60,12 @@ Add a single member specified by UPN as an owner to a group specified by display
m365 entra group member add --groupDisplayName Developers --userNames john.doe@contoso.com --role Owner
```

Add a single member specified by UPN as an owner to a group specified by group name.

```sh
m365 entra group member add --groupName Developers --userNames john.doe@contoso.com --role Owner
```

Adds multiple members specified by UPN as owners to a group specified by ID.

```sh
Expand Down
19 changes: 17 additions & 2 deletions docs/docs/cmd/entra/group/group-member-set.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,13 @@ m365 entra group member set [options]

```md definition-list
`-i, --groupId [groupId]`
: The ID of the Entra ID group. Specify `groupId` or `groupDisplayName` but not both.
: The ID of the Entra ID group. Specify `groupId`, `groupDisplayName` or `groupName` but not multiple.

`-n, --groupDisplayName [groupDisplayName]`
: The display name of the Entra ID group. Specify `groupId` or `groupDisplayName` but not both.
: (deprecated. Use option `groupName` instead) The display name of the Entra ID group. Specify `groupId`, `groupDisplayName` or `groupName` but not multiple.

`--groupName [groupName]`
: The display name of the Microsoft Entra group. Specify `groupId`, `groupDisplayName` or `groupName` but not multiple.

`--ids [ids]`
: Comma-separated list of user IDs. Specify either `ids` or `userNames` but not both.
Expand All @@ -39,6 +42,12 @@ Update a single member specified by ID to a member of a group specified by displ
m365 entra group member set --groupDisplayName Developers --ids 098b9f52-f48c-4401-819f-29c33794c3f5 --role Member
```

Update a single member specified by ID to a member of a group specified by group name

```sh
m365 entra group member set --groupName Developers --ids 098b9f52-f48c-4401-819f-29c33794c3f5 --role Member
```

Update multiple members specified by ID to members of a group specified by ID

```sh
Expand All @@ -51,6 +60,12 @@ Update a single member specified by UPN to an owner of a group specified by disp
m365 entra group member set --groupDisplayName Developers --userNames john.doe@contoso.com --role Owner
```

Update a single member specified by UPN to an owner of a group specified by group name

```sh
m365 entra group member set --groupName Developers --userNames john.doe@contoso.com --role Owner
```

Update multiple members specified by UPN to owners of a group specified by ID

```sh
Expand Down
66 changes: 66 additions & 0 deletions src/m365/entra/commands/group/group-member-add.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,32 @@ describe(commands.GROUP_MEMBER_ADD, () => {
assert.strictEqual(actual, true);
});

it(`correctly shows deprecation warning for option 'groupDisplayName'`, async () => {
const chalk = (await import('chalk')).default;
const loggerErrSpy = sinon.spy(logger, 'logToStderr');

sinon.stub(entraGroup, 'getGroupIdByDisplayName').resolves(groupId);
sinon.stub(entraUser, 'getUserIdsByUpns').resolves(userIds);

sinon.stub(request, 'post').callsFake(async opts => {
if (opts.url === 'https://graph.microsoft.com/v1.0/$batch') {
return {
responses: Array(2).fill({
status: 204,
body: {}
})
};
}

throw 'Invalid request';
});

await command.action(logger, { options: { groupDisplayName: 'IT department', ids: userIds.join(','), role: 'Member', verbose: true } });
assert(loggerErrSpy.calledWith(chalk.yellow(`Option 'groupDisplayName' is deprecated and will be removed in the next major release.`)));

sinonUtil.restore(loggerErrSpy);
});

it('successfully adds users to the group with ids', async () => {
const postStub = sinon.stub(request, 'post').callsFake(async opts => {
if (opts.url === 'https://graph.microsoft.com/v1.0/$batch') {
Expand Down Expand Up @@ -224,6 +250,46 @@ describe(commands.GROUP_MEMBER_ADD, () => {
]);
});

it('successfully adds users to the group using groupName and userNames', async () => {
sinon.stub(entraGroup, 'getGroupIdByDisplayName').resolves(groupId);
sinon.stub(entraUser, 'getUserIdsByUpns').resolves(userIds);

const postStub = sinon.stub(request, 'post').callsFake(async opts => {
if (opts.url === 'https://graph.microsoft.com/v1.0/$batch') {
return {
responses: Array(2).fill({
status: 204,
body: {}
})
};
}

throw 'Invalid request';
});

await command.action(logger, { options: { groupName: 'Contoso', userNames: userUpns.join(','), role: 'Owner', verbose: true } });
assert.deepStrictEqual(postStub.lastCall.args[0].data.requests, [
{
id: 1,
method: 'PATCH',
url: `/groups/${groupId}`,
headers: { 'content-type': 'application/json;odata.metadata=none' },
body: {
'owners@odata.bind': userIds.slice(0, 20).map(u => `https://graph.microsoft.com/v1.0/directoryObjects/${u}`)
}
},
{
id: 21,
method: 'PATCH',
url: `/groups/${groupId}`,
headers: { 'content-type': 'application/json;odata.metadata=none' },
body: {
'owners@odata.bind': userIds.slice(20, 40).map(u => `https://graph.microsoft.com/v1.0/directoryObjects/${u}`)
}
}
]);
});

it('successfully adds users to the group with names and leading spaces', async () => {
sinon.stub(entraGroup, 'getGroupIdByDisplayName').resolves(groupId);
sinon.stub(entraUser, 'getUserIdsByUpns').resolves(userIds);
Expand Down
21 changes: 15 additions & 6 deletions src/m365/entra/commands/group/group-member-add.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ interface CommandArgs {
interface Options extends GlobalOptions {
groupId?: string;
groupDisplayName?: string;
groupName?: string;
ids?: string;
userNames?: string;
role: string;
Expand Down Expand Up @@ -45,6 +46,7 @@ class EntraGroupMemberAddCommand extends GraphCommand {
Object.assign(this.telemetryProperties, {
groupId: typeof args.options.groupId !== 'undefined',
groupDisplayName: typeof args.options.groupDisplayName !== 'undefined',
groupName: typeof args.options.groupName !== 'undefined',
ids: typeof args.options.ids !== 'undefined',
userNames: typeof args.options.userNames !== 'undefined'
});
Expand All @@ -57,7 +59,10 @@ class EntraGroupMemberAddCommand extends GraphCommand {
option: '-i, --groupId [groupId]'
},
{
option: '-n, --groupDisplayName [groupDisplayName]'
option: '--groupDisplayName [groupDisplayName]'
},
{
option: '-n, --groupName [groupName]'
},
{
option: '--ids [ids]'
Expand Down Expand Up @@ -104,19 +109,23 @@ class EntraGroupMemberAddCommand extends GraphCommand {

#initOptionSets(): void {
this.optionSets.push(
{ options: ['groupId', 'groupDisplayName'] },
{ options: ['groupId', 'groupDisplayName', 'groupName'] },
{ options: ['ids', 'userNames'] }
);
}

#initTypes(): void {
this.types.string.push('groupId', 'groupDisplayName', 'ids', 'userNames', 'role');
this.types.string.push('groupId', 'groupDisplayName', 'groupName', 'ids', 'userNames', 'role');
}

public async commandAction(logger: Logger, args: CommandArgs): Promise<void> {
try {
if (args.options.groupDisplayName) {
await this.warn(logger, `Option 'groupDisplayName' is deprecated and will be removed in the next major release.`);
}

if (this.verbose) {
await logger.logToStderr(`Adding member(s) ${args.options.ids || args.options.userNames} to group ${args.options.groupId || args.options.groupDisplayName}...`);
await logger.logToStderr(`Adding member(s) ${args.options.ids || args.options.userNames} to group ${args.options.groupId || args.options.groupDisplayName || args.options.groupName}...`);
}

const groupId = await this.getGroupId(logger, args.options);
Expand Down Expand Up @@ -169,10 +178,10 @@ class EntraGroupMemberAddCommand extends GraphCommand {
}

if (this.verbose) {
await logger.logToStderr(`Retrieving ID of group ${options.groupDisplayName}...`);
await logger.logToStderr(`Retrieving ID of group ${options.groupDisplayName || options.groupName}...`);
}

return entraGroup.getGroupIdByDisplayName(options.groupDisplayName!);
return entraGroup.getGroupIdByDisplayName(options.groupDisplayName! || options.groupName!);
}

private async getUserIds(logger: Logger, options: Options): Promise<string[]> {
Expand Down
124 changes: 124 additions & 0 deletions src/m365/entra/commands/group/group-member-set.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,61 @@ describe(commands.GROUP_MEMBER_SET, () => {
assert.strictEqual(actual, true);
});

it(`correctly shows deprecation warning for option 'groupDisplayName'`, async () => {
const chalk = (await import('chalk')).default;
const loggerErrSpy = sinon.spy(logger, 'logToStderr');

sinon.stub(entraGroup, 'getGroupIdByDisplayName').resolves(groupId);
sinon.stub(entraUser, 'getUserIdsByUpns').resolves(userIds);

sinon.stub(request, 'post').callsFake(async opts => {
if (opts.url === 'https://graph.microsoft.com/v1.0/$batch' &&
opts.data.requests[0].method === 'PATCH') {
return {
responses: Array(2).fill({
status: 204,
body: {}
})
};
}

if (opts.url === 'https://graph.microsoft.com/v1.0/$batch' &&
opts.data.requests[0].method === 'GET') {
return {
responses: [
{
id: userIds[0],
status: 200,
body: 1
},
{
id: userIds[2],
status: 200,
body: 1
}
]
};
}

if (opts.url === 'https://graph.microsoft.com/v1.0/$batch' &&
opts.data.requests[0].method === 'DELETE') {
return {
responses: Array(2).fill({
status: 204,
body: {}
})
};
}

throw 'Invalid request';
});

await command.action(logger, { options: { groupDisplayName: 'Contoso', ids: userIds.join(','), role: 'Member', verbose: true } });
assert(loggerErrSpy.calledWith(chalk.yellow(`Option 'groupDisplayName' is deprecated and will be removed in the next major release.`)));

sinonUtil.restore(loggerErrSpy);
});

it('successfully updates roles for users with ids in the group', async () => {
const postStub = sinon.stub(request, 'post').callsFake(async opts => {
if (opts.url === 'https://graph.microsoft.com/v1.0/$batch' &&
Expand Down Expand Up @@ -311,6 +366,75 @@ describe(commands.GROUP_MEMBER_SET, () => {
]);
});

it('successfully updates roles for users using groupName and userNames', async () => {
sinon.stub(entraGroup, 'getGroupIdByDisplayName').resolves(groupId);
sinon.stub(entraUser, 'getUserIdsByUpns').resolves(userIds);

const postStub = sinon.stub(request, 'post').callsFake(async opts => {
if (opts.url === 'https://graph.microsoft.com/v1.0/$batch' &&
opts.data.requests[0].method === 'PATCH') {
return {
responses: Array(2).fill({
status: 204,
body: {}
})
};
}

if (opts.url === 'https://graph.microsoft.com/v1.0/$batch' &&
opts.data.requests[0].method === 'GET') {
return {
responses: [
{
id: userIds[0],
status: 200,
body: 1
},
{
id: userIds[2],
status: 200,
body: 1
}
]
};
}

if (opts.url === 'https://graph.microsoft.com/v1.0/$batch' &&
opts.data.requests[0].method === 'DELETE') {
return {
responses: Array(2).fill({
status: 204,
body: {}
})
};
}

throw 'Invalid request';
});

await command.action(logger, { options: { groupName: 'Contoso', userNames: userUpns.join(','), role: 'Owner', verbose: true } });
assert.deepStrictEqual(postStub.firstCall.args[0].data.requests, [
{
id: 1,
method: 'PATCH',
url: `/groups/${groupId}`,
headers: { 'content-type': 'application/json;odata.metadata=none' },
body: {
'owners@odata.bind': userIds.slice(0, 20).map(u => `https://graph.microsoft.com/v1.0/directoryObjects/${u}`)
}
},
{
id: 21,
method: 'PATCH',
url: `/groups/${groupId}`,
headers: { 'content-type': 'application/json;odata.metadata=none' },
body: {
'owners@odata.bind': userIds.slice(20, 40).map(u => `https://graph.microsoft.com/v1.0/directoryObjects/${u}`)
}
}
]);
});

it('successfully updates roles for users with names and leading spaces in the group', async () => {
sinon.stub(entraGroup, 'getGroupIdByDisplayName').resolves(groupId);
sinon.stub(entraUser, 'getUserIdsByUpns').resolves(userIds);
Expand Down
Loading