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 2 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
36 changes: 33 additions & 3 deletions docs/docs/cmd/spo/user/user-ensure.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,19 @@ m365 spo user ensure [options]
: Absolute URL of the site.

`--entraId [--entraId]`
: Id of the user in Entra. Specify either `entraId` or `userName`.
: Id of the user in Entra. Specify either `aadId`, `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 `aadId`, `entraId`, `userName`, `loginName`, `entraGroupId` or `entraGroupName`.

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

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

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

<Global />
Expand All @@ -41,6 +50,28 @@ Ensures a user by its user principal name.
m365 spo user ensure --webUrl https://contoso.sharepoint.com/sites/project --userName [email protected]
```

Ensures a user by its login name.

```sh
m365 spo user ensure --webUrl https://contoso.sharepoint.com/sites/Marketing --loginName "c:0t.c|tenant|e08e899f-ba40-4e91-ab36-44d4fbaa454e"
```

```sh
m365 spo user ensure --webUrl https://contoso.sharepoint.com/sites/Marketing --loginName "i:0#.f|membership|[email protected]"
```

Ensures a user by ID of the Microsoft Entra group.

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

Ensures a user by display name of the Microsoft Entra group.

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

## Response

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

</TabItem>
</Tabs>

200 changes: 199 additions & 1 deletion src/m365/spo/commands/user/user-ensure.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,10 @@ 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 +40,102 @@ 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 = {
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,6 +168,7 @@ describe(commands.USER_ENSURE, () => {

afterEach(() => {
sinonUtil.restore([
request.get,
request.post,
entraUser.getUpnByUserId
]);
Expand Down Expand Up @@ -116,6 +217,82 @@ describe(commands.USER_ENSURE, () => {
assert(loggerLogSpy.calledWith(ensuredUserResponse));
});

it('ensures user for 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));
});

it('ensures user for a specific web by entraGroupId', async () => {
sinon.stub(request, 'get').callsFake(async (opts) => {
if (opts.url === `https://graph.microsoft.com/v1.0/groups/${validEntraGroupId}`) {
return groupM365Response.value[0];
}

throw 'Invalid request';
});

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(loggerLogSpy.calledWith(ensuredGroupResponse));
});

it('ensures security group for a specific web by entraGroupName', async () => {
sinon.stub(request, 'get').callsFake(async (opts) => {
if (opts.url === `https://graph.microsoft.com/v1.0/groups?$filter=displayName eq '${validEntraSecurityGroupName}'`) {
return groupSecurityResponse;
}

throw 'Invalid request';
});

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(loggerLogSpy.calledWith(ensuredSecurityGroupResponse));
});

it('ensures user for a specific web by entraGroupName', async () => {
sinon.stub(request, 'get').callsFake(async (opts) => {
if (opts.url === `https://graph.microsoft.com/v1.0/groups?$filter=displayName eq '${validEntraGroupName}'`) {
return groupM365Response;
}

throw 'Invalid request';
});

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(loggerLogSpy.calledWith(ensuredGroupResponse));
});

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 +325,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 +352,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 +366,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