diff --git a/docs/docs/cmd/tenant/info/info-get.mdx b/docs/docs/cmd/tenant/info/info-get.mdx new file mode 100644 index 00000000000..ca0b62965ee --- /dev/null +++ b/docs/docs/cmd/tenant/info/info-get.mdx @@ -0,0 +1,101 @@ +import Global from '/docs/cmd/_global.mdx'; +import Tabs from '@theme/Tabs'; +import TabItem from '@theme/TabItem'; + +# tenant info get + +Gets information about any tenant + +## Usage + +```sh +m365 tenant info get [options] +``` + +## Options + +```md definition-list +`-d, --domainName [domainName]` +: The primary domain name of a tenant. Optionally specify either `domainName` or `tenantId` but not both. If none are specified, the tenantId of the currently signed-in user is used. + +`-i, --tenantId [tenantId]` +: The unique tenant identifier of a tenant. Optionally specify either `domainName` or `tenantId` but not both. If none are specified, the tenantId of the currently signed-in user is used. +``` + + + +## Remarks + +If no domain name or tenantId is specified, the command will return the tenant information of the currently signed in user. + +## Examples + +Get tenant information for the currently signed in user. + +```sh +m365 tenant info get +``` + +Get tenant information for the Contoso tenant. + +```sh +m365 tenant info get --domainName contoso.com +``` + +Get tenant information by id. + +```sh +m365 tenant info get --tenantId e65b162c-6f87-4eb1-a24e-1b37d3504663 +``` + +## Response + + + + + ```json + { + "tenantId": "e65b162c-6f87-4eb1-a24e-1b37d3504663", + "federationBrandName": null, + "displayName": "Contoso", + "defaultDomainName": "contoso.com" + } + ``` + + + + + ```text + defaultDomainName : contoso.com + displayName : Contoso + federationBrandName : null + tenantId : e65b162c-6f87-4eb1-a24e-1b37d3504663 + ``` + + + + + ```csv + tenantId,displayName,defaultDomainName + e65b162c-6f87-4eb1-a24e-1b37d3504663,Contoso,contoso.com + ``` + + + + + ```md + # tenant info get + + Date: 9/14/2023 + + ## Contoso + + Property | Value + ---------|------- + tenantId | e65b162c-6f87-4eb1-a24e-1b37d3504663 + displayName | Contoso + defaultDomainName | contoso.com + ``` + + + diff --git a/docs/src/config/sidebars.js b/docs/src/config/sidebars.js index 5183d89f4a5..af62ef54fbc 100644 --- a/docs/src/config/sidebars.js +++ b/docs/src/config/sidebars.js @@ -630,6 +630,15 @@ const sidebars = { } ] }, + { + info: [ + { + type: 'doc', + label: 'info get', + id: 'cmd/tenant/info/info-get' + } + ] + }, { report: [ { diff --git a/src/m365/tenant/commands.ts b/src/m365/tenant/commands.ts index 28f5dad2faa..85a06726e6c 100644 --- a/src/m365/tenant/commands.ts +++ b/src/m365/tenant/commands.ts @@ -2,13 +2,14 @@ const prefix: string = 'tenant'; export default { ID_GET: `${prefix} id get`, + INFO_GET: `${prefix} info get`, REPORT_ACTIVEUSERCOUNTS: `${prefix} report activeusercounts`, REPORT_ACTIVEUSERDETAIL: `${prefix} report activeuserdetail`, REPORT_OFFICE365ACTIVATIONCOUNTS: `${prefix} report office365activationcounts`, REPORT_OFFICE365ACTIVATIONSUSERDETAIL: `${prefix} report office365activationsuserdetail`, REPORT_OFFICE365ACTIVATIONSUSERCOUNTS: `${prefix} report office365activationsusercounts`, REPORT_SERVICESUSERCOUNTS: `${prefix} report servicesusercounts`, - SECURITY_ALERTS_LIST:`${prefix} security alerts list`, + SECURITY_ALERTS_LIST: `${prefix} security alerts list`, SERVICEANNOUNCEMENT_HEALTHISSUE_GET: `${prefix} serviceannouncement healthissue get`, SERVICEANNOUNCEMENT_HEALTH_GET: `${prefix} serviceannouncement health get`, SERVICEANNOUNCEMENT_HEALTH_LIST: `${prefix} serviceannouncement health list`, diff --git a/src/m365/tenant/commands/info/info-get.spec.ts b/src/m365/tenant/commands/info/info-get.spec.ts new file mode 100644 index 00000000000..cf70d172d0c --- /dev/null +++ b/src/m365/tenant/commands/info/info-get.spec.ts @@ -0,0 +1,189 @@ +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 { Logger } from '../../../../cli/Logger.js'; +import { CommandError } from '../../../../Command.js'; +import request from '../../../../request.js'; +import { telemetry } from '../../../../telemetry.js'; +import { accessToken } from '../../../../utils/accessToken.js'; +import { pid } from '../../../../utils/pid.js'; +import { session } from '../../../../utils/session.js'; +import { sinonUtil } from '../../../../utils/sinonUtil.js'; +import commands from '../../commands.js'; +import command from './info-get.js'; + +describe(commands.INFO_GET, () => { + const domainName = 'contoso.com'; + const tenantId = 'e65b162c-6f87-4eb1-a24e-1b37d3504663'; + const tenantInfoResponse = { + tenantId: tenantId, + federationBrandName: null, + displayName: "Contoso", + defaultDomainName: domainName + }; + + let log: any[]; + let loggerLogSpy: sinon.SinonSpy; + 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.service.connected = true; + commandInfo = Cli.getCommandInfo(command); + if (!auth.service.accessTokens[auth.defaultResource]) { + auth.service.accessTokens[auth.defaultResource] = { + expiresOn: '123', + accessToken: 'abc' + }; + } + }); + + beforeEach(() => { + log = []; + logger = { + log: async (msg: string) => { + log.push(msg); + }, + logRaw: async (msg: string) => { + log.push(msg); + }, + logToStderr: async (msg: string) => { + log.push(msg); + } + }; + loggerLogSpy = sinon.spy(logger, 'log'); + }); + + afterEach(() => { + sinonUtil.restore([ + request.get + ]); + }); + + after(() => { + sinon.restore(); + auth.service.connected = false; + }); + + it('has correct name', () => { + assert.strictEqual(command.name, commands.INFO_GET); + }); + + it('has a description', () => { + assert.notStrictEqual(command.description, null); + }); + + it('fails validation if the tenantId is not a valid guid', async () => { + const actual = await command.validate({ options: { tenantId: 'abc' } }, commandInfo); + assert.notStrictEqual(actual, true); + }); + + it('passes validation when the tenantId is a valid GUID', async () => { + const actual = await command.validate({ options: { tenantId: tenantId } }, commandInfo); + assert.strictEqual(actual, true); + }); + + it('fails validation if both domainName and tenantId are specified', async () => { + const actual = await command.validate({ options: { domainName: domainName, tenantId: tenantId } }, commandInfo); + assert.notStrictEqual(actual, true); + }); + + it('gets tenant information for the currently signed in user if no domain name or tenantId is passed', async () => { + sinon.stub(accessToken, 'getUserNameFromAccessToken').callsFake(() => { + return 'admin@contoso.com'; + }); + + sinon.stub(request, 'get').callsFake(async (opts) => { + if (opts.url === `https://graph.microsoft.com/v1.0/tenantRelationships/findTenantInformationByDomainName(domainName='contoso.com')`) { + return tenantInfoResponse; + } + + throw 'Invalid Request'; + }); + + await command.action(logger, { options: { verbose: true } }); + assert(loggerLogSpy.calledOnceWithExactly(tenantInfoResponse)); + sinonUtil.restore(accessToken.getUserNameFromAccessToken); + }); + + it('gets tenant information with correct domain name', async () => { + sinon.stub(request, 'get').callsFake(async (opts) => { + if (opts.url === `https://graph.microsoft.com/v1.0/tenantRelationships/findTenantInformationByDomainName(domainName='contoso.com')`) { + return tenantInfoResponse; + } + + throw 'Invalid Request'; + }); + + await command.action(logger, { options: { verbose: true, domainName: domainName } }); + assert(loggerLogSpy.calledOnceWithExactly(tenantInfoResponse)); + }); + + it('gets tenant information with correct tenant id', async () => { + sinon.stub(request, 'get').callsFake(async (opts) => { + if (opts.url === `https://graph.microsoft.com/v1.0/tenantRelationships/findTenantInformationByTenantId(tenantId='e65b162c-6f87-4eb1-a24e-1b37d3504663')`) { + return tenantInfoResponse; + } + + throw 'Invalid Request'; + }); + + await command.action(logger, { options: { verbose: true, tenantId: tenantId } }); + assert(loggerLogSpy.calledOnceWithExactly(tenantInfoResponse)); + }); + + it('handles error when trying to retrieve information for a non-existant tenant by id', async () => { + sinon.stub(request, 'get').callsFake(async (opts) => { + if (opts.url === `https://graph.microsoft.com/v1.0/tenantRelationships/findTenantInformationByTenantId(tenantId='e65b162c-6f87-4eb1-a24e-1b37d3504663')`) { + throw { + "error": { + "code": "Directory_ObjectNotFound", + "message": "Unable to read the company information from the directory.", + "innerError": { + "date": "2023-09-14T14:07:47", + "request-id": "3b91132c-5c79-454b-8dd4-06964e788a24", + "client-request-id": "2147e6c6-8036-cc2f-f4d0-eec89dbc48d7" + } + } + }; + } + + throw 'Invalid Request'; + }); + + await assert.rejects(command.action(logger, { options: { tenantId: tenantId } } as any), new CommandError("Unable to read the company information from the directory.")); + }); + + it('handles error when trying to retrieve information for a non-existant tenant by domain name', async () => { + sinon.stub(request, 'get').callsFake(async (opts) => { + if (opts.url === `https://graph.microsoft.com/v1.0/tenantRelationships/findTenantInformationByDomainName(domainName='xyz.com')`) { + throw { + "error": { + "code": "Directory_ObjectNotFound", + "message": "Unable to read the company information from the directory.", + "innerError": { + "date": "2023-09-14T14:07:47", + "request-id": "3b91132c-5c79-454b-8dd4-06964e788a24", + "client-request-id": "2147e6c6-8036-cc2f-f4d0-eec89dbc48d7" + } + } + }; + } + + throw 'Invalid Request'; + }); + + await assert.rejects(command.action(logger, { options: { domainName: 'xyz.com' } } as any), new CommandError("Unable to read the company information from the directory.")); + }); + + it('correctly handles random API error', async () => { + sinon.stub(request, 'get').rejects(new Error('An error has occurred')); + await assert.rejects(command.action(logger, { options: { domainName: 'xyz.com' } } as any), new CommandError('An error has occurred')); + }); +}); \ No newline at end of file diff --git a/src/m365/tenant/commands/info/info-get.ts b/src/m365/tenant/commands/info/info-get.ts new file mode 100644 index 00000000000..3a6173f3a1b --- /dev/null +++ b/src/m365/tenant/commands/info/info-get.ts @@ -0,0 +1,109 @@ +import auth from '../../../../Auth.js'; +import { Logger } from '../../../../cli/Logger.js'; +import GlobalOptions from '../../../../GlobalOptions.js'; +import GraphCommand from '../../../base/GraphCommand.js'; +import request from '../../../../request.js'; +import { accessToken } from '../../../../utils/accessToken.js'; +import { formatting } from '../../../../utils/formatting.js'; +import { validation } from '../../../../utils/validation.js'; +import commands from '../../commands.js'; + +interface CommandArgs { + options: Options; +} + +interface Options extends GlobalOptions { + domainName?: string; + tenantId?: string; +} + +class TenantInfoGetCommand extends GraphCommand { + public get name(): string { + return commands.INFO_GET; + } + + public get description(): string { + return 'Gets information about any tenant'; + } + + constructor() { + super(); + + this.#initTelemetry(); + this.#initOptions(); + this.#initValidators(); + } + + #initTelemetry(): void { + this.telemetry.push((args: CommandArgs) => { + Object.assign(this.telemetryProperties, { + domainName: typeof args.options.domainName !== 'undefined', + tenantId: typeof args.options.tenantId !== 'undefined' + }); + }); + } + + #initOptions(): void { + this.options.unshift( + { + option: '-d, --domainName [domainName]' + }, + { + option: '-i, --tenantId [tenantId]' + } + ); + } + + #initValidators(): void { + this.validators.push( + async (args: CommandArgs) => { + if (args.options.tenantId && !validation.isValidGuid(args.options.tenantId)) { + return `${args.options.tenantId} is not a valid GUID`; + } + + if (args.options.tenantId && args.options.domainName) { + return `Specify either domainName or tenantId but not both`; + } + + return true; + } + ); + } + + public async commandAction(logger: Logger, args: CommandArgs): Promise { + let domainName: string | undefined = args.options.domainName; + const tenantId: string | undefined = args.options.tenantId; + + if (!domainName && !tenantId) { + const userName: string = accessToken.getUserNameFromAccessToken(auth.service.accessTokens[auth.defaultResource].accessToken); + domainName = userName.split('@')[1]; + } + + let requestUrl = `${this.resource}/v1.0/tenantRelationships/`; + + if (tenantId) { + requestUrl += `findTenantInformationByTenantId(tenantId='${formatting.encodeQueryParameter(tenantId)}')`; + } + else { + requestUrl += `findTenantInformationByDomainName(domainName='${formatting.encodeQueryParameter(domainName!)}')`; + } + + const requestOptions: any = { + url: requestUrl, + headers: { + accept: 'application/json;odata.metadata=none' + }, + responseType: 'json' + }; + + try { + const res: any = await request.get(requestOptions); + await logger.log(res); + } + catch (err: any) { + this.handleRejectedODataJsonPromise(err); + } + } +} + +export default new TenantInfoGetCommand(); \ No newline at end of file