Skip to content

Commit

Permalink
Merge pull request #4264 from microsoft/johtaylo/issue4072
Browse files Browse the repository at this point in the history
Johtaylo/issue4072
  • Loading branch information
johnataylor authored Jul 13, 2020
2 parents f85668b + 83ed43b commit 9edaffa
Show file tree
Hide file tree
Showing 32 changed files with 1,245 additions and 12 deletions.
49 changes: 44 additions & 5 deletions Microsoft.Bot.Builder.sln
Original file line number Diff line number Diff line change
Expand Up @@ -181,9 +181,6 @@ EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Bot.Builder.Dialogs.Adaptive.Testing", "libraries\Microsoft.Bot.Builder.Dialogs.Adaptive.Testing\Microsoft.Bot.Builder.Dialogs.Adaptive.Testing.csproj", "{D921D320-0450-455F-8DF2-70EDAC5CCE68}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Skills", "Skills", "{54DA838C-8BB8-4038-8BDB-D887C02B2D9A}"
ProjectSection(SolutionItems) = preProject
tests\Skills\ReadMeForSSOTesting.md = tests\Skills\ReadMeForSSOTesting.md
EndProjectSection
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Parent", "tests\Skills\Parent\Parent.csproj", "{7F8ED2E7-A4BE-4855-BAF2-95657220E419}"
EndProject
Expand All @@ -193,6 +190,19 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Bot.Builder.Dialo
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Bot.Builder.Dialogs.Adaptive.Teams.Tests", "tests\Microsoft.Bot.Builder.Dialogs.Adaptive.Teams.Tests\Microsoft.Bot.Builder.Dialogs.Adaptive.Teams.Tests.csproj", "{DD5071A9-BAE1-45EF-814C-D071BE63CD47}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "SSO", "SSO", "{AA545986-D3E6-406D-8BD5-305B809FB399}"
ProjectSection(SolutionItems) = preProject
tests\Skills\ReadMeForSSOTesting.md = tests\Skills\ReadMeForSSOTesting.md
EndProjectSection
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "End2End", "End2End", "{1024840D-8B5A-4E39-8F6D-F7430DD3749E}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Bot1", "tests\Skills\Bot1\Bot1.csproj", "{2367D12D-2A9B-4F52-8948-78C1EDE9059A}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Bot2", "tests\Skills\Bot2\Bot2.csproj", "{36D6E1D7-2506-4096-8D76-55C3CCD32DFE}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Bot3", "tests\Skills\Bot3\Bot3.csproj", "{85463C41-8D08-4A9D-A475-68A83730431C}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand Down Expand Up @@ -769,6 +779,30 @@ Global
{DD5071A9-BAE1-45EF-814C-D071BE63CD47}.Release|Any CPU.Build.0 = Release|Any CPU
{DD5071A9-BAE1-45EF-814C-D071BE63CD47}.Release-Windows|Any CPU.ActiveCfg = Release|Any CPU
{DD5071A9-BAE1-45EF-814C-D071BE63CD47}.Release-Windows|Any CPU.Build.0 = Release|Any CPU
{2367D12D-2A9B-4F52-8948-78C1EDE9059A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{2367D12D-2A9B-4F52-8948-78C1EDE9059A}.Debug|Any CPU.Build.0 = Debug|Any CPU
{2367D12D-2A9B-4F52-8948-78C1EDE9059A}.Debug-Windows|Any CPU.ActiveCfg = Debug|Any CPU
{2367D12D-2A9B-4F52-8948-78C1EDE9059A}.Debug-Windows|Any CPU.Build.0 = Debug|Any CPU
{2367D12D-2A9B-4F52-8948-78C1EDE9059A}.Release|Any CPU.ActiveCfg = Release|Any CPU
{2367D12D-2A9B-4F52-8948-78C1EDE9059A}.Release|Any CPU.Build.0 = Release|Any CPU
{2367D12D-2A9B-4F52-8948-78C1EDE9059A}.Release-Windows|Any CPU.ActiveCfg = Release|Any CPU
{2367D12D-2A9B-4F52-8948-78C1EDE9059A}.Release-Windows|Any CPU.Build.0 = Release|Any CPU
{36D6E1D7-2506-4096-8D76-55C3CCD32DFE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{36D6E1D7-2506-4096-8D76-55C3CCD32DFE}.Debug|Any CPU.Build.0 = Debug|Any CPU
{36D6E1D7-2506-4096-8D76-55C3CCD32DFE}.Debug-Windows|Any CPU.ActiveCfg = Debug|Any CPU
{36D6E1D7-2506-4096-8D76-55C3CCD32DFE}.Debug-Windows|Any CPU.Build.0 = Debug|Any CPU
{36D6E1D7-2506-4096-8D76-55C3CCD32DFE}.Release|Any CPU.ActiveCfg = Release|Any CPU
{36D6E1D7-2506-4096-8D76-55C3CCD32DFE}.Release|Any CPU.Build.0 = Release|Any CPU
{36D6E1D7-2506-4096-8D76-55C3CCD32DFE}.Release-Windows|Any CPU.ActiveCfg = Release|Any CPU
{36D6E1D7-2506-4096-8D76-55C3CCD32DFE}.Release-Windows|Any CPU.Build.0 = Release|Any CPU
{85463C41-8D08-4A9D-A475-68A83730431C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{85463C41-8D08-4A9D-A475-68A83730431C}.Debug|Any CPU.Build.0 = Debug|Any CPU
{85463C41-8D08-4A9D-A475-68A83730431C}.Debug-Windows|Any CPU.ActiveCfg = Debug|Any CPU
{85463C41-8D08-4A9D-A475-68A83730431C}.Debug-Windows|Any CPU.Build.0 = Debug|Any CPU
{85463C41-8D08-4A9D-A475-68A83730431C}.Release|Any CPU.ActiveCfg = Release|Any CPU
{85463C41-8D08-4A9D-A475-68A83730431C}.Release|Any CPU.Build.0 = Release|Any CPU
{85463C41-8D08-4A9D-A475-68A83730431C}.Release-Windows|Any CPU.ActiveCfg = Release|Any CPU
{85463C41-8D08-4A9D-A475-68A83730431C}.Release-Windows|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand Down Expand Up @@ -848,10 +882,15 @@ Global
{2FBA2BB7-73C8-45CD-99B7-D65F817C9051} = {AD743B78-D61F-4FBF-B620-FA83CE599A50}
{D921D320-0450-455F-8DF2-70EDAC5CCE68} = {4269F3C3-6B42-419B-B64A-3E6DC0F1574A}
{54DA838C-8BB8-4038-8BDB-D887C02B2D9A} = {AD743B78-D61F-4FBF-B620-FA83CE599A50}
{7F8ED2E7-A4BE-4855-BAF2-95657220E419} = {54DA838C-8BB8-4038-8BDB-D887C02B2D9A}
{1958FBA4-BF2D-48D9-A5DB-8915F553EBD3} = {54DA838C-8BB8-4038-8BDB-D887C02B2D9A}
{7F8ED2E7-A4BE-4855-BAF2-95657220E419} = {AA545986-D3E6-406D-8BD5-305B809FB399}
{1958FBA4-BF2D-48D9-A5DB-8915F553EBD3} = {AA545986-D3E6-406D-8BD5-305B809FB399}
{23BF6935-D648-4874-91C7-1BF4D6FF64E3} = {4269F3C3-6B42-419B-B64A-3E6DC0F1574A}
{DD5071A9-BAE1-45EF-814C-D071BE63CD47} = {AD743B78-D61F-4FBF-B620-FA83CE599A50}
{AA545986-D3E6-406D-8BD5-305B809FB399} = {54DA838C-8BB8-4038-8BDB-D887C02B2D9A}
{1024840D-8B5A-4E39-8F6D-F7430DD3749E} = {54DA838C-8BB8-4038-8BDB-D887C02B2D9A}
{2367D12D-2A9B-4F52-8948-78C1EDE9059A} = {1024840D-8B5A-4E39-8F6D-F7430DD3749E}
{36D6E1D7-2506-4096-8D76-55C3CCD32DFE} = {1024840D-8B5A-4E39-8F6D-F7430DD3749E}
{85463C41-8D08-4A9D-A475-68A83730431C} = {1024840D-8B5A-4E39-8F6D-F7430DD3749E}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {7173C9F3-A7F9-496E-9078-9156E35D6E16}
Expand Down
7 changes: 5 additions & 2 deletions libraries/Microsoft.Bot.Builder/Skills/SkillHandler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,8 @@ private async Task<ResourceResponse> ProcessActivityAsync(ClaimsIdentity claimsI
throw new KeyNotFoundException();
}

ResourceResponse resourceResponse = null;

var callback = new BotCallbackHandler(async (turnContext, ct) =>
{
turnContext.TurnState.Add(SkillConversationReferenceKey, skillConversationReference);
Expand All @@ -195,13 +197,14 @@ private async Task<ResourceResponse> ProcessActivityAsync(ClaimsIdentity claimsI
await _bot.OnTurnAsync(turnContext, ct).ConfigureAwait(false);
break;
default:
await turnContext.SendActivityAsync(activity, cancellationToken).ConfigureAwait(false);
resourceResponse = await turnContext.SendActivityAsync(activity, cancellationToken).ConfigureAwait(false);
break;
}
});

await _adapter.ContinueConversationAsync(claimsIdentity, skillConversationReference.ConversationReference, skillConversationReference.OAuthScope, callback, cancellationToken).ConfigureAwait(false);
return new ResourceResponse(Guid.NewGuid().ToString("N", CultureInfo.InvariantCulture));

return resourceResponse ?? new ResourceResponse(Guid.NewGuid().ToString("N", CultureInfo.InvariantCulture));
}
}
}
14 changes: 9 additions & 5 deletions tests/Microsoft.Bot.Builder.Tests/Skills/SkillHandlerTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -96,23 +96,27 @@ public async Task LegacyConversationIdFactoryTest()
[Fact]
public async Task OnSendToConversationAsyncTest()
{
BotCallbackHandler botCallback = null;
_mockAdapter.Setup(x => x.ContinueConversationAsync(It.IsAny<ClaimsIdentity>(), It.IsAny<ConversationReference>(), It.IsAny<string>(), It.IsAny<BotCallbackHandler>(), It.IsAny<CancellationToken>()))
.Callback<ClaimsIdentity, ConversationReference, string, BotCallbackHandler, CancellationToken>((identity, reference, audience, callback, cancellationToken) =>
{
botCallback = callback;
callback(new TurnContext(_mockAdapter.Object, _conversationReference.GetContinuationActivity()), CancellationToken.None).Wait();
});

_mockAdapter.Setup(x => x.SendActivitiesAsync(It.IsAny<ITurnContext>(), It.IsAny<Activity[]>(), It.IsAny<CancellationToken>()))
.Callback<ITurnContext, Activity[], CancellationToken>((turnContext, activities, cancellationToken) =>
{
})
.Returns(Task.FromResult(new[] { new ResourceResponse { Id = "resourceId" } }));

var sut = CreateSkillHandlerForTesting();

var activity = (Activity)Activity.CreateMessageActivity();
activity.ApplyConversationReference(_conversationReference);

Assert.Null(activity.CallerId);
await sut.TestOnSendToConversationAsync(_claimsIdentity, _conversationId, activity, CancellationToken.None);
Assert.NotNull(botCallback);
await botCallback.Invoke(new TurnContext(_mockAdapter.Object, _conversationReference.GetContinuationActivity()), CancellationToken.None);
var resourceResponse = await sut.TestOnSendToConversationAsync(_claimsIdentity, _conversationId, activity, CancellationToken.None);
Assert.Null(activity.CallerId);
Assert.Equal("resourceId", resourceResponse.Id);
}

