Skip to content

Commit

Permalink
New command: m365 entra multitenant set
Browse files Browse the repository at this point in the history
  • Loading branch information
MartinM85 committed May 6, 2024
1 parent 5ca5055 commit 570c03f
Show file tree
Hide file tree
Showing 6 changed files with 298 additions and 0 deletions.
1 change: 1 addition & 0 deletions .eslintrc.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ const dictionary = [
'member',
'messaging',
'model',
'multitenant',
'm365',
'news',
'oauth2',
Expand Down
53 changes: 53 additions & 0 deletions docs/docs/cmd/entra/multitenant/multitenant-set.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import Global from '/docs/cmd/_global.mdx';

# entra multitenant set

Updates the properties of a multitenant organization

## Usage

```sh
m365 entra multitenant set [options]
```

## Options

```md definition-list
`-n, --displayName [displayName]`
: New display name of the multitenant organization. Both options, `displayName` and `description` are optional but at least must be specified.

`-d, --description [description]`
: New description of the multitenant organization. Both options, `displayName` and `description` are optional but at least must be specified.
```

<Global />

## Remarks

:::info

To use this command you must be at least **Security Administrator**.

:::

## Examples

Update display name of multitenant organization

```sh
m365 entra multitenant set --displayName 'Fabrikam organization'
```

Update display name and description of multitenant organization

```sh
m365 entra multitenant set --displayName 'Fabrikam organization' --description 'Multitenant organization between Fabrikam and Contoso'
```

## Response

The command won't return a response on success

## More information

- Multitenant organization: https://learn.microsoft.com/entra/identity/multi-tenant-organizations/overview
9 changes: 9 additions & 0 deletions docs/src/config/sidebars.ts
Original file line number Diff line number Diff line change
Expand Up @@ -413,6 +413,15 @@ const sidebars: SidebarsConfig = {
}
]
},
{
multitenant: [
{
type: 'doc',
label: 'multitenant set',
id: 'cmd/entra/multitenant/multitenant-set'
}
]
},
{
m365group: [
{
Expand Down
1 change: 1 addition & 0 deletions src/m365/entra/commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ export default {
M365GROUP_USER_LIST: `${prefix} m365group user list`,
M365GROUP_USER_REMOVE: `${prefix} m365group user remove`,
M365GROUP_USER_SET: `${prefix} m365group user set`,
MULTITENANT_SET: `${prefix} multitenant set`,
OAUTH2GRANT_ADD: `${prefix} oauth2grant add`,
OAUTH2GRANT_LIST: `${prefix} oauth2grant list`,
OAUTH2GRANT_REMOVE: `${prefix} oauth2grant remove`,
Expand Down
138 changes: 138 additions & 0 deletions src/m365/entra/commands/multitenant/multitenant-set.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
import assert from 'assert';
import sinon from 'sinon';
import auth from '../../../../Auth.js';
import { cli } from '../../../../cli/cli.js';
import { CommandInfo } from '../../../../cli/CommandInfo.js';
import commands from '../../commands.js';
import command from './multitenant-set.js';
import { telemetry } from '../../../../telemetry.js';
import { pid } from '../../../../utils/pid.js';
import { session } from '../../../../utils/session.js';
import { sinonUtil } from '../../../../utils/sinonUtil.js';
import request from '../../../../request.js';
import { Logger } from '../../../../cli/Logger.js';
import { CommandError } from '../../../../Command.js';

describe(commands.MULTITENANT_SET, () => {
let log: string[];
let logger: Logger;
let commandInfo: CommandInfo;

before(() => {
sinon.stub(auth, 'restoreAuth').resolves();
sinon.stub(telemetry, 'trackEvent').returns();
sinon.stub(pid, 'getProcessName').returns('');
sinon.stub(session, 'getId').returns('');
auth.connection.active = true;
commandInfo = cli.getCommandInfo(command);
});

beforeEach(() => {
log = [];
logger = {
log: async (msg: string) => {
log.push(msg);
},
logRaw: async (msg: string) => {
log.push(msg);
},
logToStderr: async (msg: string) => {
log.push(msg);
}
};
(command as any).pollingInterval = 0;
});

afterEach(() => {
sinonUtil.restore([
request.patch
]);
});

after(() => {
sinon.restore();
auth.connection.active = false;
});

it('has correct name', () => {
assert.strictEqual(command.name, commands.MULTITENANT_SET);
});

it('has a description', () => {
assert.notStrictEqual(command.description, null);
});

it('passes validation when only displayName is specified', async () => {
const actual = await command.validate({ options: { displayName: 'Contoso organization' } }, commandInfo);
assert.strictEqual(actual, true);
});

it('passes validation when only description is specified', async () => {
const actual = await command.validate({ options: { description: 'Contoso and partners' } }, commandInfo);
assert.strictEqual(actual, true);
});

it('passes validation when the displayName and description are specified', async () => {
const actual = await command.validate({ options: { displayName: 'Contoso organization', description: 'Contoso and partners' } }, commandInfo);
assert.strictEqual(actual, true);
});

it('fails validation when no option is specified', async () => {
const actual = await command.validate({ options: {} }, commandInfo);
assert.notStrictEqual(actual, true);
});

it('updates a displayName of a multitenant organization', async () => {
const patchRequestStub = sinon.stub(request, 'patch').callsFake(async (opts) => {
if (opts.url === 'https://graph.microsoft.com/v1.0/tenantRelationships/multiTenantOrganization') {
return;
}

throw 'Invalid request';
});

await command.action(logger, { options: { displayName: 'Contoso organization' } });
assert(patchRequestStub.called);
});

it('updates a description of a multitenant organization', async () => {
const patchRequestStub = sinon.stub(request, 'patch').callsFake(async (opts) => {
if (opts.url === 'https://graph.microsoft.com/v1.0/tenantRelationships/multiTenantOrganization') {
return;
}

throw 'Invalid request';
});

await command.action(logger, { options: { description: 'Contoso and partners' } });
assert(patchRequestStub.called);
});

it('updates displayName and description of a multitenant organization', async () => {
const patchRequestStub = sinon.stub(request, 'patch').callsFake(async (opts) => {
if (opts.url === 'https://graph.microsoft.com/v1.0/tenantRelationships/multiTenantOrganization') {
return;
}

throw 'Invalid request';
});

await command.action(logger, { options: { displayName: 'Contoso organization', description: 'Contoso and partners' } });
assert(patchRequestStub.called);
});

it('correctly handles API OData error', async () => {
sinon.stub(request, 'patch').rejects({
error: {
'odata.error': {
code: '-1, InvalidOperationException',
message: {
value: 'Invalid request'
}
}
}
});

await assert.rejects(command.action(logger, { options: {} } as any), new CommandError('Invalid request'));
});
});
96 changes: 96 additions & 0 deletions src/m365/entra/commands/multitenant/multitenant-set.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
import GlobalOptions from '../../../../GlobalOptions.js';
import { Logger } from '../../../../cli/Logger.js';
import request, { CliRequestOptions } from '../../../../request.js';
import GraphCommand from '../../../base/GraphCommand.js';
import commands from '../../commands.js';

interface CommandArgs {
options: Options;
}

interface MultitenantOrganization {
createdDateTime?: string;
displayName?: string;
description?: string;
id?: string;
state?: string;
}

interface Options extends GlobalOptions {
displayName?: string;
description?: string;
}

class EntraMultitenantSetCommand extends GraphCommand {
public get name(): string {
return commands.MULTITENANT_SET;
}

public get description(): string {
return 'Updates the properties of a multitenant organization';
}

constructor() {
super();

this.#initTelemetry();
this.#initOptions();
this.#initValidators();
}

#initTelemetry(): void {
this.telemetry.push((args: CommandArgs) => {
Object.assign(this.telemetryProperties, {
displayName: typeof args.options.displayName !== 'undefined',
description: typeof args.options.description !== 'undefined'
});
});
}

#initOptions(): void {
this.options.unshift(
{
option: '-n, --displayName [displayName]'
},
{
option: '-d, --description [description]'
}
);
}

#initValidators(): void {
this.validators.push(
async (args: CommandArgs) => {
if (!args.options.displayName && !args.options.description) {
return 'Specify either displayName or description or both.';
}

return true;
}
);
}

public async commandAction(logger: Logger, args: CommandArgs): Promise<void> {

const requestOptions: CliRequestOptions = {
url: `${this.resource}/v1.0/tenantRelationships/multiTenantOrganization`,
headers: {
accept: 'application/json;odata.metadata=none'
},
responseType: 'json',
data: {
description: args.options.description,
displayName: args.options.displayName
}
};

try {
await request.patch<MultitenantOrganization>(requestOptions);
}
catch (err: any) {
this.handleRejectedODataJsonPromise(err);
}
}
}

export default new EntraMultitenantSetCommand();

0 comments on commit 570c03f

Please sign in to comment.