Skip to content

Commit

Permalink
Extends setup with a custom Entra app
Browse files Browse the repository at this point in the history
  • Loading branch information
waldekmastykarz authored and Adam-it committed Sep 1, 2024
1 parent 309a9b8 commit b564b61
Show file tree
Hide file tree
Showing 31 changed files with 1,921 additions and 724 deletions.
6 changes: 5 additions & 1 deletion .vscode/launch.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,11 @@
// 'm365 spo site get --url /', you'd use:
// "args": ["spo", "site", "get", "--url", "/"]
// after debugging, revert changes so that they won't end up in your PR
"args": []
"args": [],
"console": "integratedTerminal",
"env": {
"NODE_OPTIONS": "--enable-source-maps"
}
},
{
"type": "node",
Expand Down
6 changes: 6 additions & 0 deletions docs/docs/_clisettings.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,11 @@ Setting name|Definition|Default value
------------|----------|-------------
`authType`|Default login method to use when running `m365 login` without the `--authType` option.|`deviceCode`
`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`
`clientId`|ID of the default Entra ID app use by the CLI to authenticate|``
`clientSecret`|Secret of the default Entra ID app use by the CLI to authenticate|``
`clientCertificateFile`|Path to the file containing the client certificate to use for authentication|``
`clientCertificateBase64Encoded`|Base64-encoded client certificate contents|``
`clientCertificatePassword`|Password to the client certificate file|``
`copyDeviceCodeToClipboard`|Automatically copy the device code to the clipboard when running `m365 login` command in device code mode|`false`
`csvEscape`|Single character used for escaping; only apply to characters matching the quote and the escape options|`"`
`csvHeader`|Display the column names on the first line|`true`
Expand All @@ -18,3 +23,4 @@ Setting name|Definition|Default value
`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
`showHelpOnFailure`|Automatically display help when executing a command failed|`true`
`showSpinner`|Display spinner when executing commands|`true`
`tenantId`|ID of the default tenant to use when authenticating with|``
19 changes: 16 additions & 3 deletions docs/docs/cmd/setup.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,9 @@ m365 setup [options]

`--scripting`
: Configure CLI for Microsoft 365 for use in scripts without prompting for additional information.

`--skipApp`
: Skip configuring an Entra app for use with CLI for Microsoft 365.
```

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

The command will ask you the following questions:

- _CLI for Microsoft 365 requires a Microsoft Entra app. Do you want to create a new app registration or use an existing one?_

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.

- _How do you plan to use the CLI?_

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.
Expand Down Expand Up @@ -71,24 +78,30 @@ The `m365 setup` command uses the following presets:

## Examples

Configure CLI for Microsoft based on your preferences interactively
Configure CLI for Microsoft 365 based on your preferences interactively

```sh
m365 setup
```

Configure CLI for Microsoft for interactive use without prompting for additional information
Configure CLI for Microsoft 365 for interactive use without prompting for additional information

```sh
m365 setup --interactive
```

Configure CLI for Microsoft for use in scripts without prompting for additional information
Configure CLI for Microsoft 365 for use in scripts without prompting for additional information

```sh
m365 setup --scripting
```

Configure CLI for Microsoft 365 without setting up an Entra app

```sh
m365 setup --skipApp
```

## Response

The command won't return a response on success.
8 changes: 7 additions & 1 deletion docs/docs/index.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,13 @@ yarn global add @pnp/cli-microsoft365

## Getting started

Start managing the settings of your Microsoft 365 tenant by logging in to it, using the `login` command, for example:
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:

```sh
m365 setup
```

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:

```sh
m365 login
Expand Down
27 changes: 12 additions & 15 deletions src/Auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ import { TokenStorage } from './auth/TokenStorage.js';
import { msalCachePlugin } from './auth/msalCachePlugin.js';
import { Logger } from './cli/Logger.js';
import { cli } from './cli/cli.js';
import config from './config.js';
import { ConnectionDetails } from './m365/commands/ConnectionDetails.js';
import request from './request.js';
import { settingsNames } from './settingsNames.js';
Expand Down Expand Up @@ -69,15 +68,13 @@ export class Connection {
// SharePoint tenantId used to execute CSOM requests
spoTenantId?: string;
// ID of the Microsoft Entra ID app used to authenticate
appId: string;
appId?: string;
// ID of the tenant where the Microsoft Entra app is registered; common if multi-tenant
tenant: string;
tenant: string = 'common';
cloudType: CloudType = CloudType.Public;

constructor() {
this.accessTokens = {};
this.appId = config.cliEntraAppId;
this.tenant = config.tenant;
this.cloudType = CloudType.Public;
}

Expand All @@ -97,18 +94,18 @@ export class Connection {
this.thumbprint = undefined;
this.spoUrl = undefined;
this.spoTenantId = undefined;
this.appId = config.cliEntraAppId;
this.tenant = config.tenant;
this.appId = cli.getClientId();
this.tenant = cli.getTenant();
}
}

export enum AuthType {
DeviceCode,
Password,
Certificate,
Identity,
Browser,
Secret
DeviceCode = 'deviceCode',
Password = 'password',
Certificate = 'certificate',
Identity = 'identity',
Browser = 'browser',
Secret = 'secret'
}

export enum CertificateType {
Expand Down Expand Up @@ -328,7 +325,7 @@ export class Auth {
}

const config = {
clientId: this.connection.appId,
clientId: this.connection.appId!,
authority: `${Auth.getEndpointForResource('https://login.microsoftonline.com', this.connection.cloudType)}/${this.connection.tenant}`,
azureCloudOptions: {
azureCloudInstance,
Expand Down Expand Up @@ -884,7 +881,7 @@ export class Auth {
const details: ConnectionDetails = {
connectionName: connection.name,
connectedAs: connection.identityName,
authType: AuthType[connection.authType],
authType: connection.authType,
appId: connection.appId,
appTenant: connection.tenant,
cloudType: CloudType[connection.cloudType]
Expand Down
20 changes: 20 additions & 0 deletions src/cli/cli.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -196,6 +196,18 @@ class MockCommandWithConfirmationPrompt extends AnonymousCommand {
}
}

class MockCommandWithInputPrompt extends AnonymousCommand {
public get name(): string {
return 'cli mock prompt';
}
public get description(): string {
return 'Mock command with prompt';
}
public async commandAction(): Promise<void> {
await cli.promptForInput({ message: `ID` });
}
}

class MockCommandWithHandleMultipleResultsFound extends AnonymousCommand {
public get name(): string {
return 'cli mock interactive prompt';
Expand Down Expand Up @@ -1146,6 +1158,14 @@ describe('cli', () => {
assert(promptStub.called);
});

it('calls input prompt tool when command shows prompt', async () => {
const promptStub: sinon.SinonStub = sinon.stub(prompt, 'forInput').resolves('abc');
const mockCommandWithInputPrompt = new MockCommandWithInputPrompt();

await cli.executeCommand(mockCommandWithInputPrompt, { options: { _: [] } });
assert(promptStub.called);
});

it('prints command output with formatting', async () => {
const commandWithOutput: MockCommandWithOutput = new MockCommandWithOutput();

Expand Down
20 changes: 19 additions & 1 deletion src/cli/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import { app } from '../utils/app.js';
import { browserUtil } from '../utils/browserUtil.js';
import { formatting } from '../utils/formatting.js';
import { md } from '../utils/md.js';
import { ConfirmationConfig, SelectionConfig, prompt } from '../utils/prompt.js';
import { ConfirmationConfig, InputConfig, SelectionConfig, prompt } from '../utils/prompt.js';
import { validation } from '../utils/validation.js';
import { zod } from '../utils/zod.js';
import { CommandInfo } from './CommandInfo.js';
Expand Down Expand Up @@ -75,6 +75,14 @@ function getSettingWithDefaultValue<TValue>(settingName: string, defaultValue: T
}
}

function getClientId(): string | undefined {
return cli.getSettingWithDefaultValue(settingsNames.clientId, process.env.CLIMICROSOFT365_ENTRAAPPID || process.env.CLIMICROSOFT365_AADAPPID);
}

function getTenant(): string {
return cli.getSettingWithDefaultValue(settingsNames.tenantId, process.env.CLIMICROSOFT365_TENANT || 'common');
}

async function execute(rawArgs: string[]): Promise<void> {
const start = process.hrtime.bigint();

Expand Down Expand Up @@ -996,6 +1004,13 @@ async function promptForConfirmation(config: ConfirmationConfig): Promise<boolea
return answer;
}

async function promptForInput(config: InputConfig): Promise<string> {
const answer = await prompt.forInput(config);
await cli.error('');

return answer;
}

async function handleMultipleResultsFound<T>(message: string, values: { [key: string]: T }): Promise<T> {
const prompt: boolean = cli.getSettingWithDefaultValue<boolean>(settingsNames.prompt, true);
if (!prompt) {
Expand Down Expand Up @@ -1036,7 +1051,9 @@ export const cli = {
closeWithError,
commands,
commandToExecute,
getClientId,
getConfig,
getTenant,
currentCommandName,
error,
execute,
Expand All @@ -1055,6 +1072,7 @@ export const cli = {
optionsFromArgs,
printAvailableCommands,
promptForConfirmation,
promptForInput,
promptForSelection,
promptForValue,
shouldTrimOutput,
Expand Down
39 changes: 0 additions & 39 deletions src/config.spec.ts

This file was deleted.

66 changes: 60 additions & 6 deletions src/config.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,65 @@
import { app } from "./utils/app.js";

const cliEntraAppId: string = '31359c7f-bd7e-475c-86db-fdb8c937548e';
import { app } from './utils/app.js';

export default {
allScopes: [
'https://graph.windows.net/Directory.AccessAsUser.All',
'https://management.azure.com/user_impersonation',
'https://admin.services.crm.dynamics.com/user_impersonation',
'https://graph.microsoft.com/AppCatalog.ReadWrite.All',
'https://graph.microsoft.com/AuditLog.Read.All',
'https://graph.microsoft.com/Bookings.Read.All',
'https://graph.microsoft.com/Calendars.Read',
'https://graph.microsoft.com/ChannelMember.ReadWrite.All',
'https://graph.microsoft.com/ChannelMessage.Read.All',
'https://graph.microsoft.com/ChannelMessage.ReadWrite',
'https://graph.microsoft.com/ChannelMessage.Send',
'https://graph.microsoft.com/ChannelSettings.ReadWrite.All',
'https://graph.microsoft.com/Chat.ReadWrite',
'https://graph.microsoft.com/Directory.AccessAsUser.All',
'https://graph.microsoft.com/Directory.ReadWrite.All',
'https://graph.microsoft.com/ExternalConnection.ReadWrite.All',
'https://graph.microsoft.com/ExternalItem.ReadWrite.All',
'https://graph.microsoft.com/Group.ReadWrite.All',
'https://graph.microsoft.com/IdentityProvider.ReadWrite.All',
'https://graph.microsoft.com/InformationProtectionPolicy.Read',
'https://graph.microsoft.com/Mail.Read.Shared',
'https://graph.microsoft.com/Mail.ReadWrite',
'https://graph.microsoft.com/Mail.Send',
'https://graph.microsoft.com/Notes.ReadWrite.All',
'https://graph.microsoft.com/OnlineMeetingArtifact.Read.All',
'https://graph.microsoft.com/OnlineMeetings.ReadWrite',
'https://graph.microsoft.com/OnlineMeetingTranscript.Read.All',
'https://graph.microsoft.com/PeopleSettings.ReadWrite.All',
'https://graph.microsoft.com/Place.Read.All',
'https://graph.microsoft.com/Policy.Read.All',
'https://graph.microsoft.com/RecordsManagement.ReadWrite.All',
'https://graph.microsoft.com/Reports.Read.All',
'https://graph.microsoft.com/RoleAssignmentSchedule.ReadWrite.Directory',
'https://graph.microsoft.com/RoleEligibilitySchedule.Read.Directory',
'https://graph.microsoft.com/SecurityEvents.Read.All',
'https://graph.microsoft.com/ServiceHealth.Read.All',
'https://graph.microsoft.com/ServiceMessage.Read.All',
'https://graph.microsoft.com/ServiceMessageViewpoint.Write',
'https://graph.microsoft.com/Sites.Read.All',
'https://graph.microsoft.com/Tasks.ReadWrite',
'https://graph.microsoft.com/Team.Create',
'https://graph.microsoft.com/TeamMember.ReadWrite.All',
'https://graph.microsoft.com/TeamsAppInstallation.ReadWriteForUser',
'https://graph.microsoft.com/TeamSettings.ReadWrite.All',
'https://graph.microsoft.com/TeamsTab.ReadWrite.All',
'https://graph.microsoft.com/User.Invite.All',
'https://manage.office.com/ActivityFeed.Read',
'https://manage.office.com/ServiceHealth.Read',
'https://analysis.windows.net/powerbi/api/Dataset.Read.All',
'https://api.powerapps.com//User',
'https://microsoft.sharepoint-df.com/AllSites.FullControl',
'https://microsoft.sharepoint-df.com/TermStore.ReadWrite.All',
'https://microsoft.sharepoint-df.com/User.ReadWrite.All'
],
applicationName: `CLI for Microsoft 365 v${app.packageJson().version}`,
delimiter: 'm365\$',
cliEntraAppId: process.env.CLIMICROSOFT365_ENTRAAPPID || process.env.CLIMICROSOFT365_AADAPPID || cliEntraAppId,
tenant: process.env.CLIMICROSOFT365_TENANT || 'common',
configstoreName: 'cli-m365-config'
configstoreName: 'cli-m365-config',
minimalScopes: [
'https://graph.microsoft.com/User.Read'
]
};
4 changes: 2 additions & 2 deletions src/m365/base/SpoCommand.spec.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import assert from 'assert';
import sinon from 'sinon';
import { telemetry } from '../../telemetry.js';
import auth from '../../Auth.js';
import auth, { AuthType } from '../../Auth.js';
import { Logger } from '../../cli/Logger.js';
import { CommandError } from '../../Command.js';
import request from '../../request.js';
Expand Down Expand Up @@ -235,7 +235,7 @@ describe('SpoCommand', () => {
});

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

const mock = new MockCommand();
await assert.rejects(mock.action(logger, { options: {} }),
Expand Down
Loading

0 comments on commit b564b61

Please sign in to comment.