[Fact]
Expand Down
24 changes: 24 additions & 0 deletions tests/Skills/Bot1/AdapterWithErrorHandler.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

using System.Threading.Tasks;
using Microsoft.Bot.Builder.Integration.AspNet.Core;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;

namespace Microsoft.BotBuilderSamples
{
public class AdapterWithErrorHandler : BotFrameworkHttpAdapter
{
public AdapterWithErrorHandler(IConfiguration configuration, ILogger<BotFrameworkHttpAdapter> logger)
: base(configuration, logger)
{
OnTurnError = (turnContext, exception) =>
{
// Log any leaked exception from the application.
logger.LogError($"Exception caught : {exception.Message}");
return Task.CompletedTask;
};
}
}
}
47 changes: 47 additions & 0 deletions tests/Skills/Bot1/AllowedSkillsClaimsValidator.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Security.Claims;
using System.Threading.Tasks;
using Microsoft.Bot.Connector.Authentication;

namespace Microsoft.BotBuilderSamples.SimpleRootBot.Authentication
{
/// <summary>
/// Sample claims validator that loads an allowed list from configuration if present
/// and checks that responses are coming from configured skills.
/// </summary>
public class AllowedSkillsClaimsValidator : ClaimsValidator
{
private readonly List<string> _allowedSkills;

public AllowedSkillsClaimsValidator(SkillsConfiguration skillsConfig)
{
if (skillsConfig == null)
{
throw new ArgumentNullException(nameof(skillsConfig));
}

// Load the appIds for the configured skills (we will only allow responses from skills we have configured).
_allowedSkills = (from skill in skillsConfig.Skills.Values select skill.AppId).ToList();
}

public override Task ValidateClaimsAsync(IList<Claim> claims)
{
if (SkillValidation.IsSkillClaim(claims))
{
// Check that the appId claim in the skill request is in the list of skills configured for this bot.
var appId = JwtTokenValidation.GetAppIdFromClaims(claims);
if (!_allowedSkills.Contains(appId))
{
throw new UnauthorizedAccessException($"Received a request from an application with an appID of \"{appId}\". To enable requests from this skill, add the skill to your configuration file.");
}
}

return Task.CompletedTask;
}
}
}
153 changes: 153 additions & 0 deletions tests/Skills/Bot1/Bot1.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Bot.Builder;
using Microsoft.Bot.Builder.Integration.AspNet.Core.Skills;
using Microsoft.Bot.Builder.Skills;
using Microsoft.Bot.Connector.Authentication;
using Microsoft.Bot.Schema;
using Microsoft.BotBuilderSamples.SimpleRootBot;
using Microsoft.Extensions.Configuration;
using Newtonsoft.Json;

