From 14b7b9cb521d1010ceba6988b02ff5b0076010fe Mon Sep 17 00:00:00 2001 From: Riley Evans Date: Wed, 19 Jun 2024 13:49:24 -0700 Subject: [PATCH] feat(Designer): Tenant selection support for OAuth connection creation (#4999) * Added tenant service and support for the tenant parameter on oauth connections * Fixed small tenant value issue * Fixed tests, added test over legay multi-auth / oauth / tenant-id * Added tests for code coverage * Added test for designer options slice --- Localize/lang/strings.json | 2 + .../Services/HttpClient.ts | 11 +- .../Services/OAuthService.ts | 5 +- .../app/AzureLogicAppsDesigner/laDesigner.tsx | 8 + .../laDesignerConsumption.tsx | 11 + .../app/LocalDesigner/localDesigner.tsx | 8 + .../app/TemplatesStandaloneDesigner.tsx | 8 +- .../src/app/designer/servicesHelper.ts | 9 + .../__test__/designerOptionsSlice.spec.ts | 23 ++ .../state/connection/connectionSelector.ts | 16 +- .../designerOptionsInterfaces.ts | 2 + .../designerOptions/designerOptionsSlice.ts | 9 +- .../lib/core/state/templates/templateSlice.ts | 5 + .../templates/TemplatesDesignerContext.tsx | 2 + .../__test__/createConnection.spec.tsx | 323 +++++------------- .../__test__/mocks/connectionParameters.ts | 287 ++++++++++++++++ .../createConnection/createConnection.tsx | 76 +++-- .../createConnectionWrapper.tsx | 6 +- .../formInputs/tenantPicker.tsx | 68 ++++ .../src/designer-client-services/index.ts | 1 + .../lib/base/index.ts | 3 + .../lib/base/tenant.ts | 39 +++ .../lib/common/azure.ts | 15 +- .../lib/httpClient.ts | 1 + .../designer-client-services/lib/tenant.ts | 23 ++ .../lib/helpers/__test__/connections.spec.ts | 34 ++ .../src/utils/src/lib/helpers/connections.ts | 13 + 27 files changed, 728 insertions(+), 280 deletions(-) create mode 100644 libs/designer/src/lib/core/state/__test__/designerOptionsSlice.spec.ts create mode 100644 libs/designer/src/lib/ui/panel/connectionsPanel/createConnection/__test__/mocks/connectionParameters.ts create mode 100644 libs/designer/src/lib/ui/panel/connectionsPanel/createConnection/formInputs/tenantPicker.tsx create mode 100644 libs/logic-apps-shared/src/designer-client-services/lib/base/tenant.ts create mode 100644 libs/logic-apps-shared/src/designer-client-services/lib/tenant.ts create mode 100644 libs/logic-apps-shared/src/utils/src/lib/helpers/__test__/connections.spec.ts diff --git a/Localize/lang/strings.json b/Localize/lang/strings.json index 4814a3d1a1c..3532d920d4f 100644 --- a/Localize/lang/strings.json +++ b/Localize/lang/strings.json @@ -793,6 +793,7 @@ "UnrrzF": "Source schema", "UnytRl": "The type for ''{authenticationKey}'' is ''{propertyType}''.", "Ur+wph": "Click to delete item", + "UsEvG2": "Tenant ID", "UtyRCH": "Enter a name for the connection", "Uxckds": "Suggested flow", "V+/c21": "General", @@ -1729,6 +1730,7 @@ "_UnrrzF.comment": "Label to inform the below schema name is for source schema", "_UnytRl.comment": "Error message when having invalid authentication property types", "_Ur+wph.comment": "Label for delete button", + "_UsEvG2.comment": "tenant dropdown label", "_UtyRCH.comment": "Placeholder text for connection name input", "_Uxckds.comment": "Title for the suggested flow section", "_V+/c21.comment": "title for general setting section", diff --git a/apps/Standalone/src/designer/app/AzureLogicAppsDesigner/Services/HttpClient.ts b/apps/Standalone/src/designer/app/AzureLogicAppsDesigner/Services/HttpClient.ts index dc3ef30d6f2..65a1450c92b 100644 --- a/apps/Standalone/src/designer/app/AzureLogicAppsDesigner/Services/HttpClient.ts +++ b/apps/Standalone/src/designer/app/AzureLogicAppsDesigner/Services/HttpClient.ts @@ -14,11 +14,12 @@ export class HttpClient implements IHttpClient { async get(options: HttpRequestOptions): Promise { const isArmId = isArmResourceId(options.uri); const requestUrl = getRequestUrl(options); - const auth = isArmId - ? { - Authorization: `Bearer ${environment.armToken}`, - } - : {}; + const auth = + isArmId || options.includeAuth + ? { + Authorization: `Bearer ${environment.armToken}`, + } + : {}; const response = await axios.get(requestUrl, { headers: { diff --git a/apps/Standalone/src/designer/app/AzureLogicAppsDesigner/Services/OAuthService.ts b/apps/Standalone/src/designer/app/AzureLogicAppsDesigner/Services/OAuthService.ts index 38200e909c3..e1b84ea4e0b 100644 --- a/apps/Standalone/src/designer/app/AzureLogicAppsDesigner/Services/OAuthService.ts +++ b/apps/Standalone/src/designer/app/AzureLogicAppsDesigner/Services/OAuthService.ts @@ -83,6 +83,8 @@ export class StandaloneOAuthPopup implements IOAuthPopup { width: windowWidth, height: windowHeight, popup: true, + top: screen.height / 2 - 600 / 2, + left: screen.width / 2 - 600 / 2, }) .map(([key, value]) => `${key}=${value}`) .join(','); @@ -96,9 +98,6 @@ export class StandaloneOAuthPopup implements IOAuthPopup { throw new Error('The browser has blocked the popup window.'); } - // eslint-disable-next-line no-restricted-globals - this._popupWindow?.moveBy((screen.width - windowWidth) / 2, (screen.height - windowHeight) / 2); - let timeoutCounter = 0; const listener = (event: MessageEvent) => { const origin = event.origin; diff --git a/apps/Standalone/src/designer/app/AzureLogicAppsDesigner/laDesigner.tsx b/apps/Standalone/src/designer/app/AzureLogicAppsDesigner/laDesigner.tsx index 43a0759c568..92327f5bad5 100644 --- a/apps/Standalone/src/designer/app/AzureLogicAppsDesigner/laDesigner.tsx +++ b/apps/Standalone/src/designer/app/AzureLogicAppsDesigner/laDesigner.tsx @@ -32,6 +32,7 @@ import { BaseChatbotService, BaseFunctionService, BaseGatewayService, + BaseTenantService, StandardConnectionService, StandardConnectorService, StandardCustomCodeService, @@ -599,6 +600,12 @@ const getDesignerServices = ( }, }); + const tenantService = new BaseTenantService({ + baseUrl: armUrl, + apiVersion: '2017-08-01', + httpClient, + }); + const operationManifestService = new StandardOperationManifestService(defaultServiceParams); const searchService = new StandardSearchService({ ...defaultServiceParams, @@ -694,6 +701,7 @@ const getDesignerServices = ( connectionService, connectorService, gatewayService, + tenantService, operationManifestService, searchService, loggerService: null, diff --git a/apps/Standalone/src/designer/app/AzureLogicAppsDesigner/laDesignerConsumption.tsx b/apps/Standalone/src/designer/app/AzureLogicAppsDesigner/laDesignerConsumption.tsx index 6cb7814eb51..86e72fccb9e 100644 --- a/apps/Standalone/src/designer/app/AzureLogicAppsDesigner/laDesignerConsumption.tsx +++ b/apps/Standalone/src/designer/app/AzureLogicAppsDesigner/laDesignerConsumption.tsx @@ -24,6 +24,7 @@ import { BaseAppServiceService, BaseFunctionService, BaseGatewayService, + BaseTenantService, ConsumptionConnectionService, ConsumptionConnectorService, ConsumptionOperationManifestService, @@ -297,6 +298,7 @@ const getDesignerServices = ( tenantId, httpClient, }); + const apimService = new BaseApiManagementService({ ...defaultServiceParams, apiVersion: '2019-12-01', @@ -311,6 +313,7 @@ const getDesignerServices = ( apiVersion: '2022-03-01', subscriptionId, }); + const connectorService = new ConsumptionConnectorService({ ...defaultServiceParams, clientSupportedOperations: [ @@ -383,6 +386,7 @@ const getDesignerServices = ( apiVersion: '2018-07-01-preview', workflowReferenceId: workflowId, }); + const gatewayService = new BaseGatewayService({ baseUrl, httpClient, @@ -392,12 +396,18 @@ const getDesignerServices = ( }, }); + const tenantService = new BaseTenantService({ + ...defaultServiceParams, + apiVersion: '2017-08-01', + }); + const operationManifestService = new ConsumptionOperationManifestService({ ...defaultServiceParams, apiVersion: '2022-09-01-preview', subscriptionId, location: location || 'location', }); + const searchService = new ConsumptionSearchService({ ...defaultServiceParams, openApiConnectionMode: false, // This should be turned on for Open Api testing. @@ -474,6 +484,7 @@ const getDesignerServices = ( connectionService, connectorService, gatewayService, + tenantService, operationManifestService, searchService, loggerService, diff --git a/apps/Standalone/src/designer/app/LocalDesigner/localDesigner.tsx b/apps/Standalone/src/designer/app/LocalDesigner/localDesigner.tsx index babca958def..3b79e588f44 100644 --- a/apps/Standalone/src/designer/app/LocalDesigner/localDesigner.tsx +++ b/apps/Standalone/src/designer/app/LocalDesigner/localDesigner.tsx @@ -17,6 +17,7 @@ import { ConsumptionConnectionService, StandardCustomCodeService, ResourceIdentityType, + BaseTenantService, } from '@microsoft/logic-apps-shared'; import type { ContentType } from '@microsoft/logic-apps-shared'; import { DesignerProvider, BJSWorkflowProvider, Designer } from '@microsoft/logic-apps-designer'; @@ -106,6 +107,12 @@ const gatewayService = new BaseGatewayService({ }, }); +const tenantService = new BaseTenantService({ + baseUrl: '/url', + apiVersion: '2017-08-01', + httpClient, +}); + const functionService = new BaseFunctionService({ baseUrl: '/url', apiVersion: '2018-11-01', @@ -177,6 +184,7 @@ export const LocalDesigner = () => { searchService: isConsumption ? searchServiceConsumption : searchServiceStandard, oAuthService, gatewayService, + tenantService, functionService, appServiceService, workflowService, diff --git a/apps/Standalone/src/templates/app/TemplatesStandaloneDesigner.tsx b/apps/Standalone/src/templates/app/TemplatesStandaloneDesigner.tsx index 33549b98acb..789ee76617f 100644 --- a/apps/Standalone/src/templates/app/TemplatesStandaloneDesigner.tsx +++ b/apps/Standalone/src/templates/app/TemplatesStandaloneDesigner.tsx @@ -6,7 +6,7 @@ import type { RootState } from '../state/Store'; import { TemplatesDesigner, TemplatesDesignerProvider } from '@microsoft/logic-apps-designer'; import { useQuery } from '@tanstack/react-query'; import { useSelector } from 'react-redux'; -import { BaseGatewayService, StandardConnectionService } from '@microsoft/logic-apps-shared'; +import { BaseGatewayService, BaseTenantService, StandardConnectionService } from '@microsoft/logic-apps-shared'; import { useAppSettings, useConnectionsData, @@ -223,6 +223,11 @@ const getServices = ( gateway: '2016-06-01', }, }); + const tenantService = new BaseTenantService({ + baseUrl: armUrl, + httpClient, + apiVersion: '2017-08-01', + }); const oAuthService = new StandaloneOAuthService({ ...defaultServiceParams, apiVersion: '2018-07-01-preview', @@ -236,6 +241,7 @@ const getServices = ( return { connectionService, gatewayService, + tenantService, oAuthService, }; }; diff --git a/apps/vs-code-react/src/app/designer/servicesHelper.ts b/apps/vs-code-react/src/app/designer/servicesHelper.ts index 31b7bcb8727..bf790816b52 100644 --- a/apps/vs-code-react/src/app/designer/servicesHelper.ts +++ b/apps/vs-code-react/src/app/designer/servicesHelper.ts @@ -14,6 +14,7 @@ import { BaseAppServiceService, HTTP_METHODS, clone, + BaseTenantService, } from '@microsoft/logic-apps-shared'; import type { ApiHubServiceDetails, @@ -48,6 +49,7 @@ export const getDesignerServices = ( searchService: StandardSearchService; oAuthService: BaseOAuthService; gatewayService: BaseGatewayService; + tenantService: BaseTenantService; workflowService: IWorkflowService; hostService: IHostService; runService: StandardRunService; @@ -241,6 +243,12 @@ export const getDesignerServices = ( }, }); + const tenantService = new BaseTenantService({ + baseUrl: armUrl, + httpClient, + apiVersion: '2017-08-01', + }); + // Workflow service needs to be implemented to get the callback url for azure resources const workflowService: IWorkflowService = { getCallbackUrl: async () => { @@ -302,6 +310,7 @@ export const getDesignerServices = ( searchService, oAuthService, gatewayService, + tenantService, workflowService, hostService, runService, diff --git a/libs/designer/src/lib/core/state/__test__/designerOptionsSlice.spec.ts b/libs/designer/src/lib/core/state/__test__/designerOptionsSlice.spec.ts new file mode 100644 index 00000000000..379fbe39678 --- /dev/null +++ b/libs/designer/src/lib/core/state/__test__/designerOptionsSlice.spec.ts @@ -0,0 +1,23 @@ +import reducer, { initDesignerOptions, initialDesignerOptionsState } from '../designerOptions/designerOptionsSlice'; +import { describe, it, expect } from 'vitest'; + +describe('designer options slice reducers', () => { + it('should initialize designer options state', async () => { + const initialOptions = { + readOnly: true, + hostOptions: { + displayRuntimeInfo: false, + suppressCastingForSerialize: undefined, + recurrenceInterval: undefined, + maxWaitingRuns: undefined, + forceEnableSplitOn: undefined, + hideUTFExpressions: undefined, + stringOverrides: undefined, + }, + }; + + const state = reducer(initialDesignerOptionsState, initDesignerOptions(initialOptions)); + + expect(state.readOnly).toEqual(true); + }); +}); diff --git a/libs/designer/src/lib/core/state/connection/connectionSelector.ts b/libs/designer/src/lib/core/state/connection/connectionSelector.ts index 02bedf67a4f..35c6fa22271 100644 --- a/libs/designer/src/lib/core/state/connection/connectionSelector.ts +++ b/libs/designer/src/lib/core/state/connection/connectionSelector.ts @@ -12,6 +12,7 @@ import { isServiceProviderOperation, getRecordEntry, type Connector, + TenantService, } from '@microsoft/logic-apps-shared'; import { useMemo } from 'react'; import type { UseQueryResult } from '@tanstack/react-query'; @@ -62,20 +63,17 @@ export const useConnectorAndSwagger = (connectorId: string | undefined, enabled ); }; -export const useGateways = (subscriptionId: string, connectorName: string): UseQueryResult => { - return useQuery( - ['gateways', { subscriptionId }, { connectorName }], - async () => GatewayService().getGateways(subscriptionId, connectorName), - { - enabled: !!connectorName, - } - ); -}; +export const useGateways = (subscriptionId: string, connectorName: string): UseQueryResult => + useQuery(['gateways', { subscriptionId }, { connectorName }], async () => GatewayService().getGateways(subscriptionId, connectorName), { + enabled: !!connectorName, + }); export const useSubscriptions = () => useQuery(['subscriptions'], async () => GatewayService().getSubscriptions()); export const useGatewayServiceConfig = () => useMemo(() => GatewayService().getConfig?.() ?? {}, []); +export const useTenants = () => useQuery(['tenants'], async () => TenantService().getTenants?.()); + export const useConnectorByNodeId = (nodeId: string): Connector | undefined => { const connectorFromManifest = useOperationManifest(useOperationInfo(nodeId)).data?.properties.connector; const storeConnectorId = useNodeConnectorId(nodeId); diff --git a/libs/designer/src/lib/core/state/designerOptions/designerOptionsInterfaces.ts b/libs/designer/src/lib/core/state/designerOptions/designerOptionsInterfaces.ts index a91a54ef1e7..7d393187a69 100644 --- a/libs/designer/src/lib/core/state/designerOptions/designerOptionsInterfaces.ts +++ b/libs/designer/src/lib/core/state/designerOptions/designerOptionsInterfaces.ts @@ -3,6 +3,7 @@ import type { IConnectionService, IConnectorService, IGatewayService, + ITenantService, ILoggerService, IOperationManifestService, ISearchService, @@ -53,6 +54,7 @@ export interface ServiceOptions { searchService: ISearchService; connectorService?: IConnectorService; gatewayService?: IGatewayService; + tenantService?: ITenantService; loggerService?: ILoggerService; oAuthService: IOAuthService; workflowService: IWorkflowService; diff --git a/libs/designer/src/lib/core/state/designerOptions/designerOptionsSlice.ts b/libs/designer/src/lib/core/state/designerOptions/designerOptionsSlice.ts index 659a2d8442f..84b3a00e7e8 100644 --- a/libs/designer/src/lib/core/state/designerOptions/designerOptionsSlice.ts +++ b/libs/designer/src/lib/core/state/designerOptions/designerOptionsSlice.ts @@ -6,6 +6,7 @@ import { InitConnectionService, InitConnectorService, InitGatewayService, + InitTenantService, InitOperationManifestService, InitSearchService, InitOAuthService, @@ -23,7 +24,7 @@ import { import { createAsyncThunk, createSlice } from '@reduxjs/toolkit'; import type { PayloadAction } from '@reduxjs/toolkit'; -const initialState: DesignerOptionsState = { +export const initialDesignerOptionsState: DesignerOptionsState = { readOnly: false, isMonitoringView: false, isDarkMode: false, @@ -49,6 +50,7 @@ export const initializeServices = createAsyncThunk( connectorService, oAuthService, gatewayService, + tenantService, loggerService, functionService, appServiceService, @@ -81,6 +83,9 @@ export const initializeServices = createAsyncThunk( if (gatewayService) { InitGatewayService(gatewayService); } + if (tenantService) { + InitTenantService(tenantService); + } if (apimService) { InitApiManagementService(apimService); } @@ -114,7 +119,7 @@ export const initializeServices = createAsyncThunk( export const designerOptionsSlice = createSlice({ name: 'designerOptions', - initialState, + initialState: initialDesignerOptionsState, reducers: { initDesignerOptions: (state: DesignerOptionsState, action: PayloadAction>) => { state.readOnly = action.payload.readOnly; diff --git a/libs/designer/src/lib/core/state/templates/templateSlice.ts b/libs/designer/src/lib/core/state/templates/templateSlice.ts index 87ddce12b21..094debe9a54 100644 --- a/libs/designer/src/lib/core/state/templates/templateSlice.ts +++ b/libs/designer/src/lib/core/state/templates/templateSlice.ts @@ -5,6 +5,7 @@ import { InitConnectionService, InitFunctionService, InitGatewayService, + InitTenantService, InitOAuthService, getIntl, getRecordEntry, @@ -54,6 +55,7 @@ export const initializeTemplateServices = createAsyncThunk( connectionService, oAuthService, gatewayService, + tenantService, apimService, functionService, appServiceService, @@ -65,6 +67,9 @@ export const initializeTemplateServices = createAsyncThunk( if (gatewayService) { InitGatewayService(gatewayService); } + if (tenantService) { + InitTenantService(tenantService); + } if (apimService) { InitApiManagementService(apimService); } diff --git a/libs/designer/src/lib/core/templates/TemplatesDesignerContext.tsx b/libs/designer/src/lib/core/templates/TemplatesDesignerContext.tsx index 55fe480de72..a311a87460c 100644 --- a/libs/designer/src/lib/core/templates/TemplatesDesignerContext.tsx +++ b/libs/designer/src/lib/core/templates/TemplatesDesignerContext.tsx @@ -5,6 +5,7 @@ import type { IConnectionService, IFunctionService, IGatewayService, + ITenantService, ILoggerService, IOAuthService, } from '@microsoft/logic-apps-shared'; @@ -17,6 +18,7 @@ export interface TemplatesDesignerContext { export interface TemplateServiceOptions { connectionService: IConnectionService; gatewayService?: IGatewayService; + tenantService?: ITenantService; loggerService?: ILoggerService; oAuthService: IOAuthService; apimService?: IApiManagementService; diff --git a/libs/designer/src/lib/ui/panel/connectionsPanel/createConnection/__test__/createConnection.spec.tsx b/libs/designer/src/lib/ui/panel/connectionsPanel/createConnection/__test__/createConnection.spec.tsx index d5040aa6a3e..161391407a7 100644 --- a/libs/designer/src/lib/ui/panel/connectionsPanel/createConnection/__test__/createConnection.spec.tsx +++ b/libs/designer/src/lib/ui/panel/connectionsPanel/createConnection/__test__/createConnection.spec.tsx @@ -8,10 +8,16 @@ import { InitConnectionService, StandardConnectionService, } from '@microsoft/logic-apps-shared'; -import type { ConnectionParameter, ConnectionParameterSets } from '@microsoft/logic-apps-shared'; +import type { ConnectionParameter, ConnectionParameterSets, Connector } from '@microsoft/logic-apps-shared'; import React, { type ReactElement } from 'react'; import * as ReactShallowRenderer from 'react-test-renderer/shallow'; import { describe, vi, beforeEach, afterEach, beforeAll, afterAll, it, test, expect } from 'vitest'; +import { + mockConnectionParameters, + mockConnectionParameterSets, + mockOauthWithTenantParameters, + mockParameterSetsWithCredentialMapping, +} from './mocks/connectionParameters'; describe('ui/createConnection', () => { let renderer: ReactShallowRenderer.ShallowRenderer; @@ -41,103 +47,28 @@ describe('ui/createConnection', () => { renderer.unmount(); }); - const getConnectionParameters = (): Record => ({ - parameterA: { - type: 'string', + const baseConnector: Connector = { + id: 'myConnectorId', + type: 'connector', + name: 'myConnector', + properties: { + iconUri: 'https://iconUri', + displayName: 'My Connector', + capabilities: ['generic'], }, - hiddenParameterB: { - type: 'string', - uiDefinition: { - constraints: { - hidden: 'true', - }, - }, - }, - hideInUIParameterC: { - type: 'string', - uiDefinition: { - constraints: { - hideInUI: 'true', - }, - }, - }, - parameterD: { - type: 'string', - }, - }); + }; - const getConnectionParameterSets = (): ConnectionParameterSets => ({ - uiDefinition: { - description: '', - displayName: '', + const connectorWithParameters: Connector = { + ...baseConnector, + properties: { + ...baseConnector.properties, + connectionParameters: mockConnectionParameters, }, - values: [ - { - name: 'parameterSetA', - uiDefinition: { - description: '', - displayName: 'first parameter set', - }, - parameters: { - parameterA: { - type: 'string', - uiDefinition: { - description: '', - displayName: '', - }, - }, - hiddenParameterB: { - type: 'string', - uiDefinition: { - description: '', - displayName: '', - constraints: { - hidden: 'true', - }, - }, - }, - hideInUIParameterC: { - type: 'string', - uiDefinition: { - description: '', - displayName: '', - constraints: { - hideInUI: 'true', - }, - }, - }, - parameterD: { - type: 'string', - uiDefinition: { - description: '', - displayName: '', - }, - }, - }, - }, - { - name: 'parameterSetB', - uiDefinition: { - description: '', - displayName: 'second parameter set', - }, - parameters: { - parameterE: { - type: 'string', - uiDefinition: { - description: '', - displayName: '', - }, - }, - }, - }, - ], - }); + }; it('should render the create connection component', () => { const props: CreateConnectionProps = { - connectorId: 'myConnectorId', - connectorDisplayName: 'My Connector', + connector: baseConnector, checkOAuthCallback: vi.fn(), }; renderer.render(); @@ -158,9 +89,7 @@ describe('ui/createConnection', () => { it('should render visible connectionParameters', () => { const props: CreateConnectionProps = { - connectorId: 'myConnectorId', - connectorDisplayName: 'My Connector', - connectionParameters: getConnectionParameters(), + connector: connectorWithParameters, checkOAuthCallback: vi.fn(), }; renderer.render(); @@ -173,21 +102,45 @@ describe('ui/createConnection', () => { expect(parameters).toHaveLength(2); expect(parameters[0].props.parameterKey).toEqual('parameterA'); - expect(parameters[0].props.parameter).toEqual(props.connectionParameters?.['parameterA']); + expect(parameters[0].props.parameter).toEqual(props.connector.properties.connectionParameters?.['parameterA']); expect(parameters[0].props.value).toBeUndefined(); expect(parameters[0].props.setValue).toEqual(expect.any(Function)); expect(parameters[1].props.parameterKey).toEqual('parameterD'); - expect(parameters[1].props.parameter).toEqual(props.connectionParameters?.['parameterD']); + expect(parameters[1].props.parameter).toEqual(props.connector.properties.connectionParameters?.['parameterD']); expect(parameters[1].props.value).toBeUndefined(); expect(parameters[1].props.setValue).toEqual(expect.any(Function)); }); + it('should render oauth with tenant selection', () => { + const props: CreateConnectionProps = { + connector: { + ...baseConnector, + properties: { + ...baseConnector.properties, + connectionParameters: mockOauthWithTenantParameters, + }, + }, + checkOAuthCallback: vi.fn(), + }; + renderer.render(); + const createConnectionContainer = renderer.getRenderOutput(); + const createConnection = findConnectionCreateDiv(createConnectionContainer); + + const parameterSetsDropdown = findParameterSetsDropdown(createConnection); + expect(parameterSetsDropdown).toBeUndefined(); + + const legacyMultiAuth = findLegacyMultiAuth(createConnection); + expect(legacyMultiAuth).toBeDefined(); + + const tenantPicker = findTenantPicker(createConnection); + expect(tenantPicker).toBeDefined(); + }); + it('should render connectionParameterSet dropdown and parameters', () => { const props: CreateConnectionProps = { - connectorId: 'myConnectorId', - connectorDisplayName: 'My Connector', - connectionParameterSets: getConnectionParameterSets(), + connector: baseConnector, + connectionParameterSets: mockConnectionParameterSets, checkOAuthCallback: vi.fn(), }; renderer.render(); @@ -236,9 +189,7 @@ describe('ui/createConnection', () => { it('should support custom parameter editor for connectionParameters', () => { const props: CreateConnectionProps = { - connectorId: 'myConnectorId', - connectorDisplayName: 'My Connector', - connectionParameters: getConnectionParameters(), + connector: connectorWithParameters, checkOAuthCallback: vi.fn(), }; renderer.render(); @@ -259,13 +210,13 @@ describe('ui/createConnection', () => { expect(parameters).toHaveLength(2); expect(parameters[0].type).toEqual(CustomConnectionParameter); expect(parameters[0].props.parameterKey).toEqual('parameterA'); - expect(parameters[0].props.parameter).toEqual(props.connectionParameters?.['parameterA']); + expect(parameters[0].props.parameter).toEqual(props.connector.properties.connectionParameters?.['parameterA']); expect(parameters[0].props.value).toBeUndefined(); expect(parameters[0].props.setValue).toEqual(expect.any(Function)); expect(parameters[1].type).toEqual(UniversalConnectionParameter); expect(parameters[1].props.parameterKey).toEqual('parameterD'); - expect(parameters[1].props.parameter).toEqual(props.connectionParameters?.['parameterD']); + expect(parameters[1].props.parameter).toEqual(props.connector.properties.connectionParameters?.['parameterD']); expect(parameters[1].props.value).toBeUndefined(); expect(parameters[1].props.setValue).toEqual(expect.any(Function)); }); @@ -286,9 +237,8 @@ describe('ui/createConnection', () => { InitConnectionParameterEditorService(connectionParameterEditorService); const props: CreateConnectionProps = { - connectorId: 'myConnectorId', - connectorDisplayName: 'My Connector', - connectionParameterSets: getConnectionParameterSets(), + connector: baseConnector, + connectionParameterSets: mockConnectionParameterSets, checkOAuthCallback: vi.fn(), }; renderer.render(); @@ -317,118 +267,6 @@ describe('ui/createConnection', () => { }); describe('custom credential mapping editor', () => { - const getConnectionParameterSetsWithCredentialMapping = (): ConnectionParameterSets => ({ - uiDefinition: { - description: '', - displayName: '', - }, - values: [ - { - name: 'parameterSetA', - uiDefinition: { - description: '', - displayName: 'first parameter set', - }, - parameters: { - parameterA: { - type: 'string', - uiDefinition: { - description: '', - displayName: '', - credentialMapping: { - mappingName: 'myCredentialMapping', - values: [ - { - credentialKeyName: 'myCredentialPasswordKey', - type: 'UserPassword', - typeEnumValue: 1, - }, - ], - }, - }, - }, - parameterB: { - type: 'string', - uiDefinition: { - description: '', - displayName: '', - credentialMapping: { - mappingName: 'myCredentialMapping', - values: [ - { - credentialKeyName: 'myCredentialUserKey', - type: 'UserPassword', - typeEnumValue: 1, - }, - ], - }, - }, - }, - hiddenParameterC: { - type: 'string', - uiDefinition: { - description: '', - displayName: '', - constraints: { - hideInUI: 'true', - }, - }, - }, - parameterD: { - type: 'string', - uiDefinition: { - description: '', - displayName: '', - }, - }, - }, - }, - { - name: 'parameterSetB', - uiDefinition: { - description: '', - displayName: 'second parameter set', - }, - parameters: { - parameterE: { - type: 'string', - uiDefinition: { - description: '', - displayName: '', - credentialMapping: { - mappingName: 'myOtherCredentialMapping', - values: [ - { - credentialKeyName: 'myCredentialPasswordKey', - type: 'UserPassword', - typeEnumValue: 1, - }, - ], - }, - }, - }, - parameterF: { - type: 'string', - uiDefinition: { - description: '', - displayName: '', - credentialMapping: { - mappingName: 'myOtherCredentialMapping', - values: [ - { - credentialKeyName: 'myCredentialPasswordKey', - type: 'UserPassword', - typeEnumValue: 1, - }, - ], - }, - }, - }, - }, - }, - ], - }); - const CustomCredentialMappingEditor = () =>
Custom CRedential Mapping Editor
; let connectionParameterEditorService: IConnectionParameterEditorService; @@ -455,11 +293,9 @@ describe('ui/createConnection', () => { }); it('should not render CustomCredentialMappingEditor when connector has no mapping metadata', () => { - const connectionParameterSets = getConnectionParameterSets(); const props: CreateConnectionProps = { - connectorId: 'myConnectorId', - connectorDisplayName: 'My Connector', - connectionParameterSets, + connector: baseConnector, + connectionParameterSets: mockConnectionParameterSets, checkOAuthCallback: vi.fn(), }; renderer.render(); @@ -476,10 +312,9 @@ describe('ui/createConnection', () => { }); it('should render CustomCredentialMappingEditor when connector has mapping metadata', () => { - const connectionParameterSets = getConnectionParameterSetsWithCredentialMapping(); + const connectionParameterSets = mockParameterSetsWithCredentialMapping; const props: CreateConnectionProps = { - connectorId: 'myConnectorId', - connectorDisplayName: 'My Connector', + connector: baseConnector, connectionParameterSets, checkOAuthCallback: vi.fn(), }; @@ -519,12 +354,10 @@ describe('ui/createConnection', () => { }); it('should render CustomCredentialMappingEditor in loading state', () => { - const connectionParameterSets = getConnectionParameterSetsWithCredentialMapping(); const props: CreateConnectionProps = { isLoading: true, - connectorId: 'myConnectorId', - connectorDisplayName: 'My Connector', - connectionParameterSets, + connector: baseConnector, + connectionParameterSets: mockParameterSetsWithCredentialMapping, checkOAuthCallback: vi.fn(), }; renderer.render(); @@ -559,11 +392,9 @@ describe('ui/createConnection', () => { ])(`should not render CustomCredentialMappingEditor when %s`, (_, setup) => { setup(); - const connectionParameterSets = getConnectionParameterSetsWithCredentialMapping(); const props: CreateConnectionProps = { - connectorId: 'myConnectorId', - connectorDisplayName: 'My Connector', - connectionParameterSets, + connector: baseConnector, + connectionParameterSets: mockParameterSetsWithCredentialMapping, checkOAuthCallback: vi.fn(), }; renderer.render(); @@ -642,4 +473,26 @@ describe('ui/createConnection', () => { } return undefined; } + + function findLegacyMultiAuth(createConnection: ReactElement) { + const connectionsParamContainer = findConnectionsParamContainer(createConnection); + for (const paramRow of React.Children.toArray(connectionsParamContainer.props.children)) { + const testId = (paramRow as ReactElement)?.props?.['data-testId']?.toString(); + if (testId === 'legacy-multi-auth') { + return paramRow; + } + } + return undefined; + } + + function findTenantPicker(createConnection: ReactElement) { + const connectionsParamContainer = findConnectionsParamContainer(createConnection); + for (const paramRow of React.Children.toArray(connectionsParamContainer.props.children)) { + const testId = (paramRow as ReactElement)?.props?.['data-testId']?.toString(); + if (testId === 'connection-param-oauth-tenants') { + return paramRow; + } + } + return undefined; + } }); diff --git a/libs/designer/src/lib/ui/panel/connectionsPanel/createConnection/__test__/mocks/connectionParameters.ts b/libs/designer/src/lib/ui/panel/connectionsPanel/createConnection/__test__/mocks/connectionParameters.ts new file mode 100644 index 00000000000..b41d980b033 --- /dev/null +++ b/libs/designer/src/lib/ui/panel/connectionsPanel/createConnection/__test__/mocks/connectionParameters.ts @@ -0,0 +1,287 @@ +import { ConnectionParameter, ConnectionParameterSets } from '@microsoft/logic-apps-shared'; + +export const mockConnectionParameters: Record = { + parameterA: { + type: 'string', + }, + hiddenParameterB: { + type: 'string', + uiDefinition: { + constraints: { + hidden: 'true', + }, + }, + }, + hideInUIParameterC: { + type: 'string', + uiDefinition: { + constraints: { + hideInUI: 'true', + }, + }, + }, + parameterD: { + type: 'string', + }, +}; + +export const mockConnectionParameterSets: ConnectionParameterSets = { + uiDefinition: { + description: '', + displayName: '', + }, + values: [ + { + name: 'parameterSetA', + uiDefinition: { + description: '', + displayName: 'first parameter set', + }, + parameters: { + parameterA: { + type: 'string', + uiDefinition: { + description: '', + displayName: '', + }, + }, + hiddenParameterB: { + type: 'string', + uiDefinition: { + description: '', + displayName: '', + constraints: { + hidden: 'true', + }, + }, + }, + hideInUIParameterC: { + type: 'string', + uiDefinition: { + description: '', + displayName: '', + constraints: { + hideInUI: 'true', + }, + }, + }, + parameterD: { + type: 'string', + uiDefinition: { + description: '', + displayName: '', + }, + }, + }, + }, + { + name: 'parameterSetB', + uiDefinition: { + description: '', + displayName: 'second parameter set', + }, + parameters: { + parameterE: { + type: 'string', + uiDefinition: { + description: '', + displayName: '', + }, + }, + }, + }, + ], +}; + +export const mockOauthWithTenantParameters: Record = { + token: { + type: 'oauthSetting', + oAuthSettings: { + identityProvider: 'aadcertificate', + clientId: '7ab7862c-4c57-491e-8a45-d52a7e023983', + scopes: [], + redirectUrl: 'https://global.consent.azure-apim.net/redirect/azureeventgrid', + properties: { + IsFirstParty: 'True', + AzureActiveDirectoryResourceId: 'https://management.core.windows.net/', + }, + }, + }, + 'token:clientId': { + type: 'string', + uiDefinition: { + displayName: 'Client ID', + description: 'Client (or Application) ID of the Microsoft Entra ID application.', + constraints: { + required: 'false', + hidden: 'true', + }, + }, + }, + 'token:clientSecret': { + type: 'securestring', + uiDefinition: { + displayName: 'Client Secret', + description: 'Client secret of the Microsoft Entra ID application.', + constraints: { + required: 'false', + hidden: 'true', + }, + }, + }, + 'token:TenantId': { + type: 'string', + uiDefinition: { + displayName: 'Tenant', + description: 'The tenant ID of for the Microsoft Entra ID application.', + constraints: { + required: 'false', + hidden: 'true', + }, + }, + }, + 'token:resourceUri': { + type: 'string', + uiDefinition: { + displayName: 'ResourceUri', + description: 'The resource you are requesting authorization to use.', + constraints: { + required: 'false', + hidden: 'true', + }, + }, + }, + 'token:grantType': { + type: 'string', + uiDefinition: { + displayName: 'Grant Type', + description: 'Grant type', + constraints: { + required: 'false', + hidden: 'true', + allowedValues: [ + { + text: 'Code', + value: 'code', + }, + { + text: 'Client Credentials', + value: 'client_credentials', + }, + ], + }, + }, + }, +}; + +export const mockParameterSetsWithCredentialMapping: ConnectionParameterSets = { + uiDefinition: { + description: '', + displayName: '', + }, + values: [ + { + name: 'parameterSetA', + uiDefinition: { + description: '', + displayName: 'first parameter set', + }, + parameters: { + parameterA: { + type: 'string', + uiDefinition: { + description: '', + displayName: '', + credentialMapping: { + mappingName: 'myCredentialMapping', + values: [ + { + credentialKeyName: 'myCredentialPasswordKey', + type: 'UserPassword', + typeEnumValue: 1, + }, + ], + }, + }, + }, + parameterB: { + type: 'string', + uiDefinition: { + description: '', + displayName: '', + credentialMapping: { + mappingName: 'myCredentialMapping', + values: [ + { + credentialKeyName: 'myCredentialUserKey', + type: 'UserPassword', + typeEnumValue: 1, + }, + ], + }, + }, + }, + hiddenParameterC: { + type: 'string', + uiDefinition: { + description: '', + displayName: '', + constraints: { + hideInUI: 'true', + }, + }, + }, + parameterD: { + type: 'string', + uiDefinition: { + description: '', + displayName: '', + }, + }, + }, + }, + { + name: 'parameterSetB', + uiDefinition: { + description: '', + displayName: 'second parameter set', + }, + parameters: { + parameterE: { + type: 'string', + uiDefinition: { + description: '', + displayName: '', + credentialMapping: { + mappingName: 'myOtherCredentialMapping', + values: [ + { + credentialKeyName: 'myCredentialPasswordKey', + type: 'UserPassword', + typeEnumValue: 1, + }, + ], + }, + }, + }, + parameterF: { + type: 'string', + uiDefinition: { + description: '', + displayName: '', + credentialMapping: { + mappingName: 'myOtherCredentialMapping', + values: [ + { + credentialKeyName: 'myCredentialPasswordKey', + type: 'UserPassword', + typeEnumValue: 1, + }, + ], + }, + }, + }, + }, + }, + ], +}; diff --git a/libs/designer/src/lib/ui/panel/connectionsPanel/createConnection/createConnection.tsx b/libs/designer/src/lib/ui/panel/connectionsPanel/createConnection/createConnection.tsx index d8f4bb5168e..131a2e0fd3e 100644 --- a/libs/designer/src/lib/ui/panel/connectionsPanel/createConnection/createConnection.tsx +++ b/libs/designer/src/lib/ui/panel/connectionsPanel/createConnection/createConnection.tsx @@ -8,8 +8,7 @@ import LegacyMultiAuth, { LegacyMultiAuthOptions } from './formInputs/legacyMult import type { ConnectionParameterProps } from './formInputs/universalConnectionParameter'; import { UniversalConnectionParameter } from './formInputs/universalConnectionParameter'; import type { IDropdownOption } from '@fluentui/react'; -import { MessageBarType, MessageBar } from '@fluentui/react'; -import { Body1Strong, Button, Divider } from '@fluentui/react-components'; +import { Body1Strong, Button, Divider, MessageBar, MessageBarActions, MessageBarBody } from '@fluentui/react-components'; import { ConnectionParameterEditorService, ConnectionService, @@ -21,6 +20,8 @@ import { getPropertyValue, isServicePrinicipalConnectionParameter, usesLegacyManagedIdentity, + isUsingAadAuthentication, + equals, } from '@microsoft/logic-apps-shared'; import type { GatewayServiceConfig, @@ -32,6 +33,7 @@ import type { Gateway, ManagedIdentity, Subscription, + Connector, } from '@microsoft/logic-apps-shared'; import type { AzureResourcePickerProps } from '@microsoft/designer-ui'; import { AzureResourcePicker, Label } from '@microsoft/designer-ui'; @@ -39,18 +41,16 @@ import fromPairs from 'lodash.frompairs'; import type { FormEvent } from 'react'; import { useCallback, useEffect, useMemo, useState } from 'react'; import { useIntl } from 'react-intl'; +import { DismissRegular } from '@fluentui/react-icons'; +import TenantPicker from './formInputs/tenantPicker'; type ParamType = ConnectionParameter | ConnectionParameterSetParameter; export interface CreateConnectionProps { nodeIds?: string[]; iconUri?: string; - connectorId: string; - connectorDisplayName: string; - connectorCapabilities?: string[]; - connectionParameters?: Record; + connector: Connector; connectionParameterSets?: ConnectionParameterSets; - connectionAlternativeParameters?: Record; identity?: ManagedIdentity; isLoading?: boolean; createConnectionCallback?: ( @@ -79,12 +79,8 @@ export const CreateConnection = (props: CreateConnectionProps) => { const { nodeIds = [], iconUri = '', - connectorId, - connectorDisplayName, - connectorCapabilities, - connectionParameters, + connector, connectionParameterSets: _connectionParameterSets, - connectionAlternativeParameters, identity, isLoading = false, createConnectionCallback, @@ -102,6 +98,15 @@ export const CreateConnection = (props: CreateConnectionProps) => { const intl = useIntl(); + const connectorId = connector?.id; + + const { + connectionParameters, + connectionAlternativeParameters, + capabilities: connectorCapabilities, + displayName: connectorDisplayName, + } = connector.properties; + const [parameterValues, setParameterValues] = useState>({}); const [selectedParamSetIndex, setSelectedParamSetIndex] = useState(0); @@ -282,6 +287,15 @@ export const CreateConnection = (props: CreateConnectionProps) => { [hasOAuth, servicePrincipalSelected, legacyManagedIdentitySelected] ); + const usingAadConnection = useMemo(() => (connector ? isUsingAadAuthentication(connector) : false), [connector]); + const showTenantIdSelection = useMemo( + () => + usingAadConnection && + isUsingOAuth && + Object.keys(connectionParameters ?? {}).some((key) => equals(key, SERVICE_PRINCIPLE_CONSTANTS.CONFIG_ITEM_KEYS.TOKEN_TENANT_ID)), + [connectionParameters, isUsingOAuth, usingAadConnection] + ); + // Don't show name for simple connections const showNameInput = useMemo( () => @@ -315,6 +329,12 @@ export const CreateConnection = (props: CreateConnectionProps) => { const submitCallback = useCallback(() => { const { visibleParameterValues, additionalParameterValues } = parseParameterValues(parameterValues, capabilityEnabledParameters); + // The OAuth tenant ID is passed a little strange, we need to manually add it here + const oauthTenantId = additionalParameterValues?.[SERVICE_PRINCIPLE_CONSTANTS.CONFIG_ITEM_KEYS.TOKEN_TENANT_ID]; + if (showTenantIdSelection && oauthTenantId) { + visibleParameterValues[SERVICE_PRINCIPLE_CONSTANTS.CONFIG_ITEM_KEYS.TOKEN_TENANT_ID] = oauthTenantId; + } + // This value needs to be passed conditionally but the parameter is hidden, so we're manually inputting it here if ( supportsServicePrincipalConnection && @@ -353,6 +373,7 @@ export const CreateConnection = (props: CreateConnectionProps) => { isUsingOAuth, capabilityEnabledParameters, servicePrincipalSelected, + showTenantIdSelection, ]); // INTL STRINGS @@ -559,13 +580,18 @@ export const CreateConnection = (props: CreateConnectionProps) => {
{/* Error Bar */} {errorMessage && ( - - {errorMessage} + + {errorMessage} + } + onClick={clearErrorCallback} + /> + } + /> )} @@ -629,6 +655,18 @@ export const CreateConnection = (props: CreateConnectionProps) => { /> )} + {/* OAuth tenant ID selection */} + {showTenantIdSelection && ( + + setParameterValues({ ...parameterValues, [SERVICE_PRINCIPLE_CONSTANTS.CONFIG_ITEM_KEYS.TOKEN_TENANT_ID]: val }) + } + /> + )} + {/* Connector Parameters */} {showConfigParameters && Object.entries(capabilityEnabledParameters)?.map( diff --git a/libs/designer/src/lib/ui/panel/connectionsPanel/createConnection/createConnectionWrapper.tsx b/libs/designer/src/lib/ui/panel/connectionsPanel/createConnection/createConnectionWrapper.tsx index 0c5f8901753..6cae25dfc8b 100644 --- a/libs/designer/src/lib/ui/panel/connectionsPanel/createConnection/createConnectionWrapper.tsx +++ b/libs/designer/src/lib/ui/panel/connectionsPanel/createConnection/createConnectionWrapper.tsx @@ -284,16 +284,12 @@ export const CreateConnectionWrapper = () => { { + const { value, setValue, isLoading } = props; + + const intl = useIntl(); + + const { data: tenants, isLoading: tenantsLoading, error: tenantsError } = useTenants(); + + const tenantOptions = useMemo( + () => + (tenants ?? []) + .map((tenant: Tenant) => ({ + key: tenant.id.split('/tenants/')?.[1] ?? tenant.id, + text: tenant.displayName, + })) + .sort((a: any, b: any) => a.text.localeCompare(b.text)), + [tenants] + ); + + const tenantDropdownLabel = intl.formatMessage({ + defaultMessage: 'Tenant ID', + id: 'UsEvG2', + description: 'tenant dropdown label', + }); + + const errorMessage = useMemo(() => (tenantsError ? parseErrorMessage(tenantsError) : undefined), [tenantsError]); + + useEffect(() => { + if (!value && tenantOptions.length > 0) { + setValue(tenantOptions[0]?.key.toString()); + } + }, [setValue, tenantOptions, value]); + + return ( +
+
+ ); +}; + +export default TenantPicker; diff --git a/libs/logic-apps-shared/src/designer-client-services/index.ts b/libs/logic-apps-shared/src/designer-client-services/index.ts index 4d1e959d54b..f2787dbb00d 100644 --- a/libs/logic-apps-shared/src/designer-client-services/index.ts +++ b/libs/logic-apps-shared/src/designer-client-services/index.ts @@ -24,3 +24,4 @@ export * from './lib/editor'; export * from './lib/connectionParameterEditor'; export * from './lib/chatbot'; export * from './lib/customcode'; +export * from './lib/tenant'; diff --git a/libs/logic-apps-shared/src/designer-client-services/lib/base/index.ts b/libs/logic-apps-shared/src/designer-client-services/lib/base/index.ts index 54e6bd55d80..8af9029cc73 100644 --- a/libs/logic-apps-shared/src/designer-client-services/lib/base/index.ts +++ b/libs/logic-apps-shared/src/designer-client-services/lib/base/index.ts @@ -30,3 +30,6 @@ export * from './operations'; // Chatbot export { BaseChatbotService } from './chatbot'; export type { ChatbotServiceOptions } from './chatbot'; +// Tenant +export { BaseTenantService } from './tenant'; +export type { BaseTenantServiceOptions } from './tenant'; diff --git a/libs/logic-apps-shared/src/designer-client-services/lib/base/tenant.ts b/libs/logic-apps-shared/src/designer-client-services/lib/base/tenant.ts new file mode 100644 index 00000000000..85cdcd66d17 --- /dev/null +++ b/libs/logic-apps-shared/src/designer-client-services/lib/base/tenant.ts @@ -0,0 +1,39 @@ +import { ArgumentException } from '../../../utils/src'; +import { getAzureResourceRecursive, type Tenant } from '../common/azure'; +import type { IHttpClient } from '../httpClient'; +import type { ITenantService } from '../tenant'; + +export interface BaseTenantServiceOptions { + baseUrl: string; + httpClient: IHttpClient; + apiVersion: string; +} + +export class BaseTenantService implements ITenantService { + constructor(public readonly options: BaseTenantServiceOptions) { + const { baseUrl, apiVersion } = options; + if (!baseUrl) { + throw new ArgumentException('baseUrl required'); + } + if (!apiVersion) { + throw new ArgumentException('apiVersion required'); + } + } + + dispose(): void { + return; + } + + async getTenants(): Promise { + const { httpClient, apiVersion, baseUrl } = this.options; + const uri = `${baseUrl}/tenants`; + const queryParameters = { 'api-version': apiVersion }; + + try { + const response = await getAzureResourceRecursive(httpClient, uri, queryParameters); + return response; + } catch (error) { + return []; + } + } +} diff --git a/libs/logic-apps-shared/src/designer-client-services/lib/common/azure.ts b/libs/logic-apps-shared/src/designer-client-services/lib/common/azure.ts index 14b675b7c17..e73239f946c 100644 --- a/libs/logic-apps-shared/src/designer-client-services/lib/common/azure.ts +++ b/libs/logic-apps-shared/src/designer-client-services/lib/common/azure.ts @@ -8,7 +8,11 @@ export interface ContinuationTokenResponse { export const getAzureResourceRecursive = async (httpClient: IHttpClient, uri: string, queryParams: any): Promise => { const requestPage = async (uri: string, value: any[], queryParameters?: any): Promise => { try { - const { nextLink, value: newValue } = await httpClient.get>({ uri, queryParameters }); + const { nextLink, value: newValue } = await httpClient.get>({ + uri, + queryParameters, + includeAuth: true, + }); value.push(...newValue); if (nextLink) { return await requestPage(nextLink, value); @@ -21,3 +25,12 @@ export const getAzureResourceRecursive = async (httpClient: IHttpClient, uri: st return requestPage(uri, [], queryParams); }; + +export interface Tenant { + id: string; + tenantId: string; + country: string; + countryCode: string; + displayName: string; + domains: string[]; +} diff --git a/libs/logic-apps-shared/src/designer-client-services/lib/httpClient.ts b/libs/logic-apps-shared/src/designer-client-services/lib/httpClient.ts index 1cc15e56aa1..15bf9315ed1 100644 --- a/libs/logic-apps-shared/src/designer-client-services/lib/httpClient.ts +++ b/libs/logic-apps-shared/src/designer-client-services/lib/httpClient.ts @@ -10,6 +10,7 @@ export interface HttpRequestOptions { headers?: Record; queryParameters?: QueryParameters; noAuth?: boolean; + includeAuth?: boolean; returnHeaders?: boolean; } diff --git a/libs/logic-apps-shared/src/designer-client-services/lib/tenant.ts b/libs/logic-apps-shared/src/designer-client-services/lib/tenant.ts new file mode 100644 index 00000000000..0a65d5a8097 --- /dev/null +++ b/libs/logic-apps-shared/src/designer-client-services/lib/tenant.ts @@ -0,0 +1,23 @@ +import { AssertionErrorCode, AssertionException } from '../../utils/src'; +import type { Tenant } from './common/azure'; + +export interface ITenantService { + /** + * Gets tenants. + */ + getTenants?(): Promise; +} + +let service: ITenantService; + +export const InitTenantService = (tenantService: ITenantService): void => { + service = tenantService; +}; + +export const TenantService = (): ITenantService => { + if (!service) { + throw new AssertionException(AssertionErrorCode.SERVICE_NOT_INITIALIZED, 'Tenant Service needs to be initialized before using'); + } + + return service; +}; diff --git a/libs/logic-apps-shared/src/utils/src/lib/helpers/__test__/connections.spec.ts b/libs/logic-apps-shared/src/utils/src/lib/helpers/__test__/connections.spec.ts new file mode 100644 index 00000000000..1c4ed0b1207 --- /dev/null +++ b/libs/logic-apps-shared/src/utils/src/lib/helpers/__test__/connections.spec.ts @@ -0,0 +1,34 @@ +import { ConnectionParameterTypes, Connector } from '../../models'; +import { lighten } from '../color'; +import { describe, vi, beforeEach, afterEach, beforeAll, afterAll, it, test, expect } from 'vitest'; +import { isUsingAadAuthentication } from '../connections'; + +describe('lib/helpers/connections', () => { + it('should properly classify AAD vs non-AAD connector authentication', () => { + const getAadConnector = (isAAD: boolean): Connector => ({ + id: 'aad-connector', + type: 'aad-connector', + name: 'aad-connector', + properties: { + displayName: 'AAD Connector', + iconUri: 'https://example.com/icon.png', + connectionParameters: { + 'oauth-param': { + type: ConnectionParameterTypes.oauthSetting, + oAuthSettings: { + identityProvider: isAAD ? 'aadcertificate' : 'other', + clientId: '123', + redirectUrl: 'https://example.com', + scopes: ['scope1'], + properties: { + IsFirstParty: 'true', + }, + }, + }, + }, + }, + }); + expect(isUsingAadAuthentication(getAadConnector(true))).toBe(true); + expect(isUsingAadAuthentication(getAadConnector(false))).toBe(false); + }); +}); diff --git a/libs/logic-apps-shared/src/utils/src/lib/helpers/connections.ts b/libs/logic-apps-shared/src/utils/src/lib/helpers/connections.ts index bb05fad4b08..3e089f515ae 100644 --- a/libs/logic-apps-shared/src/utils/src/lib/helpers/connections.ts +++ b/libs/logic-apps-shared/src/utils/src/lib/helpers/connections.ts @@ -147,6 +147,19 @@ export function isFirstPartyConnector(connector: Connector): boolean { ); } +export function isUsingAadAuthentication(connector: Connector): boolean { + const oauthParameters = getConnectionParametersWithType(connector, ConnectionParameterTypes.oauthSetting); + + return ( + !!oauthParameters && + oauthParameters.length > 0 && + !!oauthParameters[0].oAuthSettings && + !!oauthParameters[0].oAuthSettings.identityProvider && + (equals(oauthParameters[0].oAuthSettings.identityProvider, 'aadcertificate') || + equals(oauthParameters[0].oAuthSettings.identityProvider, 'aad')) + ); +} + export function getConnectionParametersWithType( connector: Connector, connectionParameterType: string,