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

Enhancement: Allow unknown options for entra group/app/administrativeunit commands. Closes #6314 #6543

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.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .eslintrc.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -213,7 +213,8 @@ module.exports = {
"Query.*",
"app_displayname",
"access_token",
"expires_on"
"expires_on",
"extension_*"
]
}
],
Expand Down
2 changes: 1 addition & 1 deletion src/Command.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 => {
Expand Down
3 changes: 2 additions & 1 deletion src/m365/commands/setup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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') {
Expand Down Expand Up @@ -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';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,10 @@ class EntraAdministrativeUnitAddCommand extends GraphCommand {
return 'Creates an administrative unit';
}

public allowUnknownOptions(): boolean | undefined {
return true;
}

constructor() {
super();

Expand Down Expand Up @@ -54,17 +58,21 @@ class EntraAdministrativeUnitAddCommand extends GraphCommand {
}

public async commandAction(logger: Logger, args: CommandArgs): Promise<void> {
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 {
Expand Down
95 changes: 95 additions & 0 deletions src/m365/entra/commands/app/app-add.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand Down Expand Up @@ -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');
Expand Down
7 changes: 6 additions & 1 deletion src/m365/entra/commands/app/app-add.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,10 @@ class EntraAppAddCommand extends GraphCommand {
return 'Creates new Entra app registration';
}

public allowUnknownOptions(): boolean | undefined {
return true;
}

constructor() {
super();

Expand Down Expand Up @@ -224,7 +228,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
Expand Down
54 changes: 53 additions & 1 deletion src/m365/entra/commands/app/app-set.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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`) {
Expand Down Expand Up @@ -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`) {
Expand Down Expand Up @@ -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);
});

Expand Down
23 changes: 23 additions & 0 deletions src/m365/entra/commands/app/app-set.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,10 @@ class EntraAppSetCommand extends GraphCommand {
return 'Updates Entra app registration';
}

public allowUnknownOptions(): boolean | undefined {
return true;
}

constructor() {
super();

Expand Down Expand Up @@ -127,6 +131,7 @@ class EntraAppSetCommand extends GraphCommand {
public async commandAction(logger: Logger, args: CommandArgs): Promise<void> {
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);
Expand Down Expand Up @@ -176,6 +181,24 @@ class EntraAppSetCommand extends GraphCommand {
return result.id;
}

private async updateUknownOptions(args: CommandArgs, objectId: string): Promise<string> {
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<string> {
if (args.options.allowPublicClientFlows === undefined) {
return objectId;
Expand Down
Loading
Loading