namespace Bot1
{
public class Bot1 : ActivityHandler
{
public static readonly string ActiveSkillPropertyName = $"{typeof(Bot1).FullName}.ActiveSkillProperty";
private readonly IStatePropertyAccessor<BotFrameworkSkill> _activeSkillProperty;
private readonly string _botId;
private readonly ConversationState _conversationState;
private readonly SkillHttpClient _skillClient;
private readonly SkillsConfiguration _skillsConfig;
private readonly BotFrameworkSkill _targetSkill;

public Bot1(ConversationState conversationState, SkillsConfiguration skillsConfig, SkillHttpClient skillClient, IConfiguration configuration)
{
_conversationState = conversationState ?? throw new ArgumentNullException(nameof(conversationState));
_skillsConfig = skillsConfig ?? throw new ArgumentNullException(nameof(skillsConfig));
_skillClient = skillClient ?? throw new ArgumentNullException(nameof(skillsConfig));
if (configuration == null)
{
throw new ArgumentNullException(nameof(configuration));
}

_botId = configuration.GetSection(MicrosoftAppCredentials.MicrosoftAppIdKey)?.Value;
if (string.IsNullOrWhiteSpace(_botId))
{
throw new ArgumentException($"{MicrosoftAppCredentials.MicrosoftAppIdKey} is not set in configuration");
}

// We use a single skill in this example.
var targetSkillId = "NextBot";
if (!_skillsConfig.Skills.TryGetValue(targetSkillId, out _targetSkill))
{
throw new ArgumentException($"Skill with ID \"{targetSkillId}\" not found in configuration");
}

// Create state property to track the active skill
_activeSkillProperty = conversationState.CreateProperty<BotFrameworkSkill>(ActiveSkillPropertyName);
}

public override async Task OnTurnAsync(ITurnContext turnContext, CancellationToken cancellationToken = default)
{
// Forward all activities except EndOfConversation to the skill.
if (turnContext.Activity.Type != ActivityTypes.EndOfConversation)
{
// Try to get the active skill
var activeSkill = await _activeSkillProperty.GetAsync(turnContext, () => null, cancellationToken);
if (activeSkill != null)
{
// Send the activity to the skill
await SendToSkill(turnContext, activeSkill, cancellationToken);
return;
}
}

await base.OnTurnAsync(turnContext, cancellationToken);

// Save any state changes that might have occured during the turn.
await _conversationState.SaveChangesAsync(turnContext, false, cancellationToken);
}

protected override async Task OnMessageActivityAsync(ITurnContext<IMessageActivity> turnContext, CancellationToken cancellationToken)
{
if (turnContext.Activity.Text.Contains("skill"))
{
await turnContext.SendActivityAsync(MessageFactory.Text("Got it, connecting you to the skill..."), cancellationToken);

// Save active skill in state
await _activeSkillProperty.SetAsync(turnContext, _targetSkill, cancellationToken);

// Send the activity to the skill
await SendToSkill(turnContext, _targetSkill, cancellationToken);
return;
}

// just respond
await turnContext.SendActivityAsync(MessageFactory.Text("Me no nothin'. Say \"skill\" and I'll patch you through"), cancellationToken);

// Save conversation state
await _conversationState.SaveChangesAsync(turnContext, force: true, cancellationToken: cancellationToken);
}

protected override async Task OnEndOfConversationActivityAsync(ITurnContext<IEndOfConversationActivity> turnContext, CancellationToken cancellationToken)
{
// forget skill invocation
await _activeSkillProperty.DeleteAsync(turnContext, cancellationToken);

// Show status message, text and value returned by the skill
var eocActivityMessage = $"Received {ActivityTypes.EndOfConversation}.\n\nCode: {turnContext.Activity.Code}";
if (!string.IsNullOrWhiteSpace(turnContext.Activity.Text))
{
eocActivityMessage += $"\n\nText: {turnContext.Activity.Text}";
}

if ((turnContext.Activity as Activity)?.Value != null)
{
eocActivityMessage += $"\n\nValue: {JsonConvert.SerializeObject((turnContext.Activity as Activity)?.Value)}";
}

await turnContext.SendActivityAsync(MessageFactory.Text(eocActivityMessage), cancellationToken);

// We are back at the root
await turnContext.SendActivityAsync(MessageFactory.Text("Back in the root bot. Say \"skill\" and I'll patch you through"), cancellationToken);

// Save conversation state
await _conversationState.SaveChangesAsync(turnContext, cancellationToken: cancellationToken);
}

protected override async Task OnMembersAddedAsync(IList<ChannelAccount> membersAdded, ITurnContext<IConversationUpdateActivity> turnContext, CancellationToken cancellationToken)
{
foreach (var member in membersAdded)
{
if (member.Id != turnContext.Activity.Recipient.Id)
{
await turnContext.SendActivityAsync(MessageFactory.Text("Hello and welcome!"), cancellationToken);
}
}
}

private async Task SendToSkill(ITurnContext turnContext, BotFrameworkSkill targetSkill, CancellationToken cancellationToken)
{
// NOTE: Always SaveChanges() before calling a skill so that any activity generated by the skill
// will have access to current accurate state.
await _conversationState.SaveChangesAsync(turnContext, force: true, cancellationToken: cancellationToken);

// route the activity to the skill
var response = await _skillClient.PostActivityAsync(_botId, targetSkill, _skillsConfig.SkillHostEndpoint, turnContext.Activity, cancellationToken);

// Check response status
if (!response.IsSuccessStatusCode())
{
throw new HttpRequestException($"Error invoking the skill id: \"{targetSkill.Id}\" at \"{targetSkill.SkillEndpoint}\" (status is {response.Status}). \r\n {response.Body}");
}
}
}
}
Loading

0 comments on commit 9edaffa

Please sign in to comment.