Skip to content

Commit

Permalink
feat(designer): Adding MSI capability for Azure functions (#4662)
Browse files Browse the repository at this point in the history
* feat(designer): Adding MSI capability for Azure functions

* Updating for back compatibility

---------

Co-authored-by: Priti Sambandam <psamband@microsoft.com>
  • Loading branch information
preetriti1 and Priti Sambandam committed Apr 23, 2024
1 parent a792a22 commit 4b60f37
Show file tree
Hide file tree
Showing 8 changed files with 182 additions and 128 deletions.
34 changes: 23 additions & 11 deletions libs/designer/src/lib/core/utils/connectors/connections.ts
Expand Up @@ -17,6 +17,7 @@ import {
ConnectionType,
getResourceName,
getRecordEntry,
getPropertyValue,
} from '@microsoft/logic-apps-shared';
import type { AssistedConnectionProps } from '@microsoft/designer-ui';
import type {
Expand Down Expand Up @@ -142,22 +143,32 @@ export function getAssistedConnectionProps(connector: Connector, manifest?: Oper
return undefined;
}

export async function getConnectionParametersForAzureConnection(connectionType?: ConnectionType, selectedSubResource?: any): Promise<any> {
export async function getConnectionParametersForAzureConnection(
connectionType?: ConnectionType,
selectedSubResource?: any,
parameterValues?: Record<string, any>,
isMultiAuthConnection?: boolean // TODO - Should remove when backend bits are ready for multi-auth in resource picker connections
): Promise<any> {
if (connectionType === ConnectionType.Function) {
const functionId = selectedSubResource?.id;
const authCodeValue = await FunctionService().fetchFunctionKey(functionId);
const triggerUrl = selectedSubResource?.properties?.invoke_url_template;
const isQueryString = isMultiAuthConnection ? equals(getPropertyValue(parameterValues ?? {}, 'type'), 'querystring') : true;
let updatedParameterValues = { ...parameterValues };

if (isQueryString) {
const authCodeValue = await FunctionService().fetchFunctionKey(functionId);
updatedParameterValues = isMultiAuthConnection
? { ...updatedParameterValues, value: authCodeValue }
: { ...updatedParameterValues, authentication: { type: 'QueryString', name: 'Code', value: authCodeValue } };
}

return {
...updatedParameterValues,
function: { id: functionId },
triggerUrl,
authentication: {
type: 'QueryString',
name: 'Code',
value: authCodeValue,
},
};
}
if (connectionType === ConnectionType.ApiManagement) {
// biome-ignore lint/style/noUselessElse: needed for future implementation
} else if (connectionType === ConnectionType.ApiManagement) {
// TODO - Need to find apps which have authentication set, check with Alex.
const apimApiId = selectedSubResource?.id;
const { api } = await ApiManagementService().fetchApiMSwagger(apimApiId);
Expand All @@ -166,13 +177,14 @@ export async function getConnectionParametersForAzureConnection(connectionType?:
const subscriptionKey = (api.securityDefinitions?.apiKeyHeader as any)?.name ?? 'NotFound';

return {
...parameterValues,
apiId: apimApiId,
baseUrl: fullUrl,
subscriptionKey,
};
}

return {};
return parameterValues;
}

export function getSupportedParameterSets(
Expand Down Expand Up @@ -249,7 +261,7 @@ function isManagedIdentitySupported(operationType: string, connectorCapabilities
return true;
}

export function isUserAssignedIdentitySupportedForInApp(connectorCapabilities: string[] = []) {
function isUserAssignedIdentitySupportedForInApp(connectorCapabilities: string[] = []) {
return !!connectorCapabilities?.find((capability) => equals(capability, 'supportsUserAssignedIdentity'));
}

Expand Down
Expand Up @@ -26,12 +26,30 @@ describe('CreateConnectionWrapper', () => {
},
};

it.each([
['default case', outputParameterValues.default],
['value property is undefined', outputParameterValues.withUndefinedValue],
])(`returns ConnectionParameterSetValue when %s`, (_, outputParameterValues) => {
expect(getConnectionParameterSetValues(selectedParameterSetName, outputParameterValues)).toEqual(
expectedConnectionParameterSetValues
const expectedConnectionParameterSetValuesWithUndefined = {
name: selectedParameterSetName,
values: {
parameter1: {
value: 'value1',
},
parameter2: {
value: undefined,
},
},
};

it.each([['default case', outputParameterValues.default]])(
`returns ConnectionParameterSetValue when %s`,
(_, outputParameterValues) => {
expect(getConnectionParameterSetValues(selectedParameterSetName, outputParameterValues)).toEqual(
expectedConnectionParameterSetValues
);
}
);

it('returns ConnectionParameterSetValue with undefined value when value property is undefined', () => {
expect(getConnectionParameterSetValues(selectedParameterSetName, outputParameterValues.withUndefinedValue)).toEqual(
expectedConnectionParameterSetValuesWithUndefined
);
});
});
Expand Down
@@ -1,5 +1,4 @@
import { needsOAuth } from '../../../../core/actions/bjsworkflow/connections';
import { isUserAssignedIdentitySupportedForInApp } from '../../../../core/utils/connectors/connections';
import { ActionList } from '../actionList/actionList';
import ConnectionMultiAuthInput from './formInputs/connectionMultiAuth';
import ConnectionNameInput from './formInputs/connectionNameInput';
Expand All @@ -16,10 +15,8 @@ import {
ConnectionService,
Capabilities,
ConnectionParameterTypes,
ResourceIdentityType,
SERVICE_PRINCIPLE_CONSTANTS,
connectorContainsAllServicePrinicipalConnectionParameters,
equals,
filterRecord,
getPropertyValue,
isServicePrinicipalConnectionParameter,
Expand Down Expand Up @@ -193,15 +190,6 @@ export const CreateConnection = (props: CreateConnectionProps) => {
[selectedParamSetIndex, showLegacyMultiAuth]
);

const showIdentityPicker = useMemo(
() =>
isMultiAuth &&
isUserAssignedIdentitySupportedForInApp(connectorCapabilities) &&
identity?.type?.toLowerCase()?.includes(ResourceIdentityType.USER_ASSIGNED.toLowerCase()) &&
equals(connectionParameterSets?.values[selectedParamSetIndex].name, 'ManagedServiceIdentity'),
[connectionParameterSets?.values, connectorCapabilities, identity?.type, isMultiAuth, selectedParamSetIndex]
);

const [selectedManagedIdentity, setSelectedManagedIdentity] = useState<string | undefined>(undefined);

const onLegacyManagedIdentityChange = useCallback((_: any, option?: IDropdownOption<any>) => {
Expand Down Expand Up @@ -340,7 +328,7 @@ export const CreateConnection = (props: CreateConnectionProps) => {
}

const alternativeParameterValues = legacyManagedIdentitySelected ? {} : undefined;
const identitySelected = legacyManagedIdentitySelected || showIdentityPicker ? selectedManagedIdentity : undefined;
const identitySelected = legacyManagedIdentitySelected ? selectedManagedIdentity : undefined;

return createConnectionCallback?.(
showNameInput ? connectionDisplayName : undefined,
Expand All @@ -356,7 +344,6 @@ export const CreateConnection = (props: CreateConnectionProps) => {
supportsServicePrincipalConnection,
unfilteredParameters,
legacyManagedIdentitySelected,
showIdentityPicker,
selectedManagedIdentity,
createConnectionCallback,
showNameInput,
Expand Down Expand Up @@ -477,7 +464,11 @@ export const CreateConnection = (props: CreateConnectionProps) => {
return isUsingOAuth ? signInButtonAria : createButtonAria;
}, [isUsingOAuth, signInButtonAria, createButtonAria]);

const showConfigParameters = useMemo(() => !resourceSelectorProps, [resourceSelectorProps]);
// TODO -This check should be removed because backend has to fix their connection parameters if it should not be shown in UI.
const showConfigParameters = useMemo(
() => !resourceSelectorProps || (resourceSelectorProps && isMultiAuth),
[resourceSelectorProps, isMultiAuth]
);

const renderConnectionParameter = (key: string, parameter: ConnectionParameterSetParameter | ConnectionParameter) => {
const connectionParameterProps: ConnectionParameterProps = {
Expand All @@ -491,6 +482,7 @@ export const CreateConnection = (props: CreateConnectionProps) => {
selectSubscriptionCallback,
availableGateways,
availableSubscriptions,
identity,
};

const customParameterOptions = ConnectionParameterEditorService()?.getConnectionParameterEditor({
Expand Down Expand Up @@ -633,16 +625,6 @@ export const CreateConnection = (props: CreateConnectionProps) => {
/>
)}

{/* Managed Identity Selection for In-App Connectors */}
{showIdentityPicker && (
<div className="param-row">
<Label className="label" required htmlFor={'connection-param-set-select'} disabled={isLoading}>
{legacyManagedIdentityLabelText}
</Label>
<LegacyManagedIdentityDropdown identity={identity} onChange={onLegacyManagedIdentityChange} disabled={isLoading} />
</div>
)}

{/* Connector Parameters */}
{showConfigParameters &&
Object.entries(capabilityEnabledParameters)?.map(
Expand Down
Expand Up @@ -23,7 +23,6 @@ import {
getAssistedConnectionProps,
getConnectionParametersForAzureConnection,
getSupportedParameterSets,
isUserAssignedIdentitySupportedForInApp,
} from '../../../../core/utils/connectors/connections';
import { CreateConnection } from './createConnection';
import { Spinner } from '@fluentui/react-components';
Expand All @@ -34,7 +33,6 @@ import {
WorkflowService,
getIconUriFromConnector,
getRecordEntry,
safeSetObjectPropertyValue,
type ConnectionCreationInfo,
type ConnectionParametersMetadata,
type Connection,
Expand Down Expand Up @@ -160,24 +158,6 @@ export const CreateConnectionWrapper = () => {
}

try {
// Assign connection parameters from resource selector experience
if (assistedConnectionProps) {
const assistedParams = await getConnectionParametersForAzureConnection(
operationManifest?.properties.connection?.type,
selectedSubResource
);
outputParameterValues = { ...outputParameterValues, ...assistedParams };
}

// Assign identity selected in parameter values for in-app connectors
if (
isUserAssignedIdentitySupportedForInApp(connector.properties.capabilities) &&
identitySelected &&
identitySelected !== constants.SYSTEM_ASSIGNED_MANAGED_IDENTITY
) {
safeSetObjectPropertyValue(outputParameterValues, ['identity'], identitySelected);
}

// If oauth, find the oauth parameter and assign the redirect url
if (isOAuthConnection && selectedParameterSet) {
const oAuthParameter = Object.entries(selectedParameterSet?.parameters).find(
Expand All @@ -191,6 +171,16 @@ export const CreateConnectionWrapper = () => {
}
}

// Assign connection parameters from resource selector experience
if (assistedConnectionProps) {
outputParameterValues = await getConnectionParametersForAzureConnection(
operationManifest?.properties.connection?.type,
selectedSubResource,
outputParameterValues,
!!selectedParameterSet // TODO: Should remove this when backend updates all connection parameters for functions and apim
);
}

const connectionInfo: ConnectionCreationInfo = {
displayName,
connectionParametersSet: selectedParameterSet
Expand Down Expand Up @@ -318,11 +308,7 @@ export function getConnectionParameterSetValues(
values: Object.keys(outputParameterValues).reduce((acc: any, key) => {
// eslint-disable-next-line no-param-reassign
acc[key] = {
value:
outputParameterValues[key] ??
// Avoid 'undefined', which causes the 'value' property to be removed when serializing as JSON object,
// and breaks contracts validation.
null,
value: outputParameterValues[key],
};
return acc;
}, {}),
Expand Down
@@ -1,6 +1,6 @@
import { Dropdown, type IDropdownOption } from '@fluentui/react';
import { getIdentityDropdownOptions, type ManagedIdentity } from '@microsoft/logic-apps-shared';
import { useEffect, useMemo } from 'react';
import { useMemo } from 'react';
import { useIntl } from 'react-intl';

interface LegacyManagedIdentityDropdownProps {
Expand All @@ -14,9 +14,6 @@ const LegacyManagedIdentityDropdown = (props: LegacyManagedIdentityDropdownProps
const intl = useIntl();
const dropdownOptions = useMemo(() => getIdentityDropdownOptions(identity, intl), [identity, intl]);

// Set value to first option on start
useEffect(() => onChange(undefined, dropdownOptions[0]), [dropdownOptions, onChange]);

return (
<Dropdown
className={'connection-parameter-input'}
Expand Down
Expand Up @@ -2,8 +2,10 @@ import { ConnectionParameterRow } from '../connectionParameterRow';
import GatewayPicker from './gatewayPicker';
import type { IDropdownOption } from '@fluentui/react';
import { Checkbox, Dropdown, TextField } from '@fluentui/react';
import type { ConnectionParameter, ConnectionParameterAllowedValue } from '@microsoft/logic-apps-shared';
import { ConnectionParameterTypes } from '@microsoft/logic-apps-shared';
import type { ConnectionParameter, ConnectionParameterAllowedValue, ManagedIdentity } from '@microsoft/logic-apps-shared';
import { ConnectionParameterTypes, equals } from '@microsoft/logic-apps-shared';
import LegacyManagedIdentityDropdown from './legacyManagedIdentityPicker';
import constants from '../../../../../common/constants';

export interface ConnectionParameterProps {
parameterKey: string;
Expand All @@ -16,6 +18,7 @@ export interface ConnectionParameterProps {
selectSubscriptionCallback?: (subscriptionId: string) => void;
availableGateways?: any[];
availableSubscriptions?: any[];
identity?: ManagedIdentity;
}

export const UniversalConnectionParameter = (props: ConnectionParameterProps) => {
Expand All @@ -30,6 +33,7 @@ export const UniversalConnectionParameter = (props: ConnectionParameterProps) =>
selectSubscriptionCallback,
availableGateways,
availableSubscriptions,
identity,
} = props;

const data = parameter?.uiDefinition;
Expand All @@ -54,6 +58,15 @@ export const UniversalConnectionParameter = (props: ConnectionParameterProps) =>
);
}

// Managed Identity picker
else if (equals(constraints?.editor, 'identitypicker')) {
const onManagedIdentityChange = (_: any, option?: IDropdownOption<any>) => {
const identitySelected = option?.key.toString() !== constants.SYSTEM_ASSIGNED_MANAGED_IDENTITY ? option?.key.toString() : undefined;
setValue(identitySelected);
};
inputComponent = <LegacyManagedIdentityDropdown identity={identity} onChange={onManagedIdentityChange} disabled={isLoading} />;
}

// Boolean parameter
else if (parameter?.type === ConnectionParameterTypes.bool) {
if (value === undefined) {
Expand Down

0 comments on commit 4b60f37

Please sign in to comment.