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

Extends "spo user ensure" command with support for specifying more options. Closes #6181 #6426

Open
wants to merge 5 commits 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
38 changes: 32 additions & 6 deletions docs/docs/cmd/spo/user/user-ensure.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -18,29 +18,56 @@ m365 spo user ensure [options]
`-u, --webUrl <webUrl>`
: Absolute URL of the site.

`--entraId [--entraId]`
: Id of the user in Entra. Specify either `entraId` or `userName`.
`--entraId [entraId]`
: Id of the user in Entra ID. Specify either `entraId`, `userName`, `loginName`, `entraGroupId` or `entraGroupName`.

`--userName [userName]`
: User's UPN (user principal name, e.g. [email protected]). Specify either `entraId` or `userName`.
: User's UPN (user principal name, e.g. [email protected]). Specify either `entraId`, `userName`, `loginName`, `entraGroupId` or `entraGroupName`.

`--loginName [loginName]`
: The login name of the principal. Specify either `entraId`, `userName`, `loginName`, `entraGroupId` or `entraGroupName`.

`--entraGroupId [entraGroupId]`
: ID of the Microsoft Entra group. Specify either `entraId`, `userName`, `loginName`, `entraGroupId` or `entraGroupName`.

`--entraGroupName [entraGroupName]`
: Display name of the Microsoft Entra group. Specify either `entraId`, `userName`, `loginName`, `entraGroupId` or `entraGroupName`.
```

<Global />

## Examples

Ensures a user by its Entra Id.
Ensure a user by its Entra Id.

```sh
m365 spo user ensure --webUrl https://contoso.sharepoint.com/sites/project --entraId e254750a-eaa4-44f6-9517-b74f65cdb747
```

Ensures a user by its user principal name.
Ensure a user by its user principal name.

```sh
m365 spo user ensure --webUrl https://contoso.sharepoint.com/sites/project --userName [email protected]
```

Ensure a user by its login name.

```sh
m365 spo user ensure --webUrl https://contoso.sharepoint.com/sites/Marketing --loginName "i:0#.f|membership|[email protected]"
nanddeepn marked this conversation as resolved.
Show resolved Hide resolved
```

Ensure a Microsoft Entra group by ID.

```sh
m365 spo user ensure --webUrl https://contoso.sharepoint.com/sites/Marketing --entraGroupId e08e899f-ba40-4e91-ab36-44d4fbaa454e
```

Ensure a Microsoft Entra group by display name.

```sh
m365 spo user ensure --webUrl https://contoso.sharepoint.com/sites/Marketing --entraGroupName "Marketing team"
```

## Response

<Tabs>
Expand Down Expand Up @@ -119,4 +146,3 @@ m365 spo user ensure --webUrl https://contoso.sharepoint.com/sites/project --use

</TabItem>
</Tabs>

196 changes: 193 additions & 3 deletions src/m365/spo/commands/user/user-ensure.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,16 @@ import { session } from '../../../../utils/session.js';
import { sinonUtil } from '../../../../utils/sinonUtil.js';
import commands from '../../commands.js';
import command from './user-ensure.js';
import { entraGroup } from '../../../../utils/entraGroup.js';

describe(commands.USER_ENSURE, () => {
const validUserName = '[email protected]';
const validEntraId = '2056d2f6-3257-4253-8cfc-b73393e414e5';
const validWebUrl = 'https://contoso.sharepoint.com';
const validEntraGroupId = '2056d2f6-3257-4253-8cfc-b73393e414e5';
const validEntraGroupName = 'Finance';
const validEntraSecurityGroupName = 'EntraGroupTest';
const validLoginName = `i:0#.f|membership|${validUserName}`;
const ensuredUserResponse = {
Id: 35,
IsHiddenInUI: false,
Expand All @@ -36,6 +41,103 @@ describe(commands.USER_ENSURE, () => {
UserPrincipalName: validUserName
};

const groupM365Response = {
value: [{
"id": "2056d2f6-3257-4253-8cfc-b73393e414e5",
"deletedDateTime": null,
"classification": null,
"createdDateTime": "2017-11-29T03:27:05Z",
"description": "This is the Contoso Finance Group. Please come here and check out the latest news, posts, files, and more.",
"displayName": "Finance",
"groupTypes": [
"Unified"
],
"mail": "[email protected]",
"mailEnabled": true,
"mailNickname": "finance",
"onPremisesLastSyncDateTime": null,
"onPremisesProvisioningErrors": [],
"onPremisesSecurityIdentifier": null,
"onPremisesSyncEnabled": null,
"preferredDataLocation": null,
"proxyAddresses": [
"SMTP:[email protected]"
],
"renewedDateTime": "2017-11-29T03:27:05Z",
"securityEnabled": false,
"visibility": "Public"
}]
};

const ensuredGroupResponse = {
Id: 35,
IsHiddenInUI: false,
LoginName: `c:0o.c|federateddirectoryclaimprovider|${validEntraGroupId}`,
Title: validEntraGroupName,
PrincipalType: 4,
Email: '[email protected]',
Expiration: '',
IsEmailAuthenticationGuestUser: false,
IsShareByEmailGuestUser: false,
IsSiteAdmin: false,
UserId: null,
UserPrincipalName: null
};

const groupSecurityResponse = {
value: [{
"id": "2056d2f6-3257-4253-8cfc-b73393e414e5",
"deletedDateTime": null,
"classification": null,
"createdDateTime": "2024-01-27T16:02:56Z",
"creationOptions": [],
"description": "Entra Group Test",
"displayName": "EntraGroupTest",
"expirationDateTime": null,
"groupTypes": [],
"isAssignableToRole": true,
"mail": null,
"mailEnabled": false,
"mailNickname": "f45205a2-d",
"membershipRule": null,
"membershipRuleProcessingState": null,
"onPremisesDomainName": null,
"onPremisesLastSyncDateTime": null,
"onPremisesNetBiosName": null,
"onPremisesSamAccountName": null,
"onPremisesSecurityIdentifier": null,
"onPremisesSyncEnabled": null,
"preferredDataLocation": null,
"preferredLanguage": null,
"proxyAddresses": [],
"renewedDateTime": "2024-01-27T16:02:56Z",
"resourceBehaviorOptions": [],
"resourceProvisioningOptions": [],
"securityEnabled": true,
"securityIdentifier": "S-1-12-1-1968173404-1154184881-1694549896-3083850660",
"theme": null,
"visibility": "Private",
"onPremisesProvisioningErrors": [],
"serviceProvisioningErrors": []
}]
};

const ensuredSecurityGroupResponse = {
logonName: 'c:0t.c|tenant|2056d2f6-3257-4253-8cfc-b73393e414e5',
Id: 35,
IsHiddenInUI: false,
LoginName: `c:0t.c|tenant||${validEntraGroupId}`,
Title: validEntraGroupName,
PrincipalType: 4,
Email: null,
Expiration: '',
IsEmailAuthenticationGuestUser: false,
IsShareByEmailGuestUser: false,
IsSiteAdmin: false,
UserId: null,
UserPrincipalName: null
};

let log: any[];
let logger: Logger;
let loggerLogSpy: sinon.SinonSpy;
Expand Down Expand Up @@ -68,8 +170,11 @@ describe(commands.USER_ENSURE, () => {

afterEach(() => {
sinonUtil.restore([
request.get,
request.post,
entraUser.getUpnByUserId
entraUser.getUpnByUserId,
entraGroup.getGroupById,
entraGroup.getGroupByDisplayName
]);
});

Expand All @@ -86,7 +191,7 @@ describe(commands.USER_ENSURE, () => {
assert.notStrictEqual(command.description, null);
});

it('ensures user for a specific web by userPrincipalName', async () => {
it('ensures user in a specific web by userPrincipalName', async () => {
sinon.stub(request, 'post').callsFake(async (opts) => {
if (opts.url === `${validWebUrl}/_api/web/ensureuser`) {
return ensuredUserResponse;
Expand All @@ -99,7 +204,7 @@ describe(commands.USER_ENSURE, () => {
assert(loggerLogSpy.calledWith(ensuredUserResponse));
});

it('ensures user for a specific web by entraId', async () => {
it('ensures user in a specific web by entraId', async () => {
sinon.stub(entraUser, 'getUpnByUserId').callsFake(async () => {
return validUserName;
});
Expand All @@ -116,6 +221,70 @@ describe(commands.USER_ENSURE, () => {
assert(loggerLogSpy.calledWith(ensuredUserResponse));
});

it('ensures user in a specific web by loginName', async () => {
sinon.stub(request, 'post').callsFake(async (opts) => {
if (opts.url === `${validWebUrl}/_api/web/ensureuser`) {
return ensuredUserResponse;
}

throw 'Invalid request';
});

await command.action(logger, { options: { verbose: true, webUrl: validWebUrl, loginName: validLoginName } });
assert(loggerLogSpy.calledWith(ensuredUserResponse));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's check for the request body here as well.

});

it('ensures user in a specific web by entraGroupId', async () => {
sinon.stub(entraGroup, 'getGroupById').callsFake(async () => {
return groupM365Response.value[0];
});
Comment on lines +238 to +240
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
sinon.stub(entraGroup, 'getGroupById').callsFake(async () => {
return groupM365Response.value[0];
});
sinon.stub(entraGroup, 'getGroupById').resolves(groupM365Response.value[0]);


const postStub = sinon.stub(request, 'post').callsFake(async (opts) => {
if (opts.url === `${validWebUrl}/_api/web/ensureuser`) {
return ensuredGroupResponse;
}

throw 'Invalid request';
});

await command.action(logger, { options: { verbose: true, webUrl: validWebUrl, entraGroupId: validEntraGroupId } });
assert.deepStrictEqual(postStub.firstCall.args[0].data, { logonName: 'c:0o.c|federateddirectoryclaimprovider|2056d2f6-3257-4253-8cfc-b73393e414e5' });
});

it('ensures security group in a specific web by entraGroupName', async () => {
sinon.stub(entraGroup, 'getGroupByDisplayName').callsFake(async () => {
return groupSecurityResponse.value[0];
});
Comment on lines +255 to +257
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
sinon.stub(entraGroup, 'getGroupByDisplayName').callsFake(async () => {
return groupSecurityResponse.value[0];
});
sinon.stub(entraGroup, 'getGroupByDisplayName').ensures(groupSecurityResponse.value[0]);


const postStub = sinon.stub(request, 'post').callsFake(async (opts) => {
if (opts.url === `${validWebUrl}/_api/web/ensureuser`) {
return ensuredSecurityGroupResponse;
}

throw 'Invalid request';
});

await command.action(logger, { options: { verbose: true, webUrl: validWebUrl, entraGroupName: validEntraSecurityGroupName } });
assert.deepStrictEqual(postStub.firstCall.args[0].data, { logonName: 'c:0t.c|tenant|2056d2f6-3257-4253-8cfc-b73393e414e5' });
});

it('ensures group in a specific web by entraGroupName', async () => {
sinon.stub(entraGroup, 'getGroupByDisplayName').callsFake(async () => {
return groupM365Response.value[0];
});
Comment on lines +272 to +274
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's use .resolves(...)


const postStub = sinon.stub(request, 'post').callsFake(async (opts) => {
if (opts.url === `${validWebUrl}/_api/web/ensureuser`) {
return ensuredGroupResponse;
}

throw 'Invalid request';
});

await command.action(logger, { options: { verbose: true, webUrl: validWebUrl, entraGroupName: validEntraGroupName } });
assert.deepStrictEqual(postStub.firstCall.args[0].data, { logonName: 'c:0o.c|federateddirectoryclaimprovider|2056d2f6-3257-4253-8cfc-b73393e414e5' });
});

it('throws error message when no user was found with a specific id', async () => {
sinon.stub(entraUser, 'getUpnByUserId').callsFake(async (id) => {
throw {
Expand Down Expand Up @@ -148,6 +317,7 @@ describe(commands.USER_ENSURE, () => {
}
}
};

sinon.stub(request, 'post').callsFake(async (opts) => {
if (opts.url === `${validWebUrl}/_api/web/ensureuser`) {
throw error;
Expand All @@ -174,6 +344,11 @@ describe(commands.USER_ENSURE, () => {
assert.notStrictEqual(actual, true);
});

it('fails validation if entraGroupId is not a valid id', async () => {
const actual = await command.validate({ options: { webUrl: validWebUrl, entraGroupId: 'invalid' } }, commandInfo);
assert.notStrictEqual(actual, true);
});

it('passes validation if the url is valid and entraId is a valid id', async () => {
const actual = await command.validate({ options: { webUrl: validWebUrl, entraId: validEntraId } }, commandInfo);
assert.strictEqual(actual, true);
Expand All @@ -183,4 +358,19 @@ describe(commands.USER_ENSURE, () => {
const actual = await command.validate({ options: { webUrl: validWebUrl, userName: validUserName } }, commandInfo);
assert.strictEqual(actual, true);
});

it('passes validation if the url is valid and loginName is passed', async () => {
const actual = await command.validate({ options: { webUrl: validWebUrl, loginName: validLoginName } }, commandInfo);
assert.strictEqual(actual, true);
});

it('passes validation if the url is valid and entraGroupName is passed', async () => {
const actual = await command.validate({ options: { webUrl: validWebUrl, entraGroupName: validEntraGroupName } }, commandInfo);
assert.strictEqual(actual, true);
});

it('passes validation if the url is valid and entraGroupId is passed', async () => {
const actual = await command.validate({ options: { webUrl: validWebUrl, entraGroupId: validEntraGroupId } }, commandInfo);
assert.strictEqual(actual, true);
});
});
Loading
Loading