Skip to content

Commit

Permalink
Updates 'aad sp get' to use MS Graph. Closes #2754
Browse files Browse the repository at this point in the history
  • Loading branch information
nanddeepn authored and waldekmastykarz committed Nov 11, 2021
1 parent 215efcb commit bd650bd
Show file tree
Hide file tree
Showing 3 changed files with 171 additions and 78 deletions.
3 changes: 2 additions & 1 deletion docs/docs/cmd/aad/sp/sp-get.md
Expand Up @@ -47,4 +47,5 @@ m365 aad sp get --objectId b2307a39-e878-458b-bc90-03bc578531dd

## More information

- Application and service principal objects in Azure Active Directory (Azure AD): [https://docs.microsoft.com/en-us/azure/active-directory/develop/active-directory-application-objects](https://docs.microsoft.com/en-us/azure/active-directory/develop/active-directory-application-objects)
- Application and service principal objects in Azure Active Directory (Azure AD): [https://docs.microsoft.com/en-us/azure/active-directory/develop/active-directory-application-objects](https://docs.microsoft.com/en-us/azure/active-directory/develop/active-directory-application-objects)
- Get servicePrincipal: [https://docs.microsoft.com/en-us/graph/api/serviceprincipal-get?view=graph-rest-1.0](https://docs.microsoft.com/en-us/graph/api/serviceprincipal-get?view=graph-rest-1.0)
176 changes: 120 additions & 56 deletions src/m365/aad/commands/sp/sp-get.spec.ts
Expand Up @@ -14,9 +14,25 @@ describe(commands.SP_GET, () => {
let logger: Logger;
let loggerLogSpy: sinon.SinonSpy;

const spAppInfo = {
"value": [
{
"id": "59e617e5-e447-4adc-8b88-00af644d7c92",
"appId": "65415bb1-9267-4313-bbf5-ae259732ee12",
"displayName": "foo",
"createdDateTime": "2021-03-07T15:04:11Z",
"description": null,
"homepage": null,
"loginUrl": null,
"logoutUrl": null,
"notes": null
}
]
};

before(() => {
sinon.stub(auth, 'restoreAuth').callsFake(() => Promise.resolve());
sinon.stub(appInsights, 'trackEvent').callsFake(() => {});
sinon.stub(appInsights, 'trackEvent').callsFake(() => { });
auth.service.connected = true;
});

Expand Down Expand Up @@ -58,50 +74,22 @@ describe(commands.SP_GET, () => {
assert.notStrictEqual(command.description, null);
});

it('retrieves information about the specified service principal using its display name (debug)', (done) => {
const sp: any = { "objectType": "ServicePrincipal", "objectId": "d03a0062-1aa6-43e1-8f49-d73e969c5812", "deletionTimestamp": null, "accountEnabled": true, "addIns": [], "alternativeNames": [], "appDisplayName": "SharePoint Online Client", "appId": "57fb890c-0dab-4253-a5e0-7188c88b2bb4", "appOwnerTenantId": null, "appRoleAssignmentRequired": false, "appRoles": [], "displayName": "SharePoint Online Client", "errorUrl": null, "homepage": null, "keyCredentials": [], "logoutUrl": null, "oauth2Permissions": [], "passwordCredentials": [], "preferredTokenSigningKeyThumbprint": null, "publisherName": null, "replyUrls": [], "samlMetadataUrl": null, "servicePrincipalNames": ["57fb890c-0dab-4253-a5e0-7188c88b2bb4"], "servicePrincipalType": "Application", "tags": [], "tokenEncryptionKeyId": null };

it('retrieves information about the specified service principal using its display name', (done) => {
sinon.stub(request, 'get').callsFake((opts) => {
if ((opts.url as string).indexOf(`/myorganization/servicePrincipals?api-version=1.6&$filter=displayName eq 'SharePoint%20Online%20Client'`) > -1) {
if (opts.headers &&
opts.headers.accept &&
opts.headers.accept.indexOf('application/json') === 0) {
return Promise.resolve({ value: [sp] });
}
if ((opts.url as string).indexOf(`/v1.0/servicePrincipals?$filter=displayName eq `) > -1) {
return Promise.resolve(spAppInfo);
}

return Promise.reject('Invalid request');
});

command.action(logger, { options: { debug: true, displayName: 'SharePoint Online Client' } }, () => {
try {
assert(loggerLogSpy.calledWith(sp));
done();
}
catch (e) {
done(e);
}
});
});

it('retrieves information about the specified service principal using its display name', (done) => {
const sp: any = { "objectType": "ServicePrincipal", "objectId": "d03a0062-1aa6-43e1-8f49-d73e969c5812", "deletionTimestamp": null, "accountEnabled": true, "addIns": [], "alternativeNames": [], "appDisplayName": "SharePoint Online Client", "appId": "57fb890c-0dab-4253-a5e0-7188c88b2bb4", "appOwnerTenantId": null, "appRoleAssignmentRequired": false, "appRoles": [], "displayName": "SharePoint Online Client", "errorUrl": null, "homepage": null, "keyCredentials": [], "logoutUrl": null, "oauth2Permissions": [], "passwordCredentials": [], "preferredTokenSigningKeyThumbprint": null, "publisherName": null, "replyUrls": [], "samlMetadataUrl": null, "servicePrincipalNames": ["57fb890c-0dab-4253-a5e0-7188c88b2bb4"], "servicePrincipalType": "Application", "tags": [], "tokenEncryptionKeyId": null };

sinon.stub(request, 'get').callsFake((opts) => {
if ((opts.url as string).indexOf(`/myorganization/servicePrincipals?api-version=1.6&$filter=displayName eq 'SharePoint%20Online%20Client'`) > -1) {
if (opts.headers &&
opts.headers.accept &&
opts.headers.accept.indexOf('application/json') === 0) {
return Promise.resolve({ value: [sp] });
}
if ((opts.url as string).indexOf(`/v1.0/servicePrincipals`) > -1) {
return Promise.resolve(spAppInfo);
}

return Promise.reject('Invalid request');
});

command.action(logger, { options: { debug: false, displayName: 'SharePoint Online Client' } }, () => {
command.action(logger, { options: { debug: true, displayName: 'foo' } }, () => {
try {
assert(loggerLogSpy.calledWith(sp));
assert(loggerLogSpy.calledWith(spAppInfo));
done();
}
catch (e) {
Expand All @@ -111,23 +99,21 @@ describe(commands.SP_GET, () => {
});

it('retrieves information about the specified service principal using its appId', (done) => {
const sp: any = { "objectType": "ServicePrincipal", "objectId": "d03a0062-1aa6-43e1-8f49-d73e969c5812", "deletionTimestamp": null, "accountEnabled": true, "addIns": [], "alternativeNames": [], "appDisplayName": "SharePoint Online Client", "appId": "57fb890c-0dab-4253-a5e0-7188c88b2bb4", "appOwnerTenantId": null, "appRoleAssignmentRequired": false, "appRoles": [], "displayName": "SharePoint Online Client", "errorUrl": null, "homepage": null, "keyCredentials": [], "logoutUrl": null, "oauth2Permissions": [], "passwordCredentials": [], "preferredTokenSigningKeyThumbprint": null, "publisherName": null, "replyUrls": [], "samlMetadataUrl": null, "servicePrincipalNames": ["57fb890c-0dab-4253-a5e0-7188c88b2bb4"], "servicePrincipalType": "Application", "tags": [], "tokenEncryptionKeyId": null };

sinon.stub(request, 'get').callsFake((opts) => {
if ((opts.url as string).indexOf(`/myorganization/servicePrincipals?api-version=1.6&$filter=appId eq '57fb890c-0dab-4253-a5e0-7188c88b2bb4'`) > -1) {
if (opts.headers &&
opts.headers.accept &&
opts.headers.accept.indexOf('application/json') === 0) {
return Promise.resolve({ value: [sp] });
}
if ((opts.url as string).indexOf(`/v1.0/servicePrincipals?$filter=appId eq `) > -1) {
return Promise.resolve(spAppInfo);
}

if ((opts.url as string).indexOf(`/v1.0/servicePrincipals`) > -1) {
return Promise.resolve(spAppInfo);
}

return Promise.reject('Invalid request');
});

command.action(logger, { options: { debug: false, appId: '57fb890c-0dab-4253-a5e0-7188c88b2bb4' } }, () => {
command.action(logger, { options: { debug: false, appId: '65415bb1-9267-4313-bbf5-ae259732ee12' } }, () => {
try {
assert(loggerLogSpy.calledWith(sp));
assert(loggerLogSpy.calledWith(spAppInfo));
done();
}
catch (e) {
Expand All @@ -137,23 +123,21 @@ describe(commands.SP_GET, () => {
});

it('retrieves information about the specified service principal using its objectId', (done) => {
const sp: any = { "objectType": "ServicePrincipal", "objectId": "d03a0062-1aa6-43e1-8f49-d73e969c5812", "deletionTimestamp": null, "accountEnabled": true, "addIns": [], "alternativeNames": [], "appDisplayName": "SharePoint Online Client", "appId": "57fb890c-0dab-4253-a5e0-7188c88b2bb4", "appOwnerTenantId": null, "appRoleAssignmentRequired": false, "appRoles": [], "displayName": "SharePoint Online Client", "errorUrl": null, "homepage": null, "keyCredentials": [], "logoutUrl": null, "oauth2Permissions": [], "passwordCredentials": [], "preferredTokenSigningKeyThumbprint": null, "publisherName": null, "replyUrls": [], "samlMetadataUrl": null, "servicePrincipalNames": ["57fb890c-0dab-4253-a5e0-7188c88b2bb4"], "servicePrincipalType": "Application", "tags": [], "tokenEncryptionKeyId": null };

sinon.stub(request, 'get').callsFake((opts) => {
if ((opts.url as string).indexOf(`/myorganization/servicePrincipals?api-version=1.6&$filter=objectId eq '57fb890c-0dab-4253-a5e0-7188c88b2bb4'`) > -1) {
if (opts.headers &&
opts.headers.accept &&
opts.headers.accept.indexOf('application/json') === 0) {
return Promise.resolve({ value: [sp] });
}
if ((opts.url as string).indexOf(`/v1.0/servicePrincipals?$filter=objectId eq `) > -1) {
return Promise.resolve(spAppInfo);
}

if ((opts.url as string).indexOf(`/v1.0/servicePrincipals`) > -1) {
return Promise.resolve(spAppInfo);
}

return Promise.reject('Invalid request');
});

command.action(logger, { options: { debug: false, objectId: '57fb890c-0dab-4253-a5e0-7188c88b2bb4' } }, () => {
command.action(logger, { options: { debug: false, objectId: '59e617e5-e447-4adc-8b88-00af644d7c92' } }, () => {
try {
assert(loggerLogSpy.calledWith(sp));
assert(loggerLogSpy.calledWith(spAppInfo));
done();
}
catch (e) {
Expand Down Expand Up @@ -211,6 +195,86 @@ describe(commands.SP_GET, () => {
});
});


it('fails when Azure AD app with same name exists', (done) => {
sinon.stub(request, 'get').callsFake((opts) => {
if ((opts.url as string).indexOf(`/v1.0/servicePrincipals?$filter=displayName eq `) > -1) {
return Promise.resolve({
"@odata.context": "https://graph.microsoft.com/v1.0/$metadata#servicePrincipals",
"value": [
{
"id": "be559819-b036-470f-858b-281c4e808403",
"appId": "ee091f63-9e48-4697-8462-7cfbf7410b8e",
"displayName": "foo",
"createdDateTime": "2021-03-07T15:04:11Z",
"description": null,
"homepage": null,
"loginUrl": null,
"logoutUrl": null,
"notes": null
},
{
"id": "93d75ef9-ba9b-4361-9a47-1f6f7478f05f",
"appId": "e9fd0957-049f-40d0-8d1d-112320fb1cbd",
"displayName": "foo",
"createdDateTime": "2021-03-07T15:04:11Z",
"description": null,
"homepage": null,
"loginUrl": null,
"logoutUrl": null,
"notes": null
}
]
});
}

return Promise.reject('Invalid request');
});

command.action(logger, {
options: {
debug: true,
displayName: 'foo'
}
}, (err?: any) => {
try {
assert.strictEqual(JSON.stringify(err), JSON.stringify(new CommandError(`Multiple Azure AD apps with name foo found: be559819-b036-470f-858b-281c4e808403,93d75ef9-ba9b-4361-9a47-1f6f7478f05f`)));
done();
}
catch (e) {
done(e);
}
});
});

it('fails when the specified Azure AD app does not exist', (done) => {
sinon.stub(request, 'get').callsFake((opts) => {
if ((opts.url as string).indexOf(`/v1.0/servicePrincipals?$filter=displayName eq `) > -1) {
return Promise.resolve({
"@odata.context": "https://graph.microsoft.com/v1.0/$metadata#servicePrincipals",
"value": []
});
}

return Promise.reject(`Invalid request`);
});

command.action(logger, {
options: {
debug: true,
displayName: 'Test App'
}
}, (err?: any) => {
try {
assert.strictEqual(JSON.stringify(err), JSON.stringify(new CommandError(`The specified Azure AD app does not exist`)));
done();
}
catch (e) {
done(e);
}
});
});

it('fails validation if neither the appId nor the displayName option specified', () => {
const actual = command.validate({ options: {} });
assert.notStrictEqual(actual, true);
Expand Down
70 changes: 49 additions & 21 deletions src/m365/aad/commands/sp/sp-get.ts
Expand Up @@ -5,7 +5,7 @@ import {
import GlobalOptions from '../../../../GlobalOptions';
import request from '../../../../request';
import Utils from '../../../../Utils';
import AadCommand from '../../../base/AadCommand';
import GraphCommand from '../../../base/GraphCommand';
import commands from '../../commands';

interface CommandArgs {
Expand All @@ -18,7 +18,7 @@ interface Options extends GlobalOptions {
objectId?: string;
}

class AadSpGetCommand extends AadCommand {
class AadSpGetCommand extends GraphCommand {
public get name(): string {
return commands.SP_GET;
}
Expand All @@ -35,39 +35,67 @@ class AadSpGetCommand extends AadCommand {
return telemetryProps;
}

public commandAction(logger: Logger, args: CommandArgs, cb: () => void): void {
if (this.verbose) {
logger.logToStderr(`Retrieving service principal information...`);
private getSpId(args: CommandArgs): Promise<string> {
if (args.options.objectId) {
return Promise.resolve(args.options.objectId);
}

let spMatchQuery: string = '';
if (args.options.appId) {
spMatchQuery = `appId eq '${encodeURIComponent(args.options.appId)}'`;
if (args.options.displayName) {
spMatchQuery = `displayName eq '${encodeURIComponent(args.options.displayName)}'`;
}
else if (args.options.objectId) {
spMatchQuery = `objectId eq '${encodeURIComponent(args.options.objectId)}'`;
}
else {
spMatchQuery = `displayName eq '${encodeURIComponent(args.options.displayName as string)}'`;
else if (args.options.appId) {
spMatchQuery = `appId eq '${encodeURIComponent(args.options.appId)}'`;
}

const requestOptions: any = {
url: `${this.resource}/myorganization/servicePrincipals?api-version=1.6&$filter=${spMatchQuery}`,
const idRequestOptions: any = {
url: `${this.resource}/v1.0/servicePrincipals?$filter=${spMatchQuery}`,
headers: {
accept: 'application/json;odata=nometadata'
accept: 'application/json;odata.metadata=none'
},
responseType: 'json'
};

request
.get<{ value: any[] }>(requestOptions)
.then((res: { value: any[] }): void => {
if (res.value && res.value.length > 0) {
logger.log(res.value[0]);
return request
.get<{ value: { id: string; }[] }>(idRequestOptions)
.then(response => {
const spItem: { id: string } | undefined = response.value[0];

if (!spItem) {
return Promise.reject(`The specified Azure AD app does not exist`);
}

if (response.value.length > 1) {
return Promise.reject(`Multiple Azure AD apps with name ${args.options.displayName} found: ${response.value.map(x => x.id)}`);
}

return Promise.resolve(spItem.id);
});
}

public commandAction(logger: Logger, args: CommandArgs, cb: () => void): void {
if (this.verbose) {
logger.logToStderr(`Retrieving service principal information...`);
}

this
.getSpId(args)
.then((id: string): Promise<void> => {
const requestOptions: any = {
url: `${this.resource}/v1.0/servicePrincipals/${id}`,
headers: {
accept: 'application/json;odata.metadata=none',
'content-type': 'application/json;odata.metadata=none'
},
responseType: 'json'
};

return request.get(requestOptions);
})
.then((res: any): void => {
logger.log(res);
cb();
}, (rawRes: any): void => this.handleRejectedODataJsonPromise(rawRes, logger, cb));
}, (err: any): void => this.handleRejectedODataJsonPromise(err, logger, cb));
}

public options(): CommandOption[] {
Expand Down

0 comments on commit bd650bd

Please sign in to comment.