Fix tenant Id design for Azure accounts (#21086)

This commit is contained in:
Cheena Malhotra
2022-11-03 11:04:37 -07:00
committed by GitHub
parent 90d0310523
commit 85fad2360c
9 changed files with 64 additions and 31 deletions

View File

@@ -133,12 +133,6 @@ export abstract class AzureAuth implements vscode.Disposable {
} }
} }
private getHomeTenant(account: AzureAccount): Tenant {
// Home is defined by the API
// Lets pick the home tenant - and fall back to commonTenant if they don't exist
return account.properties.tenants.find(t => t.tenantCategory === 'Home') ?? account.properties.tenants[0] ?? this.commonTenant;
}
public async refreshAccess(account: AzureAccount): Promise<AzureAccount> { public async refreshAccess(account: AzureAccount): Promise<AzureAccount> {
// Deprecated account - delete it. // Deprecated account - delete it.
if (account.key.accountVersion !== AzureAuth.ACCOUNT_VERSION) { if (account.key.accountVersion !== AzureAuth.ACCOUNT_VERSION) {
@@ -146,7 +140,10 @@ export abstract class AzureAuth implements vscode.Disposable {
return account; return account;
} }
try { try {
const tenant = this.getHomeTenant(account); // There can be multiple home tenants
// We want to return the one that owns the Azure account.
// Not doing so can result in token being issued for the wrong tenant
const tenant = account.properties.owningTenant;
const tokenResult = await this.getAccountSecurityToken(account, tenant.id, azdata.AzureResource.MicrosoftResourceManagement); const tokenResult = await this.getAccountSecurityToken(account, tenant.id, azdata.AzureResource.MicrosoftResourceManagement);
if (!tokenResult) { if (!tokenResult) {
account.isStale = true; account.isStale = true;
@@ -186,7 +183,14 @@ export abstract class AzureAuth implements vscode.Disposable {
return undefined; return undefined;
} }
const tenant = account.properties.tenants.find(t => t.id === tenantId); if (!account.properties.owningTenant) {
// Should never happen
throw new AzureAuthError(localize('azure.owningTenantNotFound', "Owning Tenant information not found for account."), 'Owning tenant not found.', undefined);
}
const tenant = account.properties.owningTenant?.id === tenantId
? account.properties.owningTenant
: account.properties.tenants.find(t => t.id === tenantId);
if (!tenant) { if (!tenant) {
throw new AzureAuthError(localize('azure.tenantNotFound', "Specified tenant with ID '{0}' not found.", tenantId), `Tenant ${tenantId} not found.`, undefined); throw new AzureAuthError(localize('azure.tenantNotFound', "Specified tenant with ID '{0}' not found.", tenantId), `Tenant ${tenantId} not found.`, undefined);
@@ -557,6 +561,10 @@ export abstract class AzureAuth implements vscode.Disposable {
const name = tokenClaims.name ?? tokenClaims.email ?? tokenClaims.unique_name; const name = tokenClaims.name ?? tokenClaims.email ?? tokenClaims.unique_name;
const email = tokenClaims.email ?? tokenClaims.unique_name; const email = tokenClaims.email ?? tokenClaims.unique_name;
// Read more about tid > https://learn.microsoft.com/azure/active-directory/develop/id-tokens
const owningTenant = tenants.find(t => t.id === tokenClaims.tid)
?? { 'id': tokenClaims.tid, 'displayName': 'Microsoft Account' };
let displayName = name; let displayName = name;
if (email) { if (email) {
displayName = `${displayName} - ${email}`; displayName = `${displayName} - ${email}`;
@@ -596,6 +604,7 @@ export abstract class AzureAuth implements vscode.Disposable {
properties: { properties: {
providerSettings: this.metadata, providerSettings: this.metadata,
isMsAccount: accountIssuer === 'msft', isMsAccount: accountIssuer === 'msft',
owningTenant: owningTenant,
tenants, tenants,
azureAuthType: this.authType azureAuthType: this.authType
}, },

View File

@@ -38,11 +38,17 @@ declare module 'azurecore' {
azureAuthType?: AzureAuthType azureAuthType?: AzureAuthType
providerSettings: AzureAccountProviderMetadata; providerSettings: AzureAccountProviderMetadata;
/** /**
* Whether or not the account is a Microsoft account * Whether or not the account is a Microsoft account
*/ */
isMsAccount: boolean; isMsAccount: boolean;
/**
* Represents the tenant that the user would be signing in to. For work and school accounts, the GUID is the immutable tenant ID of the organization that the user is signing in to.
* For sign-ins to the personal Microsoft account tenant (services like Xbox, Teams for Life, or Outlook), the value is 9188040d-6c67-4c5b-b112-36a304b66dad.
*/
owningTenant: Tenant;
/** /**
* A list of tenants (aka directories) that the account belongs to * A list of tenants (aka directories) that the account belongs to
*/ */
@@ -356,10 +362,10 @@ declare module 'azurecore' {
} }
export interface IAzureResourceTreeDataProvider { export interface IAzureResourceTreeDataProvider {
/** /**
* Gets the root tree item nodes for this provider - these will be used as * Gets the root tree item nodes for this provider - these will be used as
* direct children of the Account node in the Azure tree view. * direct children of the Account node in the Azure tree view.
*/ */
getRootChildren(): Promise<azdata.TreeItem[]>; getRootChildren(): Promise<azdata.TreeItem[]>;
/** /**
* Gets the children for a given {@link IAzureResourceNode} * Gets the children for a given {@link IAzureResourceNode}

View File

@@ -53,6 +53,7 @@ describe('Azure Authentication', function () {
mockAccount = { mockAccount = {
isStale: false, isStale: false,
properties: { properties: {
owningTenant: mockTenant,
tenants: [mockTenant] tenants: [mockTenant]
} }
} as AzureAccount; } as AzureAccount;

View File

@@ -35,6 +35,10 @@ const mockAccount: AzureAccount = {
properties: { properties: {
providerSettings: settings[0].metadata, providerSettings: settings[0].metadata,
isMsAccount: true, isMsAccount: true,
owningTenant: {
id: 'tenantId',
displayName: 'tenantDisplayName',
},
tenants: [] tenants: []
}, },
isStale: false isStale: false

View File

@@ -35,6 +35,10 @@ const mockAccount: AzureAccount = {
properties: { properties: {
providerSettings: settings[0].metadata, providerSettings: settings[0].metadata,
isMsAccount: true, isMsAccount: true,
owningTenant: {
id: 'tenantId',
displayName: 'tenantDisplayName',
},
tenants: [] tenants: []
}, },
isStale: false isStale: false

View File

@@ -28,13 +28,17 @@ const mockAccount: AzureAccount = {
properties: { properties: {
providerSettings: settings[0].metadata, providerSettings: settings[0].metadata,
isMsAccount: true, isMsAccount: true,
owningTenant: {
id: 'tenantId',
displayName: 'tenantDisplayName',
},
tenants: [] tenants: []
}, },
isStale: false isStale: false
}; };
const mockTenantId: string = 'mock_tenant'; const mockTenantId: string = 'mock_tenant';
const mockSubscriptionId ='mock_subscription'; const mockSubscriptionId = 'mock_subscription';
const mockSubscription: azureResource.AzureResourceSubscription = { const mockSubscription: azureResource.AzureResourceSubscription = {
id: mockSubscriptionId, id: mockSubscriptionId,
@@ -50,7 +54,7 @@ let mockResourceProvider2: TypeMoq.IMock<azureResource.IAzureResourceProvider>;
let resourceService: AzureResourceService; let resourceService: AzureResourceService;
describe('AzureResourceService.listResourceProviderIds', function(): void { describe('AzureResourceService.listResourceProviderIds', function (): void {
beforeEach(() => { beforeEach(() => {
mockResourceTreeDataProvider1 = TypeMoq.Mock.ofType<azureResource.IAzureResourceTreeDataProvider>(); mockResourceTreeDataProvider1 = TypeMoq.Mock.ofType<azureResource.IAzureResourceTreeDataProvider>();
mockResourceTreeDataProvider1.setup((o) => o.getRootChildren()).returns(() => Promise.resolve([TypeMoq.Mock.ofType<azdata.TreeItem>().object])); mockResourceTreeDataProvider1.setup((o) => o.getRootChildren()).returns(() => Promise.resolve([TypeMoq.Mock.ofType<azdata.TreeItem>().object]));
@@ -71,7 +75,7 @@ describe('AzureResourceService.listResourceProviderIds', function(): void {
resourceService.areResourceProvidersLoaded = true; resourceService.areResourceProvidersLoaded = true;
}); });
it('Should be correct when registering providers.', async function(): Promise<void> { it('Should be correct when registering providers.', async function (): Promise<void> {
resourceService.registerResourceProvider(mockResourceProvider1.object); resourceService.registerResourceProvider(mockResourceProvider1.object);
let providerIds = await resourceService.listResourceProviderIds(); let providerIds = await resourceService.listResourceProviderIds();
should(providerIds).Array(); should(providerIds).Array();
@@ -87,7 +91,7 @@ describe('AzureResourceService.listResourceProviderIds', function(): void {
}); });
}); });
describe('AzureResourceService.getRootChildren', function(): void { describe('AzureResourceService.getRootChildren', function (): void {
beforeEach(() => { beforeEach(() => {
mockResourceTreeDataProvider1 = TypeMoq.Mock.ofType<azureResource.IAzureResourceTreeDataProvider>(); mockResourceTreeDataProvider1 = TypeMoq.Mock.ofType<azureResource.IAzureResourceTreeDataProvider>();
mockResourceTreeDataProvider1.setup((o) => o.getRootChildren()).returns(() => Promise.resolve([TypeMoq.Mock.ofType<azdata.TreeItem>().object])); mockResourceTreeDataProvider1.setup((o) => o.getRootChildren()).returns(() => Promise.resolve([TypeMoq.Mock.ofType<azdata.TreeItem>().object]));
@@ -101,13 +105,13 @@ describe('AzureResourceService.getRootChildren', function(): void {
resourceService.areResourceProvidersLoaded = true; resourceService.areResourceProvidersLoaded = true;
}); });
it('Should be correct when provider id is correct.', async function(): Promise<void> { it('Should be correct when provider id is correct.', async function (): Promise<void> {
const children = await resourceService.getRootChildren(mockResourceProvider1.object.providerId, mockAccount, mockSubscription, mockTenantId); const children = await resourceService.getRootChildren(mockResourceProvider1.object.providerId, mockAccount, mockSubscription, mockTenantId);
should(children).Array(); should(children).Array();
}); });
it('Should throw exceptions when provider id is incorrect.', async function(): Promise<void> { it('Should throw exceptions when provider id is incorrect.', async function (): Promise<void> {
const providerId = 'non_existent_provider_id'; const providerId = 'non_existent_provider_id';
try { try {
await resourceService.getRootChildren(providerId, mockAccount, mockSubscription, mockTenantId); await resourceService.getRootChildren(providerId, mockAccount, mockSubscription, mockTenantId);
@@ -120,7 +124,7 @@ describe('AzureResourceService.getRootChildren', function(): void {
}); });
}); });
describe('AzureResourceService.getChildren', function(): void { describe('AzureResourceService.getChildren', function (): void {
beforeEach(() => { beforeEach(() => {
mockResourceTreeDataProvider1 = TypeMoq.Mock.ofType<azureResource.IAzureResourceTreeDataProvider>(); mockResourceTreeDataProvider1 = TypeMoq.Mock.ofType<azureResource.IAzureResourceTreeDataProvider>();
mockResourceTreeDataProvider1.setup((o) => o.getRootChildren()).returns(() => Promise.resolve([TypeMoq.Mock.ofType<azdata.TreeItem>().object])); mockResourceTreeDataProvider1.setup((o) => o.getRootChildren()).returns(() => Promise.resolve([TypeMoq.Mock.ofType<azdata.TreeItem>().object]));
@@ -135,12 +139,12 @@ describe('AzureResourceService.getChildren', function(): void {
resourceService.areResourceProvidersLoaded = true; resourceService.areResourceProvidersLoaded = true;
}); });
it('Should be correct when provider id is correct.', async function(): Promise<void> { it('Should be correct when provider id is correct.', async function (): Promise<void> {
const children = await resourceService.getChildren(mockResourceProvider1.object.providerId, TypeMoq.It.isAny()); const children = await resourceService.getChildren(mockResourceProvider1.object.providerId, TypeMoq.It.isAny());
should(children).Array(); should(children).Array();
}); });
it('Should throw exceptions when provider id is incorrect.', async function(): Promise<void> { it('Should throw exceptions when provider id is incorrect.', async function (): Promise<void> {
const providerId = 'non_existent_provider_id'; const providerId = 'non_existent_provider_id';
try { try {
await resourceService.getRootChildren(providerId, mockAccount, mockSubscription, mockTenantId); await resourceService.getRootChildren(providerId, mockAccount, mockSubscription, mockTenantId);
@@ -153,7 +157,7 @@ describe('AzureResourceService.getChildren', function(): void {
}); });
}); });
describe('AzureResourceService.getTreeItem', function(): void { describe('AzureResourceService.getTreeItem', function (): void {
beforeEach(() => { beforeEach(() => {
mockResourceTreeDataProvider1 = TypeMoq.Mock.ofType<azureResource.IAzureResourceTreeDataProvider>(); mockResourceTreeDataProvider1 = TypeMoq.Mock.ofType<azureResource.IAzureResourceTreeDataProvider>();
mockResourceTreeDataProvider1.setup((o) => o.getRootChildren()).returns(() => Promise.resolve([TypeMoq.Mock.ofType<azdata.TreeItem>().object])); mockResourceTreeDataProvider1.setup((o) => o.getRootChildren()).returns(() => Promise.resolve([TypeMoq.Mock.ofType<azdata.TreeItem>().object]));
@@ -168,12 +172,12 @@ describe('AzureResourceService.getTreeItem', function(): void {
resourceService.areResourceProvidersLoaded = true; resourceService.areResourceProvidersLoaded = true;
}); });
it('Should be correct when provider id is correct.', async function(): Promise<void> { it('Should be correct when provider id is correct.', async function (): Promise<void> {
const treeItem = await resourceService.getTreeItem(mockResourceProvider1.object.providerId, TypeMoq.It.isAny()); const treeItem = await resourceService.getTreeItem(mockResourceProvider1.object.providerId, TypeMoq.It.isAny());
should(treeItem).Object(); should(treeItem).Object();
}); });
it('Should throw exceptions when provider id is incorrect.', async function(): Promise<void> { it('Should throw exceptions when provider id is incorrect.', async function (): Promise<void> {
const providerId = 'non_existent_provider_id'; const providerId = 'non_existent_provider_id';
try { try {
await resourceService.getRootChildren(providerId, mockAccount, mockSubscription, mockTenantId); await resourceService.getRootChildren(providerId, mockAccount, mockSubscription, mockTenantId);

View File

@@ -31,6 +31,10 @@ const mockAccount: AzureAccount = {
properties: { properties: {
providerSettings: settings[0].metadata, providerSettings: settings[0].metadata,
isMsAccount: true, isMsAccount: true,
owningTenant: {
id: 'tenantId',
displayName: 'tenantDisplayName',
},
tenants: [] tenants: []
}, },
isStale: false isStale: false

View File

@@ -35,7 +35,10 @@ let mockTreeChangeHandler: TypeMoq.IMock<IAzureResourceTreeChangeHandler>;
// Mock test data // Mock test data
const mockTenantId = 'mock_tenant_id'; const mockTenantId = 'mock_tenant_id';
const mockTenant = {
id: mockTenantId,
displayName: 'Mock Tenant'
};
const mockAccount: AzureAccount = { const mockAccount: AzureAccount = {
key: { key: {
accountId: '97915f6d-84fa-4926-b60c-38db64327ad7', accountId: '97915f6d-84fa-4926-b60c-38db64327ad7',
@@ -50,11 +53,9 @@ const mockAccount: AzureAccount = {
}, },
properties: { properties: {
tenants: [ tenants: [
{ mockTenant
id: mockTenantId,
displayName: 'Mock Tenant'
}
], ],
owningTenant: mockTenant,
providerSettings: { providerSettings: {
settings: allSettings[0].metadata.settings, settings: allSettings[0].metadata.settings,
id: 'azure', id: 'azure',
@@ -360,8 +361,7 @@ describe('AzureResourceAccountTreeNode.clearCache', function (): void {
sinon.stub(azdata.accounts, 'getAccountSecurityToken').returns(Promise.resolve(mockToken)); sinon.stub(azdata.accounts, 'getAccountSecurityToken').returns(Promise.resolve(mockToken));
mockCacheService.setup((o) => o.generateKey(TypeMoq.It.isAnyString())).returns(() => generateGuid()); mockCacheService.setup((o) => o.generateKey(TypeMoq.It.isAnyString())).returns(() => generateGuid());
mockCacheService.setup((o) => o.get(TypeMoq.It.isAnyString())).returns(() => mockSubscriptionCache); mockCacheService.setup((o) => o.get(TypeMoq.It.isAnyString())).returns(() => mockSubscriptionCache);
mockCacheService.setup((o) => o.update(TypeMoq.It.isAnyString(), TypeMoq.It.isAny())).returns(() => mockCacheService.setup((o) => o.update(TypeMoq.It.isAnyString(), TypeMoq.It.isAny())).returns(() => {
{
mockSubscriptionCache = mockSubscriptions; mockSubscriptionCache = mockSubscriptions;
return Promise.resolve(); return Promise.resolve();
}); });

View File

@@ -3,6 +3,7 @@
"compilerOptions": { "compilerOptions": {
"outDir": "./out", "outDir": "./out",
"noUnusedParameters": false, "noUnusedParameters": false,
"sourceMap": true,
"typeRoots": [ "typeRoots": [
"./node_modules/@types" "./node_modules/@types"
], ],