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

New command: m365 entra multitenant remove. Closes #6009 #6034

Open
wants to merge 1 commit 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.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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
50 changes: 50 additions & 0 deletions docs/docs/cmd/entra/multitenant/multitenant-remove.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import Global from '/docs/cmd/_global.mdx';

# entra multitenant remove

Removes a multitenant organization

## Usage

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

## options

```md definition-list
`-f, --force`
: Don't prompt for confirmation.
```

<Global />

## Remarks

:::info

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

:::

## Examples

Remove the multitenant organization

```sh
m365 entra multitenant remove
```

Remove the multitenant organization without prompting for confirmation

```sh
m365 entra multitenant remove --force
```

## 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 remove',
id: 'cmd/entra/multitenant/multitenant-remove'
}
]
},
{
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_REMOVE: `${prefix} multitenant remove`,
OAUTH2GRANT_ADD: `${prefix} oauth2grant add`,
OAUTH2GRANT_LIST: `${prefix} oauth2grant list`,
OAUTH2GRANT_REMOVE: `${prefix} oauth2grant remove`,
Expand Down
229 changes: 229 additions & 0 deletions src/m365/entra/commands/multitenant/multitenant-remove.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,229 @@
import assert from 'assert';
import sinon from 'sinon';
import auth from '../../../../Auth.js';
import { Logger } from '../../../../cli/Logger.js';
import commands from '../../commands.js';
import { cli } from '../../../../cli/cli.js';
import request from '../../../../request.js';
import { sinonUtil } from '../../../../utils/sinonUtil.js';
import command from './multitenant-remove.js';
import { telemetry } from '../../../../telemetry.js';
import { pid } from '../../../../utils/pid.js';
import { session } from '../../../../utils/session.js';
import { CommandError } from '../../../../Command.js';

describe(commands.MULTITENANT_REMOVE, () => {
const tenantId = "526dcbd1-4f42-469e-be90-ba4a7c0b7802";
const organization = {
"id": "526dcbd1-4f42-469e-be90-ba4a7c0b7802"
};
const multitenantOrganizationMembers = [
{
"tenantId": "526dcbd1-4f42-469e-be90-ba4a7c0b7802"
},
{
"tenantId": "6babcaad-604b-40ac-a9d7-9fd97c0b779f"
}
];
let log: string[];
let logger: Logger;
let promptIssued: boolean;

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;
});

beforeEach(() => {
log = [];
logger = {
log: async (msg: string) => {
log.push(msg);
},
logRaw: async (msg: string) => {
log.push(msg);
},
logToStderr: async (msg: string) => {
log.push(msg);
}
};
sinon.stub(cli, 'promptForConfirmation').callsFake(() => {
promptIssued = true;
return Promise.resolve(false);
});

promptIssued = false;
});

afterEach(() => {
sinonUtil.restore([
request.delete,
request.get,
cli.handleMultipleResultsFound,
cli.promptForConfirmation,
global.setTimeout
]);
});

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

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

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

it('removes the multitenant organization without prompting for confirmation', async () => {
let i = 0;
sinon.stub(request, 'get').callsFake(async (opts) => {
if (opts.url === `https://graph.microsoft.com/v1.0/organization?$select=id`) {
return {
value: [
organization
]
};
}

if (opts.url === `https://graph.microsoft.com/v1.0/tenantRelationships/multiTenantOrganization/tenants?$select=tenantId`) {
if (i++ < 2) {
return {
value: multitenantOrganizationMembers
};
}
return {
value: [
multitenantOrganizationMembers[0]
]
};
}

throw 'Invalid request';
});
const deleteRequestStub = sinon.stub(request, 'delete').callsFake(async (opts) => {
if (opts.url === `https://graph.microsoft.com/v1.0/tenantRelationships/multiTenantOrganization/tenants/${multitenantOrganizationMembers[0].tenantId}`
|| opts.url === `https://graph.microsoft.com/v1.0/tenantRelationships/multiTenantOrganization/tenants/${multitenantOrganizationMembers[1].tenantId}`) {
return;
}

throw 'Invalid request';
});

sinon.stub(global, 'setTimeout').callsFake((fn) => {
fn();
return {} as any;
});

await command.action(logger, { options: { force: true, verbose: true } });
assert(deleteRequestStub.calledTwice);
});

it('removes the multitenant organization while prompting for confirmation', async () => {
let i = 0;
sinon.stub(request, 'get').callsFake(async (opts) => {
if (opts.url === `https://graph.microsoft.com/v1.0/organization?$select=id`) {
return {
value: [
organization
]
};
}

if (opts.url === `https://graph.microsoft.com/v1.0/tenantRelationships/multiTenantOrganization/tenants?$select=tenantId`) {
if (i++ < 2) {
return {
value: multitenantOrganizationMembers
};
}
return {
value: [
multitenantOrganizationMembers[0]
]
};
}

throw 'Invalid request';
});
const deleteRequestStub = sinon.stub(request, 'delete').callsFake(async (opts) => {
if (opts.url === `https://graph.microsoft.com/v1.0/tenantRelationships/multiTenantOrganization/tenants/${multitenantOrganizationMembers[0].tenantId}`
|| opts.url === `https://graph.microsoft.com/v1.0/tenantRelationships/multiTenantOrganization/tenants/${multitenantOrganizationMembers[1].tenantId}`) {
return;
}

throw 'Invalid request';
});

sinon.stub(global, 'setTimeout').callsFake((fn) => {
fn();
return {} as any;
});
sinonUtil.restore(cli.promptForConfirmation);
sinon.stub(cli, 'promptForConfirmation').resolves(true);

await command.action(logger, { options: { } });
assert(deleteRequestStub.calledTwice);
});

it('prompts before removing the multitenant organization when prompt option not passed', async () => {
await command.action(logger, { options: { } });

assert(promptIssued);
});

it('aborts removing the multitenant organization when prompt not confirmed', async () => {
const deleteSpy = sinon.stub(request, 'delete').resolves();

await command.action(logger, { options: { } });
assert(deleteSpy.notCalled);
});

it('throws an error when one of the tenant cannot be found', async () => {
const error = {
error: {
code: 'Request_ResourceNotFound',
message: `Resource '${tenantId}' does not exist or one of its queried reference-property objects are not present.`,
innerError: {
date: '2024-05-07T06:59:51',
'request-id': 'b7dee9ee-d85b-4e7a-8686-74852cbfd85b',
'client-request-id': 'b7dee9ee-d85b-4e7a-8686-74852cbfd85b'
}
}
};

sinon.stub(request, 'get').callsFake(async (opts) => {
if (opts.url === `https://graph.microsoft.com/v1.0/organization?$select=id`) {
return {
value: [
organization
]
};
}

if (opts.url === `https://graph.microsoft.com/v1.0/tenantRelationships/multiTenantOrganization/tenants?$select=tenantId`) {
return {
value: multitenantOrganizationMembers
};
}

throw 'Invalid request';
});

sinon.stub(request, 'delete').callsFake(async (opts) => {
if (opts.url === `https://graph.microsoft.com/v1.0/tenantRelationships/multiTenantOrganization/tenants/${multitenantOrganizationMembers[1].tenantId}`) {
throw error;
}

throw 'Invalid request';
});

await assert.rejects(command.action(logger, { options: { force: true } }),
new CommandError(error.error.message));
});
});