Skip to content

Commit b564b61

Browse files
waldekmastykarzAdam-it
authored andcommitted
Extends setup with a custom Entra app
1 parent 309a9b8 commit b564b61

31 files changed

+1921
-724
lines changed

.vscode/launch.json

+5-1
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,11 @@
1616
// 'm365 spo site get --url /', you'd use:
1717
// "args": ["spo", "site", "get", "--url", "/"]
1818
// after debugging, revert changes so that they won't end up in your PR
19-
"args": []
19+
"args": [],
20+
"console": "integratedTerminal",
21+
"env": {
22+
"NODE_OPTIONS": "--enable-source-maps"
23+
}
2024
},
2125
{
2226
"type": "node",

docs/docs/_clisettings.mdx

+6
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,11 @@ Setting name|Definition|Default value
22
------------|----------|-------------
33
`authType`|Default login method to use when running `m365 login` without the `--authType` option.|`deviceCode`
44
`autoOpenLinksInBrowser`|Automatically open the browser for all commands which return a url and expect the user to copy paste this to the browser. For example when logging in, using `m365 login` in device code mode.|`false`
5+
`clientId`|ID of the default Entra ID app use by the CLI to authenticate|``
6+
`clientSecret`|Secret of the default Entra ID app use by the CLI to authenticate|``
7+
`clientCertificateFile`|Path to the file containing the client certificate to use for authentication|``
8+
`clientCertificateBase64Encoded`|Base64-encoded client certificate contents|``
9+
`clientCertificatePassword`|Password to the client certificate file|``
510
`copyDeviceCodeToClipboard`|Automatically copy the device code to the clipboard when running `m365 login` command in device code mode|`false`
611
`csvEscape`|Single character used for escaping; only apply to characters matching the quote and the escape options|`"`
712
`csvHeader`|Display the column names on the first line|`true`
@@ -18,3 +23,4 @@ Setting name|Definition|Default value
1823
`promptListPageSize`|By default, lists of choices longer than 7 will be paginated. Use this option to control how many choices will appear on the screen at once.|7
1924
`showHelpOnFailure`|Automatically display help when executing a command failed|`true`
2025
`showSpinner`|Display spinner when executing commands|`true`
26+
`tenantId`|ID of the default tenant to use when authenticating with|``

docs/docs/cmd/setup.mdx

+16-3
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,9 @@ m365 setup [options]
1818

1919
`--scripting`
2020
: Configure CLI for Microsoft 365 for use in scripts without prompting for additional information.
21+
22+
`--skipApp`
23+
: Skip configuring an Entra app for use with CLI for Microsoft 365.
2124
```
2225

2326
<Global />
@@ -28,6 +31,10 @@ The `m365 setup` command is a wizard that helps you configure the CLI for Micros
2831

2932
The command will ask you the following questions:
3033

34+
- _CLI for Microsoft 365 requires a Microsoft Entra app. Do you want to create a new app registration or use an existing one?_
35+
36+
You can choose between using an existing Entra app or creating a new one. If you choose to create a new app, the CLI will ask you to choose between a minimal and a full set of permissions. It then signs in as Azure CLI to your tenant, creates a new app registration, and stores its information in the CLI configuration.
37+
3138
- _How do you plan to use the CLI?_
3239

3340
You can choose between **interactive** and **scripting** use. In interactive mode, the CLI for Microsoft 365 will prompt you for additional information when needed, automatically open links browser, automatically show help on errors and show spinners. In **scripting** mode, the CLI will not use interactivity to prevent blocking your scripts.
@@ -71,24 +78,30 @@ The `m365 setup` command uses the following presets:
7178

7279
## Examples
7380

74-
Configure CLI for Microsoft based on your preferences interactively
81+
Configure CLI for Microsoft 365 based on your preferences interactively
7582

7683
```sh
7784
m365 setup
7885
```
7986

80-
Configure CLI for Microsoft for interactive use without prompting for additional information
87+
Configure CLI for Microsoft 365 for interactive use without prompting for additional information
8188

8289
```sh
8390
m365 setup --interactive
8491
```
8592

86-
Configure CLI for Microsoft for use in scripts without prompting for additional information
93+
Configure CLI for Microsoft 365 for use in scripts without prompting for additional information
8794

8895
```sh
8996
m365 setup --scripting
9097
```
9198

99+
Configure CLI for Microsoft 365 without setting up an Entra app
100+
101+
```sh
102+
m365 setup --skipApp
103+
```
104+
92105
## Response
93106

94107
The command won't return a response on success.

docs/docs/index.mdx

+7-1
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,13 @@ yarn global add @pnp/cli-microsoft365
2727

2828
## Getting started
2929

30-
Start managing the settings of your Microsoft 365 tenant by logging in to it, using the `login` command, for example:
30+
Start, by configuring CLI for Microsoft 365 to your preferences. Configuration includes specifying an Entra app registration that the CLI should use. You can choose between using an existing app registration or creating a new one. To configure the CLI, run the [setup](./cmd/setup) command:
31+
32+
```sh
33+
m365 setup
34+
```
35+
36+
After configuring the CLI, you can start using it. Start managing the settings of your Microsoft 365 tenant by logging in to it, using the [login](./cmd/login) command, for example:
3137

3238
```sh
3339
m365 login

src/Auth.ts

+12-15
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@ import { TokenStorage } from './auth/TokenStorage.js';
1010
import { msalCachePlugin } from './auth/msalCachePlugin.js';
1111
import { Logger } from './cli/Logger.js';
1212
import { cli } from './cli/cli.js';
13-
import config from './config.js';
1413
import { ConnectionDetails } from './m365/commands/ConnectionDetails.js';
1514
import request from './request.js';
1615
import { settingsNames } from './settingsNames.js';
@@ -69,15 +68,13 @@ export class Connection {
6968
// SharePoint tenantId used to execute CSOM requests
7069
spoTenantId?: string;
7170
// ID of the Microsoft Entra ID app used to authenticate
72-
appId: string;
71+
appId?: string;
7372
// ID of the tenant where the Microsoft Entra app is registered; common if multi-tenant
74-
tenant: string;
73+
tenant: string = 'common';
7574
cloudType: CloudType = CloudType.Public;
7675

7776
constructor() {
7877
this.accessTokens = {};
79-
this.appId = config.cliEntraAppId;
80-
this.tenant = config.tenant;
8178
this.cloudType = CloudType.Public;
8279
}
8380

@@ -97,18 +94,18 @@ export class Connection {
9794
this.thumbprint = undefined;
9895
this.spoUrl = undefined;
9996
this.spoTenantId = undefined;
100-
this.appId = config.cliEntraAppId;
101-
this.tenant = config.tenant;
97+
this.appId = cli.getClientId();
98+
this.tenant = cli.getTenant();
10299
}
103100
}
104101

105102
export enum AuthType {
106-
DeviceCode,
107-
Password,
108-
Certificate,
109-
Identity,
110-
Browser,
111-
Secret
103+
DeviceCode = 'deviceCode',
104+
Password = 'password',
105+
Certificate = 'certificate',
106+
Identity = 'identity',
107+
Browser = 'browser',
108+
Secret = 'secret'
112109
}
113110

114111
export enum CertificateType {
@@ -328,7 +325,7 @@ export class Auth {
328325
}
329326

330327
const config = {
331-
clientId: this.connection.appId,
328+
clientId: this.connection.appId!,
332329
authority: `${Auth.getEndpointForResource('https://login.microsoftonline.com', this.connection.cloudType)}/${this.connection.tenant}`,
333330
azureCloudOptions: {
334331
azureCloudInstance,
@@ -884,7 +881,7 @@ export class Auth {
884881
const details: ConnectionDetails = {
885882
connectionName: connection.name,
886883
connectedAs: connection.identityName,
887-
authType: AuthType[connection.authType],
884+
authType: connection.authType,
888885
appId: connection.appId,
889886
appTenant: connection.tenant,
890887
cloudType: CloudType[connection.cloudType]

src/cli/cli.spec.ts

+20
Original file line numberDiff line numberDiff line change
@@ -196,6 +196,18 @@ class MockCommandWithConfirmationPrompt extends AnonymousCommand {
196196
}
197197
}
198198

199+
class MockCommandWithInputPrompt extends AnonymousCommand {
200+
public get name(): string {
201+
return 'cli mock prompt';
202+
}
203+
public get description(): string {
204+
return 'Mock command with prompt';
205+
}
206+
public async commandAction(): Promise<void> {
207+
await cli.promptForInput({ message: `ID` });
208+
}
209+
}
210+
199211
class MockCommandWithHandleMultipleResultsFound extends AnonymousCommand {
200212
public get name(): string {
201213
return 'cli mock interactive prompt';
@@ -1146,6 +1158,14 @@ describe('cli', () => {
11461158
assert(promptStub.called);
11471159
});
11481160

1161+
it('calls input prompt tool when command shows prompt', async () => {
1162+
const promptStub: sinon.SinonStub = sinon.stub(prompt, 'forInput').resolves('abc');
1163+
const mockCommandWithInputPrompt = new MockCommandWithInputPrompt();
1164+
1165+
await cli.executeCommand(mockCommandWithInputPrompt, { options: { _: [] } });
1166+
assert(promptStub.called);
1167+
});
1168+
11491169
it('prints command output with formatting', async () => {
11501170
const commandWithOutput: MockCommandWithOutput = new MockCommandWithOutput();
11511171

src/cli/cli.ts

+19-1
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ import { app } from '../utils/app.js';
1717
import { browserUtil } from '../utils/browserUtil.js';
1818
import { formatting } from '../utils/formatting.js';
1919
import { md } from '../utils/md.js';
20-
import { ConfirmationConfig, SelectionConfig, prompt } from '../utils/prompt.js';
20+
import { ConfirmationConfig, InputConfig, SelectionConfig, prompt } from '../utils/prompt.js';
2121
import { validation } from '../utils/validation.js';
2222
import { zod } from '../utils/zod.js';
2323
import { CommandInfo } from './CommandInfo.js';
@@ -75,6 +75,14 @@ function getSettingWithDefaultValue<TValue>(settingName: string, defaultValue: T
7575
}
7676
}
7777

78+
function getClientId(): string | undefined {
79+
return cli.getSettingWithDefaultValue(settingsNames.clientId, process.env.CLIMICROSOFT365_ENTRAAPPID || process.env.CLIMICROSOFT365_AADAPPID);
80+
}
81+
82+
function getTenant(): string {
83+
return cli.getSettingWithDefaultValue(settingsNames.tenantId, process.env.CLIMICROSOFT365_TENANT || 'common');
84+
}
85+
7886
async function execute(rawArgs: string[]): Promise<void> {
7987
const start = process.hrtime.bigint();
8088

@@ -996,6 +1004,13 @@ async function promptForConfirmation(config: ConfirmationConfig): Promise<boolea
9961004
return answer;
9971005
}
9981006

1007+
async function promptForInput(config: InputConfig): Promise<string> {
1008+
const answer = await prompt.forInput(config);
1009+
await cli.error('');
1010+
1011+
return answer;
1012+
}
1013+
9991014
async function handleMultipleResultsFound<T>(message: string, values: { [key: string]: T }): Promise<T> {
10001015
const prompt: boolean = cli.getSettingWithDefaultValue<boolean>(settingsNames.prompt, true);
10011016
if (!prompt) {
@@ -1036,7 +1051,9 @@ export const cli = {
10361051
closeWithError,
10371052
commands,
10381053
commandToExecute,
1054+
getClientId,
10391055
getConfig,
1056+
getTenant,
10401057
currentCommandName,
10411058
error,
10421059
execute,
@@ -1055,6 +1072,7 @@ export const cli = {
10551072
optionsFromArgs,
10561073
printAvailableCommands,
10571074
promptForConfirmation,
1075+
promptForInput,
10581076
promptForSelection,
10591077
promptForValue,
10601078
shouldTrimOutput,

src/config.spec.ts

-39
This file was deleted.

src/config.ts

+60-6
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,65 @@
1-
import { app } from "./utils/app.js";
2-
3-
const cliEntraAppId: string = '31359c7f-bd7e-475c-86db-fdb8c937548e';
1+
import { app } from './utils/app.js';
42

53
export default {
4+
allScopes: [
5+
'https://graph.windows.net/Directory.AccessAsUser.All',
6+
'https://management.azure.com/user_impersonation',
7+
'https://admin.services.crm.dynamics.com/user_impersonation',
8+
'https://graph.microsoft.com/AppCatalog.ReadWrite.All',
9+
'https://graph.microsoft.com/AuditLog.Read.All',
10+
'https://graph.microsoft.com/Bookings.Read.All',
11+
'https://graph.microsoft.com/Calendars.Read',
12+
'https://graph.microsoft.com/ChannelMember.ReadWrite.All',
13+
'https://graph.microsoft.com/ChannelMessage.Read.All',
14+
'https://graph.microsoft.com/ChannelMessage.ReadWrite',
15+
'https://graph.microsoft.com/ChannelMessage.Send',
16+
'https://graph.microsoft.com/ChannelSettings.ReadWrite.All',
17+
'https://graph.microsoft.com/Chat.ReadWrite',
18+
'https://graph.microsoft.com/Directory.AccessAsUser.All',
19+
'https://graph.microsoft.com/Directory.ReadWrite.All',
20+
'https://graph.microsoft.com/ExternalConnection.ReadWrite.All',
21+
'https://graph.microsoft.com/ExternalItem.ReadWrite.All',
22+
'https://graph.microsoft.com/Group.ReadWrite.All',
23+
'https://graph.microsoft.com/IdentityProvider.ReadWrite.All',
24+
'https://graph.microsoft.com/InformationProtectionPolicy.Read',
25+
'https://graph.microsoft.com/Mail.Read.Shared',
26+
'https://graph.microsoft.com/Mail.ReadWrite',
27+
'https://graph.microsoft.com/Mail.Send',
28+
'https://graph.microsoft.com/Notes.ReadWrite.All',
29+
'https://graph.microsoft.com/OnlineMeetingArtifact.Read.All',
30+
'https://graph.microsoft.com/OnlineMeetings.ReadWrite',
31+
'https://graph.microsoft.com/OnlineMeetingTranscript.Read.All',
32+
'https://graph.microsoft.com/PeopleSettings.ReadWrite.All',
33+
'https://graph.microsoft.com/Place.Read.All',
34+
'https://graph.microsoft.com/Policy.Read.All',
35+
'https://graph.microsoft.com/RecordsManagement.ReadWrite.All',
36+
'https://graph.microsoft.com/Reports.Read.All',
37+
'https://graph.microsoft.com/RoleAssignmentSchedule.ReadWrite.Directory',
38+
'https://graph.microsoft.com/RoleEligibilitySchedule.Read.Directory',
39+
'https://graph.microsoft.com/SecurityEvents.Read.All',
40+
'https://graph.microsoft.com/ServiceHealth.Read.All',
41+
'https://graph.microsoft.com/ServiceMessage.Read.All',
42+
'https://graph.microsoft.com/ServiceMessageViewpoint.Write',
43+
'https://graph.microsoft.com/Sites.Read.All',
44+
'https://graph.microsoft.com/Tasks.ReadWrite',
45+
'https://graph.microsoft.com/Team.Create',
46+
'https://graph.microsoft.com/TeamMember.ReadWrite.All',
47+
'https://graph.microsoft.com/TeamsAppInstallation.ReadWriteForUser',
48+
'https://graph.microsoft.com/TeamSettings.ReadWrite.All',
49+
'https://graph.microsoft.com/TeamsTab.ReadWrite.All',
50+
'https://graph.microsoft.com/User.Invite.All',
51+
'https://manage.office.com/ActivityFeed.Read',
52+
'https://manage.office.com/ServiceHealth.Read',
53+
'https://analysis.windows.net/powerbi/api/Dataset.Read.All',
54+
'https://api.powerapps.com//User',
55+
'https://microsoft.sharepoint-df.com/AllSites.FullControl',
56+
'https://microsoft.sharepoint-df.com/TermStore.ReadWrite.All',
57+
'https://microsoft.sharepoint-df.com/User.ReadWrite.All'
58+
],
659
applicationName: `CLI for Microsoft 365 v${app.packageJson().version}`,
760
delimiter: 'm365\$',
8-
cliEntraAppId: process.env.CLIMICROSOFT365_ENTRAAPPID || process.env.CLIMICROSOFT365_AADAPPID || cliEntraAppId,
9-
tenant: process.env.CLIMICROSOFT365_TENANT || 'common',
10-
configstoreName: 'cli-m365-config'
61+
configstoreName: 'cli-m365-config',
62+
minimalScopes: [
63+
'https://graph.microsoft.com/User.Read'
64+
]
1165
};

src/m365/base/SpoCommand.spec.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import assert from 'assert';
22
import sinon from 'sinon';
33
import { telemetry } from '../../telemetry.js';
4-
import auth from '../../Auth.js';
4+
import auth, { AuthType } from '../../Auth.js';
55
import { Logger } from '../../cli/Logger.js';
66
import { CommandError } from '../../Command.js';
77
import request from '../../request.js';
@@ -235,7 +235,7 @@ describe('SpoCommand', () => {
235235
});
236236

237237
it('Shows an error when CLI is connected with authType "Secret"', async () => {
238-
sinon.stub(auth.connection, 'authType').value(5);
238+
sinon.stub(auth.connection, 'authType').value(AuthType.Secret);
239239

240240
const mock = new MockCommand();
241241
await assert.rejects(mock.action(logger, { options: {} }),

0 commit comments

Comments
 (0)