Skip to content
This repository was archived by the owner on Jul 9, 2025. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from 10 commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
8279a27
add key valut token get
VanyLaw Mar 10, 2021
74e9e37
add auth before doing kv update
VanyLaw Mar 10, 2021
30b9550
Merge remote-tracking branch 'origin/main' into wenyluo/kv
VanyLaw Mar 10, 2021
b657efa
refactor the get alias and generate profile code
VanyLaw Mar 11, 2021
e88b616
add azure key vault logic and set policy process
Mar 17, 2021
8b0607c
Merge remote-tracking branch 'origin/main' into wenyluo/kv
VanyLaw Mar 17, 2021
5233813
fix generated profile not work
VanyLaw Mar 18, 2021
71a08d2
Merge remote-tracking branch 'origin/main' into wenyluo/kv
VanyLaw Mar 18, 2021
c111827
fix build
VanyLaw Mar 18, 2021
6c8d54d
Merge branch 'main' into wenyluo/kv
luhan2017 Mar 19, 2021
5aa5029
Merge branch 'main' into wenyluo/kv
luhan2017 Mar 23, 2021
1bd9bcc
Merge branch 'main' into wenyluo/kv
luhan2017 Mar 24, 2021
f174450
add exception handling
Mar 24, 2021
b710e8b
fix conflict
VanyLaw Mar 24, 2021
ae3d1d5
fix comments
VanyLaw Mar 24, 2021
5046984
Merge branch 'main' into wenyluo/kv
cwhitten Mar 24, 2021
6b1ab1a
fix ut
VanyLaw Mar 24, 2021
85aefee
Merge branch 'wenyluo/kv' of https://github.com/microsoft/BotFramewor…
VanyLaw Mar 24, 2021
90ed899
Merge branch 'main' into wenyluo/kv
cwhitten Mar 24, 2021
b23b8cf
Fix lint, comment-out unusued imports
cwhitten Mar 24, 2021
4356c9a
Merge branch 'main' into wenyluo/kv
luhan2017 Mar 24, 2021
8f79355
open modal refactor
alanlong9278 Mar 25, 2021
92d63a2
Merge branch 'main' into wenyluo/kv
luhan2017 Mar 25, 2021
a1d0702
alias & profile for open bot
alanlong9278 Mar 25, 2021
c9f068a
api
alanlong9278 Mar 25, 2021
b8065d7
fix lint
VanyLaw Mar 25, 2021
df11d27
alias
alanlong9278 Mar 25, 2021
07d1e2d
set alias
alanlong9278 Mar 25, 2021
f97204e
Merge branch 'wenyluo/kv' of https://github.com/microsoft/BotFramewor…
alanlong9278 Mar 25, 2021
8c92548
Fix lint
cwhitten Mar 25, 2021
7f561b0
Merge branch 'main' into wenyluo/kv
cwhitten Mar 25, 2021
995466c
Merge branch 'main' into wenyluo/kv
boydc2014 Mar 26, 2021
1fa075e
ut
alanlong9278 Mar 26, 2021
d17bbbc
publish notification
alanlong9278 Mar 26, 2021
3d2c68b
Merge branch 'main' into wenyluo/kv
luhan2017 Mar 26, 2021
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -258,19 +258,20 @@ export function CreateOptions(props: CreateOptionsProps) {
const decoded = decodeURIComponent(props.location.search);
const { source, payload } = querystring.parse(decoded);
if (typeof source === 'string' && typeof payload === 'string') {
const alias = getAliasFromPayload(source, payload);
// check to see if Composer currently has a bot project corresponding to the alias
axios
.get<any>(`/api/projects/alias/${alias}`)
.then((aliasRes) => {
if (aliasRes.status === 200) {
navigate(`/bot/${aliasRes.data.id}`);
return;
}
})
.catch((e) => {
setIsOpen(true);
});
getAliasFromPayload(source, payload).then((alias) => {
// check to see if Composer currently has a bot project corresponding to the alias
axios
.get<any>(`/api/projects/alias/${alias}`)
.then((aliasRes) => {
if (aliasRes.status === 200) {
navigate(`/bot/${aliasRes.data.id}`);
return;
}
})
.catch((e) => {
setIsOpen(true);
});
});
return;
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -208,7 +208,7 @@ const DefineConversation: React.FC<DefineConversationProps> = (props) => {
}, [focusedStorageFolder]);

const handleSubmit = useCallback(
(e) => {
async (e) => {
e.preventDefault();
if (hasErrors) {
return;
Expand Down Expand Up @@ -246,7 +246,7 @@ const DefineConversation: React.FC<DefineConversationProps> = (props) => {
if (payload && typeof payload === 'string' && typeof source === 'string') {
dataToSubmit.profile = JSON.parse(payload);
dataToSubmit.source = source;
dataToSubmit.alias = getAliasFromPayload(source, payload);
dataToSubmit.alias = await getAliasFromPayload(source, payload);
}
}

Expand Down
4 changes: 4 additions & 0 deletions Composer/packages/client/src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -381,6 +381,10 @@ export const graphScopes = {
scopes: ['https://graph.microsoft.com/Application.ReadWrite.All'],
targetResource: 'https://graph.microsoft.com/',
};
export const vaultScopes = {
scopes: ['https://vault.azure.net/user_impersonation'],
targetResource: 'https://vault.azure.net/',
};

export const authUrl = `https://login.microsoftonline.com/${authConfig.tenantId}/oauth2/v2.0/authorize`;

Expand Down
4 changes: 4 additions & 0 deletions Composer/packages/client/src/pages/publish/Publish.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import {
setTenantId,
getTenantIdFromCache,
} from '../../utils/auth';
import { vaultScopes } from '../../constants';
import { AuthClient } from '../../utils/authClient';
import TelemetryClient from '../../telemetry/TelemetryClient';
import { ApiStatus, PublishStatusPollingUpdater, pollingUpdaterList } from '../../utils/publishStatusPollingUpdater';
Expand Down Expand Up @@ -254,6 +255,9 @@ const Publish: React.FC<RouteComponentProps<{ projectId: string; targetName?: st
token = await AuthClient.getARMTokenForTenant(tenant);
}
}
// get keyvault token

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

remove this part.

const result = await AuthClient.getAccessToken(vaultScopes);
Comment thread
VanyLaw marked this conversation as resolved.
Outdated
console.log('kv -----------', result);

setPublishDialogVisiblity(false);
// notifications
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -324,7 +324,7 @@ export const projectDispatcher = () => {
if (profile) {
// ABS Create Flow, update publishProfile after create project
const dispatcher = await snapshot.getPromise(dispatcherState);
const newProfile = getPublishProfileFromPayload(profile, source);
const newProfile = await getPublishProfileFromPayload(profile, source);

newProfile && dispatcher.setPublishTargets([newProfile], projectId);
}
Expand Down
57 changes: 21 additions & 36 deletions Composer/packages/client/src/utils/electronUtil.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@
// Licensed under the MIT License.
import { PublishTarget } from '@bfc/shared';

import { armScopes } from '../constants';

import httpClient from './httpUtil';
import { AuthClient } from './authClient';
/**
* Returns true if the client is embedded in the Composer Electron environment.
*/
Expand All @@ -23,44 +27,25 @@ export type Profile = ABSProfile; // can include PVAProfile or other type of pro
*
* @param profile payload from bf's create protocol
*/
export function getPublishProfileFromPayload(profile: Profile, source: string): PublishTarget | undefined {
switch (source) {
case 'abs': {
const appId = profile.appId;
// parse subscriptionId ... from profile
const temp = { ...profile };
const subs = profile.resourceId.match(/subscriptions\/([\w-]*)\//);
const groups = profile.resourceId.match(/resourceGroups\/([^/]*)/);
const names = profile.resourceId.match(/botServices\/([^/]*)/);
temp.subscriptionId = (subs && subs.length > 0 && subs[1]) || '';
temp.resourceGroup = (groups && groups.length > 0 && groups[1]) || '';
temp.botName = (names && names.length > 0 && names[1]) || '';
delete temp.appId;

console.log(temp);
return {
name: `${source}-${profile.botName}`,
type: 'azurePublish',
configuration: JSON.stringify({
hostname: '',
runtimeIdentifier: 'win-x64',
settings: {
MicrosoftAppId: appId,
},
...temp,
}),
};
}
export async function getPublishProfileFromPayload(
profile: Profile,
source: string
): Promise<PublishTarget | undefined> {
try {
const resutl = await httpClient.post(`/import/${source}/generateProfile`, profile);
Comment thread
VanyLaw marked this conversation as resolved.
Outdated
console.log(resutl);
Comment thread
VanyLaw marked this conversation as resolved.
Outdated
return resutl.data;
} catch (error) {
console.log(error);
}
}

export function getAliasFromPayload(source: string, payload: string): string | undefined {
switch (source) {
case 'abs':
if (payload) {
const profile: ABSProfile = JSON.parse(payload);
const alias = `${source}-${profile.botName}-${profile.appId}`;
return alias;
}
export async function getAliasFromPayload(source: string, payload: string): Promise<string | undefined> {
try {
const resutl = await httpClient.post(`/import/${source}/getAlias`, JSON.parse(payload));
Comment thread
VanyLaw marked this conversation as resolved.
Outdated
console.log(resutl);
Comment thread
VanyLaw marked this conversation as resolved.
Outdated
return resutl.data.alias;
} catch (error) {
console.log(error);
}
}
36 changes: 36 additions & 0 deletions Composer/packages/server/src/controllers/import.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,43 @@ async function startImport(req: ImportRequest, res: Response) {
}
}

async function generateProfile(req: ImportRequest, res: Response) {
const { source } = req.params;
const payload = req.body;

const contentProvider = contentProviderFactory.getProvider({ kind: source, metadata: payload });
if (contentProvider?.generateProfile) {
try {
const profile = await contentProvider.generateProfile();
res.status(200).json(profile);
} catch (err) {
res.status(500).json({ message: err.stack });
}
} else {
res.status(400).json({ message: 'No content provider found for source: ' + source });
}
}

async function getAlias(req: ImportRequest, res: Response) {
const { source } = req.params;
const payload = req.body;

const contentProvider = contentProviderFactory.getProvider({ kind: source, metadata: payload });
if (contentProvider?.getAlias) {
try {
const alias = await contentProvider.getAlias();
res.status(200).json({ alias });
} catch (error) {
res.status(500).json({ message: error.stack });
}
} else {
res.status(400).json({ message: 'No content provider found for source: ' + source });
}
}

export const ImportController = {
authenticate,
startImport,
generateProfile,
getAlias,
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
import { PublishTarget } from '@bfc/shared';
import * as axios from 'axios';
import jwtDecode from 'jwt-decode';

import { authService } from '../services/auth/auth';

import { IContentProviderMetadata, ExternalContentProvider, BotContentInfo } from './externalContentProvider';

export type AzureBotServiceMetadata = IContentProviderMetadata & {
resourceId: string;
botName: string;
appId?: string;
appPasswordHint?: string;
subscriptionId?: string;
resourceGroup?: string;
armEndpoint?: string;
tenantId: string;
};

export class AzureBotServiceProvider extends ExternalContentProvider<AzureBotServiceMetadata> {
constructor(metadata: AzureBotServiceMetadata) {
super(metadata);
}
public async downloadBotContent(): Promise<BotContentInfo> {
throw new Error('ABS Not support');
}
public async cleanUp(): Promise<void> {
throw new Error('Method not implemented.');
}
public async getAlias(): Promise<string> {
const alias = `abs-${this.metadata.botName}-${this.metadata.appId}`;
return alias;
}

public async authenticate(): Promise<string> {
return this.getVaultAccessToken();
}

public async generateProfile(): Promise<PublishTarget> {
const appId = this.metadata.appId;
// parse subscriptionId ... from this.metadata
const temp = { ...this.metadata };
const subs = this.metadata.resourceId.match(/subscriptions\/([\w-]*)\//);
const groups = this.metadata.resourceId.match(/resourceGroups\/([^/]*)/);
const names = this.metadata.resourceId.match(/botServices\/([^/]*)/);
temp.subscriptionId = (subs && subs.length > 0 && subs[1]) || '';
temp.resourceGroup = (groups && groups.length > 0 && groups[1]) || '';
temp.botName = (names && names.length > 0 && names[1]) || '';
delete temp.appId;

// transform key vault token hint to microsoft app password
const hint = this.metadata.appPasswordHint || '';
delete temp.appPasswordHint;
const vaultNames = hint.match(/vaults\/([^/]*)/);
const secretNames = hint.match(/secrets\/([^/]*)/);
const resourceGroupNames = hint.match(/resourceGroups\/([^/]*)/);
const vaultName = (vaultNames && vaultNames.length > 0 && vaultNames[1]) || '';
const secretName = (secretNames && secretNames.length > 0 && secretNames[1]) || '';
const resourceGroupName = (resourceGroupNames && resourceGroupNames.length > 0 && resourceGroupNames[1]) || '';
const tenantId = this.metadata.tenantId;

// get token
const armToken = await this.getArmAccessToken(tenantId);
Comment thread
VanyLaw marked this conversation as resolved.
const vaultToken = await this.getVaultAccessToken();
const decoded = jwtDecode<{ oid: string }>(armToken);

// set key vault policy
await this.keyVaultUpdatePolicy(armToken, temp.subscriptionId, resourceGroupName, vaultName, tenantId, decoded.oid);
const appPwd = await this.keyVaultGetSecretValue(vaultToken, vaultName, secretName);
return {
name: `abs-${this.metadata.botName}`,
type: 'azurePublish',
configuration: JSON.stringify({
hostname: '',
runtimeIdentifier: 'win-x64',
settings: {
MicrosoftAppId: appId,
MicrosoftAppPassword: appPwd,
},
...temp,
}),
};
}

private async keyVaultUpdatePolicy(
token: string,
subscriptionId: string,
resourceGroupName: string,
vaultName: string,
tenantId: string,
objectId: string
) {
const operationKind = 'add';
const url = `https://management.azure.com/subscriptions/${subscriptionId}/resourceGroups/${resourceGroupName}/providers/Microsoft.KeyVault/vaults/${vaultName}/accessPolicies/${operationKind}?api-version=2019-09-01`;
const result = await axios.default.put(
url,
{
properties: {
accessPolicies: [
{
tenantId: tenantId,
objectId: objectId,
permissions: {
secrets: ['get', 'list'],
},
},
],
},
},
{
headers: {
Authorization: `Bearer ${token}`,
'Content-Type': 'application/json',
},
}
);
return result;
}

private async keyVaultGetSecretValue(token: string, vaultName: string, secretName: string) {
const vaultUri = `https://${vaultName}.vault.azure.net/secrets/${secretName}?api-version=7.1`;
const result = await axios.default.get(vaultUri, {
headers: { Authorization: `Bearer ${token}` },
});
return result.data.value;
}

private async getVaultAccessToken(): Promise<string> {
try {
// get an kv token
const authCredentials = {
scopes: ['https://vault.azure.net/user_impersonation'],
targetResource: 'https://vault.azure.net',
};
const accessToken = await authService.getAccessToken(authCredentials);
if (accessToken === '') {
throw 'User cancelled login flow.';
}
return accessToken;
} catch (e) {
throw `Error while trying to get a key vault token: ${e.toString()}`;
}
}

private async getArmAccessToken(tenantId: string): Promise<string> {
try {
const accessToken = await authService.getArmAccessToken(tenantId);
if (accessToken === '') {
throw 'User cancelled login flow.';
}
return accessToken;
} catch (e) {
throw `Error while trying to get a key vault token: ${e.toString()}`;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,20 +5,24 @@ import { ExternalContentProviderType } from '@botframework-composer/types';

import { ExternalContentProvider, IContentProviderMetadata } from './externalContentProvider';
import { PowerVirtualAgentsProvider, PowerVirtualAgentsMetadata } from './powerVirtualAgentsProvider';
import { AzureBotServiceMetadata, AzureBotServiceProvider } from './azureBotServiceProvider';

type ProviderInfo = PowerVirtualAgentsProviderInfo; // union with additional provider info types (ex. PowerVirtualAgentsProviderInfo | ABSProviderInfo | etc.)
type ProviderInfo = PowerVirtualAgentsProviderInfo | AzureBotServiceProviderInfo; // union with additional provider info types (ex. PowerVirtualAgentsProviderInfo | ABSProviderInfo | etc.)

type ProviderInfoBase<T extends IContentProviderMetadata> = {
kind: ExternalContentProviderType;
metadata: T;
};

type PowerVirtualAgentsProviderInfo = ProviderInfoBase<PowerVirtualAgentsMetadata> & { kind: 'pva' };
type AzureBotServiceProviderInfo = ProviderInfoBase<AzureBotServiceMetadata> & { kind: 'abs' };

function getProvider(info: ProviderInfo): ExternalContentProvider<IContentProviderMetadata> | undefined {
switch (info.kind) {
case 'pva':
return new PowerVirtualAgentsProvider(info.metadata);
case 'abs':
return new AzureBotServiceProvider(info.metadata);
default:
return undefined;
}
Expand Down
Loading