From 9d62560e42723f3327f5a0288d9c5503eb6d4a36 Mon Sep 17 00:00:00 2001 From: Martin Machacek Date: Tue, 31 Dec 2024 15:27:28 +0100 Subject: [PATCH 1/7] Enhancement: Allow unknown options for entra group/app/administrativeunit commands --- .eslintrc.cjs | 3 +- src/Command.ts | 2 +- src/m365/commands/setup.ts | 3 +- .../administrativeunit-add.spec.ts | 38 ++++++++ .../administrativeunit-add.ts | 18 +++- src/m365/entra/commands/app/app-add.spec.ts | 95 +++++++++++++++++++ src/m365/entra/commands/app/app-add.ts | 7 +- src/m365/entra/commands/app/app-set.spec.ts | 54 ++++++++++- src/m365/entra/commands/app/app-set.ts | 23 +++++ .../entra/commands/group/group-add.spec.ts | 30 ++++++ .../entra/commands/group/group-set.spec.ts | 23 +++++ src/m365/entra/commands/group/group-set.ts | 20 ++-- src/m365/entra/commands/user/user-add.spec.ts | 27 ++++++ src/m365/entra/commands/user/user-set.spec.ts | 44 +++++++++ src/utils/entraApp.ts | 8 +- 15 files changed, 377 insertions(+), 18 deletions(-) diff --git a/.eslintrc.cjs b/.eslintrc.cjs index 245f7683901..37b4798324a 100644 --- a/.eslintrc.cjs +++ b/.eslintrc.cjs @@ -215,7 +215,8 @@ module.exports = { "Query.*", "app_displayname", "access_token", - "expires_on" + "expires_on", + "extension_*" ] } ], diff --git a/src/Command.ts b/src/Command.ts index e728d79d708..52d27fdf9f8 100644 --- a/src/Command.ts +++ b/src/Command.ts @@ -501,7 +501,7 @@ export default abstract class Command { }); } - protected addUnknownOptionsToPayload(payload: any, options: any): void { + public addUnknownOptionsToPayload(payload: any, options: any): void { const unknownOptions: any = this.getUnknownOptions(options); const unknownOptionsNames: string[] = Object.getOwnPropertyNames(unknownOptions); unknownOptionsNames.forEach(o => { diff --git a/src/m365/commands/setup.ts b/src/m365/commands/setup.ts index 16bd6a797a6..ce7cf748bef 100644 --- a/src/m365/commands/setup.ts +++ b/src/m365/commands/setup.ts @@ -314,7 +314,8 @@ class SetupCommand extends AnonymousCommand { apis, logger, verbose: this.verbose, - debug: this.debug + debug: this.debug, + command: this }); appInfo.tenantId = accessToken.getTenantIdFromAccessToken(auth.connection.accessTokens[auth.defaultResource].accessToken); await entraApp.grantAdminConsent({ diff --git a/src/m365/entra/commands/administrativeunit/administrativeunit-add.spec.ts b/src/m365/entra/commands/administrativeunit/administrativeunit-add.spec.ts index 7f3b5c2521e..92d17c28449 100644 --- a/src/m365/entra/commands/administrativeunit/administrativeunit-add.spec.ts +++ b/src/m365/entra/commands/administrativeunit/administrativeunit-add.spec.ts @@ -21,6 +21,14 @@ describe(commands.ADMINISTRATIVEUNIT_ADD, () => { visibility: null }; + const administrativeUnitWithDirectoryExtensionReponse: any = { + id: 'fc33aa61-cf0e-46b6-9506-f633347202ab', + displayName: 'European Division', + description: null, + visibility: null, + extension_b7d8e648520f41d3b9c0fdeb91768a0a_jobGroupTracker: 'JobGroupN' + }; + let log: string[]; let logger: Logger; let loggerLogSpy: sinon.SinonSpy; @@ -71,6 +79,10 @@ describe(commands.ADMINISTRATIVEUNIT_ADD, () => { assert.notStrictEqual(command.description, null); }); + it('allows unknown options', () => { + assert.strictEqual(command.allowUnknownOptions(), true); + }); + it('creates an administrative unit with a specific display name', async () => { const postStub = sinon.stub(request, 'post').callsFake(async (opts) => { if (opts.url === 'https://graph.microsoft.com/v1.0/directory/administrativeUnits') { @@ -110,6 +122,32 @@ describe(commands.ADMINISTRATIVEUNIT_ADD, () => { assert(loggerLogSpy.calledOnceWith(administrativeUnitReponse)); }); + it('creates an administrative unit with unknown options', async () => { + const postStub = sinon.stub(request, 'post').callsFake(async (opts) => { + if (opts.url === 'https://graph.microsoft.com/v1.0/directory/administrativeUnits') { + return administrativeUnitWithDirectoryExtensionReponse; + } + + throw 'Invalid request'; + }); + + await command.action(logger, { + options: + { + displayName: 'European Division', + description: 'European Division Administration', + extension_b7d8e648520f41d3b9c0fdeb91768a0a_jobGroupTracker: 'JobGroupN' + } + }); + assert.deepStrictEqual(postStub.lastCall.args[0].data, { + displayName: 'European Division', + description: 'European Division Administration', + visibility: null, + extension_b7d8e648520f41d3b9c0fdeb91768a0a_jobGroupTracker: 'JobGroupN' + }); + assert(loggerLogSpy.calledOnceWith(administrativeUnitWithDirectoryExtensionReponse)); + }); + it('creates a hidden administrative unit with a specific display name and description', async () => { const privateAdministrativeUnitResponse = { ...administrativeUnitReponse }; privateAdministrativeUnitResponse.description = 'European Division Administration'; diff --git a/src/m365/entra/commands/administrativeunit/administrativeunit-add.ts b/src/m365/entra/commands/administrativeunit/administrativeunit-add.ts index 37e60e17884..72a9ac053de 100644 --- a/src/m365/entra/commands/administrativeunit/administrativeunit-add.ts +++ b/src/m365/entra/commands/administrativeunit/administrativeunit-add.ts @@ -24,6 +24,10 @@ class EntraAdministrativeUnitAddCommand extends GraphCommand { return 'Creates an administrative unit'; } + public allowUnknownOptions(): boolean | undefined { + return true; + } + constructor() { super(); @@ -54,17 +58,21 @@ class EntraAdministrativeUnitAddCommand extends GraphCommand { } public async commandAction(logger: Logger, args: CommandArgs): Promise { + const requestBody = { + description: args.options.description, + displayName: args.options.displayName, + visibility: args.options.hiddenMembership ? 'HiddenMembership' : null + }; + + this.addUnknownOptionsToPayload(requestBody, args.options); + const requestOptions: CliRequestOptions = { url: `${this.resource}/v1.0/directory/administrativeUnits`, headers: { accept: 'application/json;odata.metadata=none' }, responseType: 'json', - data: { - description: args.options.description, - displayName: args.options.displayName, - visibility: args.options.hiddenMembership ? 'HiddenMembership' : null - } + data: requestBody }; try { diff --git a/src/m365/entra/commands/app/app-add.spec.ts b/src/m365/entra/commands/app/app-add.spec.ts index af92d3acbad..819847e3ff4 100644 --- a/src/m365/entra/commands/app/app-add.spec.ts +++ b/src/m365/entra/commands/app/app-add.spec.ts @@ -203,6 +203,10 @@ describe(commands.APP_ADD, () => { assert.notStrictEqual(command.description, null); }); + it('allows unknown options', () => { + assert.strictEqual(command.allowUnknownOptions(), true); + }); + it('creates Microsoft Entra app reg with just the name', async () => { sinon.stub(request, 'get').rejects('Issues GET request'); sinon.stub(request, 'patch').rejects('Issued PATCH request'); @@ -292,6 +296,97 @@ describe(commands.APP_ADD, () => { })); }); + it('creates Microsoft Entra app reg with the name and directory extension', async () => { + sinon.stub(request, 'get').rejects('Issues GET request'); + sinon.stub(request, 'patch').rejects('Issued PATCH request'); + sinon.stub(request, 'post').callsFake(async opts => { + if (opts.url === 'https://graph.microsoft.com/v1.0/myorganization/applications' && + JSON.stringify(opts.data) === JSON.stringify({ + "displayName": "My Microsoft Entra app", + "signInAudience": "AzureADMyOrg", + "extension_b7d8e648520f41d3b9c0fdeb91768a0a_jobGroupTracker": 'JobGroupN' + })) { + return { + "id": "5b31c38c-2584-42f0-aa47-657fb3a84230", + "deletedDateTime": null, + "appId": "bc724b77-da87-43a9-b385-6ebaaf969db8", + "applicationTemplateId": null, + "createdDateTime": "2020-12-31T14:44:13.7945807Z", + "displayName": "My Microsoft Entra app", + "description": null, + "groupMembershipClaims": null, + "identifierUris": [], + "isDeviceOnlyAuthSupported": null, + "isFallbackPublicClient": null, + "notes": null, + "optionalClaims": null, + "publisherDomain": "contoso.onmicrosoft.com", + "signInAudience": "AzureADMyOrg", + "tags": [], + "tokenEncryptionKeyId": null, + "verifiedPublisher": { + "displayName": null, + "verifiedPublisherId": null, + "addedDateTime": null + }, + "spa": { + "redirectUris": [] + }, + "defaultRedirectUri": null, + "addIns": [], + "api": { + "acceptMappedClaims": null, + "knownClientApplications": [], + "requestedAccessTokenVersion": null, + "oauth2PermissionScopes": [], + "preAuthorizedApplications": [] + }, + "appRoles": [], + "info": { + "logoUrl": null, + "marketingUrl": null, + "privacyStatementUrl": null, + "supportUrl": null, + "termsOfServiceUrl": null + }, + "keyCredentials": [], + "parentalControlSettings": { + "countriesBlockedForMinors": [], + "legalAgeGroupRule": "Allow" + }, + "passwordCredentials": [], + "publicClient": { + "redirectUris": [] + }, + "requiredResourceAccess": [], + "web": { + "homePageUrl": null, + "logoutUrl": null, + "redirectUris": [], + "implicitGrantSettings": { + "enableAccessTokenIssuance": false, + "enableIdTokenIssuance": false + } + } + }; + } + + throw `Invalid POST request: ${JSON.stringify(opts, null, 2)}`; + }); + + await command.action(logger, { + options: { + name: 'My Microsoft Entra app', + extension_b7d8e648520f41d3b9c0fdeb91768a0a_jobGroupTracker: 'JobGroupN' + } + }); + assert(loggerLogSpy.calledWith({ + appId: 'bc724b77-da87-43a9-b385-6ebaaf969db8', + objectId: '5b31c38c-2584-42f0-aa47-657fb3a84230', + tenantId: '' + })); + }); + it('creates multitenant Microsoft Entra app reg', async () => { sinon.stub(request, 'get').rejects('Issues GET request'); sinon.stub(request, 'patch').rejects('Issued PATCH request'); diff --git a/src/m365/entra/commands/app/app-add.ts b/src/m365/entra/commands/app/app-add.ts index caa145571fe..a71e968d5a2 100644 --- a/src/m365/entra/commands/app/app-add.ts +++ b/src/m365/entra/commands/app/app-add.ts @@ -40,6 +40,10 @@ class EntraAppAddCommand extends GraphCommand { return 'Creates new Entra app registration'; } + public allowUnknownOptions(): boolean | undefined { + return true; + } + constructor() { super(); @@ -228,7 +232,8 @@ class EntraAppAddCommand extends GraphCommand { apis, logger, verbose: this.verbose, - debug: this.debug + debug: this.debug, + command: this }); // based on the assumption that we're adding Microsoft Entra app to the current // directory. If we in the future extend the command with allowing diff --git a/src/m365/entra/commands/app/app-set.spec.ts b/src/m365/entra/commands/app/app-set.spec.ts index 3936f23a3d6..b399c443855 100644 --- a/src/m365/entra/commands/app/app-set.spec.ts +++ b/src/m365/entra/commands/app/app-set.spec.ts @@ -73,6 +73,10 @@ describe(commands.APP_SET, () => { assert.notStrictEqual(command.description, null); }); + it('allows unknown options', () => { + assert.strictEqual(command.allowUnknownOptions(), true); + }); + it('updates uris for the specified appId', async () => { sinon.stub(request, 'get').callsFake(async opts => { if (opts.url === `https://graph.microsoft.com/v1.0/myorganization/applications?$filter=appId eq 'bc724b77-da87-43a9-b385-6ebaaf969db8'&$select=id`) { @@ -104,6 +108,46 @@ describe(commands.APP_SET, () => { }); }); + it('updates unknown options for the specified appId', async () => { + sinon.stub(request, 'get').callsFake(async opts => { + if (opts.url === `https://graph.microsoft.com/v1.0/myorganization/applications?$filter=appId eq 'bc724b77-da87-43a9-b385-6ebaaf969db8'&$select=id`) { + return { + value: [{ + id: '5b31c38c-2584-42f0-aa47-657fb3a84230' + }] + }; + } + + throw `Invalid request ${JSON.stringify(opts)}`; + }); + sinon.stub(request, 'patch').callsFake(async opts => { + if (opts.url === 'https://graph.microsoft.com/v1.0/myorganization/applications/5b31c38c-2584-42f0-aa47-657fb3a84230' && + opts.data && + Object.keys(opts.data).indexOf('extension_b7d8e648520f41d3b9c0fdeb91768a0a_jobGroupTracker') > -1 && + opts.data.extension_b7d8e648520f41d3b9c0fdeb91768a0a_jobGroupTracker === 'JobGroupN') { + return; + } + + if (opts.url === 'https://graph.microsoft.com/v1.0/myorganization/applications/5b31c38c-2584-42f0-aa47-657fb3a84230' && + opts.data && + Object.keys(opts.data).indexOf('identifierUris') > -1 && + opts.data.identifierUris[0] === 'https://contoso.com/bc724b77-da87-43a9-b385-6ebaaf969db8') { + return; + } + + throw `Invalid request ${JSON.stringify(opts)}`; + }); + + await command.action(logger, { + options: { + debug: true, + appId: 'bc724b77-da87-43a9-b385-6ebaaf969db8', + uris: 'https://contoso.com/bc724b77-da87-43a9-b385-6ebaaf969db8', + extension_b7d8e648520f41d3b9c0fdeb91768a0a_jobGroupTracker: 'JobGroupN' + } + }); + }); + it('updates multiple URIs for the specified appId', async () => { sinon.stub(request, 'get').callsFake(async opts => { if (opts.url === `https://graph.microsoft.com/v1.0/myorganization/applications?$filter=appId eq 'bc724b77-da87-43a9-b385-6ebaaf969db8'&$select=id`) { @@ -1280,7 +1324,15 @@ describe(commands.APP_SET, () => { }); it('fails validation if objectId and name specified', async () => { - const actual = await command.validate({ options: { appObjectId: '9b1b1e42-794b-4c71-93ac-5ed92488b67f', appName: 'My app' } }, commandInfo); + sinon.stub(cli, 'getSettingWithDefaultValue').callsFake((settingName, defaultValue) => { + if (settingName === settingsNames.prompt) { + return false; + } + + return defaultValue; + }); + + const actual = await command.validate({ options: { objectId: '9b1b1e42-794b-4c71-93ac-5ed92488b67f', name: 'My app' } }, commandInfo); assert.notStrictEqual(actual, true); }); diff --git a/src/m365/entra/commands/app/app-set.ts b/src/m365/entra/commands/app/app-set.ts index 21fcc119f72..a3f11c921a5 100644 --- a/src/m365/entra/commands/app/app-set.ts +++ b/src/m365/entra/commands/app/app-set.ts @@ -37,6 +37,10 @@ class EntraAppSetCommand extends GraphCommand { return 'Updates Entra app registration'; } + public allowUnknownOptions(): boolean | undefined { + return true; + } + constructor() { super(); @@ -127,6 +131,7 @@ class EntraAppSetCommand extends GraphCommand { public async commandAction(logger: Logger, args: CommandArgs): Promise { try { let objectId = await this.getAppObjectId(args, logger); + objectId = await this.updateUknownOptions(args, objectId); objectId = await this.configureUri(args, objectId, logger); objectId = await this.configureRedirectUris(args, objectId, logger); objectId = await this.updateAllowPublicClientFlows(args, objectId, logger); @@ -176,6 +181,24 @@ class EntraAppSetCommand extends GraphCommand { return result.id; } + private async updateUknownOptions(args: CommandArgs, objectId: string): Promise { + if (Object.keys(this.getUnknownOptions(args.options)).length > 0) { + const requestBody = {}; + this.addUnknownOptionsToPayload(requestBody, args.options); + + const requestOptions: CliRequestOptions = { + url: `${this.resource}/v1.0/myorganization/applications/${objectId}`, + headers: { + 'content-type': 'application/json;odata.metadata=none' + }, + responseType: 'json', + data: requestBody + }; + await request.patch(requestOptions); + } + return objectId; + } + private async updateAllowPublicClientFlows(args: CommandArgs, objectId: string, logger: Logger): Promise { if (args.options.allowPublicClientFlows === undefined) { return objectId; diff --git a/src/m365/entra/commands/group/group-add.spec.ts b/src/m365/entra/commands/group/group-add.spec.ts index 47874bc868d..f8ac02217ef 100644 --- a/src/m365/entra/commands/group/group-add.spec.ts +++ b/src/m365/entra/commands/group/group-add.spec.ts @@ -57,6 +57,10 @@ describe(commands.GROUP_ADD, () => { "onPremisesProvisioningErrors": [], "serviceProvisioningErrors": [] }; + const microsoft365GroupWithDirectoryExtension = { + ...microsoft365Group, + extension_b7d8e648520f41d3b9c0fdeb91768a0a_jobGroupTracker: 'JobGroupN' + }; const securityGroup = { "id": "bc91082e-73ad-4a97-9852-e66004c7b0b6", "deletedDateTime": null, @@ -222,6 +226,10 @@ describe(commands.GROUP_ADD, () => { assert.notStrictEqual(command.description, null); }); + it('allows unknown options', () => { + assert.strictEqual(command.allowUnknownOptions(), true); + }); + it('fails validation if the length of displayName is more than 256 characters', async () => { const displayName = 'lorem ipsum lorem ipsum lorem ipsum lorem ipsum lorem ipsum lorem ipsum lorem ipsum lorem ipsum lorem ipsum lorem ipsum lorem ipsum lorem ipsum lorem ipsum lorem ipsum lorem ipsum lorem ipsum lorem ipsum lorem ipsum lorem ipsum lorem ipsum lorem ipsum lorem ipsum'; const actual = await command.validate({ options: { displayName: displayName, type: 'security' } }, commandInfo); @@ -302,6 +310,28 @@ describe(commands.GROUP_ADD, () => { assert(loggerLogSpy.calledWith(microsoft365Group)); }); + it('successfully creates Microsoft 365 group with uknown options', async () => { + const postStub = sinon.stub(request, 'post').callsFake(async (opts) => { + if (opts.url === 'https://graph.microsoft.com/v1.0/groups') { + return microsoft365GroupWithDirectoryExtension; + } + + throw 'Invalid request'; + }); + + await command.action(logger, { options: { displayName: 'Microsoft 365 Group', description: 'Microsoft 365 group', mailNickname: 'Microsoft365Group', visibility: 'Public', type: 'microsoft365', extension_b7d8e648520f41d3b9c0fdeb91768a0a_jobGroupTracker: 'JobGroupN' } }); + assert.deepStrictEqual(postStub.lastCall.args[0].data, { + displayName: 'Microsoft 365 Group', + description: 'Microsoft 365 group', + mailNickName: 'Microsoft365Group', + visibility: 'Public', + groupTypes: ['Unified'], + mailEnabled: true, + securityEnabled: true, + extension_b7d8e648520f41d3b9c0fdeb91768a0a_jobGroupTracker: 'JobGroupN' + }); + }); + it('successfully creates security group without owners and members', async () => { sinon.stub(request, 'post').callsFake(async (opts) => { if (opts.url === 'https://graph.microsoft.com/v1.0/groups') { diff --git a/src/m365/entra/commands/group/group-set.spec.ts b/src/m365/entra/commands/group/group-set.spec.ts index 1b1897337ae..4a193dd2fb8 100644 --- a/src/m365/entra/commands/group/group-set.spec.ts +++ b/src/m365/entra/commands/group/group-set.spec.ts @@ -71,6 +71,10 @@ describe(commands.GROUP_SET, () => { assert.notStrictEqual(command.description, null); }); + it('allows unknown options', () => { + assert.strictEqual(command.allowUnknownOptions(), true); + }); + it('fails validation if the length of newDisplayName is more than 256 characters', async () => { const displayName = 'lorem ipsum lorem ipsum lorem ipsum lorem ipsum lorem ipsum lorem ipsum lorem ipsum lorem ipsum lorem ipsum lorem ipsum lorem ipsum lorem ipsum lorem ipsum lorem ipsum lorem ipsum lorem ipsum lorem ipsum lorem ipsum lorem ipsum lorem ipsum lorem ipsum lorem ipsum'; const actual = await command.validate({ options: { id: groupId, newDisplayName: displayName } }, commandInfo); @@ -161,6 +165,25 @@ describe(commands.GROUP_SET, () => { }); }); + it('successfully updates group specified by id with unknown options', async () => { + const patchRequestStub = sinon.stub(request, 'patch').callsFake(async (opts) => { + if (opts.url === `https://graph.microsoft.com/v1.0/groups/${groupId}`) { + return; + } + + throw 'Invalid request'; + }); + + await command.action(logger, { options: { id: groupId, description: 'Microsoft 365 group', mailNickname: 'Microsoft365Group', visibility: 'Public', newDisplayName: '365 group', extension_b7d8e648520f41d3b9c0fdeb91768a0a_jobGroupTracker: 'JobGroupN', verbose: true } }); + assert.deepStrictEqual(patchRequestStub.lastCall.args[0].data, { + displayName: '365 group', + description: 'Microsoft 365 group', + mailNickName: 'Microsoft365Group', + visibility: 'Public', + extension_b7d8e648520f41d3b9c0fdeb91768a0a_jobGroupTracker: 'JobGroupN' + }); + }); + it('successfully updates group specified by displayName', async () => { const patchRequestStub = sinon.stub(request, 'patch').callsFake(async (opts) => { if (opts.url === `https://graph.microsoft.com/v1.0/groups/${groupId}`) { diff --git a/src/m365/entra/commands/group/group-set.ts b/src/m365/entra/commands/group/group-set.ts index 1e1430310b0..4d177c40c1b 100644 --- a/src/m365/entra/commands/group/group-set.ts +++ b/src/m365/entra/commands/group/group-set.ts @@ -38,6 +38,10 @@ class EntraGroupSetCommand extends GraphCommand { return 'Updates a Microsoft Entra group'; } + public allowUnknownOptions(): boolean | undefined { + return true; + } + constructor(){ super(); @@ -195,17 +199,21 @@ class EntraGroupSetCommand extends GraphCommand { groupId = await entraGroup.getGroupIdByDisplayName(args.options.displayName); } + const requestBody = { + displayName: args.options.newDisplayName, + description: args.options.description === '' ? null : args.options.description, + mailNickName: args.options.mailNickname, + visibility: args.options.visibility + }; + + this.addUnknownOptionsToPayload(requestBody, args.options); + const requestOptions: CliRequestOptions = { url: `${this.resource}/v1.0/groups/${groupId}`, headers: { accept: 'application/json;odata.metadata=none' }, - data: { - displayName: args.options.newDisplayName, - description: args.options.description === '' ? null : args.options.description, - mailNickName: args.options.mailNickname, - visibility: args.options.visibility - } + data: requestBody }; await request.patch(requestOptions); diff --git a/src/m365/entra/commands/user/user-add.spec.ts b/src/m365/entra/commands/user/user-add.spec.ts index ca1bd15add3..436be8edcef 100644 --- a/src/m365/entra/commands/user/user-add.spec.ts +++ b/src/m365/entra/commands/user/user-add.spec.ts @@ -52,6 +52,16 @@ describe(commands.USER_ADD, () => { password: password }; + const userResponseWithoutPasswordAndWithDirectoryExtension = { + ...userResponseWithoutPassword, + extension_b7d8e648520f41d3b9c0fdeb91768a0a_jobGroupTracker: 'JobGroupN' + }; + + const userResponseWithPasswordAndDirectoryExtension = { + ...userResponseWithPassword, + extension_b7d8e648520f41d3b9c0fdeb91768a0a_jobGroupTracker: 'JobGroupN' + }; + const graphError = { error: { code: "Request_BadRequest", @@ -123,6 +133,10 @@ describe(commands.USER_ADD, () => { assert.notStrictEqual(command.description, null); }); + it('allows unknown options', () => { + assert.strictEqual(command.allowUnknownOptions(), true); + }); + it('creates Microsoft Entra user using a preset password but requiring the user to change it the next login', async () => { sinon.stub(request, 'post').callsFake(async (opts) => { if (opts.url === graphBaseUrl) { @@ -150,6 +164,19 @@ describe(commands.USER_ADD, () => { assert(loggerLogSpy.calledWith(userResponseWithPassword)); }); + it('creates Microsoft Entra user with uknown options', async () => { + sinon.stub(request, 'post').callsFake(async (opts) => { + if (opts.url === graphBaseUrl) { + return userResponseWithoutPasswordAndWithDirectoryExtension; + } + + throw 'Invalid request'; + }); + + await command.action(logger, { options: { userName: userName, displayName: displayName, password: password, extension_b7d8e648520f41d3b9c0fdeb91768a0a_jobGroupTracker: 'JobGroupN' } }); + assert(loggerLogSpy.calledWith(userResponseWithPasswordAndDirectoryExtension)); + }); + it('creates Microsoft Entra user and set its manager by id', async () => { sinon.stub(request, 'post').callsFake(async (opts) => { if (opts.url === graphBaseUrl) { diff --git a/src/m365/entra/commands/user/user-set.spec.ts b/src/m365/entra/commands/user/user-set.spec.ts index 3da3de2bf43..8d8386bc416 100644 --- a/src/m365/entra/commands/user/user-set.spec.ts +++ b/src/m365/entra/commands/user/user-set.spec.ts @@ -94,6 +94,10 @@ describe(commands.USER_SET, () => { assert.notStrictEqual(command.description, null); }); + it('allows unknown options', () => { + assert.strictEqual(command.allowUnknownOptions(), true); + }); + it('fails validation if neither the id nor the userName are specified', async () => { sinon.stub(cli, 'getSettingWithDefaultValue').callsFake((settingName, defaultValue) => { if (settingName === settingsNames.prompt) { @@ -283,6 +287,46 @@ describe(commands.USER_SET, () => { assert(loggerLogSpy.notCalled); }); + it('correctly updates information about the specified user with unknown options', async () => { + const patchStub = sinon.stub(request, 'patch').callsFake(async (opts) => { + if ((opts.url as string).indexOf(`/v1.0/users/`) > -1) { + return; + } + throw 'Invalid request'; + }); + + await command.action(logger, { + options: { + verbose: true, + id: id, + department: 'Sales & Marketing', + companyName: 'Contoso', + displayName: displayName, + firstName: firstName, + lastName: lastName, + usageLocation: usageLocation, + officeLocation: officeLocation, + jobTitle: jobTitle, + preferredLanguage: preferredLanguage, + accountEnabled: false, + extension_b7d8e648520f41d3b9c0fdeb91768a0a_jobGroupTracker: 'JobGroupN' + } + } as any); + assert.deepStrictEqual(patchStub.lastCall.args[0].data, { + companyName: 'Contoso', + department: 'Sales & Marketing', + displayName: displayName, + givenName: firstName, + surname: lastName, + usageLocation: usageLocation, + officeLocation: officeLocation, + jobTitle: jobTitle, + preferredLanguage: preferredLanguage, + accountEnabled: false, + extension_b7d8e648520f41d3b9c0fdeb91768a0a_jobGroupTracker: 'JobGroupN' + }); + }); + it('correctly updates user with an empty value', async () => { const patchStub = sinon.stub(request, 'patch').callsFake(async (opts) => { if (opts.url === `https://graph.microsoft.com/v1.0/users/${id}`) { diff --git a/src/utils/entraApp.ts b/src/utils/entraApp.ts index d017fcf3349..49a4eaf8d4c 100644 --- a/src/utils/entraApp.ts +++ b/src/utils/entraApp.ts @@ -3,6 +3,7 @@ import fs from 'fs'; import { Logger } from '../cli/Logger.js'; import request, { CliRequestOptions } from '../request.js'; import { odata } from './odata.js'; +import Command from '../Command.js'; export interface AppInfo { appId: string; @@ -214,12 +215,13 @@ function updateAppPermissions({ spId, resourceAccessPermission, oAuth2Permission export const entraApp = { appPermissions: [] as AppPermissions[], - createAppRegistration: async ({ options, apis, logger, verbose, debug }: { + createAppRegistration: async ({ options, apis, logger, verbose, debug, command }: { options: AppCreationOptions, apis: RequiredResourceAccess[], logger: Logger, verbose: boolean, - debug: boolean + debug: boolean, + command: Command }): Promise => { const applicationInfo: any = { displayName: options.name, @@ -263,6 +265,8 @@ export const entraApp = { applicationInfo.isFallbackPublicClient = true; } + command.addUnknownOptionsToPayload(applicationInfo, options); + if (verbose) { await logger.logToStderr(`Creating Microsoft Entra app registration...`); } From c4ab63482f7a27476eda124eba4870c293b686a2 Mon Sep 17 00:00:00 2001 From: Martin Machacek Date: Sun, 2 Feb 2025 15:37:20 +0100 Subject: [PATCH 2/7] Enhancement: Allow unknown options for entra group/app/administrativeunit commands --- src/Command.ts | 2 +- src/m365/commands/setup.ts | 2 +- src/m365/entra/commands/app/app-add.ts | 2 +- src/m365/entra/commands/app/app-set.ts | 4 ++-- src/utils/entraApp.ts | 9 +++++---- 5 files changed, 10 insertions(+), 9 deletions(-) diff --git a/src/Command.ts b/src/Command.ts index 52d27fdf9f8..e728d79d708 100644 --- a/src/Command.ts +++ b/src/Command.ts @@ -501,7 +501,7 @@ export default abstract class Command { }); } - public addUnknownOptionsToPayload(payload: any, options: any): void { + protected addUnknownOptionsToPayload(payload: any, options: any): void { const unknownOptions: any = this.getUnknownOptions(options); const unknownOptionsNames: string[] = Object.getOwnPropertyNames(unknownOptions); unknownOptionsNames.forEach(o => { diff --git a/src/m365/commands/setup.ts b/src/m365/commands/setup.ts index ce7cf748bef..1bbf70be52e 100644 --- a/src/m365/commands/setup.ts +++ b/src/m365/commands/setup.ts @@ -315,7 +315,7 @@ class SetupCommand extends AnonymousCommand { logger, verbose: this.verbose, debug: this.debug, - command: this + addUnknownOptions: this.addUnknownOptionsToPayload.bind(this) }); appInfo.tenantId = accessToken.getTenantIdFromAccessToken(auth.connection.accessTokens[auth.defaultResource].accessToken); await entraApp.grantAdminConsent({ diff --git a/src/m365/entra/commands/app/app-add.ts b/src/m365/entra/commands/app/app-add.ts index a71e968d5a2..e4d20d7716c 100644 --- a/src/m365/entra/commands/app/app-add.ts +++ b/src/m365/entra/commands/app/app-add.ts @@ -233,7 +233,7 @@ class EntraAppAddCommand extends GraphCommand { logger, verbose: this.verbose, debug: this.debug, - command: this + addUnknownOptions: this.addUnknownOptionsToPayload.bind(this) }); // based on the assumption that we're adding Microsoft Entra app to the current // directory. If we in the future extend the command with allowing diff --git a/src/m365/entra/commands/app/app-set.ts b/src/m365/entra/commands/app/app-set.ts index a3f11c921a5..8eb783a1a94 100644 --- a/src/m365/entra/commands/app/app-set.ts +++ b/src/m365/entra/commands/app/app-set.ts @@ -131,7 +131,7 @@ class EntraAppSetCommand extends GraphCommand { public async commandAction(logger: Logger, args: CommandArgs): Promise { try { let objectId = await this.getAppObjectId(args, logger); - objectId = await this.updateUknownOptions(args, objectId); + objectId = await this.updateUnknownOptions(args, objectId); objectId = await this.configureUri(args, objectId, logger); objectId = await this.configureRedirectUris(args, objectId, logger); objectId = await this.updateAllowPublicClientFlows(args, objectId, logger); @@ -181,7 +181,7 @@ class EntraAppSetCommand extends GraphCommand { return result.id; } - private async updateUknownOptions(args: CommandArgs, objectId: string): Promise { + private async updateUnknownOptions(args: CommandArgs, objectId: string): Promise { if (Object.keys(this.getUnknownOptions(args.options)).length > 0) { const requestBody = {}; this.addUnknownOptionsToPayload(requestBody, args.options); diff --git a/src/utils/entraApp.ts b/src/utils/entraApp.ts index 49a4eaf8d4c..a61c9032353 100644 --- a/src/utils/entraApp.ts +++ b/src/utils/entraApp.ts @@ -3,7 +3,6 @@ import fs from 'fs'; import { Logger } from '../cli/Logger.js'; import request, { CliRequestOptions } from '../request.js'; import { odata } from './odata.js'; -import Command from '../Command.js'; export interface AppInfo { appId: string; @@ -45,6 +44,8 @@ export interface AppPermissions { scope: string[]; } +type addUnknownOptions = (payload: any, options: any) => void; + async function getCertificateBase64Encoded({ options, logger, debug }: { options: AppCreationOptions, logger: Logger, @@ -215,13 +216,13 @@ function updateAppPermissions({ spId, resourceAccessPermission, oAuth2Permission export const entraApp = { appPermissions: [] as AppPermissions[], - createAppRegistration: async ({ options, apis, logger, verbose, debug, command }: { + createAppRegistration: async ({ options, apis, logger, verbose, debug, addUnknownOptions }: { options: AppCreationOptions, apis: RequiredResourceAccess[], logger: Logger, verbose: boolean, debug: boolean, - command: Command + addUnknownOptions: addUnknownOptions }): Promise => { const applicationInfo: any = { displayName: options.name, @@ -265,7 +266,7 @@ export const entraApp = { applicationInfo.isFallbackPublicClient = true; } - command.addUnknownOptionsToPayload(applicationInfo, options); + addUnknownOptions(applicationInfo, options); if (verbose) { await logger.logToStderr(`Creating Microsoft Entra app registration...`); From fe62018f53c747d9050914d3dcaced8fdd3155d9 Mon Sep 17 00:00:00 2001 From: Martin Machacek Date: Sun, 2 Feb 2025 16:24:58 +0100 Subject: [PATCH 3/7] Enhancement: Allow unknown options for entra group/app/administrativeunit commands --- src/Command.ts | 35 +++---------------- .../commands/adaptivecard-send.ts | 3 +- src/m365/base/SpoCommand.ts | 3 +- src/m365/commands/request.ts | 5 +-- src/m365/commands/setup.ts | 4 +-- src/m365/entra/commands/app/app-add.ts | 4 +-- src/m365/entra/commands/app/app-set.ts | 5 +-- src/m365/entra/commands/user/user-list.ts | 3 +- .../people/people-profilecardproperty-add.ts | 7 ++-- .../people/people-profilecardproperty-set.ts | 7 ++-- src/utils/entraApp.ts | 12 +++---- src/utils/optionsUtils.ts | 35 +++++++++++++++++++ 12 files changed, 69 insertions(+), 54 deletions(-) create mode 100644 src/utils/optionsUtils.ts diff --git a/src/Command.ts b/src/Command.ts index e728d79d708..38902e1497b 100644 --- a/src/Command.ts +++ b/src/Command.ts @@ -14,8 +14,9 @@ import { md } from './utils/md.js'; import { GraphResponseError } from './utils/odata.js'; import { prompt } from './utils/prompt.js'; import { zod } from './utils/zod.js'; +import { optionsUtils } from './utils/optionsUtils.js'; -interface CommandOption { +export interface CommandOption { option: string; autocomplete?: string[] } @@ -469,32 +470,8 @@ export default abstract class Command { telemetry.trackEvent(this.getUsedCommandName(), this.getTelemetryProperties(args)); } - protected getUnknownOptions(options: any): any { - const unknownOptions: any = JSON.parse(JSON.stringify(options)); - // remove minimist catch-all option - delete unknownOptions._; - - const knownOptions: CommandOption[] = this.options; - const longOptionRegex: RegExp = /--([^\s]+)/; - const shortOptionRegex: RegExp = /-([a-z])\b/; - knownOptions.forEach(o => { - const longOptionName: string = (longOptionRegex.exec(o.option) as RegExpExecArray)[1]; - delete unknownOptions[longOptionName]; - - // short names are optional so we need to check if the current command has - // one before continuing - const shortOptionMatch: RegExpExecArray | null = shortOptionRegex.exec(o.option); - if (shortOptionMatch) { - const shortOptionName: string = shortOptionMatch[1]; - delete unknownOptions[shortOptionName]; - } - }); - - return unknownOptions; - } - protected trackUnknownOptions(telemetryProps: any, options: any): void { - const unknownOptions: any = this.getUnknownOptions(options); + const unknownOptions: any = optionsUtils.getUnknownOptions(options, this.options); const unknownOptionsNames: string[] = Object.getOwnPropertyNames(unknownOptions); unknownOptionsNames.forEach(o => { telemetryProps[o] = true; @@ -502,11 +479,7 @@ export default abstract class Command { } protected addUnknownOptionsToPayload(payload: any, options: any): void { - const unknownOptions: any = this.getUnknownOptions(options); - const unknownOptionsNames: string[] = Object.getOwnPropertyNames(unknownOptions); - unknownOptionsNames.forEach(o => { - payload[o] = unknownOptions[o]; - }); + optionsUtils.addUnknownOptionsToPayload(payload, options, this.options); } private loadValuesFromAccessToken(args: CommandArgs): void { diff --git a/src/m365/adaptivecard/commands/adaptivecard-send.ts b/src/m365/adaptivecard/commands/adaptivecard-send.ts index 53e2b197fc1..b0d9cbfd934 100644 --- a/src/m365/adaptivecard/commands/adaptivecard-send.ts +++ b/src/m365/adaptivecard/commands/adaptivecard-send.ts @@ -4,6 +4,7 @@ import GlobalOptions from '../../../GlobalOptions.js'; import request, { CliRequestOptions } from '../../../request.js'; import AnonymousCommand from '../../base/AnonymousCommand.js'; import commands from '../commands.js'; +import { optionsUtils } from '../../../utils/optionsUtils.js'; interface CommandArgs { options: Options; @@ -113,7 +114,7 @@ class AdaptiveCardSendCommand extends AnonymousCommand { } public async commandAction(logger: Logger, args: CommandArgs): Promise { - const unknownOptions = this.getUnknownOptions(args.options); + const unknownOptions = optionsUtils.getUnknownOptions(args.options, this.options); const unknownOptionNames: string[] = Object.getOwnPropertyNames(unknownOptions); const card: any = await this.getCard(args, unknownOptionNames, unknownOptions); diff --git a/src/m365/base/SpoCommand.ts b/src/m365/base/SpoCommand.ts index fba2e6b4dc7..e81e56daac5 100644 --- a/src/m365/base/SpoCommand.ts +++ b/src/m365/base/SpoCommand.ts @@ -2,6 +2,7 @@ import { createRequire } from 'module'; import auth, { AuthType } from '../../Auth.js'; import { Logger } from '../../cli/Logger.js'; import Command, { CommandArgs, CommandError } from '../../Command.js'; +import { optionsUtils } from '../../utils/optionsUtils.js'; const require = createRequire(import.meta.url); const csomDefs = require('../../../csom.json'); @@ -80,7 +81,7 @@ export default abstract class SpoCommand extends Command { } protected validateUnknownCsomOptions(options: any, csomObject: string, csomPropertyType: 'get' | 'set'): string | boolean { - const unknownOptions: any = this.getUnknownOptions(options); + const unknownOptions: any = optionsUtils.getUnknownOptions(options, this.options); const optionNames: string[] = Object.getOwnPropertyNames(unknownOptions); if (optionNames.length === 0) { return true; diff --git a/src/m365/commands/request.ts b/src/m365/commands/request.ts index 9c1f0aeb9c6..412b29ffb47 100644 --- a/src/m365/commands/request.ts +++ b/src/m365/commands/request.ts @@ -7,6 +7,7 @@ import GlobalOptions from '../../GlobalOptions.js'; import { Logger } from '../../cli/Logger.js'; import request from '../../request.js'; import commands from './commands.js'; +import { optionsUtils } from '../../utils/optionsUtils.js'; interface CommandArgs { options: Options; @@ -53,7 +54,7 @@ class RequestCommand extends Command { filePath: typeof args.options.filePath !== 'undefined' }; - const unknownOptions: any = this.getUnknownOptions(args.options); + const unknownOptions: any = optionsUtils.getUnknownOptions(args.options, this.options); const unknownOptionsNames: string[] = Object.getOwnPropertyNames(unknownOptions); unknownOptionsNames.forEach(o => { properties[o] = typeof unknownOptions[o] !== 'undefined'; @@ -119,7 +120,7 @@ class RequestCommand extends Command { const method = (args.options.method || 'get').toUpperCase(); const headers: RawAxiosRequestHeaders = {}; - const unknownOptions: any = this.getUnknownOptions(args.options); + const unknownOptions: any = optionsUtils.getUnknownOptions(args.options, this.options); const unknownOptionsNames: string[] = Object.getOwnPropertyNames(unknownOptions); unknownOptionsNames.forEach(o => { headers[o] = unknownOptions[o]; diff --git a/src/m365/commands/setup.ts b/src/m365/commands/setup.ts index 1bbf70be52e..975776c3a1c 100644 --- a/src/m365/commands/setup.ts +++ b/src/m365/commands/setup.ts @@ -311,11 +311,11 @@ class SetupCommand extends AnonymousCommand { }); const appInfo: AppInfo = await entraApp.createAppRegistration({ options, + defaultOptions: this.options, apis, logger, verbose: this.verbose, - debug: this.debug, - addUnknownOptions: this.addUnknownOptionsToPayload.bind(this) + debug: this.debug }); appInfo.tenantId = accessToken.getTenantIdFromAccessToken(auth.connection.accessTokens[auth.defaultResource].accessToken); await entraApp.grantAdminConsent({ diff --git a/src/m365/entra/commands/app/app-add.ts b/src/m365/entra/commands/app/app-add.ts index e4d20d7716c..d8c9a826060 100644 --- a/src/m365/entra/commands/app/app-add.ts +++ b/src/m365/entra/commands/app/app-add.ts @@ -229,11 +229,11 @@ class EntraAppAddCommand extends GraphCommand { }); let appInfo: any = await entraApp.createAppRegistration({ options: args.options, + defaultOptions: this.options, apis, logger, verbose: this.verbose, - debug: this.debug, - addUnknownOptions: this.addUnknownOptionsToPayload.bind(this) + debug: this.debug }); // based on the assumption that we're adding Microsoft Entra app to the current // directory. If we in the future extend the command with allowing diff --git a/src/m365/entra/commands/app/app-set.ts b/src/m365/entra/commands/app/app-set.ts index 8eb783a1a94..f0fa18db266 100644 --- a/src/m365/entra/commands/app/app-set.ts +++ b/src/m365/entra/commands/app/app-set.ts @@ -7,6 +7,7 @@ import { formatting } from '../../../../utils/formatting.js'; import GraphCommand from '../../../base/GraphCommand.js'; import commands from '../../commands.js'; import { cli } from '../../../../cli/cli.js'; +import { optionsUtils } from '../../../../utils/optionsUtils.js'; interface CommandArgs { options: Options; @@ -182,9 +183,9 @@ class EntraAppSetCommand extends GraphCommand { } private async updateUnknownOptions(args: CommandArgs, objectId: string): Promise { - if (Object.keys(this.getUnknownOptions(args.options)).length > 0) { + if (Object.keys(optionsUtils.getUnknownOptions(args.options, this.options)).length > 0) { const requestBody = {}; - this.addUnknownOptionsToPayload(requestBody, args.options); + optionsUtils.addUnknownOptionsToPayload(requestBody, args.options, this.options); const requestOptions: CliRequestOptions = { url: `${this.resource}/v1.0/myorganization/applications/${objectId}`, diff --git a/src/m365/entra/commands/user/user-list.ts b/src/m365/entra/commands/user/user-list.ts index fdce96baafc..3f8eda97002 100644 --- a/src/m365/entra/commands/user/user-list.ts +++ b/src/m365/entra/commands/user/user-list.ts @@ -5,6 +5,7 @@ import { formatting } from '../../../../utils/formatting.js'; import { odata } from '../../../../utils/odata.js'; import GraphCommand from '../../../base/GraphCommand.js'; import commands from '../../commands.js'; +import { optionsUtils } from '../../../../utils/optionsUtils.js'; interface CommandArgs { options: Options; @@ -115,7 +116,7 @@ class EntraUserListCommand extends GraphCommand { private getFilter(options: Options): string | null { const filters: string[] = []; - const unknownOptions = this.getUnknownOptions(options); + const unknownOptions = optionsUtils.getUnknownOptions(options, this.options); Object.keys(unknownOptions).forEach(key => { if (typeof options[key] === 'boolean') { throw `Specify value for the ${key} property`; diff --git a/src/m365/tenant/commands/people/people-profilecardproperty-add.ts b/src/m365/tenant/commands/people/people-profilecardproperty-add.ts index 4491c4793d0..3a1f873722b 100644 --- a/src/m365/tenant/commands/people/people-profilecardproperty-add.ts +++ b/src/m365/tenant/commands/people/people-profilecardproperty-add.ts @@ -1,6 +1,7 @@ import { Logger } from '../../../../cli/Logger.js'; import GlobalOptions from '../../../../GlobalOptions.js'; import request, { CliRequestOptions } from '../../../../request.js'; +import { optionsUtils } from '../../../../utils/optionsUtils.js'; import GraphCommand from '../../../base/GraphCommand.js'; import commands from '../../commands.js'; import { profileCardPropertyNames } from './profileCardProperties.js'; @@ -34,7 +35,7 @@ class TenantPeopleProfileCardPropertyAddCommand extends GraphCommand { #initTelemetry(): void { this.telemetry.push((args: CommandArgs) => { // Add unknown options to telemetry - const unknownOptions = Object.keys(this.getUnknownOptions(args.options)); + const unknownOptions = Object.keys(optionsUtils.getUnknownOptions(args.options, this.options)); const unknownOptionsObj = unknownOptions.reduce((obj, key) => ({ ...obj, [key]: true }), {}); Object.assign(this.telemetryProperties, { @@ -73,7 +74,7 @@ class TenantPeopleProfileCardPropertyAddCommand extends GraphCommand { return `The option 'displayName' can only be used when adding customAttributes as profile card properties`; } - const unknownOptions = Object.keys(this.getUnknownOptions(args.options)); + const unknownOptions = Object.keys(optionsUtils.getUnknownOptions(args.options, this.options)); if (!propertyName.startsWith('customattribute') && unknownOptions.length > 0) { return `Unknown options like ${unknownOptions.join(', ')} are only supported with customAttributes`; @@ -151,7 +152,7 @@ class TenantPeopleProfileCardPropertyAddCommand extends GraphCommand { } private getLocalizations(options: Options): { languageTag: string, displayName: string }[] { - const unknownOptions = Object.keys(this.getUnknownOptions(options)); + const unknownOptions = Object.keys(optionsUtils.getUnknownOptions(options, this.options)); if (unknownOptions.length === 0) { return []; diff --git a/src/m365/tenant/commands/people/people-profilecardproperty-set.ts b/src/m365/tenant/commands/people/people-profilecardproperty-set.ts index 9cd9f4d3181..c48d09ec6a2 100644 --- a/src/m365/tenant/commands/people/people-profilecardproperty-set.ts +++ b/src/m365/tenant/commands/people/people-profilecardproperty-set.ts @@ -4,6 +4,7 @@ import GraphCommand from '../../../base/GraphCommand.js'; import request, { CliRequestOptions } from '../../../../request.js'; import { Localization, ProfileCardProperty, profileCardPropertyNames as allProfileCardPropertyNames } from './profileCardProperties.js'; import commands from '../../commands.js'; +import { optionsUtils } from '../../../../utils/optionsUtils.js'; interface CommandArgs { options: Options; @@ -37,7 +38,7 @@ class TenantPeopleProfileCardPropertySetCommand extends GraphCommand { #initTelemetry(): void { this.telemetry.push((args: CommandArgs) => { // Add unknown options to telemetry - const unknownOptions = Object.keys(this.getUnknownOptions(args.options)); + const unknownOptions = Object.keys(optionsUtils.getUnknownOptions(args.options, this.options)); const unknownOptionsObj = unknownOptions.reduce((obj, key) => ({ ...obj, [key]: true }), {}); Object.assign(this.telemetryProperties, { @@ -67,7 +68,7 @@ class TenantPeopleProfileCardPropertySetCommand extends GraphCommand { } // Unknown options are allowed only if they start with 'displayName-' - const unknownOptionKeys = Object.keys(this.getUnknownOptions(args.options)); + const unknownOptionKeys = Object.keys(optionsUtils.getUnknownOptions(args.options, this.options)); const invalidOptionKey = unknownOptionKeys.find(o => !o.startsWith('displayName-')); if (invalidOptionKey) { return `Invalid option: '${invalidOptionKey}'`; @@ -139,7 +140,7 @@ class TenantPeopleProfileCardPropertySetCommand extends GraphCommand { * @example Transform "--displayName-en-US 'Cost center'" to { languageTag: 'en-US', displayName: 'Cost center' } */ private getLocalizations(options: Options): Localization[] { - const unknownOptions = this.getUnknownOptions(options); + const unknownOptions = optionsUtils.getUnknownOptions(options, this.options); const result = Object.keys(unknownOptions).map(o => ({ languageTag: o.substring(o.indexOf('-') + 1), diff --git a/src/utils/entraApp.ts b/src/utils/entraApp.ts index a61c9032353..532651b22ae 100644 --- a/src/utils/entraApp.ts +++ b/src/utils/entraApp.ts @@ -3,6 +3,8 @@ import fs from 'fs'; import { Logger } from '../cli/Logger.js'; import request, { CliRequestOptions } from '../request.js'; import { odata } from './odata.js'; +import { optionsUtils } from './optionsUtils.js'; +import { CommandOption } from '../Command.js'; export interface AppInfo { appId: string; @@ -44,8 +46,6 @@ export interface AppPermissions { scope: string[]; } -type addUnknownOptions = (payload: any, options: any) => void; - async function getCertificateBase64Encoded({ options, logger, debug }: { options: AppCreationOptions, logger: Logger, @@ -216,13 +216,13 @@ function updateAppPermissions({ spId, resourceAccessPermission, oAuth2Permission export const entraApp = { appPermissions: [] as AppPermissions[], - createAppRegistration: async ({ options, apis, logger, verbose, debug, addUnknownOptions }: { + createAppRegistration: async ({ options, apis, logger, verbose, debug, defaultOptions }: { options: AppCreationOptions, + defaultOptions: CommandOption[], apis: RequiredResourceAccess[], logger: Logger, verbose: boolean, - debug: boolean, - addUnknownOptions: addUnknownOptions + debug: boolean }): Promise => { const applicationInfo: any = { displayName: options.name, @@ -266,7 +266,7 @@ export const entraApp = { applicationInfo.isFallbackPublicClient = true; } - addUnknownOptions(applicationInfo, options); + optionsUtils.addUnknownOptionsToPayload(applicationInfo, options, defaultOptions); if (verbose) { await logger.logToStderr(`Creating Microsoft Entra app registration...`); diff --git a/src/utils/optionsUtils.ts b/src/utils/optionsUtils.ts new file mode 100644 index 00000000000..1a0c6ecc27a --- /dev/null +++ b/src/utils/optionsUtils.ts @@ -0,0 +1,35 @@ +import { CommandOption } from "../Command"; + +export const optionsUtils = { + getUnknownOptions(options: any, defaultOptions: any): any { + const unknownOptions: any = JSON.parse(JSON.stringify(options)); + // remove minimist catch-all option + delete unknownOptions._; + + const knownOptions: CommandOption[] = defaultOptions; + const longOptionRegex: RegExp = /--([^\s]+)/; + const shortOptionRegex: RegExp = /-([a-z])\b/; + knownOptions.forEach(o => { + const longOptionName: string = (longOptionRegex.exec(o.option) as RegExpExecArray)[1]; + delete unknownOptions[longOptionName]; + + // short names are optional so we need to check if the current command has + // one before continuing + const shortOptionMatch: RegExpExecArray | null = shortOptionRegex.exec(o.option); + if (shortOptionMatch) { + const shortOptionName: string = shortOptionMatch[1]; + delete unknownOptions[shortOptionName]; + } + }); + + return unknownOptions; + }, + + addUnknownOptionsToPayload(payload: any, options: any, defaultOptions: any): void { + const unknownOptions: any = this.getUnknownOptions(options, defaultOptions); + const unknownOptionsNames: string[] = Object.getOwnPropertyNames(unknownOptions); + unknownOptionsNames.forEach(o => { + payload[o] = unknownOptions[o]; + }); + } +}; \ No newline at end of file From 64845e2813b05f054bdc2e6a60ed39651932df21 Mon Sep 17 00:00:00 2001 From: Martin Machacek Date: Sun, 2 Feb 2025 18:36:38 +0100 Subject: [PATCH 4/7] Enhancement: Allow unknown options for entra group/app/administrativeunit commands --- src/utils/optionsUtils.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/utils/optionsUtils.ts b/src/utils/optionsUtils.ts index 1a0c6ecc27a..442e6c7f54d 100644 --- a/src/utils/optionsUtils.ts +++ b/src/utils/optionsUtils.ts @@ -1,14 +1,14 @@ import { CommandOption } from "../Command"; +const longOptionRegex: RegExp = /--([^\s]+)/; +const shortOptionRegex: RegExp = /-([a-z])\b/; + export const optionsUtils = { - getUnknownOptions(options: any, defaultOptions: any): any { + getUnknownOptions(options: any, knownOptions: CommandOption[]): any { const unknownOptions: any = JSON.parse(JSON.stringify(options)); // remove minimist catch-all option delete unknownOptions._; - const knownOptions: CommandOption[] = defaultOptions; - const longOptionRegex: RegExp = /--([^\s]+)/; - const shortOptionRegex: RegExp = /-([a-z])\b/; knownOptions.forEach(o => { const longOptionName: string = (longOptionRegex.exec(o.option) as RegExpExecArray)[1]; delete unknownOptions[longOptionName]; @@ -25,8 +25,8 @@ export const optionsUtils = { return unknownOptions; }, - addUnknownOptionsToPayload(payload: any, options: any, defaultOptions: any): void { - const unknownOptions: any = this.getUnknownOptions(options, defaultOptions); + addUnknownOptionsToPayload(payload: any, options: any, knownOptions: CommandOption[]): void { + const unknownOptions: any = this.getUnknownOptions(options, knownOptions); const unknownOptionsNames: string[] = Object.getOwnPropertyNames(unknownOptions); unknownOptionsNames.forEach(o => { payload[o] = unknownOptions[o]; From 5997c494531a4b39c4b92dbfb293415d1ccc1d40 Mon Sep 17 00:00:00 2001 From: Martin Machacek Date: Sun, 2 Feb 2025 18:50:45 +0100 Subject: [PATCH 5/7] Enhancement: Allow unknown options for entra group/app/administrativeunit commands --- .eslintignore | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 .eslintignore diff --git a/.eslintignore b/.eslintignore new file mode 100644 index 00000000000..a164dd3be90 --- /dev/null +++ b/.eslintignore @@ -0,0 +1,2 @@ +/dist/* +/node_modules/* \ No newline at end of file From 25c4cbff770bc2127c868df6765c98f6b9bae3a7 Mon Sep 17 00:00:00 2001 From: Martin Machacek Date: Sun, 2 Feb 2025 19:03:24 +0100 Subject: [PATCH 6/7] Enhancement: Allow unknown options for entra group/app/administrativeunit commands --- .eslintrc.cjs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.eslintrc.cjs b/.eslintrc.cjs index 37b4798324a..fe125875455 100644 --- a/.eslintrc.cjs +++ b/.eslintrc.cjs @@ -161,8 +161,8 @@ module.exports = { "**/package-generate/assets/**", "**/test-projects/**", "clientsidepages.ts", - "*.d.ts", - "*.js", + "**/*.d.ts", + "**/*.js", "*.cjs" ], "rules": { From e6297b4e8c286b518e124e01a499c5b4b770b46f Mon Sep 17 00:00:00 2001 From: Martin Machacek Date: Sun, 2 Feb 2025 19:12:50 +0100 Subject: [PATCH 7/7] Enhancement: Allow unknown options for entra group/app/administrativeunit commands --- .eslintrc.cjs | 4 ++-- src/utils/optionsUtils.ts | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.eslintrc.cjs b/.eslintrc.cjs index fe125875455..37b4798324a 100644 --- a/.eslintrc.cjs +++ b/.eslintrc.cjs @@ -161,8 +161,8 @@ module.exports = { "**/package-generate/assets/**", "**/test-projects/**", "clientsidepages.ts", - "**/*.d.ts", - "**/*.js", + "*.d.ts", + "*.js", "*.cjs" ], "rules": { diff --git a/src/utils/optionsUtils.ts b/src/utils/optionsUtils.ts index 442e6c7f54d..35a15ff52e0 100644 --- a/src/utils/optionsUtils.ts +++ b/src/utils/optionsUtils.ts @@ -1,4 +1,4 @@ -import { CommandOption } from "../Command"; +import { CommandOption } from '../Command.js'; const longOptionRegex: RegExp = /--([^\s]+)/; const shortOptionRegex: RegExp = /-([a-z])\b/;