From 31eef7d89b010a1387f2ea6175ef7e107df0f0b4 Mon Sep 17 00:00:00 2001 From: Martin Battaglino Date: Wed, 14 Jul 2021 16:16:17 -0300 Subject: [PATCH 01/27] Move InvokeResponse and TypedInvokeResponse to bot-schema --- .../microsoft/bot/builder/InvokeResponse.java | 63 +-------------- .../bot/builder/TypedInvokeResponse.java | 20 +---- .../microsoft/bot/schema/InvokeResponse.java | 81 +++++++++++++++++++ .../bot/schema/TypedInvokeResponse.java | 41 ++++++++++ 4 files changed, 127 insertions(+), 78 deletions(-) create mode 100644 libraries/bot-schema/src/main/java/com/microsoft/bot/schema/InvokeResponse.java create mode 100644 libraries/bot-schema/src/main/java/com/microsoft/bot/schema/TypedInvokeResponse.java diff --git a/libraries/bot-builder/src/main/java/com/microsoft/bot/builder/InvokeResponse.java b/libraries/bot-builder/src/main/java/com/microsoft/bot/builder/InvokeResponse.java index 483c23de4..d9fcf71cc 100644 --- a/libraries/bot-builder/src/main/java/com/microsoft/bot/builder/InvokeResponse.java +++ b/libraries/bot-builder/src/main/java/com/microsoft/bot/builder/InvokeResponse.java @@ -9,18 +9,8 @@ * the resulting POST. The Body of the resulting POST will be the JSON * Serialized content from the Body property. */ -public class InvokeResponse { - - /** - * The POST that is generated in response to the incoming Invoke Activity will - * have the HTTP Status code specified by this field. - */ - private int status; - /** - * The POST that is generated in response to the incoming Invoke Activity will - * have a body generated by JSON serializing the object in the Body field. - */ - private Object body; +@Deprecated // Use the one of bot-schema +public class InvokeResponse extends com.microsoft.bot.schema.InvokeResponse { /** * Initializes new instance of InvokeResponse. @@ -29,53 +19,6 @@ public class InvokeResponse { * @param withBody The invoke response body. */ public InvokeResponse(int withStatus, Object withBody) { - status = withStatus; - body = withBody; - } - - /** - * Gets the HTTP status code for the response. - * - * @return The HTTP status code. - */ - public int getStatus() { - return status; - } - - /** - * Sets the HTTP status code for the response. - * - * @param withStatus The HTTP status code. - */ - public void setStatus(int withStatus) { - this.status = withStatus; + super(withStatus, withBody); } - - /** - * Gets the body content for the response. - * - * @return The body content. - */ - public Object getBody() { - return body; - } - - /** - * Sets the body content for the response. - * - * @param withBody The body content. - */ - public void setBody(Object withBody) { - body = withBody; - } - - /** - * Returns if the status of the request was successful. - * @return True if the status code is successful, false if not. - */ - @SuppressWarnings("MagicNumber") - public boolean getIsSuccessStatusCode() { - return status >= 200 && status <= 299; - } - } diff --git a/libraries/bot-builder/src/main/java/com/microsoft/bot/builder/TypedInvokeResponse.java b/libraries/bot-builder/src/main/java/com/microsoft/bot/builder/TypedInvokeResponse.java index 7ac36c695..942af6a30 100644 --- a/libraries/bot-builder/src/main/java/com/microsoft/bot/builder/TypedInvokeResponse.java +++ b/libraries/bot-builder/src/main/java/com/microsoft/bot/builder/TypedInvokeResponse.java @@ -10,7 +10,8 @@ * Serialized content from the Body property. * @param The type for the body of the TypedInvokeResponse. */ -public class TypedInvokeResponse extends InvokeResponse { +@Deprecated // Use the one of bot-schema +public class TypedInvokeResponse extends com.microsoft.bot.schema.TypedInvokeResponse { /** * Initializes new instance of InvokeResponse. @@ -21,21 +22,4 @@ public class TypedInvokeResponse extends InvokeResponse { public TypedInvokeResponse(int withStatus, T withBody) { super(withStatus, withBody); } - - /** - * Sets the body with a typed value. - * @param withBody the typed value to set the body to. - */ - public void setTypedBody(T withBody) { - super.setBody(withBody); - } - - /** - * Gets the body content for the response. - * - * @return The body content. - */ - public T getTypedBody() { - return (T) super.getBody(); - } } diff --git a/libraries/bot-schema/src/main/java/com/microsoft/bot/schema/InvokeResponse.java b/libraries/bot-schema/src/main/java/com/microsoft/bot/schema/InvokeResponse.java new file mode 100644 index 000000000..8900ae7cc --- /dev/null +++ b/libraries/bot-schema/src/main/java/com/microsoft/bot/schema/InvokeResponse.java @@ -0,0 +1,81 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.bot.schema; + +/** + * Tuple class containing an HTTP Status Code and a JSON Serializable object. + * The HTTP Status code is, in the invoke activity scenario, what will be set in + * the resulting POST. The Body of the resulting POST will be the JSON + * Serialized content from the Body property. + */ +public class InvokeResponse { + + /** + * The POST that is generated in response to the incoming Invoke Activity will + * have the HTTP Status code specified by this field. + */ + private int status; + /** + * The POST that is generated in response to the incoming Invoke Activity will + * have a body generated by JSON serializing the object in the Body field. + */ + private Object body; + + /** + * Initializes new instance of InvokeResponse. + * + * @param withStatus The invoke response status. + * @param withBody The invoke response body. + */ + public InvokeResponse(int withStatus, Object withBody) { + status = withStatus; + body = withBody; + } + + /** + * Gets the HTTP status code for the response. + * + * @return The HTTP status code. + */ + public int getStatus() { + return status; + } + + /** + * Sets the HTTP status code for the response. + * + * @param withStatus The HTTP status code. + */ + public void setStatus(int withStatus) { + this.status = withStatus; + } + + /** + * Gets the body content for the response. + * + * @return The body content. + */ + public Object getBody() { + return body; + } + + /** + * Sets the body content for the response. + * + * @param withBody The body content. + */ + public void setBody(Object withBody) { + body = withBody; + } + + /** + * Returns if the status of the request was successful. + * @return True if the status code is successful, false if not. + */ + @SuppressWarnings("MagicNumber") + public boolean getIsSuccessStatusCode() { + return status >= 200 && status <= 299; + } + +} diff --git a/libraries/bot-schema/src/main/java/com/microsoft/bot/schema/TypedInvokeResponse.java b/libraries/bot-schema/src/main/java/com/microsoft/bot/schema/TypedInvokeResponse.java new file mode 100644 index 000000000..6dd88e595 --- /dev/null +++ b/libraries/bot-schema/src/main/java/com/microsoft/bot/schema/TypedInvokeResponse.java @@ -0,0 +1,41 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.bot.schema; + +/** + * Tuple class containing an HTTP Status Code and a JSON Serializable object. + * The HTTP Status code is, in the invoke activity scenario, what will be set in + * the resulting POST. The Body of the resulting POST will be the JSON + * Serialized content from the Body property. + * @param The type for the body of the TypedInvokeResponse. + */ +public class TypedInvokeResponse extends InvokeResponse { + + /** + * Initializes new instance of InvokeResponse. + * + * @param withStatus The invoke response status. + * @param withBody The invoke response body. + */ + public TypedInvokeResponse(int withStatus, T withBody) { + super(withStatus, withBody); + } + + /** + * Sets the body with a typed value. + * @param withBody the typed value to set the body to. + */ + public void setTypedBody(T withBody) { + super.setBody(withBody); + } + + /** + * Gets the body content for the response. + * + * @return The body content. + */ + public T getTypedBody() { + return (T) super.getBody(); + } +} From 0f8a03aa0d11a961b9d0cad66c759af1e6069ec0 Mon Sep 17 00:00:00 2001 From: Martin Battaglino Date: Wed, 14 Jul 2021 16:17:09 -0300 Subject: [PATCH 02/27] Relocate BotFrameworkClient to bot-connector --- .../microsoft/bot/connector}/skills/BotFrameworkClient.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) rename libraries/{bot-builder/src/main/java/com/microsoft/bot/builder => bot-connector/src/main/java/com/microsoft/bot/connector}/skills/BotFrameworkClient.java (96%) diff --git a/libraries/bot-builder/src/main/java/com/microsoft/bot/builder/skills/BotFrameworkClient.java b/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/skills/BotFrameworkClient.java similarity index 96% rename from libraries/bot-builder/src/main/java/com/microsoft/bot/builder/skills/BotFrameworkClient.java rename to libraries/bot-connector/src/main/java/com/microsoft/bot/connector/skills/BotFrameworkClient.java index 0db9994bd..757e169f2 100644 --- a/libraries/bot-builder/src/main/java/com/microsoft/bot/builder/skills/BotFrameworkClient.java +++ b/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/skills/BotFrameworkClient.java @@ -1,13 +1,13 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -package com.microsoft.bot.builder.skills; +package com.microsoft.bot.connector.skills; import java.net.URI; import java.util.concurrent.CompletableFuture; -import com.microsoft.bot.builder.TypedInvokeResponse; import com.microsoft.bot.schema.Activity; +import com.microsoft.bot.schema.TypedInvokeResponse; /** * A Bot Framework client. From f4ad8ab5d1e074e6c11d927dded193fbb2d44ac8 Mon Sep 17 00:00:00 2001 From: Martin Battaglino Date: Wed, 14 Jul 2021 16:18:02 -0300 Subject: [PATCH 03/27] Add Skill handle of CloudAdapter --- .../bot/builder/skills/CloudSkillHandler.java | 151 +++++++++ .../bot/builder/skills/SkillHandlerImpl.java | 293 ++++++++++++++++++ 2 files changed, 444 insertions(+) create mode 100644 libraries/bot-builder/src/main/java/com/microsoft/bot/builder/skills/CloudSkillHandler.java create mode 100644 libraries/bot-builder/src/main/java/com/microsoft/bot/builder/skills/SkillHandlerImpl.java diff --git a/libraries/bot-builder/src/main/java/com/microsoft/bot/builder/skills/CloudSkillHandler.java b/libraries/bot-builder/src/main/java/com/microsoft/bot/builder/skills/CloudSkillHandler.java new file mode 100644 index 000000000..ce461327e --- /dev/null +++ b/libraries/bot-builder/src/main/java/com/microsoft/bot/builder/skills/CloudSkillHandler.java @@ -0,0 +1,151 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.bot.builder.skills; + +import com.microsoft.bot.builder.Bot; +import com.microsoft.bot.builder.BotAdapter; +import com.microsoft.bot.builder.CloudChannelServiceHandler; +import com.microsoft.bot.connector.authentication.BotFrameworkAuthentication; +import com.microsoft.bot.connector.authentication.ClaimsIdentity; +import com.microsoft.bot.schema.Activity; +import com.microsoft.bot.schema.ResourceResponse; + +import java.util.concurrent.CompletableFuture; + +/** + * A Bot Framework Handler for skills. + */ +public class CloudSkillHandler extends CloudChannelServiceHandler { + + // The skill conversation reference. + public static final String SKILL_CONVERSATION_REFERENCE_KEY = + "com.microsoft.bot.builder.skills.SkillConversationReference"; + + // Delegate that implements actual logic + private final SkillHandlerImpl inner; + + /** + * Initializes a new instance of the {@link CloudSkillHandler} class using BotFrameworkAuth. + * @param adapter An instance of the {@link BotAdapter} that will handle the request. + * @param bot The {@link Bot} instance. + * @param conversationIdFactory A {@link SkillConversationIdFactoryBase} to unpack the conversation ID and map it + * to the calling bot. + * @param auth Bot Framework Authentication to use. + */ + public CloudSkillHandler( + BotAdapter adapter, + Bot bot, + SkillConversationIdFactoryBase conversationIdFactory, + BotFrameworkAuthentication auth) { + super(auth); + + if (adapter == null) { + throw new IllegalArgumentException("adapter cannot be null"); + } + + if (bot == null) { + throw new IllegalArgumentException("bot cannot be null"); + } + + if (conversationIdFactory == null) { + throw new IllegalArgumentException("conversationIdFactory cannot be null"); + } + + inner = new SkillHandlerImpl( + SKILL_CONVERSATION_REFERENCE_KEY, + adapter, + bot, + conversationIdFactory, + auth::getOriginatingAudience); + } + + /** + * sendToConversation() API for Skill. + * + * This method allows you to send an activity to the end of a conversation. + * + * This is slightly different from replyToActivity(). + * * sendToConversation(conversationId) - will append the activity to the end + * of the conversation according to the timestamp or semantics of the channel. + * * replyToActivity(conversationId,ActivityId) - adds the activity as a reply + * to another activity, if the channel supports it. If the channel does not + * support nested replies, ReplyToActivity falls back to sendToConversation. + * + * Use replyToActivity when replying to a specific activity in the + * conversation. + * + * Use sendToConversation in all other cases. + * + * @param claimsIdentity claimsIdentity for the bot, should have + * AudienceClaim, AppIdClaim and ServiceUrlClaim. + * @param conversationId conversationId. + * @param activity Activity to send. + * + * @return Task for a resource response. + */ + @Override + protected CompletableFuture onSendToConversation( + ClaimsIdentity claimsIdentity, + String conversationId, + Activity activity) { + return inner.onSendToConversation(claimsIdentity, conversationId, activity); + } + + /** + * replyToActivity() API for Skill. + * + * This method allows you to reply to an activity. + * + * This is slightly different from sendToConversation(). + * * SendToConversation(conversationId) - will append the activity to the end + * of the conversation according to the timestamp or semantics of the channel. + * * ReplyToActivity(conversationId,ActivityId) - adds the activity as a reply + * to another activity, if the channel supports it. If the channel does not + * support nested replies, ReplyToActivity falls back to SendToConversation. + * + * Use ReplyToActivity when replying to a specific activity in the + * conversation. + * + * Use sendToConversation in all other cases. + * + * @param claimsIdentity claimsIdentity for the bot, should have + * AudienceClaim, AppIdClaim and ServiceUrlClaim. + * @param conversationId Conversation ID. + * @param activityId activityId the reply is to (OPTIONAL). + * @param activity Activity to send. + * + * @return Task for a resource response. + */ + @Override + protected CompletableFuture onReplyToActivity( + ClaimsIdentity claimsIdentity, + String conversationId, + String activityId, + Activity activity) { + return inner.onReplyToActivity(claimsIdentity, conversationId, activityId, activity); + } + + /** + * {@inheritDoc} + */ + @Override + protected CompletableFuture onDeleteActivity( + ClaimsIdentity claimsIdentity, + String conversationId, + String activityId) { + return inner.onDeleteActivity(claimsIdentity, conversationId, activityId); + } + + /** + * {@inheritDoc} + */ + @Override + protected CompletableFuture onUpdateActivity( + ClaimsIdentity claimsIdentity, + String conversationId, + String activityId, + Activity activity) { + return inner.onUpdateActivity(claimsIdentity, conversationId, activityId, activity); + } +} diff --git a/libraries/bot-builder/src/main/java/com/microsoft/bot/builder/skills/SkillHandlerImpl.java b/libraries/bot-builder/src/main/java/com/microsoft/bot/builder/skills/SkillHandlerImpl.java new file mode 100644 index 000000000..dc101d2a9 --- /dev/null +++ b/libraries/bot-builder/src/main/java/com/microsoft/bot/builder/skills/SkillHandlerImpl.java @@ -0,0 +1,293 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.bot.builder.skills; + +import com.fasterxml.jackson.databind.JsonNode; +import com.microsoft.bot.builder.Bot; +import com.microsoft.bot.builder.BotAdapter; +import com.microsoft.bot.builder.BotCallbackHandler; +import com.microsoft.bot.builder.TurnContext; +import com.microsoft.bot.connector.authentication.ClaimsIdentity; +import com.microsoft.bot.connector.authentication.JwtTokenValidation; +import com.microsoft.bot.schema.Activity; +import com.microsoft.bot.schema.ActivityTypes; +import com.microsoft.bot.schema.CallerIdConstants; +import com.microsoft.bot.schema.ConversationReference; +import com.microsoft.bot.schema.ResourceResponse; +import org.apache.commons.lang3.NotImplementedException; +import org.apache.commons.lang3.StringUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.Map; +import java.util.UUID; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.atomic.AtomicReference; +import java.util.function.Supplier; + +/** + * This class inherited all the implementations of {@link SkillHandler} + * class since we needed similar code for {@link CloudSkillHandler}. + * The {@link CloudSkillHandler} class differs from {@link SkillHandler} + * class only in authentication by making use of {@link BotFrameworkAuthentication} class. + * This class is internal since it is only used in skill handler classes. + */ +public class SkillHandlerImpl { + + private final String skillConversationReferenceKey; + private final BotAdapter adapter; + private final Bot bot; + private final SkillConversationIdFactoryBase conversationIdFactory; + private final Supplier getOAuthScope; + private Logger logger = LoggerFactory.getLogger(SkillHandlerImpl.class); + + public SkillHandlerImpl( + String withSkillConversationReferenceKey, + BotAdapter withAdapter, + Bot withBot, + SkillConversationIdFactoryBase withConversationIdFactory, + Supplier withGetOAuthScope) { + if (StringUtils.isBlank(withSkillConversationReferenceKey)) { + throw new IllegalArgumentException("skillConversationReferenceKey cannot be null"); + } + if (withAdapter == null) { + throw new IllegalArgumentException("adapter cannot be null"); + } + + if (withBot == null) { + throw new IllegalArgumentException("bot cannot be null"); + } + + if (withConversationIdFactory == null) { + throw new IllegalArgumentException("withConversationIdFactory cannot be null"); + } + + this.skillConversationReferenceKey = withSkillConversationReferenceKey; + this.adapter = withAdapter; + this.bot = withBot; + this.conversationIdFactory = withConversationIdFactory; + this.getOAuthScope = withGetOAuthScope; + } + + /** + * + * @param claimsIdentity claimsIdentity for the bot, should have + * AudienceClaim, AppIdClaim and ServiceUrlClaim. + * @param conversationId conversationId. + * @param activity Activity to send. + * + * @return Task for a resource response. + */ + public CompletableFuture onSendToConversation( + ClaimsIdentity claimsIdentity, + String conversationId, + Activity activity) { + return processActivity(claimsIdentity, conversationId, null, activity); + } + + /** + * @param claimsIdentity claimsIdentity for the bot, should have + * AudienceClaim, AppIdClaim and ServiceUrlClaim. + * @param conversationId Conversation ID. + * @param activityId activityId the reply is to (OPTIONAL). + * @param activity Activity to send. + * + * @return Task for a resource response. + */ + public CompletableFuture onReplyToActivity( + ClaimsIdentity claimsIdentity, + String conversationId, + String activityId, + Activity activity) { + return processActivity(claimsIdentity, conversationId, activityId, activity); + } + + /** + * @param claimsIdentity claimsIdentity for the bot, should have + * AudienceClaim, AppIdClaim and ServiceUrlClaim. + * @param conversationId Conversation ID. + * @param activityId activityId to delete. + * + * @return Task with void value. + */ + public CompletableFuture onDeleteActivity( + ClaimsIdentity claimsIdentity, + String conversationId, + String activityId) { + + SkillConversationReference skillConversationReference = getSkillConversationReference(conversationId).join(); + + BotCallbackHandler callback = turnContext -> { + turnContext.getTurnState().add(this.skillConversationReferenceKey, skillConversationReference); + return turnContext.deleteActivity(activityId); + }; + + return adapter.continueConversation(claimsIdentity, + skillConversationReference.getConversationReference(), + skillConversationReference.getOAuthScope(), + callback); + } + + /** + * @param claimsIdentity claimsIdentity for the bot, should have + * AudienceClaim, AppIdClaim and ServiceUrlClaim. + * @param conversationId Conversation ID. + * @param activityId activityId the reply is to (OPTIONAL). + * @param activity Activity to send. + * + * @return Task for a resource response. + */ + public CompletableFuture onUpdateActivity( + ClaimsIdentity claimsIdentity, + String conversationId, + String activityId, + Activity activity) { + SkillConversationReference skillConversationReference = getSkillConversationReference(conversationId).join(); + + AtomicReference resourceResponse = new AtomicReference(); + + BotCallbackHandler callback = turnContext -> { + turnContext.getTurnState().add(this.skillConversationReferenceKey, skillConversationReference); + activity.applyConversationReference(skillConversationReference.getConversationReference()); + turnContext.getActivity().setId(activityId); + String callerId = String.format("%s%s", + CallerIdConstants.BOT_TO_BOT_PREFIX, + JwtTokenValidation.getAppIdFromClaims(claimsIdentity.claims())); + turnContext.getActivity().setCallerId(callerId); + resourceResponse.set(turnContext.updateActivity(activity).join()); + return CompletableFuture.completedFuture(null); + }; + + this.adapter.continueConversation(claimsIdentity, + skillConversationReference.getConversationReference(), + skillConversationReference.getOAuthScope(), + callback); + + if (resourceResponse.get() != null) { + return CompletableFuture.completedFuture(resourceResponse.get()); + } else { + return CompletableFuture.completedFuture(new ResourceResponse(UUID.randomUUID().toString())); + } + } + + private static void applySkillActivityToTurnContext(TurnContext turnContext, Activity activity) { + // adapter.ContinueConversation() sends an event activity with ContinueConversation in the name. + // this warms up the incoming middlewares but once that's done and we hit the custom callback, + // we need to swap the values back to the ones received from the skill so the bot gets the actual activity. + turnContext.getActivity().setChannelData(activity.getChannelData()); + turnContext.getActivity().setCode(activity.getCode()); + turnContext.getActivity().setEntities(activity.getEntities()); + turnContext.getActivity().setLocale(activity.getLocale()); + turnContext.getActivity().setLocalTimestamp(activity.getLocalTimestamp()); + turnContext.getActivity().setName(activity.getName()); + for (Map.Entry entry : activity.getProperties().entrySet()) { + turnContext.getActivity().setProperties(entry.getKey(), entry.getValue()); + } + turnContext.getActivity().setRelatesTo(activity.getRelatesTo()); + turnContext.getActivity().setReplyToId(activity.getReplyToId()); + turnContext.getActivity().setTimestamp(activity.getTimestamp()); + turnContext.getActivity().setText(activity.getText()); + turnContext.getActivity().setType(activity.getType()); + turnContext.getActivity().setValue(activity.getValue()); + } + + private CompletableFuture getSkillConversationReference(String conversationId) { + + SkillConversationReference skillConversationReference; + try { + skillConversationReference = this.conversationIdFactory. + getSkillConversationReference(conversationId).join(); + } catch (NotImplementedException ex) { + if (this.logger != null) { + this.logger.warn("Got NotImplementedException when trying to call " + + "GetSkillConversationReference() on the ConversationIdFactory," + + " attempting to use deprecated GetConversationReference() method instead."); + } + + // Attempt to get SkillConversationReference using deprecated method. + // this catch should be removed once we remove the deprecated method. + // We need to use the deprecated method for backward compatibility. + ConversationReference conversationReference = + this.conversationIdFactory.getConversationReference(conversationId).join(); + skillConversationReference = new SkillConversationReference(); + skillConversationReference.setConversationReference(conversationReference); + skillConversationReference.setOAuthScope(this.getOAuthScope.get()); + } + + if (skillConversationReference == null) { + if (this.logger != null) { + this.logger.warn( + String.format("Unable to get skill conversation reference for conversationId %s.", conversationId) + ); + } + throw new RuntimeException("Key not found"); + } + + return CompletableFuture.completedFuture(skillConversationReference); + } + + private CompletableFuture processActivity( + ClaimsIdentity claimsIdentity, + String conversationId, + String replyToActivityId, + Activity activity) { + + SkillConversationReference skillConversationReference = getSkillConversationReference(conversationId).join(); + + AtomicReference resourceResponse = new AtomicReference(); + + BotCallbackHandler callback = turnContext -> { + turnContext.getTurnState().add(this.skillConversationReferenceKey, skillConversationReference); + activity.applyConversationReference(skillConversationReference.getConversationReference()); + turnContext.getActivity().setId(replyToActivityId); + String callerId = String.format("%s%s", + CallerIdConstants.BOT_TO_BOT_PREFIX, + JwtTokenValidation.getAppIdFromClaims(claimsIdentity.claims())); + turnContext.getActivity().setCallerId(callerId); + + switch (activity.getType()) { + case ActivityTypes.END_OF_CONVERSATION: + this.conversationIdFactory.deleteConversationReference(conversationId).join(); + this.sendToBot(activity, turnContext); + break; + case ActivityTypes.EVENT: + this.sendToBot(activity, turnContext); + break; + case ActivityTypes.COMMAND: + case ActivityTypes.COMMAND_RESULT: + if (activity.getName().startsWith("application/")) { + // Send to channel and capture the resource response + // for the SendActivityCall so we can return it. + resourceResponse.set(turnContext.sendActivity(activity).join()); + } else { + this.sendToBot(activity, turnContext); + } + + break; + default: + // Capture the resource response for the SendActivityCall so we can return it. + resourceResponse.set(turnContext.sendActivity(activity).join()); + break; + } + return CompletableFuture.completedFuture(null); + }; + + this.adapter.continueConversation(claimsIdentity, + skillConversationReference.getConversationReference(), + skillConversationReference.getOAuthScope(), + callback).join(); + + if (resourceResponse.get() != null) { + return CompletableFuture.completedFuture(resourceResponse.get()); + } else { + return CompletableFuture.completedFuture(new ResourceResponse(UUID.randomUUID().toString())); + } + } + + private CompletableFuture sendToBot(Activity activity, TurnContext turnContext) { + this.applySkillActivityToTurnContext(turnContext, activity); + this.bot.onTurn(turnContext).join(); + return CompletableFuture.completedFuture(null); + } +} From 99b6908751e5e7991987495a56d694f7e0ccce3d Mon Sep 17 00:00:00 2001 From: Martin Battaglino Date: Wed, 14 Jul 2021 16:18:17 -0300 Subject: [PATCH 04/27] Add Authentication configuration for CloudAdapter --- .../AuthenticateRequestResult.java | 79 +++ .../BotFrameworkAuthentication.java | 109 ++++ .../BotFrameworkAuthenticationFactory.java | 114 ++++ .../BotFrameworkClientImpl.java | 173 ++++++ .../authentication/ConnectorFactory.java | 20 + .../authentication/ConnectorFactoryImpl.java | 46 ++ .../DelegatingCredentialProvider.java | 55 ++ ...rameterizedBotFrameworkAuthentication.java | 507 ++++++++++++++++++ ...asswordServiceClientCredentialFactory.java | 165 ++++++ .../ServiceClientCredentialsFactory.java | 45 ++ .../authentication/UserTokenClient.java | 134 +++++ .../authentication/UserTokenClientImpl.java | 138 +++++ 12 files changed, 1585 insertions(+) create mode 100644 libraries/bot-connector/src/main/java/com/microsoft/bot/connector/authentication/AuthenticateRequestResult.java create mode 100644 libraries/bot-connector/src/main/java/com/microsoft/bot/connector/authentication/BotFrameworkAuthentication.java create mode 100644 libraries/bot-connector/src/main/java/com/microsoft/bot/connector/authentication/BotFrameworkAuthenticationFactory.java create mode 100644 libraries/bot-connector/src/main/java/com/microsoft/bot/connector/authentication/BotFrameworkClientImpl.java create mode 100644 libraries/bot-connector/src/main/java/com/microsoft/bot/connector/authentication/ConnectorFactory.java create mode 100644 libraries/bot-connector/src/main/java/com/microsoft/bot/connector/authentication/ConnectorFactoryImpl.java create mode 100644 libraries/bot-connector/src/main/java/com/microsoft/bot/connector/authentication/DelegatingCredentialProvider.java create mode 100644 libraries/bot-connector/src/main/java/com/microsoft/bot/connector/authentication/ParameterizedBotFrameworkAuthentication.java create mode 100644 libraries/bot-connector/src/main/java/com/microsoft/bot/connector/authentication/PasswordServiceClientCredentialFactory.java create mode 100644 libraries/bot-connector/src/main/java/com/microsoft/bot/connector/authentication/ServiceClientCredentialsFactory.java create mode 100644 libraries/bot-connector/src/main/java/com/microsoft/bot/connector/authentication/UserTokenClient.java create mode 100644 libraries/bot-connector/src/main/java/com/microsoft/bot/connector/authentication/UserTokenClientImpl.java diff --git a/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/authentication/AuthenticateRequestResult.java b/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/authentication/AuthenticateRequestResult.java new file mode 100644 index 000000000..435dc9349 --- /dev/null +++ b/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/authentication/AuthenticateRequestResult.java @@ -0,0 +1,79 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.bot.connector.authentication; + +/** + * The result from a call to authenticate a Bot Framework Protocol request. + */ +public class AuthenticateRequestResult { + + private String audience; + private ClaimsIdentity claimsIdentity; + private String callerId; + private ConnectorFactory connectorFactory; + + /** + * Gets a value for the Audience. + * @return A value for the Audience. + */ + public String getAudience() { + return audience; + } + + /** + * Sets a value for the Audience. + * @param audience A value for the Audience. + */ + public void setAudience(String audience) { + this.audience = audience; + } + + /** + * Gets a value for the ClaimsIdentity. + * @return A value for the ClaimsIdentity. + */ + public ClaimsIdentity getClaimsIdentity() { + return claimsIdentity; + } + + /** + * Sets a value for the ClaimsIdentity. + * @param claimsIdentity A value for the ClaimsIdentity. + */ + public void setClaimsIdentity(ClaimsIdentity claimsIdentity) { + this.claimsIdentity = claimsIdentity; + } + + /** + * Gets a value for the CallerId. + * @return A value for the CallerId. + */ + public String getCallerId() { + return callerId; + } + + /** + * Sets a value for the CallerId. + * @param callerId A value for the CallerId. + */ + public void setCallerId(String callerId) { + this.callerId = callerId; + } + + /** + * Gets a value for the ConnectorFactory. + * @return A value for the ConnectorFactory. + */ + public ConnectorFactory getConnectorFactory() { + return connectorFactory; + } + + /** + * Sets a value for the ConnectorFactory. + * @param connectorFactory A value for the ConnectorFactory. + */ + public void setConnectorFactory(ConnectorFactory connectorFactory) { + this.connectorFactory = connectorFactory; + } +} diff --git a/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/authentication/BotFrameworkAuthentication.java b/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/authentication/BotFrameworkAuthentication.java new file mode 100644 index 000000000..43dc71920 --- /dev/null +++ b/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/authentication/BotFrameworkAuthentication.java @@ -0,0 +1,109 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.bot.connector.authentication; + +import java.util.concurrent.CompletableFuture; + +import com.microsoft.bot.connector.skills.BotFrameworkClient; +import com.microsoft.bot.schema.Activity; +import com.microsoft.bot.schema.CallerIdConstants; + +import org.apache.commons.lang3.NotImplementedException; + +/** + * Represents a Cloud Environment used to authenticate Bot Framework Protocol + * network calls within this environment. + */ +public abstract class BotFrameworkAuthentication { + + /** + * Validate Bot Framework Protocol requests. + * + * @param activity The inbound Activity. + * @param authHeader The http auth header. + * @return Asynchronous Task with {@link AuthenticateRequestResult}. + */ + public abstract CompletableFuture authenticateRequest(Activity activity, + String authHeader); + + /** + * Validate Bot Framework Protocol requests. + * + * @param authHeader The http auth header. + * @param channelIdHeader The channel Id HTTP header. + * @return Asynchronous Task with {@link AuthenticateRequestResult}. + */ + public abstract CompletableFuture authenticateStreamingRequest(String authHeader, + String channelIdHeader); + + /** + * Creates a {@link ConnectorFactory} that can be used to create + * {@link com.microsoft.bot.connector.ConnectorClient} that use credentials from this particular cloud + * environment. + * + * @param claimsIdentity The inbound @{link Activity}'s {@link ClaimsIdentity}. + * @return A {@link ConnectorFactory}. + */ + public abstract ConnectorFactory createConnectorFactory(ClaimsIdentity claimsIdentity); + + /** + * Creates the appropriate {@link UserTokenClient} instance. + * + * @param claimsIdentity The inbound @{link Activity}'s {@link ClaimsIdentity}. + * @return Asynchronous Task with {@link UserTokenClient} instance. + */ + public abstract CompletableFuture createUserTokenClient(ClaimsIdentity claimsIdentity); + + /** + * Creates a {@link BotFrameworkClient} used for calling Skills. + * + * @return A {@link BotFrameworkClient} instance to call Skills. + */ + public BotFrameworkClient createBotFrameworkClient() { + throw new NotImplementedException("createBotFrameworkClient is not implemented"); + } + + /** + * Gets the originating audience from Bot OAuth scope. + * + * @return The originating audience. + */ + public String getOriginatingAudience() { + throw new NotImplementedException("getOriginatingAudience is not implemented"); + } + + /** + * Authenticate Bot Framework Protocol requests to Skills. + * @param authHeader The http auth header received in the skill request. + * @return A {@link ClaimsIdentity}. + */ + public CompletableFuture authenticateChannelRequest(String authHeader) { + throw new NotImplementedException("authenticateChannelRequest is not implemented"); + } + + /** + * Generates the appropriate callerId to write onto the activity, this might be + * null. + * + * @param credentialFactory A {@link ServiceClientCredentialsFactory} to use. + * @param claimsIdentity The inbound claims. + * @param callerId The default callerId to use if this is not a skill. + * @return The callerId, this might be null. + */ + protected CompletableFuture generateCallerId(ServiceClientCredentialsFactory credentialFactory, + ClaimsIdentity claimsIdentity, String callerId) { + // Is the bot accepting all incoming messages? + return credentialFactory.isAuthenticationDisabled().thenApply(isDisabled -> { + if (isDisabled) { + // Return null so that the callerId is cleared. + return null; + } + + // Is the activity from another bot? + return SkillValidation.isSkillClaim(claimsIdentity.claims()) ? String.format("%s%s", + CallerIdConstants.BOT_TO_BOT_PREFIX, JwtTokenValidation.getAppIdFromClaims(claimsIdentity.claims())) + : callerId; + }); + } +} diff --git a/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/authentication/BotFrameworkAuthenticationFactory.java b/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/authentication/BotFrameworkAuthenticationFactory.java new file mode 100644 index 000000000..3e447de36 --- /dev/null +++ b/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/authentication/BotFrameworkAuthenticationFactory.java @@ -0,0 +1,114 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.bot.connector.authentication; + +import com.microsoft.bot.schema.CallerIdConstants; +import okhttp3.OkHttpClient; +import org.apache.commons.lang3.StringUtils; + +/** + * A factory for {@link BotFrameworkAuthentication} + * which encapsulate the environment specific Bot Framework Protocol auth code. + */ +public final class BotFrameworkAuthenticationFactory { + + private BotFrameworkAuthenticationFactory() { } + + /** + * Creates the a {@link BotFrameworkAuthentication} instance for anonymous testing scenarios. + * @return A new {@link BotFrameworkAuthentication} instance. + */ + public static BotFrameworkAuthentication create() { + return create( + null, + false, + null, + null, + null, + null, + null, + null, + null, + new PasswordServiceClientCredentialFactory(), + new AuthenticationConfiguration(), + new OkHttpClient()); + } + + @SuppressWarnings("checkstyle:ParameterNumber") + public static BotFrameworkAuthentication create( + String channelService, + Boolean validateAuthority, + String toChannelFromBotLoginUrl, + String toChannelFromBotOAuthScope, + String toBotFromChannelTokenIssuer, + String oAuthUrl, + String toBotFromChannelOpenIdMetadataUrl, + String toBotFromEmulatorOpenIdMetadataUrl, + String callerId, + ServiceClientCredentialsFactory credentialFactory, + AuthenticationConfiguration authConfiguration, + OkHttpClient httpClient + ) { + if (StringUtils.isNotBlank(toChannelFromBotLoginUrl) + || StringUtils.isNotBlank(toChannelFromBotOAuthScope) + || StringUtils.isNotBlank(toBotFromChannelTokenIssuer) + || StringUtils.isNotBlank(oAuthUrl) + || StringUtils.isNotBlank(toBotFromChannelOpenIdMetadataUrl) + || StringUtils.isNotBlank(toBotFromEmulatorOpenIdMetadataUrl) + || StringUtils.isNotBlank(callerId)) { + // if we have any of the 'parameterized' properties defined we'll assume this is the parameterized code + return new ParameterizedBotFrameworkAuthentication( + validateAuthority, + toChannelFromBotLoginUrl, + toChannelFromBotOAuthScope, + toBotFromChannelTokenIssuer, + oAuthUrl, + toBotFromChannelOpenIdMetadataUrl, + toBotFromEmulatorOpenIdMetadataUrl, + callerId, + credentialFactory, + authConfiguration, + httpClient + ); + } else { + // else apply the built in default behavior, which is either the public cloud or the gov cloud + // depending on whether we have a channelService value present + if (StringUtils.isBlank(channelService)) { + return new ParameterizedBotFrameworkAuthentication( + true, + String.format( + AuthenticationConstants.TO_CHANNEL_FROM_BOT_LOGIN_URL_TEMPLATE, + AuthenticationConstants.DEFAULT_CHANNEL_AUTH_TENANT), + AuthenticationConstants.TO_CHANNEL_FROM_BOT_OAUTH_SCOPE, + AuthenticationConstants.TO_BOT_FROM_CHANNEL_TOKEN_ISSUER, + AuthenticationConstants.OAUTH_URL, + AuthenticationConstants.TO_BOT_FROM_CHANNEL_OPENID_METADATA_URL, + AuthenticationConstants.TO_BOT_FROM_EMULATOR_OPENID_METADATA_URL, + CallerIdConstants.PUBLIC_AZURE_CHANNEL, + credentialFactory, + authConfiguration, + httpClient + ); + } else if (channelService == GovernmentAuthenticationConstants.CHANNELSERVICE) { + return new ParameterizedBotFrameworkAuthentication( + true, + GovernmentAuthenticationConstants.TO_CHANNEL_FROM_BOT_LOGIN_URL, + GovernmentAuthenticationConstants.TO_CHANNEL_FROM_BOT_OAUTH_SCOPE, + GovernmentAuthenticationConstants.TO_BOT_FROM_CHANNEL_TOKEN_ISSUER, + GovernmentAuthenticationConstants.OAUTH_URL_GOV, + GovernmentAuthenticationConstants.TO_BOT_FROM_CHANNEL_OPENID_METADATA_URL, + GovernmentAuthenticationConstants.TO_BOT_FROM_EMULATOR_OPENID_METADATA_URL, + CallerIdConstants.US_GOV_CHANNEL, + credentialFactory, + authConfiguration, + httpClient + ); + } else { + // The ChannelService value is used an indicator of which built in set of constants to use. + // If it is not recognized, a full configuration is expected. + throw new IllegalArgumentException("The provided ChannelService value is not supported."); + } + } + } +} diff --git a/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/authentication/BotFrameworkClientImpl.java b/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/authentication/BotFrameworkClientImpl.java new file mode 100644 index 000000000..81a8c54ee --- /dev/null +++ b/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/authentication/BotFrameworkClientImpl.java @@ -0,0 +1,173 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.bot.connector.authentication; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.microsoft.bot.connector.Async; +import com.microsoft.bot.connector.skills.BotFrameworkClient; +import com.microsoft.bot.restclient.serializer.JacksonAdapter; +import com.microsoft.bot.schema.Activity; +import com.microsoft.bot.schema.ChannelAccount; +import com.microsoft.bot.schema.ConversationAccount; +import com.microsoft.bot.schema.ConversationReference; +import com.microsoft.bot.schema.RoleTypes; +import com.microsoft.bot.schema.Serialization; +import com.microsoft.bot.schema.TypedInvokeResponse; +import okhttp3.HttpUrl; +import okhttp3.MediaType; +import okhttp3.OkHttpClient; +import okhttp3.Request; +import okhttp3.RequestBody; +import okhttp3.Response; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; +import java.net.URI; +import java.util.concurrent.CompletableFuture; + +public class BotFrameworkClientImpl extends BotFrameworkClient { + private final ServiceClientCredentialsFactory credentialsFactory; + private final OkHttpClient httpClient; + private final String loginEndpoint; + + private final Logger logger = LoggerFactory.getLogger(BotFrameworkClientImpl.class); + + public BotFrameworkClientImpl( + ServiceClientCredentialsFactory withCredentialsFactory, + String withLoginEndpoint, + OkHttpClient withHttpClient + ) { + credentialsFactory = withCredentialsFactory; + loginEndpoint = withLoginEndpoint; + httpClient = withHttpClient != null ? withHttpClient : new OkHttpClient(); + } + + /** + * {@inheritDoc} + */ + @Override + public CompletableFuture> postActivity( + String fromBotId, + String toBotId, + URI toUri, + URI serviceUri, + String conversationId, + Activity activity, + Class type + ) { + // We are not checking fromBotId and toBotId for null to address BB-dotnet issue #5577 + // (https://github.com/microsoft/botbuilder-dotnet/issues/5577) + + if (toUri == null) { + throw new IllegalArgumentException("toUri cannot be null"); + } + + if (serviceUri == null) { + throw new IllegalArgumentException("serviceUri cannot be null"); + } + + if (conversationId == null) { + throw new IllegalArgumentException("conversationId cannot be null"); + } + + if (activity == null) { + throw new IllegalArgumentException("activity cannot be null"); + } + + if (type == null) { + throw new IllegalArgumentException("type cannot be null"); + } + + logger.info(String.format("post to skill '%s' at '%s'", toBotId, toUri)); + + return credentialsFactory.createCredentials(fromBotId, toBotId, loginEndpoint, true) + .thenCompose(credentials -> { + // Clone the activity so we can modify it before sending without impacting the original object. + Activity activityClone = Activity.clone(activity); + + // Apply the appropriate addressing to the newly created Activity. + ConversationReference conversationReference = new ConversationReference(); + conversationReference.setServiceUrl(activityClone.getServiceUrl()); + conversationReference.setActivityId(activityClone.getId()); + conversationReference.setChannelId(activityClone.getChannelId()); + conversationReference.setLocale(activityClone.getLocale()); + + ConversationAccount conversationAccount = new ConversationAccount(); + conversationAccount.setId(activityClone.getConversation().getId()); + conversationAccount.setName(activityClone.getConversation().getName()); + conversationAccount.setConversationType(activityClone.getConversation().getConversationType()); + conversationAccount.setAadObjectId(activityClone.getConversation().getAadObjectId()); + conversationAccount.setIsGroup(activityClone.getConversation().isGroup()); + for (String key : activityClone.getProperties().keySet()) { + conversationAccount.setProperties(key, activityClone.getProperties().get(key)); + } + conversationAccount.setRole(activityClone.getConversation().getRole()); + conversationAccount.setTenantId(activityClone.getConversation().getTenantId()); + + conversationReference.setConversation(conversationAccount); + activityClone.setRelatesTo(conversationReference); + activityClone.getConversation().setId(conversationId); + // Fixes: https://github.com/microsoft/botframework-sdk/issues/5785 + if (activityClone.getRecipient() == null) { + activityClone.setRecipient(new ChannelAccount()); + } + activityClone.getRecipient().setRole(RoleTypes.SKILL); + + // Create the HTTP request from the cloned Activity and send it to the Skill. + String jsonContent = ""; + try { + ObjectMapper mapper = new JacksonAdapter().serializer(); + jsonContent = mapper.writeValueAsString(activity); + } catch (JsonProcessingException e) { + return Async.completeExceptionally( + new RuntimeException("securePostActivity: Unable to serialize the Activity")); + } + + RequestBody body = RequestBody.create(MediaType.parse("application/json; charset=utf-8"), jsonContent); + Request request = buildRequest(toUri, body); + + // Add the auth header to the HTTP request. + credentials.applyCredentialsFilter(httpClient.newBuilder()); + + try { + Response response = httpClient.newCall(request).execute(); + + if (response.isSuccessful()) { + // On success assuming either JSON that can be deserialized to T or empty. + String bodyString = response.body().string(); + T result = Serialization.getAs(bodyString, type); + TypedInvokeResponse returnValue = new TypedInvokeResponse(response.code(), result); + return CompletableFuture.completedFuture(returnValue); + } else { + // Otherwise we can assume we don't have a T to deserialize + // So just log the content so it's not lost. + logger.error(String.format( + "Bot Framework call failed to '%s' returning '%d' and '%s'", + toUri, + response.code(), + response.body()) + ); + + // We want to at least propagate the status code because that is what InvokeResponse expects. + TypedInvokeResponse returnValue = new TypedInvokeResponse<>( + response.code(), + (T) response.body().string()); + return CompletableFuture.completedFuture(returnValue); + } + + } catch (IOException e) { + return Async.completeExceptionally(e); + } + }); + } + + private Request buildRequest(URI url, RequestBody body) { + HttpUrl.Builder httpBuilder = HttpUrl.parse(url.toString()).newBuilder(); + Request.Builder requestBuilder = new Request.Builder().url(httpBuilder.build()); + requestBuilder.post(body); + return requestBuilder.build(); + } +} diff --git a/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/authentication/ConnectorFactory.java b/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/authentication/ConnectorFactory.java new file mode 100644 index 000000000..7b601995f --- /dev/null +++ b/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/authentication/ConnectorFactory.java @@ -0,0 +1,20 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +package com.microsoft.bot.connector.authentication; + +import java.util.concurrent.CompletableFuture; + +import com.microsoft.bot.connector.ConnectorClient; + +public abstract class ConnectorFactory { + /** + * A factory method used to create {@link ConnectorClient} instances. + * + * @param serviceUrl The url for the client. + * @param audience The audience for the credentials the client will use. + * @return A {@link ConnectorClient} for sending activities to the audience at + * the serviceUrl. + */ + public abstract CompletableFuture create(String serviceUrl, String audience); +} diff --git a/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/authentication/ConnectorFactoryImpl.java b/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/authentication/ConnectorFactoryImpl.java new file mode 100644 index 000000000..1b128e658 --- /dev/null +++ b/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/authentication/ConnectorFactoryImpl.java @@ -0,0 +1,46 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.bot.connector.authentication; + +import com.microsoft.bot.connector.ConnectorClient; +import com.microsoft.bot.connector.rest.RestConnectorClient; +import java.util.concurrent.CompletableFuture; + +public class ConnectorFactoryImpl extends ConnectorFactory { + + private final String appId; + private final String toChannelFromBotOAuthScope; + private final String loginEndpoint; + private final Boolean validateAuthority; + private final ServiceClientCredentialsFactory credentialFactory; + + public ConnectorFactoryImpl( + String withAppId, + String withToChannelFromBotOAuthScope, + String withLoginEndpoint, + Boolean withValidateAuthority, + ServiceClientCredentialsFactory withCredentialFactory) { + this.appId = withAppId; + this.toChannelFromBotOAuthScope = withToChannelFromBotOAuthScope; + this.loginEndpoint = withLoginEndpoint; + this.validateAuthority = withValidateAuthority; + this.credentialFactory = withCredentialFactory; + } + + /** + * {@inheritDoc} + */ + @Override + public CompletableFuture create(String serviceUrl, String audience) { + // Use the credentials factory to create credentials specific to this particular cloud environment. + return credentialFactory.createCredentials(appId, + audience != null ? audience : toChannelFromBotOAuthScope, + loginEndpoint, + validateAuthority).thenCompose(credentials -> { + // A new connector client for making calls against this serviceUrl using credentials + // derived from the current appId and the specified audience. + return CompletableFuture.completedFuture(new RestConnectorClient(serviceUrl, credentials)); + }); + } +} diff --git a/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/authentication/DelegatingCredentialProvider.java b/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/authentication/DelegatingCredentialProvider.java new file mode 100644 index 000000000..a08e34e8c --- /dev/null +++ b/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/authentication/DelegatingCredentialProvider.java @@ -0,0 +1,55 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.bot.connector.authentication; + +import org.apache.commons.lang3.NotImplementedException; +import java.util.concurrent.CompletableFuture; + +/** + * This is just an internal class to allow pre-existing implementation of the request validation to be used with + * a IServiceClientCredentialFactory. + */ +public class DelegatingCredentialProvider implements CredentialProvider { + + private ServiceClientCredentialsFactory credentialsFactory; + + /** + * Initialize a {@link DelegatingCredentialProvider} class. + * @param withCredentialsFactory A {@link ServiceClientCredentialsFactory} class. + */ + public DelegatingCredentialProvider(ServiceClientCredentialsFactory withCredentialsFactory) { + if (withCredentialsFactory == null) { + throw new IllegalArgumentException("withCredentialsFactory cannot be null"); + } + + credentialsFactory = withCredentialsFactory; + } + + /** + * Gets the appPassword. + * @param appId The ID of the app to get the password for. + * @return The appPassword. + */ + public CompletableFuture getAppPassword(String appId) { + throw new NotImplementedException("getAppPassword is not implemented"); + } + + /** + * Validates if the authentication is disabled. + * @return Boolean value depending if the authentication is disabled or not. + */ + public CompletableFuture isAuthenticationDisabled() { + return credentialsFactory.isAuthenticationDisabled(); + } + + /** + * Validates if the received appId is valid. + * @param appId The app ID to validate. + * @return Boolean value depending if the received appId is valid or not. + */ + public CompletableFuture isValidAppId(String appId) { + return credentialsFactory.isValidAppId(appId); + } +} + diff --git a/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/authentication/ParameterizedBotFrameworkAuthentication.java b/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/authentication/ParameterizedBotFrameworkAuthentication.java new file mode 100644 index 000000000..7b039ab2a --- /dev/null +++ b/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/authentication/ParameterizedBotFrameworkAuthentication.java @@ -0,0 +1,507 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.bot.connector.authentication; + +import com.microsoft.bot.connector.Channels; +import com.microsoft.bot.connector.skills.BotFrameworkClient; +import com.microsoft.bot.schema.Activity; +import com.microsoft.bot.schema.RoleTypes; +import okhttp3.OkHttpClient; +import org.apache.commons.lang3.StringUtils; +import java.time.Duration; +import java.util.Arrays; +import java.util.Map; +import java.util.concurrent.CompletableFuture; + +public class ParameterizedBotFrameworkAuthentication extends BotFrameworkAuthentication { + + private Boolean validateAuthority; + private String toChannelFromBotLoginUrl; + private String toChannelFromBotOAuthScope; + private String toBotFromChannelTokenIssuer; + private String oAuthUrl; + private String toBotFromChannelOpenIdMetadataUrl; + private String toBotFromEmulatorOpenIdMetadataUrl; + private String callerId; + private ServiceClientCredentialsFactory credentialsFactory; + private AuthenticationConfiguration authConfiguration; + private final OkHttpClient httpClient; + private static final int FIVE = 5; + + @SuppressWarnings("checkstyle:ParameterNumber") + public ParameterizedBotFrameworkAuthentication( + Boolean withValidateAuthority, + String withToChannelFromBotLoginUrl, + String withToChannelFromBotOAuthScope, + String withToBotFromChannelTokenIssuer, + String withOAuthUrl, + String withToBotFromChannelOpenIdMetadataUrl, + String withToBotFromEmulatorOpenIdMetadataUrl, + String withCallerId, + ServiceClientCredentialsFactory withCredentialsFactory, + AuthenticationConfiguration withAuthConfiguration, + OkHttpClient withHttpClient + ) { + this.validateAuthority = withValidateAuthority; + this.toChannelFromBotLoginUrl = withToChannelFromBotLoginUrl; + this.toChannelFromBotOAuthScope = withToChannelFromBotOAuthScope; + this.toBotFromChannelTokenIssuer = withToBotFromChannelTokenIssuer; + this.oAuthUrl = withOAuthUrl; + this.toBotFromChannelOpenIdMetadataUrl = withToBotFromChannelOpenIdMetadataUrl; + this.toBotFromEmulatorOpenIdMetadataUrl = withToBotFromEmulatorOpenIdMetadataUrl; + this.callerId = withCallerId; + this.credentialsFactory = withCredentialsFactory; + this.authConfiguration = withAuthConfiguration; + this.httpClient = withHttpClient != null ? withHttpClient : new OkHttpClient(); + } + + /** + * {@inheritDoc} + */ + @Override + public String getOriginatingAudience() { + return this.toChannelFromBotOAuthScope; + } + + /** + * {@inheritDoc} + */ + @Override + public CompletableFuture authenticateChannelRequest(String authHeader) { + if (StringUtils.isBlank(authHeader)) { + return this.credentialsFactory.isAuthenticationDisabled().thenApply(isAuthDisabled -> { + if (!isAuthDisabled) { + throw new AuthenticationException("Unauthorized Access. Request is not authorized"); + } + + // In the scenario where auth is disabled, we still want to have the isAuthenticated flag set in the + // ClaimsIdentity. To do this requires adding in an empty claim. Since ChannelServiceHandler calls are + // always a skill callback call, we set the skill claim too. + return SkillValidation.createAnonymousSkillClaim(); + }); + } + + return jwtTokenValidationValidateAuthHeader(authHeader, "unknown", null); + } + + /** + * {@inheritDoc} + */ + @Override + public CompletableFuture authenticateRequest(Activity activity, String authHeader) { + return jwtTokenValidationAuthenticateRequest(activity, authHeader).thenCompose(claimsIdentity -> { + String outboundAudience = SkillValidation.isSkillClaim(claimsIdentity.claims()) + ? JwtTokenValidation.getAppIdFromClaims(claimsIdentity.claims()) + : this.toChannelFromBotOAuthScope; + + return generateCallerId(this.credentialsFactory, claimsIdentity, this.callerId) + .thenCompose(resultCallerId -> { + ConnectorFactoryImpl connectorFactory = new ConnectorFactoryImpl( + getAppId(claimsIdentity), + this.toChannelFromBotOAuthScope, + this.toChannelFromBotLoginUrl, + this.validateAuthority, + this.credentialsFactory); + + AuthenticateRequestResult authenticateRequestResult = new AuthenticateRequestResult(); + authenticateRequestResult.setClaimsIdentity(claimsIdentity); + authenticateRequestResult.setAudience(outboundAudience); + authenticateRequestResult.setCallerId(resultCallerId); + authenticateRequestResult.setConnectorFactory(connectorFactory); + + return CompletableFuture.completedFuture(authenticateRequestResult); + } + ); + } + ); + } + + /** + * {@inheritDoc} + */ + @Override + public CompletableFuture authenticateStreamingRequest(String authHeader, + String channelIdHeader) { + if (StringUtils.isNotBlank(channelIdHeader)) { + this.credentialsFactory.isAuthenticationDisabled().thenCompose(isAuthDisabled -> { + if (isAuthDisabled) { + return jwtTokenValidationValidateAuthHeader(authHeader, channelIdHeader, null) + .thenCompose(claimsIdentity -> { + String outboundAudience = SkillValidation.isSkillClaim(claimsIdentity.claims()) + ? JwtTokenValidation.getAppIdFromClaims(claimsIdentity.claims()) + : this.toChannelFromBotOAuthScope; + + return generateCallerId(this.credentialsFactory, claimsIdentity, this.callerId) + .thenCompose(resultCallerId -> { + AuthenticateRequestResult authenticateRequestResult = new AuthenticateRequestResult(); + authenticateRequestResult.setClaimsIdentity(claimsIdentity); + authenticateRequestResult.setAudience(outboundAudience); + authenticateRequestResult.setCallerId(resultCallerId); + + return CompletableFuture.completedFuture(authenticateRequestResult); + }); + } + ); + } + return null; + }); + } + throw new AuthenticationException("channelId header required"); + } + + /** + * {@inheritDoc} + */ + @Override + public ConnectorFactory createConnectorFactory(ClaimsIdentity claimsIdentity) { + return new ConnectorFactoryImpl( + getAppId(claimsIdentity), + this.toChannelFromBotOAuthScope, + this.toChannelFromBotLoginUrl, + this.validateAuthority, + this.credentialsFactory); + } + + /** + * {@inheritDoc} + */ + @Override + public CompletableFuture createUserTokenClient(ClaimsIdentity claimsIdentity) { + String appId = getAppId(claimsIdentity); + + return this.credentialsFactory.createCredentials( + appId, + this.toChannelFromBotOAuthScope, + this.toChannelFromBotLoginUrl, + this.validateAuthority) + .thenApply(credentials -> new UserTokenClientImpl(appId, credentials, this.oAuthUrl)); + } + + /** + * {@inheritDoc} + */ + @Override + public BotFrameworkClient createBotFrameworkClient() { + return new BotFrameworkClientImpl(this.credentialsFactory, this.toChannelFromBotLoginUrl, this.httpClient); + } + + // The following code is based on JwtTokenValidation.AuthenticateRequest + private CompletableFuture jwtTokenValidationAuthenticateRequest(Activity activity, + String authHeader) { + if (StringUtils.isBlank(authHeader)) { + this.credentialsFactory.isAuthenticationDisabled().thenApply(isAuthDisabled -> { + if (!isAuthDisabled) { + // No Auth Header. Auth is required. Request is not authorized. + throw new AuthenticationException("Unauthorized Access. Request is not authorized"); + } + return null; + }); + + // Check if the activity is for a skill call and is coming from the Emulator. + if (activity.getChannelId().equals(Channels.EMULATOR) && activity.getRecipient().getRole() + .equals(RoleTypes.SKILL)) { + // Return an anonymous claim with an anonymous skill AppId + return CompletableFuture.completedFuture(SkillValidation.createAnonymousSkillClaim()); + } + + // In the scenario where Auth is disabled, we still want to have the + // IsAuthenticated flag set in the ClaimsIdentity. To do this requires + // adding in an empty claim. + return CompletableFuture.completedFuture(new ClaimsIdentity(AuthenticationConstants.ANONYMOUS_AUTH_TYPE)); + } + + // Validate the header and extract claims. + return jwtTokenValidationValidateAuthHeader(authHeader, activity.getChannelId(), activity.getServiceUrl()); + } + + private CompletableFuture jwtTokenValidationValidateAuthHeader(String authHeader, + String channelId, + String serviceUrl) { + return jwtTokenValidationAuthenticateToken(authHeader, channelId, serviceUrl) + .thenCompose(identity -> jwtTokenValidationValidateClaims(identity.claims()).thenApply(result -> identity)); + } + + private CompletableFuture jwtTokenValidationValidateClaims(Map claims) { + if (this.authConfiguration.getClaimsValidator() != null) { + // Call the validation method if defined (it should throw an exception if the validation fails) + return this.authConfiguration.getClaimsValidator().validateClaims(claims); + } else if (SkillValidation.isSkillClaim(claims)) { + throw new AuthenticationException("ClaimsValidator is required for validation of Skill Host calls."); + } + return null; + } + + private CompletableFuture jwtTokenValidationAuthenticateToken(String authHeader, + String channelId, + String serviceUrl) { + if (SkillValidation.isSkillToken(authHeader)) { + return this.skillValidationAuthenticateChannelToken(authHeader, channelId); + } + + if (EmulatorValidation.isTokenFromEmulator(authHeader)) { + return this.emulatorValidationAuthenticateEmulatorToken(authHeader, channelId); + } + + return this.channelValidationauthenticateChannelToken(authHeader, serviceUrl, channelId); + } + + // The following code is based on SkillValidation.AuthenticateChannelToken + private CompletableFuture skillValidationAuthenticateChannelToken(String authHeader, + String channelId) { + TokenValidationParameters tokenValidationParameters = new TokenValidationParameters(); + tokenValidationParameters.validateIssuer = true; + tokenValidationParameters.validIssuers = Arrays.asList( + //TODO : presumably this table should also come from configuration. + "https://sts.windows.net/d6d49420-f39b-4df7-a1dc-d59a935871db/", // Auth v3.1, 1.0 token + "https://login.microsoftonline.com/d6d49420-f39b-4df7-a1dc-d59a935871db/v2.0", // Auth v3.1, 2.0 token + "https://sts.windows.net/f8cdef31-a31e-4b4a-93e4-5f571e91255a/", // Auth v3.2, 1.0 token + "https://login.microsoftonline.com/f8cdef31-a31e-4b4a-93e4-5f571e91255a/v2.0", // Auth v3.2, 2.0 token + "https://sts.windows.net/cab8a31a-1906-4287-a0d8-4eef66b95f6e/", // Auth for US Gov, 1.0 token + "https://login.microsoftonline.us/cab8a31a-1906-4287-a0d8-4eef66b95f6e/v2.0" // Auth for US Gov, 2.0 token + ); + tokenValidationParameters.validateAudience = false; // Audience validation takes place manually in code. + tokenValidationParameters.validateLifetime = true; + tokenValidationParameters.clockSkew = Duration.ofMinutes(FIVE); + tokenValidationParameters.requireSignedTokens = true; + + //TODO : what should the openIdMetadataUrl be here? + JwtTokenExtractor tokenExtractor = new JwtTokenExtractor( + tokenValidationParameters, + this.toBotFromEmulatorOpenIdMetadataUrl, + AuthenticationConstants.ALLOWED_SIGNING_ALGORITHMS); + + return tokenExtractor.getIdentity(authHeader, channelId, this.authConfiguration.requiredEndorsements()) + .thenCompose(identity -> skillValidationValidateIdentity(identity).thenApply(result -> identity) + ); + } + + private CompletableFuture skillValidationValidateIdentity(ClaimsIdentity identity) { + if (identity == null) { + // No valid identity. Not Authorized. + throw new AuthenticationException("SkillValidation.validateIdentity(): Invalid identity"); + } + + if (!identity.isAuthenticated()) { + // The token is in some way invalid. Not Authorized. + throw new AuthenticationException("SkillValidation.validateIdentity(): Token not authenticated"); + } + + String versionClaim = identity.getClaimValue(AuthenticationConstants.VERSION_CLAIM); + + if (versionClaim == null) { + // No version claim + throw new AuthenticationException( + String.format("SkillValidation.validateIdentity(): '%s' claim is required on skill Tokens.", + AuthenticationConstants.VERSION_CLAIM)); + } + + // Look for the "aud" claim, but only if issued from the Bot Framework + String audienceClaim = identity.getClaimValue(AuthenticationConstants.AUDIENCE_CLAIM); + if (StringUtils.isBlank(audienceClaim)) { + // Claim is not present or doesn't have a value. Not Authorized. + throw new AuthenticationException( + String.format("SkillValidation.validateIdentity(): '%s' claim is required on skill Tokens.", + AuthenticationConstants.AUDIENCE_CLAIM)); + } + + return this.credentialsFactory.isValidAppId(audienceClaim).thenCompose(result -> { + if (!result) { + // The AppId is not valid. Not Authorized. + throw new AuthenticationException("SkillValidation.validateIdentity(): Invalid audience."); + } + + String appId = JwtTokenValidation.getAppIdFromClaims(identity.claims()); + if (StringUtils.isBlank(appId)) { + // Invalid appId + throw new AuthenticationException("SkillValidation.validateIdentity(): Invalid appId."); + } + return null; + }); + } + + // The following code is based on EmulatorValidation.AuthenticateEmulatorToken + private CompletableFuture emulatorValidationAuthenticateEmulatorToken(String authHeader, + String channelId) { + TokenValidationParameters toBotFromEmulatorTokenValidationParameters = new TokenValidationParameters(); + toBotFromEmulatorTokenValidationParameters.validateIssuer = true; + toBotFromEmulatorTokenValidationParameters.validIssuers = Arrays.asList( + //TODO : presumably this table should also come from configuration + "https://sts.windows.net/d6d49420-f39b-4df7-a1dc-d59a935871db/", // Auth v3.1, 1.0 token + "https://login.microsoftonline.com/d6d49420-f39b-4df7-a1dc-d59a935871db/v2.0", // Auth v3.1, 2.0 token + "https://sts.windows.net/f8cdef31-a31e-4b4a-93e4-5f571e91255a/", // Auth v3.2, 1.0 token + "https://login.microsoftonline.com/f8cdef31-a31e-4b4a-93e4-5f571e91255a/v2.0", // Auth v3.2, 2.0 token + "https://sts.windows.net/cab8a31a-1906-4287-a0d8-4eef66b95f6e/", // Auth for US Gov, 1.0 token + "https://login.microsoftonline.us/cab8a31a-1906-4287-a0d8-4eef66b95f6e/v2.0" // Auth for US Gov, 2.0 token + ); + // Audience validation takes place manually in code. + toBotFromEmulatorTokenValidationParameters.validateAudience = false; + toBotFromEmulatorTokenValidationParameters.validateLifetime = true; + toBotFromEmulatorTokenValidationParameters.clockSkew = Duration.ofMinutes(FIVE); + toBotFromEmulatorTokenValidationParameters.requireSignedTokens = true; + + JwtTokenExtractor tokenExtractor = new JwtTokenExtractor( + toBotFromEmulatorTokenValidationParameters, + this.toBotFromEmulatorOpenIdMetadataUrl, + AuthenticationConstants.ALLOWED_SIGNING_ALGORITHMS); + + return tokenExtractor.getIdentity(authHeader, channelId, this.authConfiguration.requiredEndorsements()) + .thenCompose(identity -> { + if (identity == null) { + // No valid identity. Not Authorized. + throw new AuthenticationException("Unauthorized. No valid identity."); + } + + if (!identity.isAuthenticated()) { + // The token is in some way invalid. Not Authorized. + throw new AuthenticationException("Unauthorized. Is not authenticated"); + } + + // Now check that the AppID in the claimset matches + // what we're looking for. Note that in a multi-tenant bot, this value + // comes from developer code that may be reaching out to a service, hence the + // Async validation. + String versionClaim = identity.getClaimValue(AuthenticationConstants.VERSION_CLAIM); + + if (versionClaim == null) { + throw new AuthenticationException("Unauthorized. 'ver' claim is required on Emulator Tokens."); + } + + String tokenVersion = versionClaim; + String appId = ""; + + // The Emulator, depending on Version, sends the AppId via either the + // appid claim (Version 1) or the Authorized Party claim (Version 2). + if (StringUtils.isBlank(tokenVersion) || tokenVersion.equals("1.0")) { + // either no Version or a version of "1.0" means we should look for + // the claim in the "appid" claim. + String appIdClaim = identity.getClaimValue(AuthenticationConstants.APPID_CLAIM); + + if (appIdClaim == null) { + // No claim around AppID. Not Authorized. + throw new AuthenticationException( + "Unauthorized. 'appid' claim is required on Emulator Token version '1.0'."); + } + + appId = appIdClaim; + } else if (tokenVersion.equals("2.0")) { + // Emulator, "2.0" puts the AppId in the "azp" claim. + String appZClaim = identity.getClaimValue(AuthenticationConstants.AUTHORIZED_PARTY); + + if (appZClaim == null) { + // No claim around AppID. Not Authorized. + throw new AuthenticationException( + "Unauthorized. 'azp' claim is required on Emulator Token version '2.0'."); + } + + appId = appZClaim; + } else { + // Unknown Version. Not Authorized. + throw new AuthenticationException( + String.format("Unauthorized. Unknown Emulator Token version %s.", tokenVersion)); + } + + return this.credentialsFactory.isValidAppId(appId).thenApply(result -> { + if (!result) { + throw new AuthenticationException("Unauthorized. Invalid AppId passed on token"); + } + return identity; + }); + }); + } + + // The following code is based on GovernmentChannelValidation.AuthenticateChannelToken + private CompletableFuture channelValidationauthenticateChannelToken(String authHeader, + String serviceUrl, + String channelId) { + TokenValidationParameters tokenValidationParameters = this.channelValidationGetTokenValidationParameters(); + + JwtTokenExtractor tokenExtractor = new JwtTokenExtractor( + tokenValidationParameters, + this.toBotFromChannelOpenIdMetadataUrl, + AuthenticationConstants.ALLOWED_SIGNING_ALGORITHMS); + + return tokenExtractor.getIdentity(authHeader, channelId, this.authConfiguration.requiredEndorsements()) + .thenCompose(identity -> governmentChannelValidationValidateIdentity(identity, serviceUrl) + .thenApply(result -> identity)); + } + + private TokenValidationParameters channelValidationGetTokenValidationParameters() { + TokenValidationParameters tokenValidationParameters = new TokenValidationParameters(); + tokenValidationParameters.validateIssuer = true; + tokenValidationParameters.validIssuers = Arrays.asList(this.toBotFromChannelTokenIssuer); + + // Audience validation takes place in JwtTokenExtractor + tokenValidationParameters.validateAudience = false; + tokenValidationParameters.validateLifetime = true; + tokenValidationParameters.clockSkew = Duration.ofMinutes(FIVE); + tokenValidationParameters.requireSignedTokens = true; + tokenValidationParameters.validateIssuerSigningKey = true; + + return tokenValidationParameters; + } + + private CompletableFuture governmentChannelValidationValidateIdentity(ClaimsIdentity identity, + String serviceUrl) { + if (identity == null) { + // No valid identity. Not Authorized. + throw new AuthenticationException("Unauthorized. No valid identity."); + } + + if (!identity.isAuthenticated()) { + // The token is in some way invalid. Not Authorized. + throw new AuthenticationException("Unauthorized. Is not authenticated"); + } + + // Now check that the AppID in the claimset matches + // what we're looking for. Note that in a multi-tenant bot, this value + // comes from developer code that may be reaching out to a service, hence the + // async validation. + + // Look for the "aud" claim, but only if issued from the Bot Framework + if (!identity.getClaimValue(AuthenticationConstants.ISSUER_CLAIM).equals(this.toBotFromChannelTokenIssuer)) { + // The relevant Audiance Claim MUST be present. Not Authorized. + throw new AuthenticationException("Unauthorized. Issuer Claim MUST be present."); + } + // The AppId from the claim in the token must match the AppId specified by the developer. + // In this case, the token is destined for the app, so we find the app ID in the audience claim. + String audienceClaim = identity.getClaimValue(AuthenticationConstants.AUDIENCE_CLAIM); + if (StringUtils.isBlank(audienceClaim)) { + // Claim is not present or is present, but doesn't have a value. Not Authorized. + throw new AuthenticationException("Unauthorized. Issuer Claim MUST be present."); + } + + return this.credentialsFactory.isValidAppId(audienceClaim).thenCompose(result -> { + if (!result) { + // The AppId is not valid. Not Authorized. + throw new AuthenticationException("Invalid AppId passed on token: " + audienceClaim); + } + + if (serviceUrl != null) { + String serviceUrlClaim = identity.getClaimValue(AuthenticationConstants.SERVICE_URL_CLAIM); + + if (StringUtils.isBlank(serviceUrlClaim)) { + // Claim must be present. Not Authorized. + throw new AuthenticationException("Unauthorized. ServiceUrl claim should be present"); + } + + if (!serviceUrlClaim.equalsIgnoreCase(serviceUrlClaim)) { + // Claim must match. Not Authorized. + throw new AuthenticationException("Unauthorized. ServiceUrl claim do not match."); + } + } + return null; + }); + } + + private String getAppId(ClaimsIdentity claimsIdentity) { + // For requests from channel App Id is in Audience claim of JWT token. For emulator it is in AppId claim. For + // unauthenticated requests we have anonymous claimsIdentity provided auth is disabled. + // For Activities coming from Emulator AppId claim contains the Bot's AAD AppId. + String audienceClaim = claimsIdentity.getClaimValue(AuthenticationConstants.AUDIENCE_CLAIM); + String appId = audienceClaim != null + ? audienceClaim + : claimsIdentity.getClaimValue(AuthenticationConstants.APPID_CLAIM); + return appId; + } +} diff --git a/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/authentication/PasswordServiceClientCredentialFactory.java b/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/authentication/PasswordServiceClientCredentialFactory.java new file mode 100644 index 000000000..345dd8d5a --- /dev/null +++ b/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/authentication/PasswordServiceClientCredentialFactory.java @@ -0,0 +1,165 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.bot.connector.authentication; + +import java.util.concurrent.CompletableFuture; + +import com.microsoft.bot.restclient.credentials.ServiceClientCredentials; +import com.nimbusds.oauth2.sdk.util.StringUtils; + +/** + * A simple implementation of the {@link ServiceClientCredentialsFactory} + * interface. + */ +public class PasswordServiceClientCredentialFactory extends ServiceClientCredentialsFactory { + + private String appId; + private String password; + + /** + * Gets the app ID for this credential. + * + * @return The app ID for this credential. + */ + public String getAppId() { + return appId; + } + + /** + * Sets the app ID for this credential. + * + * @param appId The app ID for this credential. + */ + public void setAppId(String appId) { + this.appId = appId; + } + + /** + * Gets the app password for this credential. + * + * @return The app password for this credential. + */ + public String getPassword() { + return password; + } + + /** + * Sets the app password for this credential. + * + * @param password The app password for this credential. + */ + public void setPassword(String password) { + this.password = password; + } + + /** + * Initializes a new instance of the + * {@link PasswordServiceClientCredentialFactory} class. with empty credentials. + */ + public PasswordServiceClientCredentialFactory() { + + } + + /** + * Initializes a new instance of the + * {@link PasswordServiceClientCredentialFactory} class. with the provided + * credentials. + * + * @param withAppId The app ID. + * @param withPassword The app password. + */ + public PasswordServiceClientCredentialFactory(String withAppId, String withPassword) { + this.appId = withAppId; + this.password = withPassword; + } + + /** + * {@inheritDoc} + */ + @Override + public CompletableFuture isValidAppId(String appId) { + return CompletableFuture.completedFuture(appId == this.appId); + } + + /** + * {@inheritDoc} + */ + @Override + public CompletableFuture isAuthenticationDisabled() { + return CompletableFuture.completedFuture(StringUtils.isBlank(this.appId)); + } + + /** + * {@inheritDoc} + */ + @Override + public CompletableFuture createCredentials(String appId, String oauthScope, + String loginEndpoint, Boolean validateAuthority) { + if (this.isAuthenticationDisabled().join()) { + return CompletableFuture.completedFuture(MicrosoftAppCredentials.empty()); + } + + if (!this.isValidAppId(appId).join()) { + throw new IllegalArgumentException("Invalid appId."); + } + + if (loginEndpoint.toLowerCase() + .startsWith(AuthenticationConstants.TO_CHANNEL_FROM_BOT_LOGIN_URL_TEMPLATE.toLowerCase())) { + // TODO : Unpack necessity of these empty credentials based on the loginEndpoint + // as no tokens are fetched when auth is disabled. + ServiceClientCredentials credentials = appId == null ? MicrosoftAppCredentials.empty() + : new MicrosoftAppCredentials(appId, this.password); + return CompletableFuture.completedFuture(credentials); + } else if (loginEndpoint + .equalsIgnoreCase(GovernmentAuthenticationConstants.TO_CHANNEL_FROM_BOT_LOGIN_URL)) { + ServiceClientCredentials credentials = appId == null ? MicrosoftGovernmentAppCredentials.empty() + : new MicrosoftGovernmentAppCredentials(appId, this.password); + return CompletableFuture.completedFuture(credentials); + } else { + ServiceClientCredentials credentials = appId == null + ? new PrivateCloudAppCredentials(null, null, null, loginEndpoint, validateAuthority) + : new PrivateCloudAppCredentials(appId, this.password, oauthScope, loginEndpoint, + validateAuthority); + return CompletableFuture.completedFuture(credentials); + } + } + + public class PrivateCloudAppCredentials extends MicrosoftAppCredentials { + private final String oauthEndpoint; + private final Boolean validateAuthority; + + /** + * Gets the OAuth endpoint to use. + * + * @return The OAuthEndpoint to use. + */ + public String oAuthEndpoint() { + return oauthEndpoint; + } + + /** + * Gets a value indicating whether to validate the Authority. + * + * @return The ValidateAuthority value to use. + */ + public Boolean getValidateAuthority() { + return validateAuthority; + } + + public PrivateCloudAppCredentials(String appId, String password, String oAuthScope, String withOauthEndpoint, + Boolean withValidateAuthority) { + super(appId, password, null, oAuthScope); + this.oauthEndpoint = withOauthEndpoint; + this.validateAuthority = withValidateAuthority; + } + + /** + * {@inheritDoc} + */ + @Override + public Boolean validateAuthority() { + return validateAuthority; + } + } +} diff --git a/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/authentication/ServiceClientCredentialsFactory.java b/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/authentication/ServiceClientCredentialsFactory.java new file mode 100644 index 000000000..e8d84f433 --- /dev/null +++ b/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/authentication/ServiceClientCredentialsFactory.java @@ -0,0 +1,45 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.bot.connector.authentication; + +import java.util.concurrent.CompletableFuture; + +import com.microsoft.bot.restclient.credentials.ServiceClientCredentials; + +/** + * The ServiceClientCredentialsFactory abstract class that allows Bots to + * provide their own ServiceClientCredentials for bot to bot channel or skill + * bot to parent bot calls. + */ +public abstract class ServiceClientCredentialsFactory { + + /** + * Validates an app ID. + * + * @param appId The app ID to validate. + * @return The result is true if `appId` is valid for the controller; otherwise, + * false. + */ + public abstract CompletableFuture isValidAppId(String appId); + + /** + * Checks whether bot authentication is disabled. + * + * @return If bot authentication is disabled, the result is true; otherwise, + * false. + */ + public abstract CompletableFuture isAuthenticationDisabled(); + + /** + * A factory method for creating ServiceClientCredentials. + * + * @param appId The appId. + * @param audience The audience. + * @param loginEndpoint The login url. + * @param validateAuthority he validate authority value to use. + * @return A {@link ServiceClientCredentials}. + */ + public abstract CompletableFuture createCredentials(String appId, String audience, + String loginEndpoint, Boolean validateAuthority); +} diff --git a/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/authentication/UserTokenClient.java b/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/authentication/UserTokenClient.java new file mode 100644 index 000000000..3cb30b129 --- /dev/null +++ b/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/authentication/UserTokenClient.java @@ -0,0 +1,134 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.bot.connector.authentication; + +import java.nio.charset.StandardCharsets; +import java.util.Base64; +import java.util.List; +import java.util.Map; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionException; + +import com.microsoft.bot.schema.Activity; +import com.microsoft.bot.schema.Serialization; +import com.microsoft.bot.schema.SignInResource; +import com.microsoft.bot.schema.TokenExchangeRequest; +import com.microsoft.bot.schema.TokenExchangeState; +import com.microsoft.bot.schema.TokenResponse; +import com.microsoft.bot.schema.TokenStatus; +import com.nimbusds.oauth2.sdk.util.StringUtils; + +/** + * Client for access user token service. + */ +public abstract class UserTokenClient { + + /** + * Attempts to retrieve the token for a user that's in a login flow. + * + * @param userId The user id that will be associated with the token. + * @param connectionName Name of the auth connection to use. + * @param channelId The channel Id that will be associated with the token. + * @param magicCode (Optional) Optional user entered code to validate. + * @return A {@link TokenResponse} object. + */ + public abstract CompletableFuture getUserToken(String userId, String connectionName, + String channelId, String magicCode); + + /** + * Get the raw signin link to be sent to the user for signin for a connection + * name. + * + * @param connectionName Name of the auth connection to use. + * @param activity The {@link Activity} from which to derive the token + * exchange state. + * @param finalRedirect The final URL that the OAuth flow will redirect to. + * @return A {@link SignInResource} + */ + public abstract CompletableFuture getSignInResource(String connectionName, Activity activity, + String finalRedirect); + + /** + * Signs the user out with the token server. + * + * @param userId The user id that will be associated with the token. + * @param connectionName Name of the auth connection to use. + * @param channelId The channel Id that will be associated with the token. + * @return A Task representing the result of the asynchronous operation. + */ + public abstract CompletableFuture signOutUser(String userId, String connectionName, String channelId); + + /** + * Retrieves the token status for each configured connection for the given user. + * + * @param userId The user id that will be associated with the token. + * @param channelId The channel Id that will be associated with the token. + * @param includeFilter The includeFilter. + * @return A list of {@link TokenStatus} objects. + */ + public abstract CompletableFuture> getTokenStatus(String userId, String channelId, + String includeFilter); + + /** + * Retrieves Azure Active Directory tokens for particular resources on a + * configured connection. + * + * @param userId The user id that will be associated with the token. + * @param connectionName Name of the auth connection to use. + * @param resourceUrls The list of resource URLs to retrieve tokens for. + * @param channelId The channel Id that will be associated with the token. + * @return A Dictionary of resourceUrls to the corresponding + * {@link TokenResponse}. + */ + public abstract CompletableFuture> getAadTokens(String userId, String connectionName, + List resourceUrls, String channelId); + + /** + * Performs a token exchange operation such as for single sign-on. + * + * @param userId The user id that will be associated with the token. + * @param connectionName Name of the auth connection to use. + * @param channelId The channel Id that will be associated with the token. + * @param exchangeRequest The exchange request details, either a token to + * exchange or a uri to exchange. + * @return A {@link TokenResponse} object. + */ + public abstract CompletableFuture exchangeToken(String userId, String connectionName, + String channelId, TokenExchangeRequest exchangeRequest); + + /** + * Helper function to create the base64 encoded token exchange state used in + * getSignInResource calls. + * + * @param appId The appId to include in the token exchange state. + * @param connectionName The connectionName to include in the token exchange + * state. + * @param activity The {@link Activity} from which to derive the token + * exchange state. + * @return Base64 encoded token exchange state + */ + protected static String createTokenExchangeState(String appId, String connectionName, Activity activity) { + if (StringUtils.isBlank(appId)) { + throw new IllegalArgumentException("appId"); + } + if (StringUtils.isBlank(appId)) { + throw new IllegalArgumentException("connectionName"); + } + if (activity == null) { + throw new IllegalArgumentException("activity"); + } + + TokenExchangeState tokenExchangeState = new TokenExchangeState(); + tokenExchangeState.setConnectionName(connectionName); + tokenExchangeState.setConversation(activity.getConversationReference()); + tokenExchangeState.setRelatesTo(activity.getRelatesTo()); + tokenExchangeState.setMsAppId(appId); + try { + String serializedState = Serialization.toString(tokenExchangeState); + return Base64.getEncoder().encodeToString(serializedState.getBytes(StandardCharsets.UTF_8)); + } catch (Throwable t) { + throw new CompletionException(t); + } + } +} diff --git a/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/authentication/UserTokenClientImpl.java b/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/authentication/UserTokenClientImpl.java new file mode 100644 index 000000000..79e46bfd4 --- /dev/null +++ b/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/authentication/UserTokenClientImpl.java @@ -0,0 +1,138 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.bot.connector.authentication; + +import java.util.List; +import java.util.Map; +import java.util.concurrent.CompletableFuture; + +import com.microsoft.bot.connector.OAuthClient; +import com.microsoft.bot.connector.rest.RestOAuthClient; +import com.microsoft.bot.restclient.credentials.ServiceClientCredentials; +import com.microsoft.bot.schema.AadResourceUrls; +import com.microsoft.bot.schema.Activity; +import com.microsoft.bot.schema.SignInResource; +import com.microsoft.bot.schema.TokenExchangeRequest; +import com.microsoft.bot.schema.TokenResponse; +import com.microsoft.bot.schema.TokenStatus; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class UserTokenClientImpl extends UserTokenClient { + + private final String appId; + private final OAuthClient client; + /** + * The... ummm... logger. + */ + private final Logger logger = LoggerFactory.getLogger(UserTokenClientImpl.class); + + public UserTokenClientImpl(String withAppId, ServiceClientCredentials credentials, String oauthEndpoint) { + super(); + this.appId = withAppId; + this.client = new RestOAuthClient(oauthEndpoint, credentials); + } + + /** + * {@inheritDoc} + */ + @Override + public CompletableFuture getUserToken(String userId, String connectionName, String channelId, + String magicCode) { + if (userId == null) { + throw new IllegalArgumentException("userId cannot be null"); + } + if (connectionName == null) { + throw new IllegalArgumentException("connectionName cannot be null"); + } + + logger.info(String.format("getToken ConnectionName: %s", connectionName)); + return client.getUserToken().getToken(userId, connectionName, channelId, magicCode); + } + + /** + * {@inheritDoc} + */ + @Override + public CompletableFuture getSignInResource(String connectionName, Activity activity, + String finalRedirect) { + if (connectionName == null) { + throw new IllegalArgumentException("connectionName cannot be null"); + } + if (activity == null) { + throw new IllegalArgumentException("activity cannot be null"); + } + + logger.info(String.format("getSignInResource ConnectionName: %s", connectionName)); + String state = createTokenExchangeState(appId, connectionName, activity); + return client.getBotSignIn().getSignInResource(state); + } + + /** + * {@inheritDoc} + */ + @Override + public CompletableFuture signOutUser(String userId, String connectionName, String channelId) { + if (userId == null) { + throw new IllegalArgumentException("userId cannot be null"); + } + if (connectionName == null) { + throw new IllegalArgumentException("connectionName cannot be null"); + } + + logger.info(String.format("signOutUser ConnectionName: %s", connectionName)); + return client.getUserToken().signOut(userId, connectionName, channelId).thenApply(signOutResult -> null); + } + + /** + * {@inheritDoc} + */ + @Override + public CompletableFuture> getTokenStatus(String userId, String channelId, String includeFilter) { + if (userId == null) { + throw new IllegalArgumentException("userId cannot be null"); + } + if (channelId == null) { + throw new IllegalArgumentException("channelId cannot be null"); + } + + logger.info("getTokenStatus"); + return client.getUserToken().getTokenStatus(userId, channelId, includeFilter); + } + + /** + * {@inheritDoc} + */ + @Override + public CompletableFuture> getAadTokens(String userId, String connectionName, + List resourceUrls, String channelId) { + if (userId == null) { + throw new IllegalArgumentException("userId cannot be null"); + } + if (connectionName == null) { + throw new IllegalArgumentException("connectionName cannot be null"); + } + + logger.info(String.format("getAadTokens ConnectionName: %s", connectionName)); + return client.getUserToken().getAadTokens(userId, connectionName, new AadResourceUrls(resourceUrls), channelId); + } + + /** + * {@inheritDoc} + */ + @Override + public CompletableFuture exchangeToken(String userId, String connectionName, String channelId, + TokenExchangeRequest exchangeRequest) { + if (userId == null) { + throw new IllegalArgumentException("userId cannot be null"); + } + if (connectionName == null) { + throw new IllegalArgumentException("connectionName cannot be null"); + } + + logger.info(String.format("exchangeToken ConnectionName: %s", connectionName)); + return client.getUserToken().exchangeToken(userId, connectionName, channelId, exchangeRequest); + } +} From fe8cd7c8b7b9bb60598a250ae4fd5171f4852e1d Mon Sep 17 00:00:00 2001 From: Martin Battaglino Date: Wed, 14 Jul 2021 16:19:28 -0300 Subject: [PATCH 05/27] Add main classes of CloudAdapter and CloudAdapterBase --- .../builder/ChannelServiceHandlerBase.java | 560 +++++++++++++++++ .../bot/builder/CloudAdapterBase.java | 570 ++++++++++++++++++ .../builder/CloudChannelServiceHandler.java | 35 ++ .../bot/integration/CloudAdapter.java | 55 ++ ...nfigurationBotFrameworkAuthentication.java | 126 ++++ ...urationServiceClientCredentialFactory.java | 28 + 6 files changed, 1374 insertions(+) create mode 100644 libraries/bot-builder/src/main/java/com/microsoft/bot/builder/ChannelServiceHandlerBase.java create mode 100644 libraries/bot-builder/src/main/java/com/microsoft/bot/builder/CloudAdapterBase.java create mode 100644 libraries/bot-builder/src/main/java/com/microsoft/bot/builder/CloudChannelServiceHandler.java create mode 100644 libraries/bot-integration-core/src/main/java/com/microsoft/bot/integration/CloudAdapter.java create mode 100644 libraries/bot-integration-core/src/main/java/com/microsoft/bot/integration/ConfigurationBotFrameworkAuthentication.java create mode 100644 libraries/bot-integration-core/src/main/java/com/microsoft/bot/integration/ConfigurationServiceClientCredentialFactory.java diff --git a/libraries/bot-builder/src/main/java/com/microsoft/bot/builder/ChannelServiceHandlerBase.java b/libraries/bot-builder/src/main/java/com/microsoft/bot/builder/ChannelServiceHandlerBase.java new file mode 100644 index 000000000..b67a35530 --- /dev/null +++ b/libraries/bot-builder/src/main/java/com/microsoft/bot/builder/ChannelServiceHandlerBase.java @@ -0,0 +1,560 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.bot.builder; + +import com.microsoft.bot.connector.authentication.ClaimsIdentity; +import com.microsoft.bot.schema.Activity; +import com.microsoft.bot.schema.AttachmentData; +import com.microsoft.bot.schema.ChannelAccount; +import com.microsoft.bot.schema.ConversationParameters; +import com.microsoft.bot.schema.ConversationResourceResponse; +import com.microsoft.bot.schema.ConversationsResult; +import com.microsoft.bot.schema.PagedMembersResult; +import com.microsoft.bot.schema.ResourceResponse; +import com.microsoft.bot.schema.Transcript; +import org.apache.commons.lang3.NotImplementedException; + +import java.util.List; +import java.util.concurrent.CompletableFuture; + +/** + * Base class for Bot Framework protocol implementation. + */ +public abstract class ChannelServiceHandlerBase { + + /** + * Sends an activity to the end of a conversation. + * + * @param authHeader The authentication header. + * @param conversationId The conversation Id. + * @param activity The activity to send. + * + * @return A {@link CompletableFuture {TResult}} representing the + * result of the asynchronous operation. + */ + public CompletableFuture handleSendToConversation( + String authHeader, + String conversationId, + Activity activity) { + + return authenticate(authHeader).thenCompose(claimsIdentity -> { + return onSendToConversation(claimsIdentity, conversationId, activity); + }); + } + + /** + * Sends a reply to an activity. + * + * @param authHeader The authentication header. + * @param conversationId The conversation Id. + * @param activityId The activity Id the reply is to. + * @param activity The activity to send. + * + * @return A {@link CompletableFuture{TResult}} representing the + * result of the asynchronous operation. + */ + public CompletableFuture handleReplyToActivity( + String authHeader, + String conversationId, + String activityId, + Activity activity) { + + return authenticate(authHeader).thenCompose(claimsIdentity -> { + return onReplyToActivity(claimsIdentity, conversationId, activityId, activity); + }); + } + + /** + * Edits a previously sent existing activity. + * + * @param authHeader The authentication header. + * @param conversationId The conversation Id. + * @param activityId The activity Id to update. + * @param activity The replacement activity. + * + * @return A {@link CompletableFuture{TResult}} representing the + * result of the asynchronous operation. + */ + public CompletableFuture handleUpdateActivity( + String authHeader, + String conversationId, + String activityId, + Activity activity) { + return authenticate(authHeader).thenCompose(claimsIdentity -> { + return onUpdateActivity(claimsIdentity, conversationId, activityId, activity); + }); + } + + /** + * Deletes an existing activity. + * + * @param authHeader The authentication header. + * @param conversationId The conversation Id. + * @param activityId The activity Id. + * + * @return A {@link CompletableFuture} representing the result of + * the asynchronous operation. + */ + public CompletableFuture handleDeleteActivity(String authHeader, String conversationId, String activityId) { + return authenticate(authHeader).thenCompose(claimsIdentity -> { + return onDeleteActivity(claimsIdentity, conversationId, activityId); + }); + } + + /** + * Enumerates the members of an activity. + * + * @param authHeader The authentication header. + * @param conversationId The conversation Id. + * @param activityId The activity Id. + * + * @return A {@link CompletableFuture{TResult}} representing the + * result of the asynchronous operation. + */ + public CompletableFuture> handleGetActivityMembers( + String authHeader, + String conversationId, + String activityId) { + return authenticate(authHeader).thenCompose(claimsIdentity -> { + return onGetActivityMembers(claimsIdentity, conversationId, activityId); + }); + } + + /** + * Create a new Conversation. + * + * @param authHeader The authentication header. + * @param parameters Parameters to create the conversation from. + * + * @return A {@link CompletableFuture{TResult}} representing the + * result of the asynchronous operation. + */ + public CompletableFuture handleCreateConversation( + String authHeader, + ConversationParameters parameters) { + return authenticate(authHeader).thenCompose(claimsIdentity -> { + return onCreateConversation(claimsIdentity, parameters); + }); + } + + /** + * Lists the Conversations in which the bot has participated. + * + * @param authHeader The authentication header. + * @param conversationId The conversation Id. + * @param continuationToken A skip or continuation token. + * + * @return A {@link CompletableFuture{TResult}} representing the + * result of the asynchronous operation. + */ + public CompletableFuture handleGetConversations( + String authHeader, + String conversationId, + String continuationToken) { + return authenticate(authHeader).thenCompose(claimsIdentity -> { + return onGetConversations(claimsIdentity, conversationId, continuationToken); + }); + } + + /** + * Enumerates the members of a conversation. + * + * @param authHeader The authentication header. + * @param conversationId The conversation Id. + * + * @return A {@link CompletableFuture{TResult}} representing the + * result of the asynchronous operation. + */ + public CompletableFuture> handleGetConversationMembers( + String authHeader, + String conversationId) { + return authenticate(authHeader).thenCompose(claimsIdentity -> { + return onGetConversationMembers(claimsIdentity, conversationId); + }); + } + + /** + * Enumerates the members of a conversation one page at a time. + * + * @param authHeader The authentication header. + * @param conversationId The conversation Id. + * @param pageSize Suggested page size. + * @param continuationToken A continuation token. + * + * @return A {@link CompletableFuture{TResult}} representing the + * result of the asynchronous operation. + */ + public CompletableFuture handleGetConversationPagedMembers( + String authHeader, + String conversationId, + Integer pageSize, + String continuationToken) { + return authenticate(authHeader).thenCompose(claimsIdentity -> { + return onGetConversationPagedMembers(claimsIdentity, conversationId, pageSize, continuationToken); + }); + } + + /** + * Deletes a member from a conversation. + * + * @param authHeader The authentication header. + * @param conversationId The conversation Id. + * @param memberId Id of the member to delete from this + * conversation. + * + * @return A {@link CompletableFuture} representing the + * asynchronous operation. + */ + public CompletableFuture handleDeleteConversationMember( + String authHeader, + String conversationId, + String memberId) { + return authenticate(authHeader).thenCompose(claimsIdentity -> { + return onDeleteConversationMember(claimsIdentity, conversationId, memberId); + }); + } + + /** + * Uploads the historic activities of the conversation. + * + * @param authHeader The authentication header. + * @param conversationId The conversation Id. + * @param transcript Transcript of activities. + * + * @return A {@link CompletableFuture{TResult}} representing the + * result of the asynchronous operation. + */ + public CompletableFuture handleSendConversationHistory( + String authHeader, + String conversationId, + Transcript transcript) { + return authenticate(authHeader).thenCompose(claimsIdentity -> { + return onSendConversationHistory(claimsIdentity, conversationId, transcript); + }); + } + + /** + * Stores data in a compliant store when dealing with enterprises. + * + * @param authHeader The authentication header. + * @param conversationId The conversation Id. + * @param attachmentUpload Attachment data. + * + * @return A {@link CompletableFuture{TResult}} representing the + * result of the asynchronous operation. + */ + public CompletableFuture handleUploadAttachment( + String authHeader, + String conversationId, + AttachmentData attachmentUpload) { + return authenticate(authHeader).thenCompose(claimsIdentity -> { + return onUploadAttachment(claimsIdentity, conversationId, attachmentUpload); + }); + } + + /** + * Helper to authenticate the header token and extract the claims. + * @param authHeader The auth header containing JWT token. + * @return A {@link ClaimsIdentity} representing the claims associated with given header. + */ + protected abstract CompletableFuture authenticate(String authHeader); + + /** + * SendToConversation() API for Skill. + * + * This method allows you to send an activity to the end of a conversation. + * This is slightly different from ReplyToActivity(). * + * SendToConversation(conversationId) - will append the activity to the end + * of the conversation according to the timestamp or semantics of the + * channel. * ReplyToActivity(conversationId,ActivityId) - adds the + * activity as a reply to another activity, if the channel supports it. If + * the channel does not support nested replies, ReplyToActivity falls back + * to SendToConversation. Use ReplyToActivity when replying to a specific + * activity in the conversation. Use SendToConversation in all other cases. + * + * @param claimsIdentity claimsIdentity for the bot, should have + * AudienceClaim, AppIdClaim and ServiceUrlClaim. + * @param conversationId conversationId. + * @param activity Activity to send. + * + * @return Task for a resource response. + */ + protected CompletableFuture onSendToConversation( + ClaimsIdentity claimsIdentity, + String conversationId, + Activity activity) { + throw new NotImplementedException("onSendToConversation is not implemented"); + } + + /** + * OnReplyToActivity() API. + * + * Override this method allows to reply to an Activity. This is slightly + * different from SendToConversation(). * + * SendToConversation(conversationId) - will append the activity to the end + * of the conversation according to the timestamp or semantics of the + * channel. * ReplyToActivity(conversationId,ActivityId) - adds the + * activity as a reply to another activity, if the channel supports it. If + * the channel does not support nested replies, ReplyToActivity falls back + * to SendToConversation. Use ReplyToActivity when replying to a specific + * activity in the conversation. Use SendToConversation in all other cases. + * + * @param claimsIdentity claimsIdentity for the bot, should have + * AudienceClaim, AppIdClaim and ServiceUrlClaim. + * @param conversationId Conversation D. + * @param activityId activityId the reply is to (OPTIONAL). + * @param activity Activity to send. + * + * @return Task for a resource response. + */ + protected CompletableFuture onReplyToActivity( + ClaimsIdentity claimsIdentity, + String conversationId, + String activityId, + Activity activity) { + throw new NotImplementedException("onReplyToActivity is not implemented"); + } + + /** + * OnUpdateActivity() API. + * + * Override this method to edit a previously sent existing activity. Some + * channels allow you to edit an existing activity to reflect the new state + * of a bot conversation. For example, you can remove buttons after someone + * has clicked "Approve" button. + * + * @param claimsIdentity claimsIdentity for the bot, should have + * AudienceClaim, AppIdClaim and ServiceUrlClaim. + * @param conversationId Conversation D. + * @param activityId activityId to update. + * @param activity replacement Activity. + * + * @return Task for a resource response. + */ + protected CompletableFuture onUpdateActivity( + ClaimsIdentity claimsIdentity, + String conversationId, + String activityId, + Activity activity) { + throw new NotImplementedException("onUpdateActivity is not implemented"); + } + + /** + * OnDeleteActivity() API. + * + * Override this method to Delete an existing activity. Some channels allow + * you to delete an existing activity, and if successful this method will + * remove the specified activity. + * + * @param claimsIdentity claimsIdentity for the bot, should have + * AudienceClaim, AppIdClaim and ServiceUrlClaim. + * @param conversationId Conversation D. + * @param activityId activityId to delete. + * + * @return Task for a resource response. + */ + protected CompletableFuture onDeleteActivity( + ClaimsIdentity claimsIdentity, + String conversationId, + String activityId) { + throw new NotImplementedException("onDeleteActivity is not implemented"); + } + + /** + * OnGetActivityMembers() API. + * + * Override this method to enumerate the members of an activity. This REST + * API takes a ConversationId and a ActivityId, returning an array of + * ChannelAccount Objects representing the members of the particular + * activity in the conversation. + * + * @param claimsIdentity claimsIdentity for the bot, should have + * AudienceClaim, AppIdClaim and ServiceUrlClaim. + * @param conversationId Conversation D. + * @param activityId Activity D. + * + * @return Task with result. + */ + protected CompletableFuture> onGetActivityMembers( + ClaimsIdentity claimsIdentity, + String conversationId, + String activityId) { + throw new NotImplementedException("onGetActivityMembers is not implemented"); + } + + /** + * CreateConversation() API. + * + * Override this method to create a new Conversation. POST to this method + * with a * Bot being the bot creating the conversation * IsGroup set to + * true if this is not a direct message (default instanceof false) * Array + * containing the members to include in the conversation The return value + * is a ResourceResponse which contains a conversation D which is suitable + * for use in the message payload and REST API URIs. Most channels only + * support the semantics of bots initiating a direct message conversation. + * An example of how to do that would be: var resource = + * connector.getconversations().CreateConversation(new + * ConversationParameters(){ Bot = bot, members = new ChannelAccount[] { + * new ChannelAccount("user1") } ); + * connect.getConversations().OnSendToConversation(resource.getId(), new + * Activity() ... ) ; end. + * + * @param claimsIdentity claimsIdentity for the bot, should have + * AudienceClaim, AppIdClaim and ServiceUrlClaim. + * @param parameters Parameters to create the conversation + * from. + * + * @return Task for a conversation resource response. + */ + protected CompletableFuture onCreateConversation( + ClaimsIdentity claimsIdentity, + ConversationParameters parameters) { + throw new NotImplementedException("onCreateConversation is not implemented"); + } + + /** + * OnGetConversations() API for Skill. + * + * Override this method to list the Conversations in which this bot has + * participated. GET from this method with a skip token The return value is + * a ConversationsResult, which contains an array of ConversationMembers + * and a skip token. If the skip token is not empty, then there are further + * values to be returned. Call this method again with the returned token to + * get more values. Each ConversationMembers Object contains the D of the + * conversation and an array of ChannelAccounts that describe the members + * of the conversation. + * + * @param claimsIdentity claimsIdentity for the bot, should have + * AudienceClaim, AppIdClaim and ServiceUrlClaim. + * @param conversationId conversationId. + * @param continuationToken skip or continuation token. + * + * @return Task for ConversationsResult. + */ + protected CompletableFuture onGetConversations( + ClaimsIdentity claimsIdentity, + String conversationId, + String continuationToken) { + throw new NotImplementedException("onGetConversations is not implemented"); + } + + /** + * GetConversationMembers() API for Skill. + * + * Override this method to enumerate the members of a conversation. This + * REST API takes a ConversationId and returns an array of ChannelAccount + * Objects representing the members of the conversation. + * + * @param claimsIdentity claimsIdentity for the bot, should have + * AudienceClaim, AppIdClaim and ServiceUrlClaim. + * @param conversationId Conversation ID. + * + * @return Task for a response. + */ + protected CompletableFuture> onGetConversationMembers( + ClaimsIdentity claimsIdentity, + String conversationId) { + throw new NotImplementedException("onGetConversationMembers is not implemented"); + } + + /** + * GetConversationPagedMembers() API for Skill. + * + * Override this method to enumerate the members of a conversation one page + * at a time. This REST API takes a ConversationId. Optionally a pageSize + * and/or continuationToken can be provided. It returns a + * PagedMembersResult, which contains an array of ChannelAccounts + * representing the members of the conversation and a continuation token + * that can be used to get more values. One page of ChannelAccounts records + * are returned with each call. The number of records in a page may vary + * between channels and calls. The pageSize parameter can be used as a + * suggestion. If there are no additional results the response will not + * contain a continuation token. If there are no members in the + * conversation the Members will be empty or not present in the response. A + * response to a request that has a continuation token from a prior request + * may rarely return members from a previous request. + * + * @param claimsIdentity claimsIdentity for the bot, should have + * AudienceClaim, AppIdClaim and ServiceUrlClaim. + * @param conversationId Conversation ID. + * @param pageSize Suggested page size. + * @param continuationToken Continuation Token. + * + * @return Task for a response. + */ + protected CompletableFuture onGetConversationPagedMembers( + ClaimsIdentity claimsIdentity, + String conversationId, + Integer pageSize, + String continuationToken) { + throw new NotImplementedException("onGetConversationPagedMembers is not implemented"); + } + + /** + * DeleteConversationMember() API for Skill. + * + * Override this method to deletes a member from a conversation. This REST + * API takes a ConversationId and a memberId (of type String) and removes + * that member from the conversation. If that member was the last member of + * the conversation, the conversation will also be deleted. + * + * @param claimsIdentity claimsIdentity for the bot, should have + * AudienceClaim, AppIdClaim and ServiceUrlClaim. + * @param conversationId Conversation ID. + * @param memberId ID of the member to delete from this + * conversation. + * + * @return Task. + */ + protected CompletableFuture onDeleteConversationMember( + ClaimsIdentity claimsIdentity, + String conversationId, + String memberId) { + throw new NotImplementedException("onDeleteConversationMember is not implemented"); + } + + /** + * SendConversationHistory() API for Skill. + * + * Override this method to this method allows you to upload the historic + * activities to the conversation. Sender must ensure that the historic + * activities have unique ids and appropriate timestamps. The ids are used + * by the client to deal with duplicate activities and the timestamps are + * used by the client to render the activities in the right order. + * + * @param claimsIdentity claimsIdentity for the bot, should have + * AudienceClaim, AppIdClaim and ServiceUrlClaim. + * @param conversationId Conversation ID. + * @param transcript Transcript of activities. + * + * @return Task for a resource response. + */ + protected CompletableFuture onSendConversationHistory( + ClaimsIdentity claimsIdentity, + String conversationId, + Transcript transcript) { + throw new NotImplementedException("onSendConversationHistory is not implemented"); + } + + /** + * UploadAttachment() API. + * + * Override this method to store data in a compliant store when dealing + * with enterprises. The response is a ResourceResponse which contains an + * AttachmentId which is suitable for using with the attachments API. + * + * @param claimsIdentity claimsIdentity for the bot, should have + * AudienceClaim, AppIdClaim and ServiceUrlClaim. + * @param conversationId Conversation ID. + * @param attachmentUpload Attachment data. + * + * @return Task with result. + */ + protected CompletableFuture onUploadAttachment( + ClaimsIdentity claimsIdentity, + String conversationId, + AttachmentData attachmentUpload) { + throw new NotImplementedException("onUploadAttachment is not implemented"); + } +} diff --git a/libraries/bot-builder/src/main/java/com/microsoft/bot/builder/CloudAdapterBase.java b/libraries/bot-builder/src/main/java/com/microsoft/bot/builder/CloudAdapterBase.java new file mode 100644 index 000000000..392e78121 --- /dev/null +++ b/libraries/bot-builder/src/main/java/com/microsoft/bot/builder/CloudAdapterBase.java @@ -0,0 +1,570 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.bot.builder; + +import com.microsoft.bot.connector.Async; +import com.microsoft.bot.connector.Channels; +import com.microsoft.bot.connector.ConnectorClient; +import com.microsoft.bot.connector.ExecutorFactory; + +import com.microsoft.bot.connector.authentication.AuthenticateRequestResult; +import com.microsoft.bot.connector.authentication.AuthenticationConstants; +import com.microsoft.bot.connector.authentication.BotFrameworkAuthentication; +import com.microsoft.bot.connector.authentication.ClaimsIdentity; +import com.microsoft.bot.connector.authentication.ConnectorFactory; +import com.microsoft.bot.connector.authentication.UserTokenClient; +import com.microsoft.bot.schema.Activity; +import com.microsoft.bot.schema.ActivityEventNames; +import com.microsoft.bot.schema.ActivityTypes; +import com.microsoft.bot.schema.ConversationAccount; +import com.microsoft.bot.schema.ConversationParameters; +import com.microsoft.bot.schema.ConversationReference; +import com.microsoft.bot.schema.ConversationResourceResponse; +import com.microsoft.bot.schema.DeliveryModes; +import com.microsoft.bot.schema.ExpectedReplies; +import com.microsoft.bot.schema.InvokeResponse; +import com.microsoft.bot.schema.ResourceResponse; +import org.apache.commons.lang3.StringUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.net.HttpURLConnection; +import java.util.HashMap; +import java.util.List; +import java.util.UUID; +import java.util.concurrent.CompletableFuture; + +/** + * An adapter that implements the Bot Framework Protocol and can be hosted in different cloud environments + * both public and private. + */ +public abstract class CloudAdapterBase extends BotAdapter { + public static final String CONNECTOR_FACTORY_KEY = "ConnectorFactory"; + public static final String USER_TOKEN_CLIENT_KEY = "UserTokenClient"; + + private static final Integer DEFAULT_MS_DELAY = 1000; + + private BotFrameworkAuthentication botFrameworkAuthentication; + private Logger logger = LoggerFactory.getLogger(CloudAdapterBase.class); + + /** + * Gets a {@link Logger} to use within this adapter and its subclasses. + * @return The {@link Logger} instance for this adapter. + */ + public Logger getLogger() { + return logger; + } + + /** + * Gets the {@link BotFrameworkAuthentication} instance for this adapter. + * @return The {@link BotFrameworkAuthentication} instance for this adapter. + */ + public BotFrameworkAuthentication getBotFrameworkAuthentication() { + return botFrameworkAuthentication; + } + + /** + * Initializes a new instance of the {@link CloudAdapterBase} class. + * @param withBotFrameworkAuthentication The cloud environment used for validating and creating tokens. + */ + protected CloudAdapterBase(BotFrameworkAuthentication withBotFrameworkAuthentication) { + if (withBotFrameworkAuthentication == null) { + throw new IllegalArgumentException("withBotFrameworkAuthentication cannot be null"); + } + this.botFrameworkAuthentication = withBotFrameworkAuthentication; + } + + /** + * {@inheritDoc} + */ + @Override + public CompletableFuture sendActivities(TurnContext context, List activities) { + if (context == null) { + return Async.completeExceptionally(new IllegalArgumentException("context")); + } + + if (activities == null) { + return Async.completeExceptionally(new IllegalArgumentException("activities")); + } + + if (activities.size() == 0) { + return Async.completeExceptionally( + new IllegalArgumentException("Expecting one or more activities, but the array was empty.") + ); + } + + logger.info(String.format("sendActivities for %d activities.", activities.size())); + + return CompletableFuture.supplyAsync(() -> { + ResourceResponse[] responses = new ResourceResponse[activities.size()]; + + for (int index = 0; index < activities.size(); index++) { + Activity activity = activities.get(index); + + activity.setId(null); + ResourceResponse response; + + logger.info(String.format("Sending activity. ReplyToId: %s", activity.getReplyToId())); + + if (activity.isType(ActivityTypes.DELAY)) { + int delayMs = activity.getValue() != null + ? ((Number) activity.getValue()).intValue() + : DEFAULT_MS_DELAY; + try { + Thread.sleep(delayMs); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + response = null; + } else if (activity.isType(ActivityTypes.INVOKE_RESPONSE)) { + context.getTurnState().add(BotFrameworkAdapter.INVOKE_RESPONSE_KEY, activity); + response = null; + } else if ( + activity.isType(ActivityTypes.TRACE) + && !StringUtils.equals(activity.getChannelId(), Channels.EMULATOR) + ) { + // no-op + response = null; + } else if (StringUtils.isNotBlank(activity.getReplyToId())) { + ConnectorClient connectorClient = context.getTurnState() + .get(BotFrameworkAdapter.CONNECTOR_CLIENT_KEY); + response = connectorClient.getConversations().replyToActivity(activity).join(); + } else { + ConnectorClient connectorClient = context.getTurnState() + .get(BotFrameworkAdapter.CONNECTOR_CLIENT_KEY); + response = connectorClient.getConversations().sendToConversation(activity).join(); + } + if (response == null) { + response = new ResourceResponse((activity.getId() == null) ? "" : activity.getId()); + } + + responses[index] = response; + } + + return responses; + }, ExecutorFactory.getExecutor()); + } + + /** + * {@inheritDoc} + */ + @Override + public CompletableFuture updateActivity(TurnContext context, Activity activity) { + if (context == null) { + return Async.completeExceptionally(new IllegalArgumentException("context")); + } + + if (activity == null) { + return Async.completeExceptionally(new IllegalArgumentException("activity")); + } + + logger.info(String.format("updateActivity activityId: %d", activity.getId())); + + ConnectorClient connectorClient = context.getTurnState().get(BotFrameworkAdapter.CONNECTOR_CLIENT_KEY); + return connectorClient.getConversations().updateActivity(activity); + } + + /** + * {@inheritDoc} + */ + @Override + public CompletableFuture deleteActivity(TurnContext context, ConversationReference reference) { + if (context == null) { + return Async.completeExceptionally(new IllegalArgumentException("context")); + } + + if (reference == null) { + return Async.completeExceptionally(new IllegalArgumentException("reference")); + } + + logger.info(String.format("deleteActivity Conversation id: %d, activityId: %d", + reference.getConversation().getId(), + reference.getActivityId())); + + ConnectorClient connectorClient = context.getTurnState().get(BotFrameworkAdapter.CONNECTOR_CLIENT_KEY); + return connectorClient.getConversations() + .deleteActivity(reference.getConversation().getId(), reference.getActivityId()); + } + + /** + * {@inheritDoc} + */ + @Override + public CompletableFuture continueConversation( + String botAppId, + ConversationReference reference, + BotCallbackHandler callback + ) { + if (reference == null) { + return Async.completeExceptionally(new IllegalArgumentException("reference")); + } + + return processProactive(createClaimsIdentity(botAppId, null), + reference.getContinuationActivity(), + null, + callback); + } + + /** + * {@inheritDoc} + */ + @Override + public CompletableFuture continueConversation( + ClaimsIdentity claimsIdentity, + ConversationReference reference, + BotCallbackHandler callback + ) { + if (reference == null) { + return Async.completeExceptionally(new IllegalArgumentException("reference")); + } + + return processProactive(claimsIdentity, reference.getContinuationActivity(), null, callback); + } + + /** + * {@inheritDoc} + */ + @Override + public CompletableFuture continueConversation( + ClaimsIdentity claimsIdentity, + ConversationReference reference, + String audience, + BotCallbackHandler callback + ) { + if (claimsIdentity == null) { + return Async.completeExceptionally(new IllegalArgumentException("claimsIdentity")); + } + if (reference == null) { + return Async.completeExceptionally(new IllegalArgumentException("reference")); + } + if (callback == null) { + return Async.completeExceptionally(new IllegalArgumentException("callback")); + } + + return processProactive(claimsIdentity, reference.getContinuationActivity(), audience, callback); + } + + /** + * {@inheritDoc} + */ + @Override + public CompletableFuture continueConversation( + String botAppId, + Activity continuationActivity, + BotCallbackHandler callback + ) { + if (callback == null) { + return Async.completeExceptionally(new IllegalArgumentException("callback")); + } + validateContinuationActivity(continuationActivity); + + return processProactive(createClaimsIdentity(botAppId, null), continuationActivity, null, callback); + } + + /** + * {@inheritDoc} + */ + @Override + public CompletableFuture continueConversation( + ClaimsIdentity claimsIdentity, + Activity continuationActivity, + BotCallbackHandler callback + ) { + if (claimsIdentity == null) { + return Async.completeExceptionally(new IllegalArgumentException("claimsIdentity")); + } + if (callback == null) { + return Async.completeExceptionally(new IllegalArgumentException("callback")); + } + validateContinuationActivity(continuationActivity); + + return processProactive(claimsIdentity, continuationActivity, null, callback); + } + + /** + * {@inheritDoc} + */ + @Override + public CompletableFuture continueConversation( + ClaimsIdentity claimsIdentity, + Activity continuationActivity, + String audience, + BotCallbackHandler callback + ) { + if (claimsIdentity == null) { + return Async.completeExceptionally(new IllegalArgumentException("claimsIdentity")); + } + if (callback == null) { + return Async.completeExceptionally(new IllegalArgumentException("callback")); + } + validateContinuationActivity(continuationActivity); + + return processProactive(claimsIdentity, continuationActivity, audience, callback); + } + + /** + * {@inheritDoc} + */ + @Override + public CompletableFuture createConversation(String botAppId, String channelId, String serviceUrl, + String audience, ConversationParameters conversationParameters, + BotCallbackHandler callback) { + if (StringUtils.isBlank(serviceUrl)) { + throw new IllegalArgumentException("serviceUrl cannot be null or empty"); + } + if (conversationParameters == null) { + throw new IllegalArgumentException("conversationParameters cannot be null"); + } + if (callback == null) { + throw new IllegalArgumentException("callback cannot be null"); + } + + logger.info( + String.format( + "createConversation for channel: %s", + channelId + ) + ); + + // Create a ClaimsIdentity, to create the connector and for adding to the turn context. + ClaimsIdentity claimsIdentity = this.createClaimsIdentity(botAppId, audience); + claimsIdentity.claims().put(AuthenticationConstants.SERVICE_URL_CLAIM, serviceUrl); + + // Create the connector factory. + ConnectorFactory connectorFactory = this.botFrameworkAuthentication.createConnectorFactory(claimsIdentity); + + // Create the connector client to use for outbound requests. + return connectorFactory.create(serviceUrl, audience).thenCompose(connectorClient -> { + // Make the actual create conversation call using the connector. + return connectorClient.getConversations().createConversation(conversationParameters). + thenCompose(createConversationResult -> { + // Create the create activity to communicate the results to the application. + Activity createActivity = this.createCreateActivity(createConversationResult, + channelId, + serviceUrl, + conversationParameters); + + // Create a UserTokenClient instance for the application to use. (For example, in the OAuthPrompt.) + return this.botFrameworkAuthentication.createUserTokenClient(claimsIdentity). + thenCompose(userTokenClient -> { + TurnContextImpl context = this.createTurnContext(createActivity, claimsIdentity, null, + connectorClient, userTokenClient, callback, + connectorFactory); + + // Run the pipeline + return this.runPipeline(context, callback).thenApply(result -> null); + }); + }); + }); + } + + /** + * The implementation for continue conversation. + * @param claimsIdentity A {@link ClaimsIdentity} for the conversation. + * @param continuationActivity The continuation {@link Activity} used to create the {@link TurnContext}. + * @param audience The audience for the call. + * @param callback The method to call for the resulting bot turn. + * @return A task that represents the work queued to execute. + */ + protected CompletableFuture processProactive( + ClaimsIdentity claimsIdentity, + Activity continuationActivity, + String audience, + BotCallbackHandler callback + ) { + logger.info( + String.format( + "processProactive for Conversation Id: %s", + continuationActivity.getConversation().getId())); + + // Create the connector factory and the inbound request, extracting parameters + // and then create a connector for outbound requests. + ConnectorFactory connectorFactory = this.botFrameworkAuthentication.createConnectorFactory(claimsIdentity); + + // Create the connector client to use for outbound requests. + return connectorFactory.create(continuationActivity.getServiceUrl(), audience).thenCompose(connectorClient -> { + // Create a UserTokenClient instance for the application to use. (For example, in the OAuthPrompt.) + return this.botFrameworkAuthentication.createUserTokenClient(claimsIdentity).thenCompose(userTokenClient + -> { + // Create a turn context and run the pipeline. + TurnContext context = createTurnContext( + continuationActivity, + claimsIdentity, + audience, + connectorClient, + userTokenClient, + callback, + connectorFactory); + // Run the pipeline + return runPipeline(context, callback); + }); + }); + } + + /** + * The implementation for processing an Activity sent to this bot. + * @param authHeader The authorization header from the http request. + * @param activity The {@link Activity} to process. + * @param callback The method to call for the resulting bot turn. + * @return A task that represents the work queued to execute. Containing the InvokeResponse if there is one. + */ + protected CompletableFuture processActivity( + String authHeader, + Activity activity, + BotCallbackHandler callback) { + logger.info("processActivity"); + + // Authenticate the inbound request, + // extracting parameters and create a ConnectorFactory for creating a Connector for outbound requests. + return this.botFrameworkAuthentication.authenticateRequest(activity, authHeader).thenCompose( + authenticateRequestResult -> processActivity(authenticateRequestResult, activity, callback)); + } + + /** + * The implementation for processing an Activity sent to this bot. + * @param authenticateRequestResult The authentication results for this turn. + * @param activity The {@link Activity} to process. + * @param callbackHandler The method to call for the resulting bot turn. + * @return A task that represents the work queued to execute. Containing the InvokeResponse if there is one. + */ + protected CompletableFuture processActivity( + AuthenticateRequestResult authenticateRequestResult, + Activity activity, + BotCallbackHandler callbackHandler + ) { + // Set the callerId on the activity. + activity.setCallerId(authenticateRequestResult.getCallerId()); + + // Create the connector client to use for outbound requests. + return authenticateRequestResult.getConnectorFactory().create( + activity.getServiceUrl(), + authenticateRequestResult.getAudience()) + .thenCompose(connectorClient -> { + // Create a UserTokenClient instance for the application to use. + // (For example, it would be used in a sign-in prompt.) + return this.botFrameworkAuthentication.createUserTokenClient(authenticateRequestResult.getClaimsIdentity()) + .thenCompose(userTokenClient -> { + // Create a turn context and run the pipeline. + TurnContextImpl context = createTurnContext( + activity, + authenticateRequestResult.getClaimsIdentity(), + authenticateRequestResult.getAudience(), + connectorClient, + userTokenClient, + callbackHandler, + authenticateRequestResult.getConnectorFactory()); + + // Run the pipeline + return runPipeline(context, callbackHandler).thenApply(task -> { + // If there are any results they will have been left on the TurnContext. + return CompletableFuture.completedFuture(processTurnResults(context)); + }).thenApply(null); + }); + }); + } + + /** + * This is a helper to create the ClaimsIdentity structure from an appId that will be added to the TurnContext. + * It is intended for use in proactive and named-pipe scenarios. + * @param botAppId The bot's application id. + * @param audience The audience for the claims identity + * @return A {@link ClaimsIdentity} with the audience and appId claims set to the appId. + */ + protected ClaimsIdentity createClaimsIdentity(String botAppId, String audience) { + if (botAppId == null) { + botAppId = ""; + } + // Related to issue: https://github.com/microsoft/botframework-sdk/issues/6331 + if (audience == null) { + audience = botAppId; + } + + // Hand craft Claims Identity. + HashMap claims = new HashMap(); + // Adding claims for both Emulator and Channel. + claims.put(AuthenticationConstants.AUDIENCE_CLAIM, audience); + claims.put(AuthenticationConstants.APPID_CLAIM, botAppId); + ClaimsIdentity claimsIdentity = new ClaimsIdentity("anonymous", claims); + + return claimsIdentity; + } + + private Activity createCreateActivity(ConversationResourceResponse createConversationResult, String channelId, + String serviceUrl, ConversationParameters conversationParameters) { + // Create a conversation update activity to represent the result. + Activity activity = Activity.createEventActivity(); + activity.setName(ActivityEventNames.CREATE_CONVERSATION); + activity.setChannelId(channelId); + activity.setServiceUrl(serviceUrl); + String activityId = createConversationResult.getActivityId() != null + ? createConversationResult.getActivityId() + : UUID.randomUUID().toString(); + activity.setId(activityId); + ConversationAccount conversation = new ConversationAccount(); + conversation.setId(createConversationResult.getId()); + conversation.setTenantId(conversationParameters.getTenantId()); + activity.setConversation(conversation); + activity.setChannelData(conversationParameters.getChannelData()); + activity.setRecipient(conversationParameters.getBot()); + return activity; + } + + private TurnContextImpl createTurnContext( + Activity activity, + ClaimsIdentity claimsIdentity, + String oauthScope, + ConnectorClient connectorClient, + UserTokenClient userTokenClient, + BotCallbackHandler callback, + ConnectorFactory connectorFactory + ) { + TurnContextImpl turnContext = new TurnContextImpl(this, activity); + turnContext.getTurnState().add(BotAdapter.BOT_IDENTITY_KEY, claimsIdentity); + turnContext.getTurnState().add(BotFrameworkAdapter.CONNECTOR_CLIENT_KEY, connectorClient); + turnContext.getTurnState().add(CloudAdapterBase.USER_TOKEN_CLIENT_KEY, userTokenClient); + turnContext.getTurnState().add(TurnContextImpl.BOT_CALLBACK_HANDLER_KEY, callback); + turnContext.getTurnState().add(CloudAdapterBase.CONNECTOR_FACTORY_KEY, connectorFactory); + // in non-skills scenarios the oauth scope value here will be null, so we are checking the value + if (oauthScope != null) { + turnContext.getTurnState().add(BotAdapter.OAUTH_SCOPE_KEY, oauthScope); + } + + return turnContext; + } + + private void validateContinuationActivity(Activity continuationActivity) { + if (continuationActivity == null) { + throw new IllegalArgumentException("continuationActivity"); + } + if (continuationActivity.getConversation() == null) { + throw new IllegalArgumentException("The continuation Activity should contain a Conversation value."); + } + if (continuationActivity.getServiceUrl() == null) { + throw new IllegalArgumentException("The continuation Activity should contain a ServiceUrl value."); + } + } + + private InvokeResponse processTurnResults(TurnContextImpl turnContext) { + // Handle ExpectedReplies scenarios where the all the activities have been buffered + // and sent back at once in an invoke response. + if (turnContext.getActivity().getDeliveryMode().equals(DeliveryModes.EXPECT_REPLIES)) { + return new InvokeResponse( + HttpURLConnection.HTTP_OK, + new ExpectedReplies(turnContext.getBufferedReplyActivities())); + } + + // Handle Invoke scenarios where the Bot will return a specific body and return code. + if (turnContext.getActivity().isType(ActivityTypes.INVOKE)) { + Activity activityInvokeResponse = turnContext.getTurnState().get(BotFrameworkAdapter.INVOKE_RESPONSE_KEY); + if (activityInvokeResponse == null) { + return new InvokeResponse(HttpURLConnection.HTTP_NOT_IMPLEMENTED, null); + } + + return (InvokeResponse) activityInvokeResponse.getValue(); + } + + // No body to return. + return null; + } +} diff --git a/libraries/bot-builder/src/main/java/com/microsoft/bot/builder/CloudChannelServiceHandler.java b/libraries/bot-builder/src/main/java/com/microsoft/bot/builder/CloudChannelServiceHandler.java new file mode 100644 index 000000000..6993743c5 --- /dev/null +++ b/libraries/bot-builder/src/main/java/com/microsoft/bot/builder/CloudChannelServiceHandler.java @@ -0,0 +1,35 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.bot.builder; + +import com.microsoft.bot.connector.authentication.BotFrameworkAuthentication; +import com.microsoft.bot.connector.authentication.ClaimsIdentity; +import java.util.concurrent.CompletableFuture; + +/** + * A class to help with the implementation of the Bot Framework protocol using BotFrameworkAuthentication. + */ +public class CloudChannelServiceHandler extends ChannelServiceHandlerBase { + private final BotFrameworkAuthentication auth; + + /** + * Initializes a new instance of the {@link CloudChannelServiceHandler} class, using Bot Framework Authentication. + * @param withAuth The Bot Framework Authentication object. + */ + public CloudChannelServiceHandler(BotFrameworkAuthentication withAuth) { + if (withAuth == null) { + throw new IllegalArgumentException("withAuth cannot be null"); + } + + auth = withAuth; + } + + /** + * {@inheritDoc} + */ + @Override + protected CompletableFuture authenticate(String authHeader) { + return auth.authenticateChannelRequest(authHeader); + } +} diff --git a/libraries/bot-integration-core/src/main/java/com/microsoft/bot/integration/CloudAdapter.java b/libraries/bot-integration-core/src/main/java/com/microsoft/bot/integration/CloudAdapter.java new file mode 100644 index 000000000..ce0387f26 --- /dev/null +++ b/libraries/bot-integration-core/src/main/java/com/microsoft/bot/integration/CloudAdapter.java @@ -0,0 +1,55 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.bot.integration; + +import com.microsoft.bot.builder.Bot; +import com.microsoft.bot.builder.CloudAdapterBase; +import com.microsoft.bot.connector.authentication.BotFrameworkAuthentication; +import com.microsoft.bot.connector.authentication.BotFrameworkAuthenticationFactory; +import com.microsoft.bot.schema.Activity; +import java.util.concurrent.CompletableFuture; + +/** + * An adapter that implements the Bot Framework Protocol and can be hosted in different cloud environments + * both public and private. + */ +public class CloudAdapter extends CloudAdapterBase { + + /** + * Initializes a new instance of the "CloudAdapter" class. (Public cloud. No auth. For testing.) + */ + public CloudAdapter() { + this(BotFrameworkAuthenticationFactory.create()); + } + + /** + * Initializes a new instance of the "CloudAdapter" class. + * @param botFrameworkAuthentication + * The BotFrameworkAuthentication this adapter should use. + */ + public CloudAdapter(BotFrameworkAuthentication botFrameworkAuthentication) { + super(botFrameworkAuthentication); + } + + /** + * Initializes a new instance of the "CloudAdapter" class. + * @param configuration + * The Configuration instance. + */ + public CloudAdapter(Configuration configuration) { + this(new ConfigurationBotFrameworkAuthentication(configuration, null, null)); + } + + /** + * Process the inbound HTTP request with the bot resulting in the outbound http response, this method can be called + * directly from a Controller. + * @param authHeader + * @param activity + * @param bot The Bot implementation to use for this request. + * @return void + */ + public CompletableFuture processIncomingActivity(String authHeader, Activity activity, Bot bot) { + return processActivity(authHeader, activity, bot::onTurn).thenApply(result -> null); + } +} diff --git a/libraries/bot-integration-core/src/main/java/com/microsoft/bot/integration/ConfigurationBotFrameworkAuthentication.java b/libraries/bot-integration-core/src/main/java/com/microsoft/bot/integration/ConfigurationBotFrameworkAuthentication.java new file mode 100644 index 000000000..81212dd0b --- /dev/null +++ b/libraries/bot-integration-core/src/main/java/com/microsoft/bot/integration/ConfigurationBotFrameworkAuthentication.java @@ -0,0 +1,126 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.bot.integration; + +import com.microsoft.bot.connector.authentication.AuthenticateRequestResult; +import com.microsoft.bot.connector.authentication.AuthenticationConfiguration; +import com.microsoft.bot.connector.authentication.AuthenticationConstants; +import com.microsoft.bot.connector.authentication.BotFrameworkAuthentication; +import com.microsoft.bot.connector.authentication.BotFrameworkAuthenticationFactory; +import com.microsoft.bot.connector.authentication.ClaimsIdentity; +import com.microsoft.bot.connector.authentication.ConnectorFactory; +import com.microsoft.bot.connector.authentication.ServiceClientCredentialsFactory; +import com.microsoft.bot.connector.authentication.UserTokenClient; +import com.microsoft.bot.connector.skills.BotFrameworkClient; +import com.microsoft.bot.schema.Activity; +import okhttp3.OkHttpClient; + +import java.util.concurrent.CompletableFuture; + +import javax.annotation.Nullable; + +/** + * Creates a BotFrameworkAuthentication instance from configuration. + */ +public class ConfigurationBotFrameworkAuthentication extends BotFrameworkAuthentication { + private final BotFrameworkAuthentication inner; + + /** + * Initializes a new instance of the ConfigurationBotFrameworkAuthentication + * class. + * + * @param configuration A {@link Configuration} instance. + * @param credentialsFactory A {@link ServiceClientCredentialsFactory} instance. + * @param authConfiguration An {@link AuthenticationConfiguration} instance. + */ + public ConfigurationBotFrameworkAuthentication(Configuration configuration, + @Nullable ServiceClientCredentialsFactory credentialsFactory, + @Nullable AuthenticationConfiguration authConfiguration) { + String channelService = configuration.getProperty("ChannelService"); + String validateAuthority = configuration.getProperty("ValidateAuthority"); + String toChannelFromBotLoginUrl = configuration.getProperty("ToChannelFromBotLoginUrl"); + String toChannelFromBotOAuthScope = configuration.getProperty("ToChannelFromBotOAuthScope"); + String toBotFromChannelTokenIssuer = configuration.getProperty("ToBotFromChannelTokenIssuer"); + String oAuthUrl = configuration.getProperty("OAuthUrl") != null ? configuration.getProperty("OAuthUrl") + : configuration.getProperty(AuthenticationConstants.OAUTH_URL_KEY); + String toBotFromChannelOpenIdMetadataUrl = configuration + .getProperty("ToBotFromChannelOpenIdMetadataUrl") != null + ? configuration.getProperty("ToBotFromChannelOpenIdMetadataUrl") + : configuration.getProperty(AuthenticationConstants.BOT_OPENID_METADATA_KEY); + String toBotFromEmulatorOpenIdMetadataUrl = configuration.getProperty("ToBotFromEmulatorOpenIdMetadataUrl"); + String callerId = configuration.getProperty("CallerId"); + + inner = BotFrameworkAuthenticationFactory.create( + channelService, + Boolean.parseBoolean(validateAuthority != null ? validateAuthority : "true"), + toChannelFromBotLoginUrl, + toChannelFromBotOAuthScope, + toBotFromChannelTokenIssuer, + oAuthUrl, + toBotFromChannelOpenIdMetadataUrl, + toBotFromEmulatorOpenIdMetadataUrl, + callerId, + credentialsFactory != null ? credentialsFactory + : new ConfigurationServiceClientCredentialFactory(configuration), + authConfiguration != null ? authConfiguration : new AuthenticationConfiguration(), + new OkHttpClient()); + } + + /** + * {@inheritDoc} + */ + @Override + public String getOriginatingAudience() { + return inner.getOriginatingAudience(); + } + + /** + * {@inheritDoc} + */ + @Override + public CompletableFuture authenticateChannelRequest(String authHeader) { + return inner.authenticateChannelRequest(authHeader); + } + + /** + * {@inheritDoc} + */ + @Override + public CompletableFuture authenticateRequest(Activity activity, String authHeader) { + return authenticateRequest(activity, authHeader); + } + + /** + * {@inheritDoc} + */ + @Override + public CompletableFuture authenticateStreamingRequest(String authHeader, + String channelIdHeader) { + return inner.authenticateStreamingRequest(authHeader, channelIdHeader); + } + + /** + * {@inheritDoc} + */ + @Override + public ConnectorFactory createConnectorFactory(ClaimsIdentity claimsIdentity) { + return inner.createConnectorFactory(claimsIdentity); + } + + /** + * {@inheritDoc} + */ + @Override + public CompletableFuture createUserTokenClient(ClaimsIdentity claimsIdentity) { + return inner.createUserTokenClient(claimsIdentity); + } + + /** + * {@inheritDoc} + */ + @Override + public BotFrameworkClient createBotFrameworkClient() { + return inner.createBotFrameworkClient(); + } +} diff --git a/libraries/bot-integration-core/src/main/java/com/microsoft/bot/integration/ConfigurationServiceClientCredentialFactory.java b/libraries/bot-integration-core/src/main/java/com/microsoft/bot/integration/ConfigurationServiceClientCredentialFactory.java new file mode 100644 index 000000000..7dded7c4a --- /dev/null +++ b/libraries/bot-integration-core/src/main/java/com/microsoft/bot/integration/ConfigurationServiceClientCredentialFactory.java @@ -0,0 +1,28 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.bot.integration; + +import com.microsoft.bot.connector.authentication.MicrosoftAppCredentials; +import com.microsoft.bot.connector.authentication.PasswordServiceClientCredentialFactory; + +/** + * Credential provider which uses {@link Configuration} to lookup appId and password. + * This will populate the {@link PasswordServiceClientCredentialFactory#getAppId()} from an configuration entry with the + * key of {@link MicrosoftAppCredentials#MICROSOFTAPPID} and the + * {@link PasswordServiceClientCredentialFactory#getPassword()} + * from a configuration entry with the key of {@link MicrosoftAppCredentials#MICROSOFTAPPPASSWORD}. + * + * NOTE: if the keys are not present, a null value will be used. + */ +public class ConfigurationServiceClientCredentialFactory extends PasswordServiceClientCredentialFactory { + + /** + * Initializes a new instance of the {@link ConfigurationServiceClientCredentialFactory} class. + * @param configuration An instance of {@link Configuration}. + */ + public ConfigurationServiceClientCredentialFactory(Configuration configuration) { + super(configuration.getProperty(MicrosoftAppCredentials.MICROSOFTAPPID), + configuration.getProperty(MicrosoftAppCredentials.MICROSOFTAPPPASSWORD)); + } +} From 9df6befe20837261aafb794e20033631db57d490 Mon Sep 17 00:00:00 2001 From: Martin Battaglino Date: Wed, 14 Jul 2021 16:21:46 -0300 Subject: [PATCH 06/27] Update necessary classes due to CloudAdapter and relocation of changes --- .../com/microsoft/bot/builder/BotAdapter.java | 33 ++ .../bot/builder/ChannelServiceHandler.java | 550 +----------------- .../bot/builder/TurnContextImpl.java | 2 + .../bot/builder/skills/SkillHandler.java | 214 +------ .../authentication/AppCredentials.java | 8 + .../AuthenticationConstants.java | 10 + .../CertificateAppCredentials.java | 4 +- .../authentication/ClaimsIdentity.java | 9 + .../MicrosoftAppCredentials.java | 7 +- .../authentication/OAuthConfiguration.java | 33 +- .../restclient/ServiceResponseBuilder.java | 8 +- .../bot/dialogs/SkillDialogOptions.java | 2 +- .../integration/BotFrameworkHttpClient.java | 4 +- .../bot/integration/SkillHttpClient.java | 2 +- 14 files changed, 133 insertions(+), 753 deletions(-) diff --git a/libraries/bot-builder/src/main/java/com/microsoft/bot/builder/BotAdapter.java b/libraries/bot-builder/src/main/java/com/microsoft/bot/builder/BotAdapter.java index 94cdf5662..7418d4698 100644 --- a/libraries/bot-builder/src/main/java/com/microsoft/bot/builder/BotAdapter.java +++ b/libraries/bot-builder/src/main/java/com/microsoft/bot/builder/BotAdapter.java @@ -6,6 +6,7 @@ import com.microsoft.bot.connector.Async; import com.microsoft.bot.connector.authentication.ClaimsIdentity; import com.microsoft.bot.schema.Activity; +import com.microsoft.bot.schema.ConversationParameters; import com.microsoft.bot.schema.ConversationReference; import com.microsoft.bot.schema.ResourceResponse; @@ -374,4 +375,36 @@ public CompletableFuture continueConversation( ) { return Async.completeExceptionally(new NotImplementedException("continueConversation")); } + + /** + * Creates a conversation on the specified channel. + * + *

+ * To start a conversation, your bot must know its account information + * and the user's account information on that channel. + * Most _channels only support initiating a direct message (non-group) conversation. + * The adapter attempts to create a new conversation on the channel, and + * then sends a conversationUpdate activity through its middleware pipeline + * to the callback method. + * If the conversation is established with the + * specified users, the ID of the activity's {@link Activity#conversation} + * will contain the ID of the new conversation. + *

+ * @param botAppId The application ID of the bot. + * @param channelId The ID for the channel. + * @param serviceUrl The channel's service URL endpoint. + * @param audience The audience for the connector. + * @param conversationParameters The conversation information to use to create the conversation. + * @param callback The method to call for the resulting bot turn. + * @return A task that represents the work queued to execute. + */ + public CompletableFuture createConversation( + String botAppId, + String channelId, + String serviceUrl, + String audience, + ConversationParameters conversationParameters, BotCallbackHandler callback + ) { + return Async.completeExceptionally(new NotImplementedException("createConversation")); + } } diff --git a/libraries/bot-builder/src/main/java/com/microsoft/bot/builder/ChannelServiceHandler.java b/libraries/bot-builder/src/main/java/com/microsoft/bot/builder/ChannelServiceHandler.java index 14a3cde5d..047c41947 100644 --- a/libraries/bot-builder/src/main/java/com/microsoft/bot/builder/ChannelServiceHandler.java +++ b/libraries/bot-builder/src/main/java/com/microsoft/bot/builder/ChannelServiceHandler.java @@ -1,8 +1,5 @@ package com.microsoft.bot.builder; -import java.util.List; -import java.util.concurrent.CompletableFuture; - import com.microsoft.bot.connector.Async; import com.microsoft.bot.connector.authentication.AuthenticationConfiguration; import com.microsoft.bot.connector.authentication.AuthenticationException; @@ -11,23 +8,14 @@ import com.microsoft.bot.connector.authentication.CredentialProvider; import com.microsoft.bot.connector.authentication.JwtTokenValidation; import com.microsoft.bot.connector.authentication.SkillValidation; -import com.microsoft.bot.schema.Activity; -import com.microsoft.bot.schema.AttachmentData; -import com.microsoft.bot.schema.ChannelAccount; -import com.microsoft.bot.schema.ConversationParameters; -import com.microsoft.bot.schema.ConversationResourceResponse; -import com.microsoft.bot.schema.ConversationsResult; -import com.microsoft.bot.schema.PagedMembersResult; -import com.microsoft.bot.schema.ResourceResponse; -import com.microsoft.bot.schema.Transcript; - -import org.apache.commons.lang3.NotImplementedException; import org.apache.commons.lang3.StringUtils; +import java.util.concurrent.CompletableFuture; + /** * A class to help with the implementation of the Bot Framework protocol. */ -public class ChannelServiceHandler { +public class ChannelServiceHandler extends ChannelServiceHandlerBase { private ChannelProvider channelProvider; @@ -60,534 +48,6 @@ public ChannelServiceHandler( this.channelProvider = channelProvider; } - /** - * Sends an activity to the end of a conversation. - * - * @param authHeader The authentication header. - * @param conversationId The conversation Id. - * @param activity The activity to send. - * - * @return A {@link CompletableFuture{TResult}} representing the - * result of the asynchronous operation. - */ - public CompletableFuture handleSendToConversation( - String authHeader, - String conversationId, - Activity activity) { - - return authenticate(authHeader).thenCompose(claimsIdentity -> { - return onSendToConversation(claimsIdentity, conversationId, activity); - }); - } - - /** - * Sends a reply to an activity. - * - * @param authHeader The authentication header. - * @param conversationId The conversation Id. - * @param activityId The activity Id the reply is to. - * @param activity The activity to send. - * - * @return A {@link CompletableFuture{TResult}} representing the - * result of the asynchronous operation. - */ - public CompletableFuture handleReplyToActivity( - String authHeader, - String conversationId, - String activityId, - Activity activity) { - - return authenticate(authHeader).thenCompose(claimsIdentity -> { - return onReplyToActivity(claimsIdentity, conversationId, activityId, activity); - }); - } - - /** - * Edits a previously sent existing activity. - * - * @param authHeader The authentication header. - * @param conversationId The conversation Id. - * @param activityId The activity Id to update. - * @param activity The replacement activity. - * - * @return A {@link CompletableFuture{TResult}} representing the - * result of the asynchronous operation. - */ - public CompletableFuture handleUpdateActivity( - String authHeader, - String conversationId, - String activityId, - Activity activity) { - return authenticate(authHeader).thenCompose(claimsIdentity -> { - return onUpdateActivity(claimsIdentity, conversationId, activityId, activity); - }); - } - - /** - * Deletes an existing activity. - * - * @param authHeader The authentication header. - * @param conversationId The conversation Id. - * @param activityId The activity Id. - * - * @return A {@link CompletableFuture} representing the result of - * the asynchronous operation. - */ - public CompletableFuture handleDeleteActivity(String authHeader, String conversationId, String activityId) { - return authenticate(authHeader).thenCompose(claimsIdentity -> { - return onDeleteActivity(claimsIdentity, conversationId, activityId); - }); - } - - /** - * Enumerates the members of an activity. - * - * @param authHeader The authentication header. - * @param conversationId The conversation Id. - * @param activityId The activity Id. - * - * @return A {@link CompletableFuture{TResult}} representing the - * result of the asynchronous operation. - */ - public CompletableFuture> handleGetActivityMembers( - String authHeader, - String conversationId, - String activityId) { - return authenticate(authHeader).thenCompose(claimsIdentity -> { - return onGetActivityMembers(claimsIdentity, conversationId, activityId); - }); - } - - /** - * Create a new Conversation. - * - * @param authHeader The authentication header. - * @param parameters Parameters to create the conversation from. - * - * @return A {@link CompletableFuture{TResult}} representing the - * result of the asynchronous operation. - */ - public CompletableFuture handleCreateConversation( - String authHeader, - ConversationParameters parameters) { - return authenticate(authHeader).thenCompose(claimsIdentity -> { - return onCreateConversation(claimsIdentity, parameters); - }); - } - - /** - * Lists the Conversations in which the bot has participated. - * - * @param authHeader The authentication header. - * @param conversationId The conversation Id. - * @param continuationToken A skip or continuation token. - * - * @return A {@link CompletableFuture{TResult}} representing the - * result of the asynchronous operation. - */ - public CompletableFuture handleGetConversations( - String authHeader, - String conversationId, - String continuationToken) { - return authenticate(authHeader).thenCompose(claimsIdentity -> { - return onGetConversations(claimsIdentity, conversationId, continuationToken); - }); - } - - /** - * Enumerates the members of a conversation. - * - * @param authHeader The authentication header. - * @param conversationId The conversation Id. - * - * @return A {@link CompletableFuture{TResult}} representing the - * result of the asynchronous operation. - */ - public CompletableFuture> handleGetConversationMembers( - String authHeader, - String conversationId) { - return authenticate(authHeader).thenCompose(claimsIdentity -> { - return onGetConversationMembers(claimsIdentity, conversationId); - }); - } - - /** - * Enumerates the members of a conversation one page at a time. - * - * @param authHeader The authentication header. - * @param conversationId The conversation Id. - * @param pageSize Suggested page size. - * @param continuationToken A continuation token. - * - * @return A {@link CompletableFuture{TResult}} representing the - * result of the asynchronous operation. - */ - public CompletableFuture handleGetConversationPagedMembers( - String authHeader, - String conversationId, - Integer pageSize, - String continuationToken) { - return authenticate(authHeader).thenCompose(claimsIdentity -> { - return onGetConversationPagedMembers(claimsIdentity, conversationId, pageSize, continuationToken); - }); - } - - /** - * Deletes a member from a conversation. - * - * @param authHeader The authentication header. - * @param conversationId The conversation Id. - * @param memberId Id of the member to delete from this - * conversation. - * - * @return A {@link CompletableFuture} representing the - * asynchronous operation. - */ - public CompletableFuture handleDeleteConversationMember( - String authHeader, - String conversationId, - String memberId) { - return authenticate(authHeader).thenCompose(claimsIdentity -> { - return onDeleteConversationMember(claimsIdentity, conversationId, memberId); - }); - } - - /** - * Uploads the historic activities of the conversation. - * - * @param authHeader The authentication header. - * @param conversationId The conversation Id. - * @param transcript Transcript of activities. - * - * @return A {@link CompletableFuture{TResult}} representing the - * result of the asynchronous operation. - */ - public CompletableFuture handleSendConversationHistory( - String authHeader, - String conversationId, - Transcript transcript) { - return authenticate(authHeader).thenCompose(claimsIdentity -> { - return onSendConversationHistory(claimsIdentity, conversationId, transcript); - }); - } - - /** - * Stores data in a compliant store when dealing with enterprises. - * - * @param authHeader The authentication header. - * @param conversationId The conversation Id. - * @param attachmentUpload Attachment data. - * - * @return A {@link CompletableFuture{TResult}} representing the - * result of the asynchronous operation. - */ - public CompletableFuture handleUploadAttachment( - String authHeader, - String conversationId, - AttachmentData attachmentUpload) { - return authenticate(authHeader).thenCompose(claimsIdentity -> { - return onUploadAttachment(claimsIdentity, conversationId, attachmentUpload); - }); - } - - /** - * SendToConversation() API for Skill. - * - * This method allows you to send an activity to the end of a conversation. - * This is slightly different from ReplyToActivity(). * - * SendToConversation(conversationId) - will append the activity to the end - * of the conversation according to the timestamp or semantics of the - * channel. * ReplyToActivity(conversationId,ActivityId) - adds the - * activity as a reply to another activity, if the channel supports it. If - * the channel does not support nested replies, ReplyToActivity falls back - * to SendToConversation. Use ReplyToActivity when replying to a specific - * activity in the conversation. Use SendToConversation in all other cases. - * - * @param claimsIdentity claimsIdentity for the bot, should have - * AudienceClaim, AppIdClaim and ServiceUrlClaim. - * @param conversationId conversationId. - * @param activity Activity to send. - * - * @return task for a resource response. - */ - protected CompletableFuture onSendToConversation( - ClaimsIdentity claimsIdentity, - String conversationId, - Activity activity) { - throw new NotImplementedException("onSendToConversation is not implemented"); - } - - /** - * OnReplyToActivity() API. - * - * Override this method allows to reply to an Activity. This is slightly - * different from SendToConversation(). * - * SendToConversation(conversationId) - will append the activity to the end - * of the conversation according to the timestamp or semantics of the - * channel. * ReplyToActivity(conversationId,ActivityId) - adds the - * activity as a reply to another activity, if the channel supports it. If - * the channel does not support nested replies, ReplyToActivity falls back - * to SendToConversation. Use ReplyToActivity when replying to a specific - * activity in the conversation. Use SendToConversation in all other cases. - * - * @param claimsIdentity claimsIdentity for the bot, should have - * AudienceClaim, AppIdClaim and ServiceUrlClaim. - * @param conversationId Conversation D. - * @param activityId activityId the reply is to (OPTONAL). - * @param activity Activity to send. - * - * @return task for a resource response. - */ - protected CompletableFuture onReplyToActivity( - ClaimsIdentity claimsIdentity, - String conversationId, - String activityId, - Activity activity) { - throw new NotImplementedException("onReplyToActivity is not implemented"); - } - - /** - * OnUpdateActivity() API. - * - * Override this method to edit a previously sent existing activity. Some - * channels allow you to edit an existing activity to reflect the new state - * of a bot conversation. For example, you can remove buttons after someone - * has clicked "Approve" button. - * - * @param claimsIdentity claimsIdentity for the bot, should have - * AudienceClaim, AppIdClaim and ServiceUrlClaim. - * @param conversationId Conversation D. - * @param activityId activityId to update. - * @param activity replacement Activity. - * - * @return task for a resource response. - */ - protected CompletableFuture onUpdateActivity( - ClaimsIdentity claimsIdentity, - String conversationId, - String activityId, - Activity activity) { - throw new NotImplementedException("onUpdateActivity is not implemented"); - } - - /** - * OnDeleteActivity() API. - * - * Override this method to Delete an existing activity. Some channels allow - * you to delete an existing activity, and if successful this method will - * remove the specified activity. - * - * @param claimsIdentity claimsIdentity for the bot, should have - * AudienceClaim, AppIdClaim and ServiceUrlClaim. - * @param conversationId Conversation D. - * @param activityId activityId to delete. - * - * @return task for a resource response. - */ - protected CompletableFuture onDeleteActivity( - ClaimsIdentity claimsIdentity, - String conversationId, - String activityId) { - throw new NotImplementedException("onDeleteActivity is not implemented"); - } - - /** - * OnGetActivityMembers() API. - * - * Override this method to enumerate the members of an activity. This REST - * API takes a ConversationId and a ActivityId, returning an array of - * ChannelAccount Objects representing the members of the particular - * activity in the conversation. - * - * @param claimsIdentity claimsIdentity for the bot, should have - * AudienceClaim, AppIdClaim and ServiceUrlClaim. - * @param conversationId Conversation D. - * @param activityId Activity D. - * - * @return task with result. - */ - protected CompletableFuture> onGetActivityMembers( - ClaimsIdentity claimsIdentity, - String conversationId, - String activityId) { - throw new NotImplementedException("onGetActivityMembers is not implemented"); - } - - /** - * CreateConversation() API. - * - * Override this method to create a new Conversation. POST to this method - * with a * Bot being the bot creating the conversation * IsGroup set to - * true if this is not a direct message (default instanceof false) * Array - * containing the members to include in the conversation The return value - * is a ResourceResponse which contains a conversation D which is suitable - * for use in the message payload and REST API URIs. Most channels only - * support the semantics of bots initiating a direct message conversation. - * An example of how to do that would be: var resource = - * connector.getconversations().CreateConversation(new - * ConversationParameters(){ Bot = bot, members = new ChannelAccount[] { - * new ChannelAccount("user1") } ); - * connect.getConversations().OnSendToConversation(resource.getId(), new - * Activity() ... ) ; end. - * - * @param claimsIdentity claimsIdentity for the bot, should have - * AudienceClaim, AppIdClaim and ServiceUrlClaim. - * @param parameters Parameters to create the conversation - * from. - * - * @return task for a conversation resource response. - */ - protected CompletableFuture onCreateConversation( - ClaimsIdentity claimsIdentity, - ConversationParameters parameters) { - throw new NotImplementedException("onCreateConversation is not implemented"); - } - - /** - * OnGetConversations() API for Skill. - * - * Override this method to list the Conversations in which this bot has - * participated. GET from this method with a skip token The return value is - * a ConversationsResult, which contains an array of ConversationMembers - * and a skip token. If the skip token is not empty, then there are further - * values to be returned. Call this method again with the returned token to - * get more values. Each ConversationMembers Object contains the D of the - * conversation and an array of ChannelAccounts that describe the members - * of the conversation. - * - * @param claimsIdentity claimsIdentity for the bot, should have - * AudienceClaim, AppIdClaim and ServiceUrlClaim. - * @param conversationId conversationId. - * @param continuationToken skip or continuation token. - * - * @return task for ConversationsResult. - */ - protected CompletableFuture onGetConversations( - ClaimsIdentity claimsIdentity, - String conversationId, - String continuationToken) { - throw new NotImplementedException("onGetConversationMembers is not implemented"); - } - - /** - * GetConversationMembers() API for Skill. - * - * Override this method to enumerate the members of a conversation. This - * REST API takes a ConversationId and returns an array of ChannelAccount - * Objects representing the members of the conversation. - * - * @param claimsIdentity claimsIdentity for the bot, should have - * AudienceClaim, AppIdClaim and ServiceUrlClaim. - * @param conversationId Conversation D. - * - * @return task for a response. - */ - protected CompletableFuture> onGetConversationMembers( - ClaimsIdentity claimsIdentity, - String conversationId) { - throw new NotImplementedException("onGetConversationMembers is not implemented"); - } - - /** - * GetConversationPagedMembers() API for Skill. - * - * Override this method to enumerate the members of a conversation one page - * at a time. This REST API takes a ConversationId. Optionally a pageSize - * and/or continuationToken can be provided. It returns a - * PagedMembersResult, which contains an array of ChannelAccounts - * representing the members of the conversation and a continuation token - * that can be used to get more values. One page of ChannelAccounts records - * are returned with each call. The number of records in a page may vary - * between channels and calls. The pageSize parameter can be used as a - * suggestion. If there are no additional results the response will not - * contain a continuation token. If there are no members in the - * conversation the Members will be empty or not present in the response. A - * response to a request that has a continuation token from a prior request - * may rarely return members from a previous request. - * - * @param claimsIdentity claimsIdentity for the bot, should have - * AudienceClaim, AppIdClaim and ServiceUrlClaim. - * @param conversationId Conversation D. - * @param pageSize Suggested page size. - * @param continuationToken Continuation Token. - * - * @return task for a response. - */ - protected CompletableFuture onGetConversationPagedMembers( - ClaimsIdentity claimsIdentity, - String conversationId, - Integer pageSize, - String continuationToken) { - throw new NotImplementedException("onGetConversationPagedMembers is not implemented"); - } - - /** - * DeleteConversationMember() API for Skill. - * - * Override this method to deletes a member from a conversation. This REST - * API takes a ConversationId and a memberId (of type String) and removes - * that member from the conversation. If that member was the last member of - * the conversation, the conversation will also be deleted. - * - * @param claimsIdentity claimsIdentity for the bot, should have - * AudienceClaim, AppIdClaim and ServiceUrlClaim. - * @param conversationId Conversation D. - * @param memberId D of the member to delete from this - * conversation. - * - * @return task. - */ - protected CompletableFuture onDeleteConversationMember( - ClaimsIdentity claimsIdentity, - String conversationId, - String memberId) { - throw new NotImplementedException("onDeleteConversationMember is not implemented"); - } - - /** - * SendConversationHistory() API for Skill. - * - * Override this method to this method allows you to upload the historic - * activities to the conversation. Sender must ensure that the historic - * activities have unique ids and appropriate timestamps. The ids are used - * by the client to deal with duplicate activities and the timestamps are - * used by the client to render the activities in the right order. - * - * @param claimsIdentity claimsIdentity for the bot, should have - * AudienceClaim, AppIdClaim and ServiceUrlClaim. - * @param conversationId Conversation D. - * @param transcript Transcript of activities. - * - * @return task for a resource response. - */ - protected CompletableFuture onSendConversationHistory( - ClaimsIdentity claimsIdentity, - String conversationId, - Transcript transcript) { - throw new NotImplementedException("onSendConversationHistory is not implemented"); - } - - /** - * UploadAttachment() API. - * - * Override this method to store data in a compliant store when dealing - * with enterprises. The response is a ResourceResponse which contains an - * AttachmentId which is suitable for using with the attachments API. - * - * @param claimsIdentity claimsIdentity for the bot, should have - * AudienceClaim, AppIdClaim and ServiceUrlClaim. - * @param conversationId Conversation D. - * @param attachmentUpload Attachment data. - * - * @return task with result. - */ - protected CompletableFuture onUploadAttachment( - ClaimsIdentity claimsIdentity, - String conversationId, - AttachmentData attachmentUpload) { - throw new NotImplementedException("onUploadAttachment is not implemented"); - } - /** * Helper to authenticate the header. * @@ -597,8 +57,10 @@ protected CompletableFuture onUploadAttachment( * HttpClient)} , we should move this code somewhere in that library when * we refactor auth, for now we keep it private to avoid adding more public * static functions that we will need to deprecate later. + * @param authHeader The Bearer token included as part of the request. + * @return A task that represents the work queued to execute. */ - private CompletableFuture authenticate(String authHeader) { + protected CompletableFuture authenticate(String authHeader) { if (StringUtils.isEmpty(authHeader)) { return credentialProvider.isAuthenticationDisabled().thenCompose(isAuthDisabled -> { if (!isAuthDisabled) { diff --git a/libraries/bot-builder/src/main/java/com/microsoft/bot/builder/TurnContextImpl.java b/libraries/bot-builder/src/main/java/com/microsoft/bot/builder/TurnContextImpl.java index 1a513a793..061ad83ba 100644 --- a/libraries/bot-builder/src/main/java/com/microsoft/bot/builder/TurnContextImpl.java +++ b/libraries/bot-builder/src/main/java/com/microsoft/bot/builder/TurnContextImpl.java @@ -29,6 +29,8 @@ * {@link Middleware} */ public class TurnContextImpl implements TurnContext, AutoCloseable { + public static final String BOT_CALLBACK_HANDLER_KEY = "BotCallbackHandler"; + /** * The bot adapter that created this context object. */ diff --git a/libraries/bot-builder/src/main/java/com/microsoft/bot/builder/skills/SkillHandler.java b/libraries/bot-builder/src/main/java/com/microsoft/bot/builder/skills/SkillHandler.java index e2107182f..ebe80a0cf 100644 --- a/libraries/bot-builder/src/main/java/com/microsoft/bot/builder/skills/SkillHandler.java +++ b/libraries/bot-builder/src/main/java/com/microsoft/bot/builder/skills/SkillHandler.java @@ -3,34 +3,20 @@ package com.microsoft.bot.builder.skills; -import java.util.Map; -import java.util.UUID; import java.util.concurrent.CompletableFuture; -import java.util.concurrent.atomic.AtomicReference; -import com.fasterxml.jackson.databind.JsonNode; import com.microsoft.bot.builder.Bot; import com.microsoft.bot.builder.BotAdapter; -import com.microsoft.bot.builder.BotCallbackHandler; import com.microsoft.bot.builder.ChannelServiceHandler; -import com.microsoft.bot.builder.TurnContext; import com.microsoft.bot.connector.authentication.AuthenticationConfiguration; import com.microsoft.bot.connector.authentication.AuthenticationConstants; import com.microsoft.bot.connector.authentication.ChannelProvider; import com.microsoft.bot.connector.authentication.ClaimsIdentity; import com.microsoft.bot.connector.authentication.CredentialProvider; import com.microsoft.bot.connector.authentication.GovernmentAuthenticationConstants; -import com.microsoft.bot.connector.authentication.JwtTokenValidation; import com.microsoft.bot.schema.Activity; -import com.microsoft.bot.schema.ActivityTypes; -import com.microsoft.bot.schema.CallerIdConstants; -import com.microsoft.bot.schema.ConversationReference; import com.microsoft.bot.schema.ResourceResponse; -import org.apache.commons.lang3.NotImplementedException; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - /** * A Bot Framework Handler for skills. */ @@ -42,23 +28,15 @@ public class SkillHandler extends ChannelServiceHandler { public static final String SKILL_CONVERSATION_REFERENCE_KEY = "com.microsoft.bot.builder.skills.SkillConversationReference"; - private final BotAdapter adapter; - private final Bot bot; - private final SkillConversationIdFactoryBase conversationIdFactory; - - /** - * The slf4j Logger to use. Note that slf4j is configured by providing Log4j - * dependencies in the POM, and corresponding Log4j configuration in the - * 'resources' folder. - */ - private Logger logger = LoggerFactory.getLogger(SkillHandler.class); + // Delegate that implements actual logic + private final SkillHandlerImpl inner; /** * Initializes a new instance of the {@link SkillHandler} class, using a * credential provider. * * @param adapter An instance of the {@link BotAdapter} that will handle the request. - * @param bot The {@link IBot} instance. + * @param bot The {@link Bot} instance. * @param conversationIdFactory A {@link SkillConversationIdFactoryBase} to unpack the conversation ID and * map it to the calling bot. * @param credentialProvider The credential provider. @@ -92,9 +70,14 @@ public SkillHandler( throw new IllegalArgumentException("conversationIdFactory cannot be null"); } - this.adapter = adapter; - this.bot = bot; - this.conversationIdFactory = conversationIdFactory; + this.inner = new SkillHandlerImpl( + SKILL_CONVERSATION_REFERENCE_KEY, + adapter, + bot, + conversationIdFactory, + () -> this.getChannelProvider() != null && this.getChannelProvider().isGovernment() + ? GovernmentAuthenticationConstants.TO_CHANNEL_FROM_BOT_OAUTH_SCOPE + : AuthenticationConstants.TO_CHANNEL_FROM_BOT_OAUTH_SCOPE); } /** @@ -115,14 +98,14 @@ public SkillHandler( * @param conversationId conversationId. * @param activity Activity to send. * - * @return task for a resource response. + * @return Task for a resource response. */ @Override protected CompletableFuture onSendToConversation( ClaimsIdentity claimsIdentity, String conversationId, Activity activity) { - return processActivity(claimsIdentity, conversationId, null, activity); + return inner.onSendToConversation(claimsIdentity, conversationId, activity); } /** @@ -144,7 +127,7 @@ protected CompletableFuture onSendToConversation( * @param activityId activityId the reply is to (OPTIONAL). * @param activity Activity to send. * - * @return task for a resource response. + * @return Task for a resource response. */ @Override protected CompletableFuture onReplyToActivity( @@ -152,31 +135,22 @@ protected CompletableFuture onReplyToActivity( String conversationId, String activityId, Activity activity) { - return processActivity(claimsIdentity, conversationId, activityId, activity); + return inner.onReplyToActivity(claimsIdentity, conversationId, activityId, activity); } /** + * {@inheritDoc} */ @Override protected CompletableFuture onDeleteActivity( ClaimsIdentity claimsIdentity, String conversationId, String activityId) { - - SkillConversationReference skillConversationReference = getSkillConversationReference(conversationId).join(); - - BotCallbackHandler callback = turnContext -> { - turnContext.getTurnState().add(SKILL_CONVERSATION_REFERENCE_KEY, skillConversationReference); - return turnContext.deleteActivity(activityId); - }; - - return adapter.continueConversation(claimsIdentity, - skillConversationReference.getConversationReference(), - skillConversationReference.getOAuthScope(), - callback); + return inner.onDeleteActivity(claimsIdentity, conversationId, activityId); } /** + * {@inheritDoc} */ @Override protected CompletableFuture onUpdateActivity( @@ -184,157 +158,7 @@ protected CompletableFuture onUpdateActivity( String conversationId, String activityId, Activity activity) { - SkillConversationReference skillConversationReference = getSkillConversationReference(conversationId).join(); - - AtomicReference resourceResponse = new AtomicReference(); - - BotCallbackHandler callback = turnContext -> { - turnContext.getTurnState().add(SKILL_CONVERSATION_REFERENCE_KEY, skillConversationReference); - activity.applyConversationReference(skillConversationReference.getConversationReference()); - turnContext.getActivity().setId(activityId); - String callerId = String.format("%s%s", - CallerIdConstants.BOT_TO_BOT_PREFIX, - JwtTokenValidation.getAppIdFromClaims(claimsIdentity.claims())); - turnContext.getActivity().setCallerId(callerId); - resourceResponse.set(turnContext.updateActivity(activity).join()); - return CompletableFuture.completedFuture(null); - }; - - adapter.continueConversation(claimsIdentity, - skillConversationReference.getConversationReference(), - skillConversationReference.getOAuthScope(), - callback); - - if (resourceResponse.get() != null) { - return CompletableFuture.completedFuture(resourceResponse.get()); - } else { - return CompletableFuture.completedFuture(new ResourceResponse(UUID.randomUUID().toString())); - } - } - - private static void applyEoCToTurnContextActivity(TurnContext turnContext, Activity endOfConversationActivity) { - // transform the turnContext.Activity to be the EndOfConversation. - turnContext.getActivity().setType(endOfConversationActivity.getType()); - turnContext.getActivity().setText(endOfConversationActivity.getText()); - turnContext.getActivity().setCode(endOfConversationActivity.getCode()); - - turnContext.getActivity().setReplyToId(endOfConversationActivity.getReplyToId()); - turnContext.getActivity().setValue(endOfConversationActivity.getValue()); - turnContext.getActivity().setEntities(endOfConversationActivity.getEntities()); - turnContext.getActivity().setLocale(endOfConversationActivity.getLocale()); - turnContext.getActivity().setLocalTimestamp(endOfConversationActivity.getLocalTimestamp()); - turnContext.getActivity().setTimestamp(endOfConversationActivity.getTimestamp()); - turnContext.getActivity().setChannelData(endOfConversationActivity.getChannelData()); - for (Map.Entry entry : endOfConversationActivity.getProperties().entrySet()) { - turnContext.getActivity().setProperties(entry.getKey(), entry.getValue()); - } - } - - private static void applyEventToTurnContextActivity(TurnContext turnContext, Activity eventActivity) { - // transform the turnContext.Activity to be the EventActivity. - turnContext.getActivity().setType(eventActivity.getType()); - turnContext.getActivity().setName(eventActivity.getName()); - turnContext.getActivity().setValue(eventActivity.getValue()); - turnContext.getActivity().setRelatesTo(eventActivity.getRelatesTo()); - - turnContext.getActivity().setReplyToId(eventActivity.getReplyToId()); - turnContext.getActivity().setValue(eventActivity.getValue()); - turnContext.getActivity().setEntities(eventActivity.getEntities()); - turnContext.getActivity().setLocale(eventActivity.getLocale()); - turnContext.getActivity().setLocalTimestamp(eventActivity.getLocalTimestamp()); - turnContext.getActivity().setTimestamp(eventActivity.getTimestamp()); - turnContext.getActivity().setChannelData(eventActivity.getChannelData()); - for (Map.Entry entry : eventActivity.getProperties().entrySet()) { - turnContext.getActivity().setProperties(entry.getKey(), entry.getValue()); - } - } - - private CompletableFuture getSkillConversationReference(String conversationId) { - - SkillConversationReference skillConversationReference; - try { - skillConversationReference = conversationIdFactory.getSkillConversationReference(conversationId).join(); - } catch (NotImplementedException ex) { - if (logger != null) { - logger.warn("Got NotImplementedException when trying to call " - + "GetSkillConversationReference() on the ConversationIdFactory," - + " attempting to use deprecated GetConversationReference() method instead."); - } - - // Attempt to get SkillConversationReference using deprecated method. - // this catch should be removed once we remove the deprecated method. - // We need to use the deprecated method for backward compatibility. - ConversationReference conversationReference = - conversationIdFactory.getConversationReference(conversationId).join(); - skillConversationReference = new SkillConversationReference(); - skillConversationReference.setConversationReference(conversationReference); - if (getChannelProvider() != null && getChannelProvider().isGovernment()) { - skillConversationReference.setOAuthScope( - GovernmentAuthenticationConstants.TO_CHANNEL_FROM_BOT_OAUTH_SCOPE); - } else { - skillConversationReference.setOAuthScope( - AuthenticationConstants.TO_CHANNEL_FROM_BOT_OAUTH_SCOPE); - } - } - - if (skillConversationReference == null) { - if (logger != null) { - logger.warn( - String.format("Unable to get skill conversation reference for conversationId %s.", conversationId) - ); - } - throw new RuntimeException("Key not found"); - } - - return CompletableFuture.completedFuture(skillConversationReference); - } - - private CompletableFuture processActivity( - ClaimsIdentity claimsIdentity, - String conversationId, - String replyToActivityId, - Activity activity) { - - SkillConversationReference skillConversationReference = getSkillConversationReference(conversationId).join(); - - AtomicReference resourceResponse = new AtomicReference(); - - BotCallbackHandler callback = turnContext -> { - turnContext.getTurnState().add(SKILL_CONVERSATION_REFERENCE_KEY, skillConversationReference); - activity.applyConversationReference(skillConversationReference.getConversationReference()); - turnContext.getActivity().setId(replyToActivityId); - String callerId = String.format("%s%s", - CallerIdConstants.BOT_TO_BOT_PREFIX, - JwtTokenValidation.getAppIdFromClaims(claimsIdentity.claims())); - turnContext.getActivity().setCallerId(callerId); - - switch (activity.getType()) { - case ActivityTypes.END_OF_CONVERSATION: - conversationIdFactory.deleteConversationReference(conversationId).join(); - applyEoCToTurnContextActivity(turnContext, activity); - bot.onTurn(turnContext).join(); - break; - case ActivityTypes.EVENT: - applyEventToTurnContextActivity(turnContext, activity); - bot.onTurn(turnContext).join(); - break; - default: - resourceResponse.set(turnContext.sendActivity(activity).join()); - break; - } - return CompletableFuture.completedFuture(null); - }; - - adapter.continueConversation(claimsIdentity, - skillConversationReference.getConversationReference(), - skillConversationReference.getOAuthScope(), - callback).join(); - - if (resourceResponse.get() != null) { - return CompletableFuture.completedFuture(resourceResponse.get()); - } else { - return CompletableFuture.completedFuture(new ResourceResponse(UUID.randomUUID().toString())); - } + return inner.onUpdateActivity(claimsIdentity, conversationId, activityId, activity); } } diff --git a/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/authentication/AppCredentials.java b/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/authentication/AppCredentials.java index 5be313ce4..143513e6f 100644 --- a/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/authentication/AppCredentials.java +++ b/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/authentication/AppCredentials.java @@ -133,6 +133,14 @@ protected void setAuthTenant(String withAuthTenant) { authTenant = withAuthTenant; } + /** + * Gets a value indicating whether to validate the Authority. + * @return The validateAuthority value to use. + */ + public Boolean validateAuthority() { + return true; + } + /** * Gets an OAuth access token. * diff --git a/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/authentication/AuthenticationConstants.java b/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/authentication/AuthenticationConstants.java index 3d9cbfb9b..3f7610699 100644 --- a/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/authentication/AuthenticationConstants.java +++ b/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/authentication/AuthenticationConstants.java @@ -115,6 +115,16 @@ private AuthenticationConstants() { */ public static final String AUDIENCE_CLAIM = "aud"; + /** + * Issuer Claim. From RFC 7519. + * https://tools.ietf.org/html/rfc7519#section-4.1.1 + * The "iss" (issuer) claim identifies the principal that issued the + * JWT. The processing of this claim is generally application specific. + * The "iss" value is a case-sensitive string containing a StringOrURI + * value. Use of this claim is OPTIONAL. + */ + public static final String ISSUER_CLAIM = "iss"; + /** * From RFC 7515 https://tools.ietf.org/html/rfc7515#section-4.1.4 The "kid" * (key ID) Header Parameter is a hint indicating which key was used to secure diff --git a/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/authentication/CertificateAppCredentials.java b/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/authentication/CertificateAppCredentials.java index 22af28a2a..6fd3f5a33 100644 --- a/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/authentication/CertificateAppCredentials.java +++ b/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/authentication/CertificateAppCredentials.java @@ -41,7 +41,9 @@ public CertificateAppCredentials(CertificateAppCredentialsOptions withOptions) // going to create this now instead of lazy loading so we don't have some // awkward InputStream hanging around. authenticator = - new CertificateAuthenticator(withOptions, new OAuthConfiguration(oAuthEndpoint(), oAuthScope())); + new CertificateAuthenticator(withOptions, new OAuthConfiguration(oAuthEndpoint(), + oAuthScope(), + validateAuthority())); } /** diff --git a/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/authentication/ClaimsIdentity.java b/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/authentication/ClaimsIdentity.java index 00c88e90c..425c25220 100644 --- a/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/authentication/ClaimsIdentity.java +++ b/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/authentication/ClaimsIdentity.java @@ -101,4 +101,13 @@ public String getIssuer() { public String getType() { return type; } + + /** + * Returns a claim value (if its present). + * @param claimType The claim type to look for + * @return The claim value or null if not found + */ + public String getClaimValue(String claimType) { + return claims().get(claimType); + } } diff --git a/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/authentication/MicrosoftAppCredentials.java b/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/authentication/MicrosoftAppCredentials.java index 801b9c04e..c8bf41390 100644 --- a/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/authentication/MicrosoftAppCredentials.java +++ b/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/authentication/MicrosoftAppCredentials.java @@ -9,13 +9,14 @@ * MicrosoftAppCredentials auth implementation and cache. */ public class MicrosoftAppCredentials extends AppCredentials { + /** - * The configuration property for the Microsoft app Password. + * The configuration property for the Microsoft app ID. */ public static final String MICROSOFTAPPID = "MicrosoftAppId"; /** - * The configuration property for the Microsoft app ID. + * The configuration property for the Microsoft app Password. */ public static final String MICROSOFTAPPPASSWORD = "MicrosoftAppPassword"; @@ -104,7 +105,7 @@ protected Authenticator buildAuthenticator() throws MalformedURLException { return new CredentialsAuthenticator( getAppId(), getAppPassword(), - new OAuthConfiguration(oAuthEndpoint(), oAuthScope()) + new OAuthConfiguration(oAuthEndpoint(), oAuthScope(), validateAuthority()) ); } } diff --git a/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/authentication/OAuthConfiguration.java b/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/authentication/OAuthConfiguration.java index 9376c4865..2045e042e 100644 --- a/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/authentication/OAuthConfiguration.java +++ b/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/authentication/OAuthConfiguration.java @@ -10,16 +10,29 @@ public class OAuthConfiguration { private String scope; private String authority; + private Boolean validateAuthority; /** * Construct with authority and scope. - * + * * @param withAuthority The auth authority. * @param withScope The auth scope. + * @param withValidateAuthority Whether the Authority should be validated. */ - public OAuthConfiguration(String withAuthority, String withScope) { + public OAuthConfiguration(String withAuthority, String withScope, Boolean withValidateAuthority) { this.authority = withAuthority; this.scope = withScope; + this.validateAuthority = withValidateAuthority; + } + + /** + * Construct with authority and scope. + * + * @param withAuthority The auth authority. + * @param withScope The auth scope. + */ + public OAuthConfiguration(String withAuthority, String withScope) { + this(withAuthority, withScope, null); } /** @@ -57,4 +70,20 @@ public void setScope(String withScope) { public String getScope() { return scope; } + + /** + * Gets a value indicating whether the Authority should be validated. + * @return Boolean value indicating whether the Authority should be validated. + */ + public Boolean getValidateAuthority() { + return validateAuthority; + } + + /** + * Sets a value indicating whether the Authority should be validated. + * @param withValidateAuthority Boolean value indicating whether the Authority should be validated. + */ + public void setValidateAuthority(Boolean withValidateAuthority) { + this.validateAuthority = withValidateAuthority; + } } diff --git a/libraries/bot-connector/src/main/java/com/microsoft/bot/restclient/ServiceResponseBuilder.java b/libraries/bot-connector/src/main/java/com/microsoft/bot/restclient/ServiceResponseBuilder.java index c94f7bbd6..e14514628 100644 --- a/libraries/bot-connector/src/main/java/com/microsoft/bot/restclient/ServiceResponseBuilder.java +++ b/libraries/bot-connector/src/main/java/com/microsoft/bot/restclient/ServiceResponseBuilder.java @@ -117,8 +117,8 @@ public ServiceResponse build(Response response) throws IOExcept throw exceptionType.getConstructor(String.class, Response.class, (Class) responseTypes.get(0)) .newInstance("Status code " + statusCode + ", " + responseContent, response, buildBody(statusCode, responseBody)); } catch (InstantiationException | IllegalAccessException | InvocationTargetException | NoSuchMethodException e) { - throw new IOException("Status code " + statusCode + ", but an instance of " + exceptionType.getCanonicalName() - + " cannot be created.", e); + throw new IOException("Status code " + statusCode + ", but an instance of " + + exceptionType.getCanonicalName() + " cannot be created.", e); } } } @@ -136,8 +136,8 @@ public ServiceResponse buildEmpty(Response response) throws IOException throw exceptionType.getConstructor(String.class, Response.class) .newInstance("Status code " + statusCode, response); } catch (InstantiationException | IllegalAccessException | InvocationTargetException | NoSuchMethodException e) { - throw new IOException("Status code " + statusCode + ", but an instance of " + exceptionType.getCanonicalName() - + " cannot be created.", e); + throw new IOException("Status code " + statusCode + ", but an instance of " + + exceptionType.getCanonicalName() + " cannot be created.", e); } } } diff --git a/libraries/bot-dialogs/src/main/java/com/microsoft/bot/dialogs/SkillDialogOptions.java b/libraries/bot-dialogs/src/main/java/com/microsoft/bot/dialogs/SkillDialogOptions.java index 53eb9dac1..45ba03236 100644 --- a/libraries/bot-dialogs/src/main/java/com/microsoft/bot/dialogs/SkillDialogOptions.java +++ b/libraries/bot-dialogs/src/main/java/com/microsoft/bot/dialogs/SkillDialogOptions.java @@ -8,9 +8,9 @@ import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonProperty; import com.microsoft.bot.builder.ConversationState; -import com.microsoft.bot.builder.skills.BotFrameworkClient; import com.microsoft.bot.builder.skills.BotFrameworkSkill; import com.microsoft.bot.builder.skills.SkillConversationIdFactoryBase; +import com.microsoft.bot.connector.skills.BotFrameworkClient; /** * Defines the options that will be used to execute a {@link SkillDialog} . diff --git a/libraries/bot-integration-core/src/main/java/com/microsoft/bot/integration/BotFrameworkHttpClient.java b/libraries/bot-integration-core/src/main/java/com/microsoft/bot/integration/BotFrameworkHttpClient.java index e7cea3c96..36fec30c7 100644 --- a/libraries/bot-integration-core/src/main/java/com/microsoft/bot/integration/BotFrameworkHttpClient.java +++ b/libraries/bot-integration-core/src/main/java/com/microsoft/bot/integration/BotFrameworkHttpClient.java @@ -10,6 +10,7 @@ import com.microsoft.bot.connector.authentication.CredentialProvider; import com.microsoft.bot.connector.authentication.MicrosoftAppCredentials; import com.microsoft.bot.connector.authentication.MicrosoftGovernmentAppCredentials; +import com.microsoft.bot.connector.skills.BotFrameworkClient; import com.microsoft.bot.restclient.serializer.JacksonAdapter; import okhttp3.HttpUrl; @@ -22,8 +23,7 @@ import com.microsoft.bot.connector.authentication.ChannelProvider; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; -import com.microsoft.bot.builder.TypedInvokeResponse; -import com.microsoft.bot.builder.skills.BotFrameworkClient; +import com.microsoft.bot.schema.TypedInvokeResponse; import com.microsoft.bot.connector.Async; import com.microsoft.bot.connector.authentication.AppCredentials; import com.microsoft.bot.schema.Activity; diff --git a/libraries/bot-integration-core/src/main/java/com/microsoft/bot/integration/SkillHttpClient.java b/libraries/bot-integration-core/src/main/java/com/microsoft/bot/integration/SkillHttpClient.java index 2d42006f5..31a4e8eba 100644 --- a/libraries/bot-integration-core/src/main/java/com/microsoft/bot/integration/SkillHttpClient.java +++ b/libraries/bot-integration-core/src/main/java/com/microsoft/bot/integration/SkillHttpClient.java @@ -7,7 +7,7 @@ import java.util.concurrent.CompletableFuture; import com.microsoft.bot.builder.skills.BotFrameworkSkill; -import com.microsoft.bot.builder.TypedInvokeResponse; +import com.microsoft.bot.schema.TypedInvokeResponse; import com.microsoft.bot.builder.skills.SkillConversationIdFactoryBase; import com.microsoft.bot.builder.skills.SkillConversationIdFactoryOptions; import com.microsoft.bot.schema.Activity; From 18c72cb83b413ca62edb7e22162d8282da4e45f8 Mon Sep 17 00:00:00 2001 From: Martin Battaglino Date: Wed, 14 Jul 2021 16:22:07 -0300 Subject: [PATCH 07/27] Add package-info --- .../com/microsoft/bot/connector/skills/package-info.java | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 libraries/bot-connector/src/main/java/com/microsoft/bot/connector/skills/package-info.java diff --git a/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/skills/package-info.java b/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/skills/package-info.java new file mode 100644 index 000000000..7988bd9ef --- /dev/null +++ b/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/skills/package-info.java @@ -0,0 +1,8 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for +// license information. + +/** + * This package contains the implementation classes for bot-connector. + */ +package com.microsoft.bot.connector.skills; From 683ed687b09218cb6f96fbcff9a801e5eb1e11a4 Mon Sep 17 00:00:00 2001 From: Martin Battaglino Date: Wed, 14 Jul 2021 16:22:18 -0300 Subject: [PATCH 08/27] Add mockito in pom --- libraries/bot-integration-core/pom.xml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/libraries/bot-integration-core/pom.xml b/libraries/bot-integration-core/pom.xml index c2e219372..a749c72a3 100644 --- a/libraries/bot-integration-core/pom.xml +++ b/libraries/bot-integration-core/pom.xml @@ -65,6 +65,11 @@ com.microsoft.bot bot-builder + + org.mockito + mockito-core + test + From 9ae6d8d7ebad2a31dcb4ef986604d4c56c6a8d92 Mon Sep 17 00:00:00 2001 From: Martin Battaglino Date: Wed, 14 Jul 2021 16:22:38 -0300 Subject: [PATCH 09/27] Add CloudAdapter tests --- .../CloudChannelServiceHandlerTests.java | 60 ++ .../bot/builder/CloudSkillHandlerTests.java | 452 +++++++++++ ...otFrameworkAuthenticationFactoryTests.java | 159 ++++ .../BotFrameworkAuthenticationTests.java | 107 +++ ...rdServiceClientCredentialFactoryTests.java | 101 +++ .../bot/dialogs/SkillDialogTests.java | 27 +- .../CloudAdapterWithErrorHandler.java | 107 +++ .../bot/integration/CloudAdapterTests.java | 715 ++++++++++++++++++ .../bot/integration/DelayHelper.java | 48 ++ 9 files changed, 1762 insertions(+), 14 deletions(-) create mode 100644 libraries/bot-builder/src/test/java/com/microsoft/bot/builder/CloudChannelServiceHandlerTests.java create mode 100644 libraries/bot-builder/src/test/java/com/microsoft/bot/builder/CloudSkillHandlerTests.java create mode 100644 libraries/bot-connector/src/test/java/com/microsoft/bot/connector/BotFrameworkAuthenticationFactoryTests.java create mode 100644 libraries/bot-connector/src/test/java/com/microsoft/bot/connector/BotFrameworkAuthenticationTests.java create mode 100644 libraries/bot-connector/src/test/java/com/microsoft/bot/connector/PasswordServiceClientCredentialFactoryTests.java create mode 100644 libraries/bot-integration-core/src/main/java/com/microsoft/bot/integration/CloudAdapterWithErrorHandler.java create mode 100644 libraries/bot-integration-core/src/test/java/com/microsoft/bot/integration/CloudAdapterTests.java create mode 100644 libraries/bot-integration-core/src/test/java/com/microsoft/bot/integration/DelayHelper.java diff --git a/libraries/bot-builder/src/test/java/com/microsoft/bot/builder/CloudChannelServiceHandlerTests.java b/libraries/bot-builder/src/test/java/com/microsoft/bot/builder/CloudChannelServiceHandlerTests.java new file mode 100644 index 000000000..b70c54392 --- /dev/null +++ b/libraries/bot-builder/src/test/java/com/microsoft/bot/builder/CloudChannelServiceHandlerTests.java @@ -0,0 +1,60 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.bot.builder; + +import com.microsoft.bot.connector.authentication.AuthenticationConstants; +import com.microsoft.bot.connector.authentication.BotFrameworkAuthentication; +import com.microsoft.bot.connector.authentication.BotFrameworkAuthenticationFactory; +import com.microsoft.bot.connector.authentication.ClaimsIdentity; +import com.microsoft.bot.connector.authentication.JwtTokenValidation; +import com.microsoft.bot.schema.Activity; +import com.microsoft.bot.schema.ActivityTypes; +import com.microsoft.bot.schema.ResourceResponse; +import org.junit.Assert; +import org.junit.Test; + +import java.util.concurrent.CompletableFuture; + +public class CloudChannelServiceHandlerTests { + + @Test + public void authenticateSetsAnonymousSkillClaim() { + TestCloudChannelServiceHandler sut = new TestCloudChannelServiceHandler( + BotFrameworkAuthenticationFactory.create()); + sut.handleReplyToActivity( + null, + "123", + "456", + new Activity(ActivityTypes.MESSAGE)).join(); + + Assert.assertEquals(AuthenticationConstants.ANONYMOUS_AUTH_TYPE, sut.getClaimsIdentity().getType()); + Assert.assertEquals(AuthenticationConstants.ANONYMOUS_SKILL_APPID, JwtTokenValidation.getAppIdFromClaims(sut.getClaimsIdentity().claims())); + } + + private class TestCloudChannelServiceHandler extends CloudChannelServiceHandler { + + private ClaimsIdentity claimsIdentity; + + public ClaimsIdentity getClaimsIdentity() { + return claimsIdentity; + } + + /** + * {@inheritDoc} + */ + public TestCloudChannelServiceHandler(BotFrameworkAuthentication withAuth) { + super(withAuth); + } + + @Override + protected CompletableFuture onReplyToActivity( + ClaimsIdentity claimsIdentity, + String conversationId, + String activityId, + Activity activity) { + this.claimsIdentity = claimsIdentity; + return CompletableFuture.completedFuture(new ResourceResponse()); + } + } +} diff --git a/libraries/bot-builder/src/test/java/com/microsoft/bot/builder/CloudSkillHandlerTests.java b/libraries/bot-builder/src/test/java/com/microsoft/bot/builder/CloudSkillHandlerTests.java new file mode 100644 index 000000000..74d623ab6 --- /dev/null +++ b/libraries/bot-builder/src/test/java/com/microsoft/bot/builder/CloudSkillHandlerTests.java @@ -0,0 +1,452 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.bot.builder; + +import com.microsoft.bot.builder.skills.BotFrameworkSkill; +import com.microsoft.bot.builder.skills.CloudSkillHandler; +import com.microsoft.bot.builder.skills.SkillConversationIdFactoryBase; +import com.microsoft.bot.builder.skills.SkillConversationIdFactoryOptions; +import com.microsoft.bot.builder.skills.SkillConversationReference; +import com.microsoft.bot.connector.authentication.AuthenticateRequestResult; +import com.microsoft.bot.connector.authentication.AuthenticationConstants; +import com.microsoft.bot.connector.authentication.BotFrameworkAuthentication; +import com.microsoft.bot.connector.authentication.ClaimsIdentity; +import com.microsoft.bot.connector.authentication.ConnectorFactory; +import com.microsoft.bot.connector.authentication.UserTokenClient; +import com.microsoft.bot.restclient.serializer.JacksonAdapter; +import com.microsoft.bot.schema.Activity; +import com.microsoft.bot.schema.ActivityTypes; +import com.microsoft.bot.schema.CallerIdConstants; +import com.microsoft.bot.schema.ConversationAccount; +import com.microsoft.bot.schema.ConversationReference; +import com.microsoft.bot.schema.ResourceResponse; +import org.apache.commons.lang3.StringUtils; +import org.junit.Assert; +import org.junit.Test; + +import java.io.IOException; +import java.net.URI; +import java.net.URISyntaxException; +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.UUID; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ConcurrentHashMap; + + +public class CloudSkillHandlerTests { + + private static final String TEST_SKILL_ID = UUID.randomUUID().toString().replace("-", ""); + private static final String TEST_AUTH_HEADER = ""; // Empty since claims extraction is being mocked + + @Test + public void testSendAndReplyToConversation() { + List theoryCases = new ArrayList<>(); + theoryCases.add(new String[]{ActivityTypes.MESSAGE, null}); + theoryCases.add(new String[]{ActivityTypes.MESSAGE, "replyToId"}); + theoryCases.add(new String[]{ActivityTypes.EVENT, null}); + theoryCases.add(new String[]{ActivityTypes.EVENT, "replyToId"}); + theoryCases.add(new String[]{ActivityTypes.END_OF_CONVERSATION, null}); + theoryCases.add(new String[]{ActivityTypes.END_OF_CONVERSATION, "replyToId"}); + + for (String[] theoryCase : theoryCases) { + String activityType = theoryCase[0]; + String replyToId = theoryCase[1]; + + // Arrange + CloudSkillHandlerTestMocks mockObjects = new CloudSkillHandlerTestMocks(); + Activity activity = new Activity(activityType); + activity.setReplyToId(replyToId); + String conversationId = mockObjects.createAndApplyConversationId(activity).join(); + + // Act + CloudSkillHandler sut = new CloudSkillHandler( + mockObjects.getAdapter(), + mockObjects.getBot(), + mockObjects.getConversationIdFactory(), + mockObjects.getAuth()); + + ResourceResponse response = replyToId == null + ? sut.handleSendToConversation(TEST_AUTH_HEADER, conversationId, activity).join() + : sut.handleReplyToActivity(TEST_AUTH_HEADER, conversationId, replyToId, activity).join(); + + // Assert + // Assert the turnContext + Assert.assertEquals( + CallerIdConstants.BOT_TO_BOT_PREFIX.concat(TEST_SKILL_ID), + mockObjects.getTurnContext().getActivity().getCallerId()); + Assert.assertNotNull( + mockObjects.getTurnContext().getTurnState().get(CloudSkillHandler.SKILL_CONVERSATION_REFERENCE_KEY)); + + // Assert based on activity type, + if (activityType.equals(ActivityTypes.MESSAGE)) { + // Should be sent to the channel and not to the bot. + Assert.assertNotNull(mockObjects.getChannelActivity()); + Assert.assertNull(mockObjects.getBotActivity()); + + // We should get the resourceId returned by the mock. + Assert.assertEquals("resourceId", response.getId()); + + // Assert the activity sent to the channel. + Assert.assertEquals(activityType, mockObjects.getChannelActivity().getType()); + Assert.assertNull(mockObjects.getChannelActivity().getCallerId()); + Assert.assertEquals(replyToId, mockObjects.getChannelActivity().getReplyToId()); + } else { + // Should be sent to the bot and not to the channel. + Assert.assertNull(mockObjects.getChannelActivity()); + Assert.assertNotNull(mockObjects.getBotActivity()); + + // If the activity is bounced back to the bot we will get a GUID and not the mocked resourceId. + Assert.assertNotEquals("resourceId", response.getId()); + + // Assert the activity sent back to the bot. + Assert.assertEquals(activityType, mockObjects.getBotActivity().getType()); + Assert.assertEquals(replyToId, mockObjects.getBotActivity().getReplyToId()); + } + } + } + + @Test + public void testCommandActivities() { + List theoryCases = new ArrayList<>(); + theoryCases.add(new String[]{ActivityTypes.COMMAND, "application/myApplicationCommand", null}); + theoryCases.add(new String[]{ActivityTypes.COMMAND, "application/myApplicationCommand", "replyToId"}); + theoryCases.add(new String[]{ActivityTypes.COMMAND, "other/myBotCommand", null}); + theoryCases.add(new String[]{ActivityTypes.COMMAND, "other/myBotCommand", "replyToId"}); + theoryCases.add(new String[]{ActivityTypes.COMMAND_RESULT, "application/myApplicationCommandResult", null}); + theoryCases.add(new String[]{ActivityTypes.COMMAND_RESULT, "application/myApplicationCommandResult", "replyToId"}); + theoryCases.add(new String[]{ActivityTypes.COMMAND_RESULT, "other/myBotCommand", null}); + theoryCases.add(new String[]{ActivityTypes.COMMAND_RESULT, "other/myBotCommand", "replyToId"}); + + for (String[] theoryCase : theoryCases) { + String commandActivityType = theoryCase[0]; + String name = theoryCase[1]; + String replyToId = theoryCase[2]; + + // Arrange + CloudSkillHandlerTestMocks mockObjects = new CloudSkillHandlerTestMocks(); + Activity activity = new Activity(commandActivityType); + activity.setName(name); + activity.setReplyToId(replyToId); + String conversationId = mockObjects.createAndApplyConversationId(activity).join(); + + // Act + CloudSkillHandler sut = new CloudSkillHandler( + mockObjects.getAdapter(), + mockObjects.getBot(), + mockObjects.getConversationIdFactory(), + mockObjects.getAuth()); + + ResourceResponse response = replyToId == null + ? sut.handleSendToConversation(TEST_AUTH_HEADER, conversationId, activity).join() + : sut.handleReplyToActivity(TEST_AUTH_HEADER, conversationId, replyToId, activity).join(); + + // Assert + // Assert the turnContext + Assert.assertEquals( + CallerIdConstants.BOT_TO_BOT_PREFIX.concat(TEST_SKILL_ID), + mockObjects.getTurnContext().getActivity().getCallerId()); + Assert.assertNotNull( + mockObjects.getTurnContext().getTurnState().get(CloudSkillHandler.SKILL_CONVERSATION_REFERENCE_KEY)); + + if (StringUtils.startsWith(name, "application/")) { + // Should be sent to the channel and not to the bot. + Assert.assertNotNull(mockObjects.getChannelActivity()); + Assert.assertNull(mockObjects.getBotActivity()); + + // We should get the resourceId returned by the mock. + Assert.assertEquals("resourceId", response.getId()); + } else { + // Should be sent to the bot and not to the channel. + Assert.assertNull(mockObjects.getChannelActivity()); + Assert.assertNotNull(mockObjects.getBotActivity()); + + // If the activity is bounced back to the bot we will get a GUID and not the mocked resourceId. + Assert.assertNotEquals("resourceId", response.getId()); + } + } + } + + @Test + public void testDeleteActivity() { + // Arrange + CloudSkillHandlerTestMocks mockObjects = new CloudSkillHandlerTestMocks(); + Activity activity = new Activity(ActivityTypes.MESSAGE); + String conversationId = mockObjects.createAndApplyConversationId(activity).join(); + String activityToDelete = UUID.randomUUID().toString(); + + // Act + CloudSkillHandler sut = new CloudSkillHandler( + mockObjects.getAdapter(), + mockObjects.getBot(), + mockObjects.getConversationIdFactory(), + mockObjects.getAuth()); + sut.handleDeleteActivity(TEST_AUTH_HEADER, conversationId, activityToDelete).join(); + + // Assert + Assert.assertNotNull(mockObjects.getTurnContext().getTurnState().get(CloudSkillHandler.SKILL_CONVERSATION_REFERENCE_KEY)); + Assert.assertEquals(activityToDelete, mockObjects.getActivityIdToDelete()); + } + + @Test + public void testUpdateActivity() { + // Arrange + CloudSkillHandlerTestMocks mockObjects = new CloudSkillHandlerTestMocks(); + Activity activity = new Activity(ActivityTypes.MESSAGE); + activity.setText(String.format("TestUpdate %s.", LocalDateTime.now())); + String conversationId = mockObjects.createAndApplyConversationId(activity).join(); + String activityToUpdate = UUID.randomUUID().toString(); + + // Act + CloudSkillHandler sut = new CloudSkillHandler( + mockObjects.getAdapter(), + mockObjects.getBot(), + mockObjects.getConversationIdFactory(), + mockObjects.getAuth()); + ResourceResponse response = sut.handleUpdateActivity(TEST_AUTH_HEADER, conversationId, activityToUpdate, activity).join(); + + // Assert + Assert.assertEquals("resourceId", response.getId()); + Assert.assertNotNull(mockObjects.getTurnContext().getTurnState().get(CloudSkillHandler.SKILL_CONVERSATION_REFERENCE_KEY)); + Assert.assertEquals(activityToUpdate, mockObjects.getTurnContext().getActivity().getId()); + Assert.assertEquals(activity.getText(), mockObjects.getUpdateActivity().getText()); + } + + /** + * Helper class with mocks for adapter, bot and auth needed to instantiate CloudSkillHandler and run tests. + * This class also captures the turnContext and activities sent back to the bot and the channel so we can run asserts on them. + */ + private static class CloudSkillHandlerTestMocks { + private static final String TEST_BOT_ID = UUID.randomUUID().toString().replace("-", ""); + private static final String TEST_BOT_ENDPOINT = "http://testbot.com/api/messages"; + private static final String TEST_SKILL_ENDPOINT = "http://testskill.com/api/messages"; + + private final SkillConversationIdFactoryBase conversationIdFactory; + private final BotAdapter adapter; + private final BotFrameworkAuthentication auth; + private final Bot bot; + private TurnContext turnContext; + private Activity channelActivity; + private Activity botActivity; + private Activity updateActivity; + private String activityToDelete; + + public CloudSkillHandlerTestMocks() { + adapter = createMockAdapter(); + auth = createMockBotFrameworkAuthentication(); + bot = createMockBot(); + conversationIdFactory = new TestSkillConversationIdFactory(); + } + + public SkillConversationIdFactoryBase getConversationIdFactory() { + return conversationIdFactory; + } + + public BotAdapter getAdapter() { + return adapter; + } + + public BotFrameworkAuthentication getAuth() { return auth; } + + public Bot getBot() { return bot; } + + // Gets the TurnContext created to call the bot. + public TurnContext getTurnContext() { + return turnContext; + } + + /** + * @return the Activity sent to the channel. + */ + public Activity getChannelActivity() { + return channelActivity; + } + + /** + * @return the Activity sent to the Bot. + */ + public Activity getBotActivity() { + return botActivity; + } + + /** + * @return the update activity. + */ + public Activity getUpdateActivity() { + return updateActivity; + } + + /** + * @return the Activity sent to the Bot. + */ + public String getActivityIdToDelete() { + return activityToDelete; + } + + public CompletableFuture createAndApplyConversationId(Activity activity) { + ConversationReference conversationReference = new ConversationReference(); + ConversationAccount conversationAccount = new ConversationAccount(); + conversationAccount.setId(TEST_BOT_ID); + conversationReference.setConversation(conversationAccount); + conversationReference.setServiceUrl(TEST_BOT_ENDPOINT); + + activity.applyConversationReference(conversationReference); + + BotFrameworkSkill skill = new BotFrameworkSkill(); + skill.setAppId(TEST_SKILL_ID); + skill.setId("skill"); + + try { + skill.setSkillEndpoint(new URI(TEST_SKILL_ENDPOINT)); + } + catch (URISyntaxException ignored) { + } + + SkillConversationIdFactoryOptions options = new SkillConversationIdFactoryOptions(); + options.setFromBotOAuthScope(TEST_BOT_ID); + options.setFromBotId(TEST_BOT_ID); + options.setActivity(activity); + options.setBotFrameworkSkill(skill); + + return getConversationIdFactory().createSkillConversationId(options); + } + + private BotAdapter createMockAdapter() { + return new BotAdapter() { + + // Mock the adapter sendActivities method + @Override + public CompletableFuture sendActivities(TurnContext context, List activities) { + // (this for the cases where activity is sent back to the parent or channel) + // Capture the activity sent to the channel + channelActivity = activities.get(0); + // Do nothing, we don't want the activities sent to the channel in the tests. + return CompletableFuture.completedFuture(new ResourceResponse[]{new ResourceResponse("resourceId")}); + } + + // Mock the updateActivity method + @Override + public CompletableFuture updateActivity(TurnContext context, Activity activity) { + updateActivity = activity; + return CompletableFuture.completedFuture(new ResourceResponse("resourceId")); + } + + // Mock the deleteActivity method + @Override + public CompletableFuture deleteActivity(TurnContext context, ConversationReference reference) { + // Capture the activity id to delete so we can assert it. + activityToDelete = reference.getActivityId(); + return CompletableFuture.completedFuture(null); + } + + @Override + public CompletableFuture continueConversation( + ClaimsIdentity claimsIdentity, + ConversationReference reference, + String audience, + BotCallbackHandler callback + ) { + // Mock the adapter ContinueConversationAsync method + // This code block catches and executes the custom bot callback created by the service handler. + turnContext = new TurnContextImpl(adapter, reference.getContinuationActivity()); + return callback.invoke(turnContext).thenApply(val -> null); + } + }; + } + + private Bot createMockBot() { + return new Bot() { + @Override + public CompletableFuture onTurn(TurnContext turnContext) { + botActivity = turnContext.getActivity(); + return CompletableFuture.completedFuture(null); + } + }; + } + + private BotFrameworkAuthentication createMockBotFrameworkAuthentication() { + return new BotFrameworkAuthentication() { + public CompletableFuture authenticateChannelRequest(String authHeader) { + HashMap claims = new HashMap<>(); + claims.put(AuthenticationConstants.AUDIENCE_CLAIM, TEST_BOT_ID); + claims.put(AuthenticationConstants.APPID_CLAIM, TEST_SKILL_ID); + claims.put(AuthenticationConstants.SERVICE_URL_CLAIM, TEST_BOT_ENDPOINT); + ClaimsIdentity claimsIdentity = new ClaimsIdentity(AuthenticationConstants.ANONYMOUS_AUTH_TYPE, AuthenticationConstants.ANONYMOUS_AUTH_TYPE, claims); + + return CompletableFuture.completedFuture(claimsIdentity); + } + + @Override + public CompletableFuture authenticateRequest(Activity activity, String authHeader) { + return CompletableFuture.completedFuture(null); + } + + @Override + public CompletableFuture authenticateStreamingRequest(String authHeader, String channelIdHeader) { + return CompletableFuture.completedFuture(null); + } + + @Override + public ConnectorFactory createConnectorFactory(ClaimsIdentity claimsIdentity) { + return null; + } + + @Override + public CompletableFuture createUserTokenClient(ClaimsIdentity claimsIdentity) { + return CompletableFuture.completedFuture(null); + } + }; + } + } + + private static class TestSkillConversationIdFactory extends SkillConversationIdFactoryBase { + private final ConcurrentHashMap conversationRefs = new ConcurrentHashMap<>(); + + public CompletableFuture createSkillConversationId(SkillConversationIdFactoryOptions options) { + SkillConversationReference skillConversationReference = new SkillConversationReference(); + skillConversationReference.setConversationReference(options.getActivity().getConversationReference()); + skillConversationReference.setOAuthScope(options.getFromBotOAuthScope()); + + String key = + String.format( + "%s-%s-%s-%s-skillconvo", + options.getFromBotId(), + options.getBotFrameworkSkill().getAppId(), + skillConversationReference.getConversationReference().getConversation().getId(), + skillConversationReference.getConversationReference().getChannelId()); + + JacksonAdapter jacksonAdapter = new JacksonAdapter(); + try { + conversationRefs.putIfAbsent(key, jacksonAdapter.serialize(skillConversationReference)); + } + catch (IOException ignored) { + } + + return CompletableFuture.completedFuture(key); + } + + @Override + public CompletableFuture getSkillConversationReference(String skillConversationId) { + SkillConversationReference conversationReference = null; + try { + JacksonAdapter jacksonAdapter = new JacksonAdapter(); + conversationReference = jacksonAdapter.deserialize( + conversationRefs.get(skillConversationId), + SkillConversationReference.class); + } + catch (IOException ignored) { + } + + return CompletableFuture.completedFuture(conversationReference); + } + + @Override + public CompletableFuture deleteConversationReference(String skillConversationId) { + conversationRefs.remove(skillConversationId); + return CompletableFuture.completedFuture(null); + } + } +} diff --git a/libraries/bot-connector/src/test/java/com/microsoft/bot/connector/BotFrameworkAuthenticationFactoryTests.java b/libraries/bot-connector/src/test/java/com/microsoft/bot/connector/BotFrameworkAuthenticationFactoryTests.java new file mode 100644 index 000000000..fa5ceb3ea --- /dev/null +++ b/libraries/bot-connector/src/test/java/com/microsoft/bot/connector/BotFrameworkAuthenticationFactoryTests.java @@ -0,0 +1,159 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.bot.connector; + +import com.microsoft.bot.connector.authentication.AuthenticationConstants; +import com.microsoft.bot.connector.authentication.BotFrameworkAuthentication; +import com.microsoft.bot.connector.authentication.BotFrameworkAuthenticationFactory; +import com.microsoft.bot.connector.authentication.ClaimsIdentity; +import com.microsoft.bot.connector.authentication.ConnectorFactory; +import com.microsoft.bot.connector.authentication.GovernmentAuthenticationConstants; +import com.microsoft.bot.connector.authentication.PasswordServiceClientCredentialFactory; +import com.microsoft.bot.connector.authentication.SkillValidation; +import com.microsoft.bot.connector.authentication.UserTokenClient; +import org.hamcrest.MatcherAssert; +import org.junit.Assert; +import org.junit.Test; + +import java.util.HashMap; + +import static org.hamcrest.CoreMatchers.instanceOf; + +public class BotFrameworkAuthenticationFactoryTests { + @Test + public void shouldCreateAnonymousBotFrameworkAuthentication() { + BotFrameworkAuthentication bfA = BotFrameworkAuthenticationFactory.create(); + MatcherAssert.assertThat(bfA, instanceOf(BotFrameworkAuthentication.class)); + } + + @Test + public void shouldCreateBotFrameworkAuthenticationConfiguredForValidChannelServices() { + BotFrameworkAuthentication bfA = BotFrameworkAuthenticationFactory.create( + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null); + Assert.assertEquals( + bfA.getOriginatingAudience(), + AuthenticationConstants.TO_CHANNEL_FROM_BOT_OAUTH_SCOPE); + + BotFrameworkAuthentication gBfA = BotFrameworkAuthenticationFactory.create( + GovernmentAuthenticationConstants.CHANNELSERVICE, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null); + Assert.assertEquals( + gBfA.getOriginatingAudience(), + GovernmentAuthenticationConstants.TO_CHANNEL_FROM_BOT_OAUTH_SCOPE); + } + + @Test + public void shouldThrowWithAnUnknownChannelService() { + Assert.assertThrows(IllegalArgumentException.class, () -> BotFrameworkAuthenticationFactory.create( + "Unknown", + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null)); + } + + /** + * These tests replicate the flow in CloudAdapterBase.processProactive(). + * + * The CloudAdapterBase's BotFrameworkAuthentication (normally and practically the ParameterizedBotFrameworkAuthentication) is + * used to create and set on the TurnState the following values: + * - ConnectorFactory + * - ConnectorClient + * - UserTokenClient + */ + String HOST_SERVICE_URL = "https://bot.host.serviceurl"; + String HOST_AUDIENCE = "host-bot-app-id"; + + @Test + public void shouldNotThrowErrorsWhenAuthIsDisabledAndAnonymousSkillClaimsAreUsed() { + PasswordServiceClientCredentialFactory credsFactory = new PasswordServiceClientCredentialFactory("", ""); + BotFrameworkAuthentication pBFA = BotFrameworkAuthenticationFactory.create( + "", + null, + null, + null, + null, + null, + null, + null, + null, + credsFactory, + null, + null); + + Assert.assertEquals(pBFA.getOriginatingAudience(), AuthenticationConstants.TO_CHANNEL_FROM_BOT_OAUTH_SCOPE); + ClaimsIdentity claimsIdentity = SkillValidation.createAnonymousSkillClaim(); + + // The order of creation for the connectorFactory, connectorClient and userTokenClient mirrors the existing flow in + // CloudAdapterBase.processProactive(). + ConnectorFactory connectorFactory = pBFA.createConnectorFactory(claimsIdentity); + // When authentication is disabled, MicrosoftAppCredentials (an implementation of ServiceClientCredentials) + // with appId and appPassword fields are created and passed to the newly created ConnectorFactory. + ConnectorClient connectorClient = connectorFactory.create(HOST_SERVICE_URL, "UnusedAudienceWhenAuthIsDisabled").join(); + // If authentication was enabled 'UnusedAudienceWhenAuthIsDisabled' would have been used, + // but is unnecessary with disabled authentication. + UserTokenClient userTokenClient = pBFA.createUserTokenClient(claimsIdentity).join(); + } + + @Test + public void shouldNotThrowErrorsWhenAuthIsDisabledAndAuthenticatedSkillClaimsAreUsed() { + String APP_ID = "app-id"; + String APP_PASSWORD = "app-password"; + PasswordServiceClientCredentialFactory credsFactory = new PasswordServiceClientCredentialFactory(APP_ID, APP_PASSWORD); + BotFrameworkAuthentication pBFA = BotFrameworkAuthenticationFactory.create( + "", + null, + null, + null, + null, + null, + null, + null, + null, + credsFactory, + null, + null); + + Assert.assertEquals(pBFA.getOriginatingAudience(), AuthenticationConstants.TO_CHANNEL_FROM_BOT_OAUTH_SCOPE); + HashMap claims = new HashMap(); + claims.put(AuthenticationConstants.AUTHORIZED_PARTY, HOST_AUDIENCE); + claims.put(AuthenticationConstants.AUDIENCE_CLAIM, APP_ID); + claims.put(AuthenticationConstants.VERSION_CLAIM, "2.0"); + ClaimsIdentity claimsIdentity = new ClaimsIdentity("anonymous", claims); + + ConnectorFactory connectorFactory = pBFA.createConnectorFactory(claimsIdentity); + + ConnectorClient connectorClient = connectorFactory.create(HOST_SERVICE_URL, HOST_AUDIENCE).join(); + + UserTokenClient userTokenClient = pBFA.createUserTokenClient(claimsIdentity).join(); + } +} diff --git a/libraries/bot-connector/src/test/java/com/microsoft/bot/connector/BotFrameworkAuthenticationTests.java b/libraries/bot-connector/src/test/java/com/microsoft/bot/connector/BotFrameworkAuthenticationTests.java new file mode 100644 index 000000000..a0f074fdd --- /dev/null +++ b/libraries/bot-connector/src/test/java/com/microsoft/bot/connector/BotFrameworkAuthenticationTests.java @@ -0,0 +1,107 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MT License. + +package com.microsoft.bot.connector; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.microsoft.bot.connector.authentication.AuthenticationConfiguration; +import com.microsoft.bot.connector.authentication.AuthenticationConstants; +import com.microsoft.bot.connector.authentication.BotFrameworkAuthentication; +import com.microsoft.bot.connector.authentication.BotFrameworkAuthenticationFactory; +import com.microsoft.bot.connector.authentication.MicrosoftAppCredentials; +import com.microsoft.bot.connector.authentication.ServiceClientCredentialsFactory; +import com.microsoft.bot.connector.skills.BotFrameworkClient; +import com.microsoft.bot.schema.Activity; +import com.microsoft.bot.schema.ActivityTypes; +import com.microsoft.bot.schema.ConversationAccount; +import com.microsoft.bot.schema.TypedInvokeResponse; +import okhttp3.Call; +import okhttp3.OkHttpClient; +import okhttp3.Request; +import okhttp3.Response; +import okhttp3.Protocol; +import okhttp3.ResponseBody; +import okhttp3.MediaType; +import org.junit.Assert; +import org.junit.Test; +import org.mockito.Mockito; + +import java.io.IOException; +import java.net.URI; +import java.net.URISyntaxException; +import java.util.concurrent.CompletableFuture; + +public class BotFrameworkAuthenticationTests { + + @Test + public void createsBotFrameworkClient() throws URISyntaxException, IOException { + // Arrange + String fromBotId = "from-bot-id"; + String toBotId = "to-bot-id"; + String loginUrl = String.format(AuthenticationConstants.TO_CHANNEL_FROM_BOT_LOGIN_URL_TEMPLATE, AuthenticationConstants.DEFAULT_CHANNEL_AUTH_TENANT); + URI toUrl = new URI("http://test1.com/test"); + + ServiceClientCredentialsFactory credentialFactoryMock = Mockito.mock(ServiceClientCredentialsFactory.class); + Mockito.when( + credentialFactoryMock.createCredentials( + fromBotId, + toBotId, + loginUrl, + Boolean.TRUE) + ).thenReturn(CompletableFuture.completedFuture(MicrosoftAppCredentials.empty())); + + OkHttpClient httpClientMock = Mockito.mock(OkHttpClient.class); + Call remoteCall = Mockito.mock(Call.class); + + Response response = new Response.Builder() + .request(new Request.Builder().url(toUrl.toString()).build()) + .protocol(Protocol.HTTP_1_1) + .code(200).message("").body( + ResponseBody.create( + MediaType.parse("application/json; charset=utf-8"), + "{\"hello\": \"world\"}" + )) + .build(); + + Mockito.when(remoteCall.execute()).thenReturn(response); + Mockito.when(httpClientMock.newCall(Mockito.any())).thenReturn(remoteCall); + Mockito.when(httpClientMock.newBuilder()).thenReturn(new OkHttpClient.Builder()); + + BotFrameworkAuthentication bfa = BotFrameworkAuthenticationFactory.create( + null, + true, + null, + null, + null, + null, + null, + null, + null, + credentialFactoryMock, + new AuthenticationConfiguration(), + httpClientMock + ); + + URI serviceUrl = new URI("http://root-bot/service-url"); + String conversationId = "conversation-id"; + ConversationAccount conversationAccount = new ConversationAccount(); + conversationAccount.setId("conversationiid"); + conversationAccount.setName("conversation-name"); + Activity activity = new Activity(ActivityTypes.MESSAGE); + activity.setChannelId("channel-id"); + activity.setServiceUrl("service-url"); + activity.setLocale("locale"); + activity.setConversation(conversationAccount); + + // Act + BotFrameworkClient bfc = bfa.createBotFrameworkClient(); + TypedInvokeResponse invokeResponse = bfc.postActivity(fromBotId, toBotId, toUrl, serviceUrl, conversationId, activity, Object.class).join(); + + ObjectMapper mapper = new ObjectMapper().findAndRegisterModules(); + JsonNode testData = mapper.readTree(invokeResponse.getBody().toString()); + + // Assert + Assert.assertEquals("world", testData.get("hello").asText()); + } +} diff --git a/libraries/bot-connector/src/test/java/com/microsoft/bot/connector/PasswordServiceClientCredentialFactoryTests.java b/libraries/bot-connector/src/test/java/com/microsoft/bot/connector/PasswordServiceClientCredentialFactoryTests.java new file mode 100644 index 000000000..91010a5d3 --- /dev/null +++ b/libraries/bot-connector/src/test/java/com/microsoft/bot/connector/PasswordServiceClientCredentialFactoryTests.java @@ -0,0 +1,101 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.bot.connector; + +import com.microsoft.bot.connector.authentication.AuthenticationConstants; +import com.microsoft.bot.connector.authentication.GovernmentAuthenticationConstants; +import com.microsoft.bot.connector.authentication.MicrosoftAppCredentials; +import com.microsoft.bot.connector.authentication.PasswordServiceClientCredentialFactory; +import com.microsoft.bot.restclient.credentials.ServiceClientCredentials; +import org.junit.Assert; +import org.junit.Test; + +import java.util.Arrays; +import java.util.List; +import java.util.stream.Collectors; +import java.util.stream.IntStream; + +public class PasswordServiceClientCredentialFactoryTests { + + private final static String APP_ID = "2cd87869-38a0-4182-9251-d056e8f0ac24"; + private final static String APP_PASSWORD = "password"; + + @Test + public void shouldSetAppIdAndPasswordDuringConstruction() { + PasswordServiceClientCredentialFactory credFactory = new PasswordServiceClientCredentialFactory(APP_ID, APP_PASSWORD); + Assert.assertEquals(APP_ID, credFactory.getAppId()); + Assert.assertEquals(APP_PASSWORD, credFactory.getPassword()); + } + + @Test + public void isValidAppIdShouldWork() { + PasswordServiceClientCredentialFactory credFactory = new PasswordServiceClientCredentialFactory(APP_ID, APP_PASSWORD); + Assert.assertTrue(credFactory.isValidAppId(APP_ID).join()); + Assert.assertFalse(credFactory.isValidAppId("invalid-app-id").join()); + } + + @Test + public void isAuthenticationDisabledShouldWork() { + PasswordServiceClientCredentialFactory credFactory = new PasswordServiceClientCredentialFactory(APP_ID, APP_PASSWORD); + Assert.assertFalse(credFactory.isAuthenticationDisabled().join()); + credFactory.setAppId(null); + Assert.assertTrue(credFactory.isAuthenticationDisabled().join()); + } + + @Test + public void createCredentialsShouldWork() { + PasswordServiceClientCredentialFactory credFactory = new PasswordServiceClientCredentialFactory(APP_ID, APP_PASSWORD); + List testArg1 = Arrays.asList( + APP_ID, + AuthenticationConstants.TO_CHANNEL_FROM_BOT_OAUTH_SCOPE, + String.format(AuthenticationConstants.TO_CHANNEL_FROM_BOT_LOGIN_URL_TEMPLATE, AuthenticationConstants.DEFAULT_CHANNEL_AUTH_TENANT) + ); + List testArg2 = Arrays.asList( + APP_ID, + AuthenticationConstants.TO_CHANNEL_FROM_BOT_OAUTH_SCOPE, + String.format(AuthenticationConstants.TO_CHANNEL_FROM_BOT_LOGIN_URL_TEMPLATE, AuthenticationConstants.DEFAULT_CHANNEL_AUTH_TENANT) + ); + List testArg3 = Arrays.asList( + APP_ID, + GovernmentAuthenticationConstants.TO_CHANNEL_FROM_BOT_OAUTH_SCOPE, + GovernmentAuthenticationConstants.TO_CHANNEL_FROM_BOT_LOGIN_URL + ); + List testArg4 = Arrays.asList( + APP_ID, + "CustomAudience", + "https://custom.login-endpoint.com/custom-tenant" + ); + List> testArgs = Arrays.asList(testArg1, testArg2, testArg3, testArg4); + List credentials = testArgs.stream().map( + args -> credFactory.createCredentials(args.get(0), args.get(1), args.get(2), null).join()) + .collect(Collectors.toList()); + + IntStream.range(0, credentials.size()).forEach(idx -> { + // The PasswordServiceClientCredentialFactory generates subclasses of the AppCredentials class. + Assert.assertEquals(APP_ID, ((MicrosoftAppCredentials) credentials.get(idx)).getAppId()); + Assert.assertEquals(testArgs.get(idx).get(1), ((MicrosoftAppCredentials) credentials.get(idx)).oAuthScope()); + Assert.assertEquals(testArgs.get(idx).get(2).toLowerCase(), ((MicrosoftAppCredentials) credentials.get(idx)).oAuthEndpoint()); + }); + } + + @Test + public void createCredentialsShouldAlwaysReturnEmptyCredentialsWhenAuthIsDisabled() { + PasswordServiceClientCredentialFactory credFactory = new PasswordServiceClientCredentialFactory("", ""); + ServiceClientCredentials credentials = credFactory.createCredentials(null, null, null, null).join(); + + // When authentication is disabled, a MicrosoftAppCredentials with empty strings for appId and appPassword is returned. + Assert.assertNull(((MicrosoftAppCredentials)credentials).getAppId()); + Assert.assertNull(((MicrosoftAppCredentials)credentials).getAppPassword()); + + credentials = credFactory.createCredentials(APP_ID, null, null, null).join(); + Assert.assertNull(((MicrosoftAppCredentials)credentials).getAppId()); + Assert.assertNull(((MicrosoftAppCredentials)credentials).getAppPassword()); + } + + @Test + public void createCredentialsShouldThrowWhenAppIdIsInvalid() { + PasswordServiceClientCredentialFactory credFactory = new PasswordServiceClientCredentialFactory(APP_ID, APP_PASSWORD); + Assert.assertThrows(IllegalArgumentException.class, () -> credFactory.createCredentials("badAppId", null, null, null).join()); + } +} diff --git a/libraries/bot-dialogs/src/test/java/com/microsoft/bot/dialogs/SkillDialogTests.java b/libraries/bot-dialogs/src/test/java/com/microsoft/bot/dialogs/SkillDialogTests.java index 8c18e32dd..59f176929 100644 --- a/libraries/bot-dialogs/src/test/java/com/microsoft/bot/dialogs/SkillDialogTests.java +++ b/libraries/bot-dialogs/src/test/java/com/microsoft/bot/dialogs/SkillDialogTests.java @@ -3,28 +3,18 @@ package com.microsoft.bot.dialogs; -import java.net.URI; -import java.net.URISyntaxException; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.UUID; -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.CompletionException; - import com.microsoft.bot.builder.AutoSaveStateMiddleware; import com.microsoft.bot.builder.ConversationState; import com.microsoft.bot.builder.MemoryStorage; import com.microsoft.bot.builder.MessageFactory; -import com.microsoft.bot.builder.TypedInvokeResponse; +import com.microsoft.bot.schema.TypedInvokeResponse; import com.microsoft.bot.builder.adapters.TestAdapter; -import com.microsoft.bot.builder.skills.BotFrameworkClient; import com.microsoft.bot.builder.skills.BotFrameworkSkill; import com.microsoft.bot.builder.skills.SkillConversationIdFactoryBase; import com.microsoft.bot.builder.skills.SkillConversationIdFactoryOptions; import com.microsoft.bot.builder.skills.SkillConversationReference; import com.microsoft.bot.connector.Channels; +import com.microsoft.bot.connector.skills.BotFrameworkClient; import com.microsoft.bot.schema.Activity; import com.microsoft.bot.schema.ActivityTypes; import com.microsoft.bot.schema.Attachment; @@ -34,10 +24,19 @@ import com.microsoft.bot.schema.ExpectedReplies; import com.microsoft.bot.schema.OAuthCard; import com.microsoft.bot.schema.TokenExchangeResource; - import org.junit.Assert; import org.junit.Test; +import java.net.URI; +import java.net.URISyntaxException; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.UUID; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionException; + /** * Tests for SkillsDialog. */ @@ -96,7 +95,7 @@ public void BeginDialogCallsSkill_Expect_Replies() { class MockFrameworkClient extends BotFrameworkClient { int returnStatus = 200; - ExpectedReplies expectedReplies = null;; + ExpectedReplies expectedReplies = null; MockFrameworkClient() { diff --git a/libraries/bot-integration-core/src/main/java/com/microsoft/bot/integration/CloudAdapterWithErrorHandler.java b/libraries/bot-integration-core/src/main/java/com/microsoft/bot/integration/CloudAdapterWithErrorHandler.java new file mode 100644 index 000000000..05cfa62ef --- /dev/null +++ b/libraries/bot-integration-core/src/main/java/com/microsoft/bot/integration/CloudAdapterWithErrorHandler.java @@ -0,0 +1,107 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.bot.integration; + +import java.util.concurrent.CompletableFuture; + +import com.microsoft.bot.builder.ConversationState; +import com.microsoft.bot.builder.MessageFactory; +import com.microsoft.bot.builder.TurnContext; +import com.microsoft.bot.connector.Channels; +import com.microsoft.bot.schema.Activity; +import com.microsoft.bot.schema.ActivityTypes; +import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.exception.ExceptionUtils; +import org.slf4j.LoggerFactory; + +/** + * An Adapter that provides exception handling. + */ +public class CloudAdapterWithErrorHandler extends CloudAdapter { + private static final String ERROR_MSG_ONE = "The bot encountered an error or bug."; + private static final String ERROR_MSG_TWO = + "To continue to run this bot, please fix the bot source code."; + + /** + * Constructs an error handling BotFrameworkHttpAdapter by providing an + * {@link com.microsoft.bot.builder.OnTurnErrorHandler}. + * + *

+ * For this sample, a simple message is displayed. For a production Bot, a more + * informative message or action is likely preferred. + *

+ * + * @param withConfiguration The Configuration object to use. + */ + public CloudAdapterWithErrorHandler(Configuration withConfiguration) { + super(withConfiguration); + + setOnTurnError((turnContext, exception) -> { + LoggerFactory.getLogger(AdapterWithErrorHandler.class).error("onTurnError", exception); + + return turnContext.sendActivities( + MessageFactory.text(ERROR_MSG_ONE), MessageFactory.text(ERROR_MSG_TWO) + ).thenCompose(resourceResponse -> sendTraceActivity(turnContext, exception)); + }); + } + + /** + * Constructs an error handling BotFrameworkHttpAdapter by providing an + * {@link com.microsoft.bot.builder.OnTurnErrorHandler}. + * + *

+ * For this sample, a simple message is displayed. For a production Bot, a more + * informative message or action is likely preferred. + *

+ * + * @param withConfiguration The Configuration object to use. + * @param withConversationState For ConversationState. + */ + public CloudAdapterWithErrorHandler( + Configuration withConfiguration, + ConversationState withConversationState + ) { + super(withConfiguration); + + setOnTurnError((turnContext, exception) -> { + LoggerFactory.getLogger(AdapterWithErrorHandler.class).error("onTurnError", exception); + + return turnContext.sendActivities( + MessageFactory.text(ERROR_MSG_ONE), MessageFactory.text(ERROR_MSG_TWO) + ).thenCompose(resourceResponse -> sendTraceActivity(turnContext, exception)) + .thenCompose(stageResult -> { + if (withConversationState != null) { + // Delete the conversationState for the current conversation to prevent the + // bot from getting stuck in a error-loop caused by being in a bad state. + // ConversationState should be thought of as similar to "cookie-state" in a + // Web pages. + return withConversationState.delete(turnContext) + .exceptionally(deleteException -> { + LoggerFactory.getLogger(AdapterWithErrorHandler.class) + .error("ConversationState.delete", deleteException); + return null; + }); + } + return CompletableFuture.completedFuture(null); + }); + }); + } + + private CompletableFuture sendTraceActivity( + TurnContext turnContext, + Throwable exception + ) { + if (StringUtils.equals(turnContext.getActivity().getChannelId(), Channels.EMULATOR)) { + Activity traceActivity = new Activity(ActivityTypes.TRACE); + traceActivity.setLabel("TurnError"); + traceActivity.setName("OnTurnError Trace"); + traceActivity.setValue(ExceptionUtils.getStackTrace(exception)); + traceActivity.setValueType("https://www.botframework.com/schemas/error"); + + return turnContext.sendActivity(traceActivity).thenApply(resourceResponse -> null); + } + + return CompletableFuture.completedFuture(null); + } +} diff --git a/libraries/bot-integration-core/src/test/java/com/microsoft/bot/integration/CloudAdapterTests.java b/libraries/bot-integration-core/src/test/java/com/microsoft/bot/integration/CloudAdapterTests.java new file mode 100644 index 000000000..4c9c58fe3 --- /dev/null +++ b/libraries/bot-integration-core/src/test/java/com/microsoft/bot/integration/CloudAdapterTests.java @@ -0,0 +1,715 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.bot.integration; + +import com.microsoft.bot.builder.Bot; +import com.microsoft.bot.builder.BotAdapter; +import com.microsoft.bot.builder.BotCallbackHandler; +import com.microsoft.bot.builder.BotFrameworkAdapter; +import com.microsoft.bot.builder.CloudAdapterBase; +import com.microsoft.bot.builder.MessageFactory; +import com.microsoft.bot.builder.TurnContext; +import com.microsoft.bot.builder.TurnContextImpl; +import com.microsoft.bot.connector.ConnectorClient; +import com.microsoft.bot.connector.Conversations; +import com.microsoft.bot.connector.authentication.AuthenticateRequestResult; +import com.microsoft.bot.connector.authentication.AuthenticationConfiguration; +import com.microsoft.bot.connector.authentication.BotFrameworkAuthentication; +import com.microsoft.bot.connector.authentication.BotFrameworkAuthenticationFactory; +import com.microsoft.bot.connector.authentication.ClaimsIdentity; +import com.microsoft.bot.connector.authentication.ConnectorFactory; +import com.microsoft.bot.connector.authentication.GovernmentAuthenticationConstants; +import com.microsoft.bot.connector.authentication.PasswordServiceClientCredentialFactory; +import com.microsoft.bot.connector.authentication.UserTokenClient; +import com.microsoft.bot.connector.rest.RestConnectorClient; +import com.microsoft.bot.restclient.ServiceClient; +import com.microsoft.bot.restclient.credentials.ServiceClientCredentials; +import com.microsoft.bot.restclient.serializer.JacksonAdapter; +import com.microsoft.bot.schema.Activity; +import com.microsoft.bot.schema.ActivityTypes; +import com.microsoft.bot.schema.ChannelAccount; +import com.microsoft.bot.schema.ConversationAccount; +import com.microsoft.bot.schema.ConversationParameters; +import com.microsoft.bot.schema.ConversationReference; +import com.microsoft.bot.schema.ConversationResourceResponse; +import com.microsoft.bot.schema.SignInResource; +import com.microsoft.bot.schema.TokenExchangeRequest; +import com.microsoft.bot.schema.TokenExchangeState; +import com.microsoft.bot.schema.TokenResponse; +import com.microsoft.bot.schema.TokenStatus; +import okhttp3.Call; +import okhttp3.Interceptor; +import okhttp3.MediaType; +import okhttp3.OkHttpClient; +import okhttp3.Protocol; +import okhttp3.Request; +import okhttp3.Response; +import okhttp3.ResponseBody; +import org.apache.commons.lang3.StringUtils; +import org.junit.Assert; +import org.junit.Test; +import org.mockito.Mockito; +import retrofit2.Retrofit; + +import java.io.IOException; +import java.util.Arrays; +import java.util.Base64; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Properties; +import java.util.concurrent.CompletableFuture; + +public class CloudAdapterTests { + + @Test + public void basicMessageActivity() { + // Arrange + Bot botMock = Mockito.mock(Bot.class); + Mockito.when( + botMock.onTurn( + Mockito.any(TurnContext.class) + ) + ).thenReturn(CompletableFuture.completedFuture(null)); + + // Act + CloudAdapter adapter = new CloudAdapter(); + adapter.processIncomingActivity("", createMessageActivity(), botMock); + + // Assert + Mockito.verify(botMock, Mockito.times(1)).onTurn(Mockito.any(TurnContext.class)); + } + + @Test + public void constructorWithConfiguration() { + Properties appSettings = new Properties(); + appSettings.put("MicrosoftAppId", "appId"); + appSettings.put("MicrosoftAppPassword", "appPassword"); + appSettings.put("ChannelService", GovernmentAuthenticationConstants.CHANNELSERVICE); + + ConfigurationTest configuration = new ConfigurationTest(); + configuration.setProperties(appSettings); + + // Act + CloudAdapter adapter = new CloudAdapter(configuration); + + // Assert + + // TODO: work out what might be a reasonable side effect + } + + @Test + public void injectCloudEnvironment() { + // Arrange + Bot botMock = Mockito.mock(Bot.class); + Mockito.when( + botMock.onTurn( + Mockito.any(TurnContext.class) + ) + ).thenReturn(CompletableFuture.completedFuture(null)); + + AuthenticateRequestResult authenticateRequestResult = new AuthenticateRequestResult(); + authenticateRequestResult.setClaimsIdentity(new ClaimsIdentity("")); + authenticateRequestResult.setConnectorFactory(new TestConnectorFactory()); + authenticateRequestResult.setAudience("audience"); + authenticateRequestResult.setCallerId("callerId"); + + TestUserTokenClient userTokenClient = new TestUserTokenClient("appId"); + + BotFrameworkAuthentication cloudEnvironmentMock = Mockito.mock(BotFrameworkAuthentication.class); + Mockito.when( + cloudEnvironmentMock.authenticateRequest( + Mockito.any(Activity.class), + Mockito.any(String.class)) + ).thenReturn(CompletableFuture.completedFuture(authenticateRequestResult)); + Mockito.when( + cloudEnvironmentMock.createUserTokenClient( + Mockito.any(ClaimsIdentity.class)) + ).thenReturn(CompletableFuture.completedFuture(userTokenClient)); + + // Act + CloudAdapter adapter = new CloudAdapter(cloudEnvironmentMock); + adapter.processIncomingActivity("", createMessageActivity(), botMock); + + // Assert + Mockito.verify(botMock, Mockito.times(1)).onTurn(Mockito.any (TurnContext.class)); + Mockito.verify(cloudEnvironmentMock, Mockito.times(1)).authenticateRequest(Mockito.any(Activity.class), Mockito.anyString()); + } + + @Test + public void cloudAdapterProvidesUserTokenClient() { + // this is just a basic test to verify the wire-up of a UserTokenClient in the CloudAdapter + // there is also some coverage for the internal code that creates the TokenExchangeState string + + // Arrange + String appId = "appId"; + String userId = "userId"; + String channelId = "channelId"; + String conversationId = "conversationId"; + String recipientId = "botId"; + String relatesToActivityId = "relatesToActivityId"; + String connectionName = "connectionName"; + + AuthenticateRequestResult authenticateRequestResult = new AuthenticateRequestResult(); + authenticateRequestResult.setClaimsIdentity(new ClaimsIdentity("")); + authenticateRequestResult.setConnectorFactory(new TestConnectorFactory()); + authenticateRequestResult.setAudience("audience"); + authenticateRequestResult.setCallerId("callerId"); + + TestUserTokenClient userTokenClient = new TestUserTokenClient(appId); + + BotFrameworkAuthentication cloudEnvironmentMock = Mockito.mock(BotFrameworkAuthentication.class); + Mockito.when( + cloudEnvironmentMock.authenticateRequest( + Mockito.any(Activity.class), + Mockito.anyString()) + ).thenReturn(CompletableFuture.completedFuture(authenticateRequestResult)); + Mockito.when( + cloudEnvironmentMock.createUserTokenClient( + Mockito.any(ClaimsIdentity.class)) + ).thenReturn(CompletableFuture.completedFuture(userTokenClient)); + + UserTokenClientBot bot = new UserTokenClientBot(connectionName); + + // Act + Activity activity = createMessageActivity(userId, channelId, conversationId, recipientId, relatesToActivityId); + CloudAdapter adapter = new CloudAdapter(cloudEnvironmentMock); + adapter.processIncomingActivity("", activity, bot); + + // Assert + Object[] args_ExchangeToken = userTokenClient.getRecord().get("exchangeToken"); + Assert.assertEquals(userId, args_ExchangeToken[0]); + Assert.assertEquals(connectionName, args_ExchangeToken[1]); + Assert.assertEquals(channelId, args_ExchangeToken[2]); + Assert.assertEquals("TokenExchangeRequest", args_ExchangeToken[3].getClass().getSimpleName()); + + Object[] args_GetAadTokens = userTokenClient.getRecord().get("getAadTokens"); + Assert.assertEquals(userId, args_GetAadTokens[0]); + Assert.assertEquals(connectionName, args_GetAadTokens[1]); + Assert.assertEquals("x", ((List)args_GetAadTokens[2]).get(0)); + Assert.assertEquals("y", ((List)args_GetAadTokens[2]).get(1)); + + Assert.assertEquals(channelId, args_GetAadTokens[3]); + + Object[] args_GetSignInResource = userTokenClient.getRecord().get("getSignInResource"); + + // this code is testing the internal CreateTokenExchangeState function by doing the work in reverse + String state = (String) args_GetSignInResource[0]; + String json; + TokenExchangeState tokenExchangeState = null; + + try { + JacksonAdapter jacksonAdapter = new JacksonAdapter(); + json = new String(Base64.getDecoder().decode(state)); + tokenExchangeState = jacksonAdapter.deserialize(json, TokenExchangeState.class); + } catch (IOException e) { + } + + Assert.assertEquals(connectionName, tokenExchangeState.getConnectionName()); + Assert.assertEquals(appId, tokenExchangeState.getMsAppId()); + Assert.assertEquals(conversationId, tokenExchangeState.getConversation().getConversation().getId()); + Assert.assertEquals(recipientId, tokenExchangeState.getConversation().getBot().getId()); + Assert.assertEquals(relatesToActivityId, tokenExchangeState.getRelatesTo().getActivityId()); + + Assert.assertEquals("finalRedirect", args_GetSignInResource[1]); + + Object[] args_GetTokenStatus = userTokenClient.getRecord().get("getTokenStatus"); + Assert.assertEquals(userId, args_GetTokenStatus[0]); + Assert.assertEquals(channelId, args_GetTokenStatus[1]); + Assert.assertEquals("includeFilter", args_GetTokenStatus[2]); + + Object[] args_GetUserToken = userTokenClient.getRecord().get("getUserToken"); + Assert.assertEquals(userId, args_GetUserToken[0]); + Assert.assertEquals(connectionName, args_GetUserToken[1]); + Assert.assertEquals(channelId, args_GetUserToken[2]); + Assert.assertEquals("magicCode", args_GetUserToken[3]); + + Object[] args_SignOutUser = userTokenClient.getRecord().get("signOutUser"); + Assert.assertEquals(userId, args_SignOutUser[0]); + Assert.assertEquals(connectionName, args_SignOutUser[1]); + Assert.assertEquals(channelId, args_SignOutUser[2]); + } + + @Test + public void cloudAdapterConnectorFactory() { + // this is just a basic test to verify the wire-up of a ConnectorFactory in the CloudAdapter + + // Arrange + ClaimsIdentity claimsIdentity = new ClaimsIdentity(""); + + AuthenticateRequestResult authenticateRequestResult = new AuthenticateRequestResult(); + authenticateRequestResult.setClaimsIdentity(claimsIdentity); + authenticateRequestResult.setConnectorFactory(new TestConnectorFactory()); + authenticateRequestResult.setAudience("audience"); + authenticateRequestResult.setCallerId("callerId"); + + TestUserTokenClient userTokenClient = new TestUserTokenClient("appId"); + + BotFrameworkAuthentication cloudEnvironmentMock = Mockito.mock(BotFrameworkAuthentication.class); + Mockito.when( + cloudEnvironmentMock.authenticateRequest( + Mockito.any(Activity.class), + Mockito.anyString()) + ).thenReturn(CompletableFuture.completedFuture(authenticateRequestResult)); + Mockito.when( + cloudEnvironmentMock.createConnectorFactory( + Mockito.any(ClaimsIdentity.class)) + ).thenReturn(new TestConnectorFactory()); + Mockito.when( + cloudEnvironmentMock.createUserTokenClient( + Mockito.any(ClaimsIdentity.class)) + ).thenReturn(CompletableFuture.completedFuture(userTokenClient)); + + ConnectorFactoryBot bot = new ConnectorFactoryBot(); + + // Act + CloudAdapter adapter = new CloudAdapter(cloudEnvironmentMock); + adapter.processIncomingActivity("", createMessageActivity(), bot); + + // Assert + Assert.assertEquals("audience", bot.authorization); + Assert.assertEquals(claimsIdentity, bot.identity); + Assert.assertEquals(userTokenClient, bot.userTokenClient); + Assert.assertTrue(bot.connectorClient != null); + Assert.assertTrue(bot.botCallbackHandler != null); + } + + @Test + public void cloudAdapterContinueConversation() { + // Arrange + ClaimsIdentity claimsIdentity = new ClaimsIdentity(""); + + AuthenticateRequestResult authenticateRequestResult = new AuthenticateRequestResult(); + authenticateRequestResult.setClaimsIdentity(claimsIdentity); + authenticateRequestResult.setConnectorFactory(new TestConnectorFactory()); + authenticateRequestResult.setAudience("audience"); + authenticateRequestResult.setCallerId("callerId"); + + TestUserTokenClient userTokenClient = new TestUserTokenClient("appId"); + + BotFrameworkAuthentication cloudEnvironmentMock = Mockito.mock(BotFrameworkAuthentication.class); + Mockito.when( + cloudEnvironmentMock.authenticateRequest( + Mockito.any(Activity.class), + Mockito.anyString()) + ).thenReturn(CompletableFuture.completedFuture(authenticateRequestResult)); + Mockito.when( + cloudEnvironmentMock.createConnectorFactory( + Mockito.any(ClaimsIdentity.class)) + ).thenReturn(new TestConnectorFactory()); + Mockito.when( + cloudEnvironmentMock.createUserTokenClient( + Mockito.any(ClaimsIdentity.class)) + ).thenReturn(CompletableFuture.completedFuture(userTokenClient)); + + // NOTE: present in C# but not used + ConnectorFactoryBot bot = new ConnectorFactoryBot(); + + String expectedServiceUrl = "http://serviceUrl"; + + ConversationAccount conversationAccount = new ConversationAccount(); + conversationAccount.setId("conversation Id"); + + Activity continuationActivity = new Activity(ActivityTypes.EVENT); + continuationActivity.setServiceUrl(expectedServiceUrl); + continuationActivity.setConversation(conversationAccount); + + ConversationReference conversationReference = new ConversationReference(); + conversationReference.setServiceUrl(expectedServiceUrl); + conversationReference.setConversation(conversationAccount); + + final String[] actualServiceUrl1 = {""}; + final String[] actualServiceUrl2 = {""}; + final String[] actualServiceUrl3 = {""}; + final String[] actualServiceUrl4 = {""}; + final String[] actualServiceUrl5 = {""}; + final String[] actualServiceUrl6 = {""}; + + BotCallbackHandler callback1 = (t) -> { + actualServiceUrl1[0] = t.getActivity().getServiceUrl(); + return CompletableFuture.completedFuture(null); + }; + + BotCallbackHandler callback2 = (t) -> { + actualServiceUrl2[0] = t.getActivity().getServiceUrl(); + return CompletableFuture.completedFuture(null); + }; + + BotCallbackHandler callback3 = (t) -> { + actualServiceUrl3[0] = t.getActivity().getServiceUrl(); + return CompletableFuture.completedFuture(null); + }; + + BotCallbackHandler callback4 = (t) -> { + actualServiceUrl4[0] = t.getActivity().getServiceUrl(); + return CompletableFuture.completedFuture(null); + }; + + BotCallbackHandler callback5 = (t) -> { + actualServiceUrl5[0] = t.getActivity().getServiceUrl(); + return CompletableFuture.completedFuture(null); + }; + + BotCallbackHandler callback6 = (t) -> { + actualServiceUrl6[0] = t.getActivity().getServiceUrl(); + return CompletableFuture.completedFuture(null); + }; + + // Act + CloudAdapter adapter = new CloudAdapter(cloudEnvironmentMock); + adapter.continueConversation("botAppId", continuationActivity, callback1); + adapter.continueConversation(claimsIdentity, continuationActivity, callback2); + adapter.continueConversation(claimsIdentity, continuationActivity, "audience", callback3); + adapter.continueConversation("botAppId", conversationReference, callback4); + adapter.continueConversation(claimsIdentity, conversationReference, callback5); + adapter.continueConversation(claimsIdentity, conversationReference, "audience", callback6); + + // Assert + Assert.assertEquals(expectedServiceUrl, actualServiceUrl1[0]); + Assert.assertEquals(expectedServiceUrl, actualServiceUrl2[0]); + Assert.assertEquals(expectedServiceUrl, actualServiceUrl3[0]); + Assert.assertEquals(expectedServiceUrl, actualServiceUrl4[0]); + Assert.assertEquals(expectedServiceUrl, actualServiceUrl5[0]); + Assert.assertEquals(expectedServiceUrl, actualServiceUrl6[0]); + } + + @Test + public void cloudAdapterDelay() { + DelayHelper.test(new CloudAdapter()); + } + + @Test + public void cloudAdapterCreateConversation() { + // Arrange + ClaimsIdentity claimsIdentity = new ClaimsIdentity(""); + + AuthenticateRequestResult authenticateRequestResult = new AuthenticateRequestResult(); + authenticateRequestResult.setClaimsIdentity(claimsIdentity); + authenticateRequestResult.setConnectorFactory(new TestConnectorFactory()); + authenticateRequestResult.setAudience("audience"); + authenticateRequestResult.setCallerId("callerId"); + + TestUserTokenClient userTokenClient = new TestUserTokenClient("appId"); + + ConversationResourceResponse conversationResourceResponse = new ConversationResourceResponse(); + Conversations conversationsMock = Mockito.mock(Conversations.class); + Mockito.when( + conversationsMock.createConversation( + Mockito.any(ConversationParameters.class)) + ).thenReturn(CompletableFuture.completedFuture(conversationResourceResponse)); + + ConnectorClient connectorMock = Mockito.mock(ConnectorClient.class); + Mockito.when( + connectorMock.getConversations() + ).thenReturn(conversationsMock); + + String expectedServiceUrl = "http://serviceUrl"; + String expectedAudience = "audience"; + + ConnectorFactory connectorFactoryMock = Mockito.mock(ConnectorFactory.class); + Mockito.when( + connectorFactoryMock.create( + expectedServiceUrl, + expectedAudience) + ).thenReturn(CompletableFuture.completedFuture(connectorMock)); + + BotFrameworkAuthentication cloudEnvironmentMock = Mockito.mock(BotFrameworkAuthentication.class); + Mockito.when( + cloudEnvironmentMock.authenticateRequest( + Mockito.any(Activity.class), + Mockito.anyString()) + ).thenReturn(CompletableFuture.completedFuture(authenticateRequestResult)); + Mockito.when( + cloudEnvironmentMock.createConnectorFactory( + Mockito.any(ClaimsIdentity.class)) + ).thenReturn(connectorFactoryMock); + Mockito.when( + cloudEnvironmentMock.createUserTokenClient( + Mockito.any(ClaimsIdentity.class)) + ).thenReturn(CompletableFuture.completedFuture(userTokenClient)); + + String expectedChannelId = "expected-channel-id"; + final String[] actualChannelId = {""}; + + BotCallbackHandler callback1 = (t) -> { + actualChannelId[0] = t.getActivity().getChannelId(); + return CompletableFuture.completedFuture(null); + }; + + ConversationParameters conversationParameters = new ConversationParameters(); + conversationParameters.setIsGroup(false); + conversationParameters.setBot(new ChannelAccount()); + conversationParameters.setMembers(Arrays.asList(new ChannelAccount())); + conversationParameters.setTenantId("tenantId"); + + // Act + CloudAdapter adapter = new CloudAdapter(cloudEnvironmentMock); + adapter.createConversation("botAppId", expectedChannelId, expectedServiceUrl, expectedAudience, conversationParameters, callback1).join(); + + // Assert + Assert.assertEquals(expectedChannelId, actualChannelId[0]); + } + + private static Activity createMessageActivity() { + return createMessageActivity("userId", "channelId", "conversationId", "botId", "relatesToActivityId"); + } + + private static Activity createMessageActivity(String userId, String channelId, String conversationId, String recipient, String relatesToActivityId) { + ConversationAccount conversationAccount = new ConversationAccount(); + conversationAccount.setId(conversationId); + + ChannelAccount fromChannelAccount = new ChannelAccount(); + fromChannelAccount.setId(userId); + + ChannelAccount toChannelAccount = new ChannelAccount(); + toChannelAccount.setId(recipient); + + ConversationReference conversationReference = new ConversationReference(); + conversationReference.setActivityId(relatesToActivityId); + + Activity activity = new Activity(ActivityTypes.MESSAGE); + activity.setText("hi"); + activity.setServiceUrl("http://localhost"); + activity.setChannelId(channelId); + activity.setConversation(conversationAccount); + activity.setFrom(fromChannelAccount); + activity.setLocale("locale"); + activity.setRecipient(toChannelAccount); + activity.setRelatesTo(conversationReference); + + return activity; + } + + private static Response createInternalHttpResponse() { + return new Response.Builder() + .request(new Request.Builder().url("http://localhost").build()) + .protocol(Protocol.HTTP_1_1) + .code(200) + .message("") + .body(ResponseBody.create( + MediaType.get("application/json; charset=utf-8"), + "{\"id\": \"sendActivityId\"}")) + .build(); + } + + private class MessageBot implements Bot { + public CompletableFuture onTurn(TurnContext turnContext) { + return turnContext.sendActivity(MessageFactory.text("rage.rage.against.the.dying.of.the.light")).thenApply(result -> null); + } + } + + private class UserTokenClientBot implements Bot { + private String connectionName; + + public UserTokenClientBot(String withConnectionName) { + connectionName = withConnectionName; + } + + public CompletableFuture onTurn(TurnContext turnContext) { + // in the product the following calls are made from within the sign-in prompt begin and continue methods + + UserTokenClient userTokenClient = turnContext.getTurnState().get(CloudAdapterBase.USER_TOKEN_CLIENT_KEY); + + userTokenClient.exchangeToken( + turnContext.getActivity().getFrom().getId(), + connectionName, + turnContext.getActivity().getChannelId(), + new TokenExchangeRequest()).join(); + + userTokenClient.getAadTokens( + turnContext.getActivity().getFrom().getId(), + connectionName, + Arrays.asList("x", "y"), + turnContext.getActivity().getChannelId()).join(); + + userTokenClient.getSignInResource( + connectionName, + turnContext.getActivity(), + "finalRedirect").join(); + + userTokenClient.getTokenStatus( + turnContext.getActivity().getFrom().getId(), + turnContext.getActivity().getChannelId(), + "includeFilter").join(); + + userTokenClient.getUserToken( + turnContext.getActivity().getFrom().getId(), + connectionName, + turnContext.getActivity().getChannelId(), + "magicCode").join(); + + // in the product code the sign-out call is generally run as a general intercept before any dialog logic + + userTokenClient.signOutUser( + turnContext.getActivity().getFrom().getId(), + connectionName, + turnContext.getActivity().getChannelId()).join(); + return null; + } + } + + private class TestUserTokenClient extends UserTokenClient { + private String appId; + + public TestUserTokenClient(String withAppId) { + appId = withAppId; + } + + private Map record = new HashMap<>(); + + public Map getRecord() { + return record; + } + + @Override + public CompletableFuture exchangeToken(String userId, String connectionName, String channelId, TokenExchangeRequest exchangeRequest) { + capture("exchangeToken", userId, connectionName, channelId, exchangeRequest); + return CompletableFuture.completedFuture(new TokenResponse() { }); + } + + @Override + public CompletableFuture getSignInResource(String connectionName, Activity activity, String finalRedirect) { + String state = createTokenExchangeState(appId, connectionName, activity); + capture("getSignInResource", state, finalRedirect); + return CompletableFuture.completedFuture(new SignInResource() { }); + } + + @Override + public CompletableFuture> getTokenStatus(String userId, String channelId, String includeFilter) { + capture("getTokenStatus", userId, channelId, includeFilter); + return CompletableFuture.completedFuture(Arrays.asList(new TokenStatus[0])); + } + + @Override + public CompletableFuture> getAadTokens(String userId, String connectionName, List resourceUrls, String channelId) { + capture("getAadTokens", userId, connectionName, resourceUrls, channelId); + return CompletableFuture.completedFuture(new HashMap() { }); + } + + @Override + public CompletableFuture getUserToken(String userId, String connectionName, String channelId, String magicCode) { + capture("getUserToken", userId, connectionName, channelId, magicCode); + return CompletableFuture.completedFuture(new TokenResponse()); + } + + @Override + public CompletableFuture signOutUser(String userId, String connectionName, String channelId) { + capture("signOutUser", userId, connectionName, channelId); + return CompletableFuture.completedFuture(null); + } + + private void capture(String name, Object... args) { + record.put(name, args); + } + } + + private class ConnectorFactoryBot implements Bot { + private ClaimsIdentity identity; + private ConnectorClient connectorClient; + private UserTokenClient userTokenClient; + private BotCallbackHandler botCallbackHandler; + private String oAuthScope; + private String authorization; + + public CompletableFuture onTurn(TurnContext turnContext) { + // verify the bot-framework protocol TurnState has been setup by the adapter + identity = turnContext.getTurnState().get(BotFrameworkAdapter.BOT_IDENTITY_KEY); + connectorClient = turnContext.getTurnState().get(BotFrameworkAdapter.CONNECTOR_CLIENT_KEY); + userTokenClient = turnContext.getTurnState().get(CloudAdapterBase.USER_TOKEN_CLIENT_KEY); + botCallbackHandler = turnContext.getTurnState().get(TurnContextImpl.BOT_CALLBACK_HANDLER_KEY); + oAuthScope = turnContext.getTurnState().get(BotAdapter.OAUTH_SCOPE_KEY); + + ConnectorFactory connectorFactory = turnContext.getTurnState().get(CloudAdapterBase.CONNECTOR_FACTORY_KEY); + + return connectorFactory.create("http://localhost/originalServiceUrl", oAuthScope).thenCompose(connector -> { + OkHttpClient.Builder builder = new OkHttpClient.Builder(); + connector.credentials().applyCredentialsFilter(builder); + ServiceClient serviceClient = new ServiceClient("http://localhost", builder, new Retrofit.Builder()) { }; + try { + Response response = serviceClient.httpClient().newCall(new Request.Builder().url("http://localhost").build()).execute(); + authorization = response.header("Authorization"); + } catch (IOException e) { + e.printStackTrace(); + } + return null; + } + ); + } + } + + private class TestCredentials implements ServiceClientCredentials { + private String testToken; + + public String getTestToken() { + return testToken; + } + + public TestCredentials(String withTestToken) { + testToken = withTestToken; + } + + @Override + public void applyCredentialsFilter(OkHttpClient.Builder clientBuilder) { + clientBuilder.addInterceptor(new TestCredentialsInterceptor(this)); + } + } + public class TestCredentialsInterceptor implements Interceptor { + private TestCredentials credentials; + + public TestCredentialsInterceptor(TestCredentials withCredentials) { + credentials = withCredentials; + } + + @Override + public Response intercept(Chain chain) throws IOException { + String header = chain.request().header("Authorization"); + Assert.assertNull(header); + return new Response.Builder() + .header("Authorization", credentials.getTestToken()) + .request(chain.request()) + .code(200) + .message("OK") + .protocol(Protocol.HTTP_1_1) + .body(ResponseBody.create(MediaType.parse("text/plain"), "azure rocks")) + .build(); + } + } + + private class TestConnectorFactory extends ConnectorFactory { + @Override + public CompletableFuture create(String serviceUrl, String audience) { + TestCredentials credentials = new TestCredentials(StringUtils.isNotBlank(audience) ? audience : "test-token"); + return CompletableFuture.completedFuture(new RestConnectorClient(serviceUrl, credentials)); + } + } + + private class ConfigurationTest implements Configuration { + private Properties properties; + + public void setProperties(Properties withProperties) { + this.properties = withProperties; + } + + @Override + public String getProperty(String key) { + return properties.getProperty(key); + } + + @Override + public Properties getProperties() { + return this.properties; + } + + @Override + public String[] getProperties(String key) { + String baseProperty = properties.getProperty(key); + if (baseProperty != null) { + String[] splitProperties = baseProperty.split(","); + return splitProperties; + } else { + return null; + } + } + } +} diff --git a/libraries/bot-integration-core/src/test/java/com/microsoft/bot/integration/DelayHelper.java b/libraries/bot-integration-core/src/test/java/com/microsoft/bot/integration/DelayHelper.java new file mode 100644 index 000000000..457e5c1a3 --- /dev/null +++ b/libraries/bot-integration-core/src/test/java/com/microsoft/bot/integration/DelayHelper.java @@ -0,0 +1,48 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MT License. + +package com.microsoft.bot.integration; + +import com.microsoft.bot.builder.BotAdapter; +import com.microsoft.bot.builder.TurnContextImpl; +import com.microsoft.bot.schema.Activity; +import com.microsoft.bot.schema.ActivityTypes; +import org.apache.commons.lang3.time.StopWatch; +import org.junit.Assert; + +import java.util.Arrays; +import java.util.List; +import java.util.concurrent.CompletableFuture; + +public class DelayHelper { + + public static CompletableFuture test(BotAdapter adapter) { + TurnContextImpl turnContext = new TurnContextImpl(adapter, new Activity(ActivityTypes.MESSAGE)); + + Activity activity1 = new Activity(ActivityTypes.DELAY); + activity1.setValue(275); + Activity activity2 = new Activity(ActivityTypes.DELAY); + activity2.setValue(275L); + Activity activity3 = new Activity(ActivityTypes.DELAY); + activity3.setValue(275F); + Activity activity4 = new Activity(ActivityTypes.DELAY); + activity4.setValue(275D); + List activities = Arrays.asList( + activity1, + activity2, + activity3, + activity4 + ); + + StopWatch sw = new StopWatch(); + + sw.start(); + + adapter.sendActivities(turnContext, activities).join(); + + sw.stop(); + + Assert.assertTrue("Delay only lasted " + sw.getTime(), sw.getTime() > 1); + return CompletableFuture.completedFuture(null); + } +} From 2613e99af872dda4b1fc984f9f52bab6cbfc8cac Mon Sep 17 00:00:00 2001 From: Martin Battaglino Date: Wed, 14 Jul 2021 16:22:58 -0300 Subject: [PATCH 10/27] Add getCloudAdapter in BotDependencyConfiguration returning a new CloudAdapter --- .../spring/BotDependencyConfiguration.java | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/libraries/bot-integration-spring/src/main/java/com/microsoft/bot/integration/spring/BotDependencyConfiguration.java b/libraries/bot-integration-spring/src/main/java/com/microsoft/bot/integration/spring/BotDependencyConfiguration.java index 025a7de73..2a71ac27d 100644 --- a/libraries/bot-integration-spring/src/main/java/com/microsoft/bot/integration/spring/BotDependencyConfiguration.java +++ b/libraries/bot-integration-spring/src/main/java/com/microsoft/bot/integration/spring/BotDependencyConfiguration.java @@ -14,6 +14,7 @@ import com.microsoft.bot.connector.authentication.CredentialProvider; import com.microsoft.bot.integration.BotFrameworkHttpAdapter; import com.microsoft.bot.integration.ClasspathPropertiesConfiguration; +import com.microsoft.bot.integration.CloudAdapter; import com.microsoft.bot.integration.Configuration; import com.microsoft.bot.integration.ConfigurationChannelProvider; import com.microsoft.bot.integration.ConfigurationCredentialProvider; @@ -127,6 +128,21 @@ public BotFrameworkHttpAdapter getBotFrameworkHttpAdaptor(Configuration configur return new BotFrameworkHttpAdapter(configuration); } + /** + * Returns the CloudAdapter for the application. + * + * By default, it uses the {@link CloudAdapter} class. + * + * @param configuration The Configuration object to read from. + * @return A CloudAdapter object. + * + * @see #getConfiguration() + */ + @Bean + public CloudAdapter getCloudAdapter(Configuration configuration) { + return new CloudAdapter(configuration); + } + /** * Returns a {@link Storage} object. Default scope of Singleton. * From d6f9afcfc20b542c6baf9e217de77a37a5bbc2a9 Mon Sep 17 00:00:00 2001 From: Leandro Dardick Date: Thu, 12 Aug 2021 14:47:50 -0300 Subject: [PATCH 11/27] Added a Rest Controller for CloudAdapter --- .../bot/builder/CloudAdapterBase.java | 11 +- .../bot/integration/CloudAdapter.java | 8 +- .../spring/BotCloudAdapterController.java | 110 ++++++++++++++++++ 3 files changed, 122 insertions(+), 7 deletions(-) create mode 100644 libraries/bot-integration-spring/src/main/java/com/microsoft/bot/integration/spring/BotCloudAdapterController.java diff --git a/libraries/bot-builder/src/main/java/com/microsoft/bot/builder/CloudAdapterBase.java b/libraries/bot-builder/src/main/java/com/microsoft/bot/builder/CloudAdapterBase.java index 392e78121..ea2ea9fab 100644 --- a/libraries/bot-builder/src/main/java/com/microsoft/bot/builder/CloudAdapterBase.java +++ b/libraries/bot-builder/src/main/java/com/microsoft/bot/builder/CloudAdapterBase.java @@ -456,10 +456,10 @@ protected CompletableFuture processActivity( authenticateRequestResult.getConnectorFactory()); // Run the pipeline - return runPipeline(context, callbackHandler).thenApply(task -> { + return (CompletableFuture) runPipeline(context, callbackHandler).thenApply(task -> { // If there are any results they will have been left on the TurnContext. - return CompletableFuture.completedFuture(processTurnResults(context)); - }).thenApply(null); + return processTurnResults(context); + }); }); }); } @@ -548,7 +548,10 @@ private void validateContinuationActivity(Activity continuationActivity) { private InvokeResponse processTurnResults(TurnContextImpl turnContext) { // Handle ExpectedReplies scenarios where the all the activities have been buffered // and sent back at once in an invoke response. - if (turnContext.getActivity().getDeliveryMode().equals(DeliveryModes.EXPECT_REPLIES)) { + if ( + DeliveryModes + .fromString(turnContext.getActivity().getDeliveryMode()) == DeliveryModes.EXPECT_REPLIES + ) { return new InvokeResponse( HttpURLConnection.HTTP_OK, new ExpectedReplies(turnContext.getBufferedReplyActivities())); diff --git a/libraries/bot-integration-core/src/main/java/com/microsoft/bot/integration/CloudAdapter.java b/libraries/bot-integration-core/src/main/java/com/microsoft/bot/integration/CloudAdapter.java index ce0387f26..a06e76c4b 100644 --- a/libraries/bot-integration-core/src/main/java/com/microsoft/bot/integration/CloudAdapter.java +++ b/libraries/bot-integration-core/src/main/java/com/microsoft/bot/integration/CloudAdapter.java @@ -8,6 +8,8 @@ import com.microsoft.bot.connector.authentication.BotFrameworkAuthentication; import com.microsoft.bot.connector.authentication.BotFrameworkAuthenticationFactory; import com.microsoft.bot.schema.Activity; +import com.microsoft.bot.schema.InvokeResponse; + import java.util.concurrent.CompletableFuture; /** @@ -47,9 +49,9 @@ public CloudAdapter(Configuration configuration) { * @param authHeader * @param activity * @param bot The Bot implementation to use for this request. - * @return void + * @return A CompletableFuture with the invoke response */ - public CompletableFuture processIncomingActivity(String authHeader, Activity activity, Bot bot) { - return processActivity(authHeader, activity, bot::onTurn).thenApply(result -> null); + public CompletableFuture processIncomingActivity(String authHeader, Activity activity, Bot bot) { + return processActivity(authHeader, activity, bot::onTurn); } } diff --git a/libraries/bot-integration-spring/src/main/java/com/microsoft/bot/integration/spring/BotCloudAdapterController.java b/libraries/bot-integration-spring/src/main/java/com/microsoft/bot/integration/spring/BotCloudAdapterController.java new file mode 100644 index 000000000..52a4f95b4 --- /dev/null +++ b/libraries/bot-integration-spring/src/main/java/com/microsoft/bot/integration/spring/BotCloudAdapterController.java @@ -0,0 +1,110 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.bot.integration.spring; + +import com.microsoft.bot.builder.Bot; +import com.microsoft.bot.connector.authentication.AuthenticationException; +import com.microsoft.bot.integration.CloudAdapter; +import com.microsoft.bot.schema.Activity; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestHeader; +import org.springframework.web.bind.annotation.RestController; + +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionException; + +/** + * This is the default controller that will receive incoming Channel Activity + * messages. + * + *

+ * This controller is suitable in most cases. Bots that want to use this + * controller should do so by using the @Import({BotCloudAdapterController.class}) + * annotation. See any of the samples Application class for an example. + *

+ */ +@RestController +public class BotCloudAdapterController { + /** + * The slf4j Logger to use. Note that slf4j is configured by providing Log4j + * dependencies in the POM, and corresponding Log4j configuration in the + * 'resources' folder. + */ + private Logger logger = LoggerFactory.getLogger(BotController.class); + + /** + * The CloudAdapter to use. Note is is provided by dependency + * injection via the constructor. + */ + private final CloudAdapter adapter; + + /** + * The Bot to use. Note is is provided by dependency + * injection via the constructor. + */ + private final Bot bot; + + /** + * Spring will use this constructor for creation. + * + *

+ * The Bot application should define class that implements {@link Bot} and + * annotate it with @Component. + *

+ * + * @see BotDependencyConfiguration + * + * @param withAdapter The CloudAdapter to use. + * @param withBot The Bot to use. + */ + public BotCloudAdapterController(CloudAdapter withAdapter, Bot withBot) { + adapter = withAdapter; + bot = withBot; + } + + /** + * This will receive incoming Channel Activities. + * + * @param activity The incoming Activity. + * @param authHeader The incoming Authorization header. + * @return The request response. + */ + @PostMapping("/api/messages") + public CompletableFuture> incoming( + @RequestBody Activity activity, + @RequestHeader(value = "Authorization", defaultValue = "") String authHeader + ) { + + return adapter.processIncomingActivity(authHeader, activity, bot) + + .handle((result, exception) -> { + if (exception == null) { + if (result != null) { + return new ResponseEntity<>( + result.getBody(), + HttpStatus.valueOf(result.getStatus()) + ); + } + return new ResponseEntity<>(HttpStatus.ACCEPTED); + } + + logger.error("Exception handling message", exception); + + if (exception instanceof CompletionException) { + if (exception.getCause() instanceof AuthenticationException) { + return new ResponseEntity<>(HttpStatus.UNAUTHORIZED); + } else { + return new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR); + } + } else { + return new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR); + } + }); + } +} From a8966191e96db5dab17d5517bc86af7b30d30edd Mon Sep 17 00:00:00 2001 From: Leandro Dardick Date: Thu, 12 Aug 2021 14:49:23 -0300 Subject: [PATCH 12/27] Fixed an incorrect recursive call causing a stack overflow --- .../integration/ConfigurationBotFrameworkAuthentication.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libraries/bot-integration-core/src/main/java/com/microsoft/bot/integration/ConfigurationBotFrameworkAuthentication.java b/libraries/bot-integration-core/src/main/java/com/microsoft/bot/integration/ConfigurationBotFrameworkAuthentication.java index 81212dd0b..b7c0e0e06 100644 --- a/libraries/bot-integration-core/src/main/java/com/microsoft/bot/integration/ConfigurationBotFrameworkAuthentication.java +++ b/libraries/bot-integration-core/src/main/java/com/microsoft/bot/integration/ConfigurationBotFrameworkAuthentication.java @@ -88,7 +88,7 @@ public CompletableFuture authenticateChannelRequest(String authH */ @Override public CompletableFuture authenticateRequest(Activity activity, String authHeader) { - return authenticateRequest(activity, authHeader); + return inner.authenticateRequest(activity, authHeader); } /** From 667aa8bd1bf5cf557cc69661f14850e09bf2a4e0 Mon Sep 17 00:00:00 2001 From: Leandro Dardick Date: Thu, 12 Aug 2021 14:49:34 -0300 Subject: [PATCH 13/27] Fixed string comparison --- .../authentication/PasswordServiceClientCredentialFactory.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/authentication/PasswordServiceClientCredentialFactory.java b/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/authentication/PasswordServiceClientCredentialFactory.java index 345dd8d5a..215d8fd28 100644 --- a/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/authentication/PasswordServiceClientCredentialFactory.java +++ b/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/authentication/PasswordServiceClientCredentialFactory.java @@ -79,7 +79,7 @@ public PasswordServiceClientCredentialFactory(String withAppId, String withPassw */ @Override public CompletableFuture isValidAppId(String appId) { - return CompletableFuture.completedFuture(appId == this.appId); + return CompletableFuture.completedFuture(appId.equals(this.appId)); } /** From ffc0df8ecc3feebe7a4383cc03e4ac58263999a8 Mon Sep 17 00:00:00 2001 From: Leandro Dardick Date: Thu, 12 Aug 2021 14:50:01 -0300 Subject: [PATCH 14/27] Fixed return types for CompleatableFuture --- ...rameterizedBotFrameworkAuthentication.java | 63 ++++++++++--------- 1 file changed, 33 insertions(+), 30 deletions(-) diff --git a/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/authentication/ParameterizedBotFrameworkAuthentication.java b/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/authentication/ParameterizedBotFrameworkAuthentication.java index 7b039ab2a..9871cb4c4 100644 --- a/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/authentication/ParameterizedBotFrameworkAuthentication.java +++ b/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/authentication/ParameterizedBotFrameworkAuthentication.java @@ -3,12 +3,14 @@ package com.microsoft.bot.connector.authentication; +import com.microsoft.bot.connector.Async; import com.microsoft.bot.connector.Channels; import com.microsoft.bot.connector.skills.BotFrameworkClient; import com.microsoft.bot.schema.Activity; import com.microsoft.bot.schema.RoleTypes; import okhttp3.OkHttpClient; import org.apache.commons.lang3.StringUtils; + import java.time.Duration; import java.util.Arrays; import java.util.Map; @@ -123,31 +125,30 @@ public CompletableFuture authenticateRequest(Activity @Override public CompletableFuture authenticateStreamingRequest(String authHeader, String channelIdHeader) { - if (StringUtils.isNotBlank(channelIdHeader)) { - this.credentialsFactory.isAuthenticationDisabled().thenCompose(isAuthDisabled -> { - if (isAuthDisabled) { - return jwtTokenValidationValidateAuthHeader(authHeader, channelIdHeader, null) - .thenCompose(claimsIdentity -> { - String outboundAudience = SkillValidation.isSkillClaim(claimsIdentity.claims()) - ? JwtTokenValidation.getAppIdFromClaims(claimsIdentity.claims()) - : this.toChannelFromBotOAuthScope; - - return generateCallerId(this.credentialsFactory, claimsIdentity, this.callerId) - .thenCompose(resultCallerId -> { - AuthenticateRequestResult authenticateRequestResult = new AuthenticateRequestResult(); - authenticateRequestResult.setClaimsIdentity(claimsIdentity); - authenticateRequestResult.setAudience(outboundAudience); - authenticateRequestResult.setCallerId(resultCallerId); - - return CompletableFuture.completedFuture(authenticateRequestResult); - }); - } - ); - } - return null; - }); - } - throw new AuthenticationException("channelId header required"); + + return this.credentialsFactory.isAuthenticationDisabled().thenCompose(isAuthDisabled -> { + + if (StringUtils.isBlank(channelIdHeader) && !isAuthDisabled) { + throw new AuthenticationException("channelId header required when authentication is enabled"); + } + + return jwtTokenValidationValidateAuthHeader(authHeader, channelIdHeader, null) + .thenCompose(claimsIdentity -> { + String outboundAudience = SkillValidation.isSkillClaim(claimsIdentity.claims()) + ? JwtTokenValidation.getAppIdFromClaims(claimsIdentity.claims()) + : this.toChannelFromBotOAuthScope; + + return generateCallerId(this.credentialsFactory, claimsIdentity, this.callerId) + .thenCompose(callerId -> { + AuthenticateRequestResult authenticateRequestResult = new AuthenticateRequestResult(); + authenticateRequestResult.setClaimsIdentity(claimsIdentity); + authenticateRequestResult.setAudience(outboundAudience); + authenticateRequestResult.setCallerId(callerId); + + return CompletableFuture.completedFuture(authenticateRequestResult); + }); + }); + }); } /** @@ -229,7 +230,7 @@ private CompletableFuture jwtTokenValidationValidateClaims(Map jwtTokenValidationAuthenticateToken(String authHeader, @@ -316,7 +317,7 @@ private CompletableFuture skillValidationValidateIdentity(ClaimsIdentity i // Invalid appId throw new AuthenticationException("SkillValidation.validateIdentity(): Invalid appId."); } - return null; + return CompletableFuture.completedFuture(null); }); } @@ -422,8 +423,10 @@ private CompletableFuture channelValidationauthenticateChannelTo AuthenticationConstants.ALLOWED_SIGNING_ALGORITHMS); return tokenExtractor.getIdentity(authHeader, channelId, this.authConfiguration.requiredEndorsements()) - .thenCompose(identity -> governmentChannelValidationValidateIdentity(identity, serviceUrl) - .thenApply(result -> identity)); + .thenCompose(identity -> { + governmentChannelValidationValidateIdentity(identity, serviceUrl).join(); + return CompletableFuture.completedFuture(identity); + }); } private TokenValidationParameters channelValidationGetTokenValidationParameters() { @@ -490,7 +493,7 @@ private CompletableFuture governmentChannelValidationValidateIdentity(Clai throw new AuthenticationException("Unauthorized. ServiceUrl claim do not match."); } } - return null; + return CompletableFuture.completedFuture(null); }); } From 0132fcf1bd3d29be536fd79716170a41d5656f30 Mon Sep 17 00:00:00 2001 From: Leandro Dardick Date: Thu, 12 Aug 2021 14:50:18 -0300 Subject: [PATCH 15/27] Added CloudAdapterWithInspection --- .../CloudAdapterWithInspection.java | 44 +++++++++++++++++++ 1 file changed, 44 insertions(+) create mode 100644 libraries/bot-integration-core/src/main/java/com/microsoft/bot/integration/CloudAdapterWithInspection.java diff --git a/libraries/bot-integration-core/src/main/java/com/microsoft/bot/integration/CloudAdapterWithInspection.java b/libraries/bot-integration-core/src/main/java/com/microsoft/bot/integration/CloudAdapterWithInspection.java new file mode 100644 index 000000000..a9baad2a8 --- /dev/null +++ b/libraries/bot-integration-core/src/main/java/com/microsoft/bot/integration/CloudAdapterWithInspection.java @@ -0,0 +1,44 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.bot.integration; + +import com.microsoft.bot.builder.ConversationState; +import com.microsoft.bot.builder.UserState; +import com.microsoft.bot.builder.inspection.InspectionMiddleware; +import com.microsoft.bot.builder.inspection.InspectionState; +import com.microsoft.bot.connector.authentication.MicrosoftAppCredentials; + +/** + * A CloudAdapter that use InspectionMiddleware to forward message + * and state information. + * + *

+ * See the Inspection sample for details on how this is used. + *

+ */ +public class CloudAdapterWithInspection extends CloudAdapterWithErrorHandler { + /** + * Uses InspectionMiddleware to track ConversationState and UserState. + * + * @param configuration The Configuration + * @param inspectionState The InspectionState + * @param userState The UserState + * @param conversationState The ConversationState + */ + public CloudAdapterWithInspection( + Configuration configuration, + InspectionState inspectionState, + UserState userState, + ConversationState conversationState + ) { + super(configuration); + + MicrosoftAppCredentials credentials = new MicrosoftAppCredentials( + configuration.getProperty("MicrosoftAppId"), + configuration.getProperty("MicrosoftAppPassword") + ); + + use(new InspectionMiddleware(inspectionState, userState, conversationState, credentials)); + } +} From 196cc60448c8cb53ab7acca395b106bea2b5e86f Mon Sep 17 00:00:00 2001 From: Leandro Dardick Date: Wed, 11 Aug 2021 11:37:28 -0300 Subject: [PATCH 16/27] Fixed incorrect type reference in CloudAdapterWithErrorHandler --- .../bot/integration/CloudAdapterWithErrorHandler.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/libraries/bot-integration-core/src/main/java/com/microsoft/bot/integration/CloudAdapterWithErrorHandler.java b/libraries/bot-integration-core/src/main/java/com/microsoft/bot/integration/CloudAdapterWithErrorHandler.java index 05cfa62ef..5843666d8 100644 --- a/libraries/bot-integration-core/src/main/java/com/microsoft/bot/integration/CloudAdapterWithErrorHandler.java +++ b/libraries/bot-integration-core/src/main/java/com/microsoft/bot/integration/CloudAdapterWithErrorHandler.java @@ -38,7 +38,7 @@ public CloudAdapterWithErrorHandler(Configuration withConfiguration) { super(withConfiguration); setOnTurnError((turnContext, exception) -> { - LoggerFactory.getLogger(AdapterWithErrorHandler.class).error("onTurnError", exception); + LoggerFactory.getLogger(CloudAdapterWithErrorHandler.class).error("onTurnError", exception); return turnContext.sendActivities( MessageFactory.text(ERROR_MSG_ONE), MessageFactory.text(ERROR_MSG_TWO) @@ -65,7 +65,7 @@ public CloudAdapterWithErrorHandler( super(withConfiguration); setOnTurnError((turnContext, exception) -> { - LoggerFactory.getLogger(AdapterWithErrorHandler.class).error("onTurnError", exception); + LoggerFactory.getLogger(CloudAdapterWithErrorHandler.class).error("onTurnError", exception); return turnContext.sendActivities( MessageFactory.text(ERROR_MSG_ONE), MessageFactory.text(ERROR_MSG_TWO) From d1b9035ab8961060a5dfeb077d1b6ca726aef58b Mon Sep 17 00:00:00 2001 From: matiasroldan6 Date: Tue, 10 Aug 2021 16:47:01 -0300 Subject: [PATCH 17/27] Add UserTokenAccess client and rewire OAuthPrompt (Mirror C# PR MS5213) --- .../bot/dialogs/prompts/OAuthPrompt.java | 144 ++++-------------- .../bot/dialogs/prompts/UserTokenAccess.java | 106 +++++++++++++ 2 files changed, 138 insertions(+), 112 deletions(-) create mode 100644 libraries/bot-dialogs/src/main/java/com/microsoft/bot/dialogs/prompts/UserTokenAccess.java diff --git a/libraries/bot-dialogs/src/main/java/com/microsoft/bot/dialogs/prompts/OAuthPrompt.java b/libraries/bot-dialogs/src/main/java/com/microsoft/bot/dialogs/prompts/OAuthPrompt.java index c2fb156bd..084e656f7 100644 --- a/libraries/bot-dialogs/src/main/java/com/microsoft/bot/dialogs/prompts/OAuthPrompt.java +++ b/libraries/bot-dialogs/src/main/java/com/microsoft/bot/dialogs/prompts/OAuthPrompt.java @@ -3,25 +3,11 @@ package com.microsoft.bot.dialogs.prompts; -import java.net.HttpURLConnection; -import java.time.Duration; -import java.time.OffsetDateTime; -import java.time.ZoneId; -import java.time.temporal.ChronoUnit; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.Map; -import java.util.concurrent.CompletableFuture; -import java.util.regex.Matcher; -import java.util.regex.Pattern; - import com.microsoft.bot.builder.BotAdapter; import com.microsoft.bot.builder.BotAssert; -import com.microsoft.bot.builder.ConnectorClientBuilder; import com.microsoft.bot.builder.InvokeResponse; import com.microsoft.bot.builder.TurnContext; import com.microsoft.bot.builder.TurnStateConstants; -import com.microsoft.bot.builder.UserTokenProvider; import com.microsoft.bot.connector.Async; import com.microsoft.bot.connector.Channels; import com.microsoft.bot.connector.ConnectorClient; @@ -46,9 +32,20 @@ import com.microsoft.bot.schema.TokenExchangeInvokeResponse; import com.microsoft.bot.schema.TokenExchangeRequest; import com.microsoft.bot.schema.TokenResponse; - import org.apache.commons.lang3.StringUtils; +import java.net.HttpURLConnection; +import java.time.Duration; +import java.time.OffsetDateTime; +import java.time.ZoneId; +import java.time.temporal.ChronoUnit; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.CompletableFuture; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + /** * Creates a new prompt that asks the user to sign in using the Bot Frameworks * Single Sign On (SSO)service. @@ -140,15 +137,6 @@ public static CompletableFuture sendOAuthCard(OAuthPromptSettings settings Activity prompt) { BotAssert.contextNotNull(turnContext); - BotAdapter adapter = turnContext.getAdapter(); - - if (!(adapter instanceof UserTokenProvider)) { - return Async.completeExceptionally( - new UnsupportedOperationException("OAuthPrompt.Prompt(): not supported by the current adapter")); - } - - UserTokenProvider tokenAdapter = (UserTokenProvider) adapter; - // Ensure prompt initialized if (prompt == null) { prompt = Activity.createMessageActivity(); @@ -161,9 +149,8 @@ public static CompletableFuture sendOAuthCard(OAuthPromptSettings settings // Append appropriate card if missing if (!channelSupportsOAuthCard(turnContext.getActivity().getChannelId())) { if (!prompt.getAttachments().stream().anyMatch(s -> s.getContent() instanceof SigninCard)) { - SignInResource signInResource = tokenAdapter - .getSignInResource(turnContext, settings.getOAuthAppCredentials(), settings.getConnectionName(), - turnContext.getActivity().getFrom().getId(), null) + SignInResource signInResource = UserTokenAccess + .getSignInResource(turnContext, settings) .join(); CardAction cardAction = new CardAction(); @@ -187,9 +174,8 @@ public static CompletableFuture sendOAuthCard(OAuthPromptSettings settings } } else if (!prompt.getAttachments().stream().anyMatch(s -> s.getContent() instanceof OAuthCard)) { ActionTypes cardActionType = ActionTypes.SIGNIN; - SignInResource signInResource = tokenAdapter - .getSignInResource(turnContext, settings.getOAuthAppCredentials(), settings.getConnectionName(), - turnContext.getActivity().getFrom().getId(), null) + SignInResource signInResource = UserTokenAccess + .getSignInResource(turnContext, settings) .join(); String value = signInResource.getSignInLink(); @@ -277,21 +263,14 @@ public static CompletableFuture> recognize // set the ServiceUrl to the skill host's Url dc.getContext().getActivity().setServiceUrl(callerInfo.getCallerServiceUrl()); - Object adapter = turnContext.getAdapter(); - // recreate a ConnectorClient and set it in TurnState so replies use the correct one - if (!(adapter instanceof ConnectorClientBuilder)) { - return Async.completeExceptionally( - new UnsupportedOperationException( - "OAuthPrompt: ConnectorClientProvider interface not implemented by the current adapter" - )); - } - - ConnectorClientBuilder connectorClientProvider = (ConnectorClientBuilder) adapter; + String serviceUrl = dc.getContext().getActivity().getServiceUrl(); + String audience = callerInfo.getScope(); ClaimsIdentity claimsIdentity = turnContext.getTurnState().get(BotAdapter.BOT_IDENTITY_KEY); - ConnectorClient connectorClient = connectorClientProvider.createConnectorClient( - dc.getContext().getActivity().getServiceUrl(), + ConnectorClient connectorClient = UserTokenAccess.createConnectorClient( + turnContext, + serviceUrl, claimsIdentity, - callerInfo.getScope()).join(); + audience).join(); if (turnContext.getTurnState().get(ConnectorClient.class) != null) { turnContext.getTurnState().replace(connectorClient); @@ -306,16 +285,6 @@ public static CompletableFuture> recognize magicCode = (String) values.get("state"); } - Object adapterObject = turnContext.getAdapter(); - if (!(adapterObject instanceof UserTokenProvider)) { - return Async.completeExceptionally( - new UnsupportedOperationException( - "OAuthPrompt.Recognize(): not supported by the current adapter" - )); - } - - UserTokenProvider adapter = (UserTokenProvider) adapterObject; - // Getting the token follows a different flow in Teams. At the signin completion, Teams // will send the bot an "invoke" activity that contains a "magic" code. This code MUST // then be used to try fetching the token from Botframework service within some time @@ -324,10 +293,9 @@ public static CompletableFuture> recognize // If it fails with a non-retriable error, we return 404. Teams will not (still work in // progress) retry in that case. try { - TokenResponse token = adapter.getUserToken( + TokenResponse token = UserTokenAccess.getUserToken( turnContext, - settings.getOAuthAppCredentials(), - settings.getConnectionName(), + settings, magicCode).join(); if (token != null) { @@ -363,28 +331,14 @@ public static CompletableFuture> recognize + "when sending the InvokeActivityInvalid ConnectionName in the " + "TokenExchangeInvokeRequest"); sendInvokeResponse(turnContext, HttpURLConnection.HTTP_BAD_REQUEST, response).join(); - } else if (!(turnContext.getAdapter() instanceof UserTokenProvider)) { - TokenExchangeInvokeResponse response = new TokenExchangeInvokeResponse(); - response.setId(tokenExchangeRequest.getId()); - response.setConnectionName(settings.getConnectionName()); - response.setFailureDetail("The bot's BotAdapter does not support token exchange " - + "operations. Ensure the bot's Adapter supports the UserTokenProvider interface."); - - sendInvokeResponse(turnContext, HttpURLConnection.HTTP_BAD_REQUEST, response).join(); - return Async.completeExceptionally( - new UnsupportedOperationException( - "OAuthPrompt.Recognize(): not supported by the current adapter" - )); } else { TokenResponse tokenExchangeResponse = null; try { - UserTokenProvider adapter = (UserTokenProvider) turnContext.getAdapter(); TokenExchangeRequest tokenExchangeReq = new TokenExchangeRequest(); tokenExchangeReq.setToken(tokenExchangeRequest.getToken()); - tokenExchangeResponse = adapter.exchangeToken( + tokenExchangeResponse = UserTokenAccess.exchangeToken( turnContext, - settings.getConnectionName(), - turnContext.getActivity().getFrom().getId(), + settings, tokenExchangeReq).join(); } catch (Exception ex) { // Ignore Exceptions @@ -421,16 +375,8 @@ public static CompletableFuture> recognize Matcher m = r.matcher(turnContext.getActivity().getText()); if (m.find()) { - if (!(turnContext.getAdapter() instanceof UserTokenProvider)) { - return Async.completeExceptionally( - new UnsupportedOperationException( - "OAuthPrompt.Recognize(): not supported by the current adapter" - )); - } - UserTokenProvider adapter = (UserTokenProvider) turnContext.getAdapter(); - TokenResponse token = adapter.getUserToken(turnContext, - settings.getOAuthAppCredentials(), - settings.getConnectionName(), + TokenResponse token = UserTokenAccess.getUserToken(turnContext, + settings, m.group(0)).join(); if (token != null) { result.setSucceeded(true); @@ -507,17 +453,8 @@ public CompletableFuture beginDialog(DialogContext dc, Object setCallerInfoInDialogState(state, dc.getContext()); // Attempt to get the users token - if (!(dc.getContext().getAdapter() instanceof UserTokenProvider)) { - return Async.completeExceptionally( - new UnsupportedOperationException( - "OAuthPrompt.Recognize(): not supported by the current adapter" - )); - } - - UserTokenProvider adapter = (UserTokenProvider) dc.getContext().getAdapter(); - TokenResponse output = adapter.getUserToken(dc.getContext(), - settings.getOAuthAppCredentials(), - settings.getConnectionName(), + TokenResponse output = UserTokenAccess.getUserToken(dc.getContext(), + settings, null).join(); if (output != null) { // Return token @@ -622,15 +559,7 @@ public CompletableFuture continueDialog(DialogContext dc) { * successfully signs in, the result contains the user's token. */ public CompletableFuture getUserToken(TurnContext turnContext) { - if (!(turnContext.getAdapter() instanceof UserTokenProvider)) { - return Async.completeExceptionally( - new UnsupportedOperationException( - "OAuthPrompt.GetUserToken(): not supported by the current adapter" - )); - } - return ((UserTokenProvider) turnContext.getAdapter()).getUserToken(turnContext, - settings.getOAuthAppCredentials(), - settings.getConnectionName(), null); + return UserTokenAccess.getUserToken(turnContext, settings, null); } /** @@ -641,12 +570,6 @@ public CompletableFuture getUserToken(TurnContext turnContext) { * @return A task that represents the work queued to execute. */ public CompletableFuture signOutUser(TurnContext turnContext) { - if (!(turnContext.getAdapter() instanceof UserTokenProvider)) { - return Async.completeExceptionally( - new UnsupportedOperationException( - "OAuthPrompt.SignOutUser(): not supported by the current adapter" - )); - } String id = ""; if (turnContext.getActivity() != null && turnContext.getActivity() != null @@ -655,10 +578,7 @@ public CompletableFuture signOutUser(TurnContext turnContext) { } // Sign out user - return ((UserTokenProvider) turnContext.getAdapter()).signOutUser(turnContext, - settings.getOAuthAppCredentials(), - settings.getConnectionName(), - id); + return UserTokenAccess.signOutUser(turnContext, settings); } private static CallerInfo createCallerInfo(TurnContext turnContext) { diff --git a/libraries/bot-dialogs/src/main/java/com/microsoft/bot/dialogs/prompts/UserTokenAccess.java b/libraries/bot-dialogs/src/main/java/com/microsoft/bot/dialogs/prompts/UserTokenAccess.java new file mode 100644 index 000000000..20dc30854 --- /dev/null +++ b/libraries/bot-dialogs/src/main/java/com/microsoft/bot/dialogs/prompts/UserTokenAccess.java @@ -0,0 +1,106 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MT License. + +package com.microsoft.bot.dialogs.prompts; + +import com.microsoft.bot.builder.ConnectorClientBuilder; +import com.microsoft.bot.builder.TurnContext; +import com.microsoft.bot.connector.ConnectorClient; +import com.microsoft.bot.connector.authentication.ClaimsIdentity; +import com.microsoft.bot.connector.authentication.ConnectorFactory; +import com.microsoft.bot.connector.authentication.UserTokenClient; +import com.microsoft.bot.schema.SignInResource; +import com.microsoft.bot.schema.TokenExchangeRequest; +import com.microsoft.bot.schema.TokenResponse; + +import java.util.concurrent.CompletableFuture; + +public final class UserTokenAccess { + + private UserTokenAccess() { + } + + public static CompletableFuture getUserToken( + TurnContext turnContext, + OAuthPromptSettings settings, + String magicCode) { + + UserTokenClient userTokenClient = turnContext.getTurnState().get(UserTokenClient.class); + + if (userTokenClient != null) { + return userTokenClient.getUserToken( + turnContext.getActivity().getFrom().getId(), + settings.getConnectionName(), + turnContext.getActivity().getChannelId(), + magicCode); + } else { + throw new UnsupportedOperationException("OAuth prompt is not supported by the current adapter"); + } + } + + public static CompletableFuture getSignInResource( + TurnContext turnContext, + OAuthPromptSettings settings) { + + UserTokenClient userTokenClient = turnContext.getTurnState().get(UserTokenClient.class); + + if (userTokenClient != null) { + return userTokenClient.getSignInResource( + settings.getConnectionName(), + turnContext.getActivity(), + null); + } else { + throw new UnsupportedOperationException("OAuth prompt is not supported by the current adapter"); + } + } + + public static CompletableFuture signOutUser( + TurnContext turnContext, + OAuthPromptSettings settings) { + + UserTokenClient userTokenClient = turnContext.getTurnState().get(UserTokenClient.class); + + if (userTokenClient != null) { + return userTokenClient.signOutUser( + turnContext.getActivity().getFrom().getId(), + settings.getConnectionName(), + turnContext.getActivity().getChannelId()); + } else { + throw new UnsupportedOperationException("OAuth prompt is not supported by the current adapter"); + } + } + + public static CompletableFuture exchangeToken( + TurnContext turnContext, + OAuthPromptSettings settings, + TokenExchangeRequest tokenExchangeRequest) { + + UserTokenClient userTokenClient = turnContext.getTurnState().get(UserTokenClient.class); + + if (userTokenClient != null) { + String userId = turnContext.getActivity().getFrom().getId(); + String channelId = turnContext.getActivity().getChannelId(); + return userTokenClient.exchangeToken(userId, settings.getConnectionName(), channelId, tokenExchangeRequest); + } else { + throw new UnsupportedOperationException("OAuth prompt is not supported by the current adapter"); + } + } + + public static CompletableFuture createConnectorClient( + TurnContext turnContext, + String serviceUrl, + ClaimsIdentity claimsIdentity, + String audience) { + + ConnectorFactory connectorFactory = turnContext.getTurnState().get(ConnectorFactory.class); + + if (connectorFactory != null) { + return connectorFactory.create(serviceUrl, audience); + } else if (turnContext.getAdapter() instanceof ConnectorClientBuilder) { + ConnectorClientBuilder connectorClientProvider = (ConnectorClientBuilder) turnContext.getAdapter(); + return connectorClientProvider.createConnectorClient(serviceUrl, claimsIdentity, audience); + } else { + throw new UnsupportedOperationException("OAuth prompt is not supported by the current adapter"); + } + } +} From 541de5168396ba2e4600eac76a8fd6c8ac35f06e Mon Sep 17 00:00:00 2001 From: matiasroldan6 Date: Wed, 11 Aug 2021 16:53:35 -0300 Subject: [PATCH 18/27] Add evaluation for instanceof UserTokenProvider in methods --- .../bot/dialogs/prompts/UserTokenAccess.java | 26 +++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/libraries/bot-dialogs/src/main/java/com/microsoft/bot/dialogs/prompts/UserTokenAccess.java b/libraries/bot-dialogs/src/main/java/com/microsoft/bot/dialogs/prompts/UserTokenAccess.java index 20dc30854..c90149779 100644 --- a/libraries/bot-dialogs/src/main/java/com/microsoft/bot/dialogs/prompts/UserTokenAccess.java +++ b/libraries/bot-dialogs/src/main/java/com/microsoft/bot/dialogs/prompts/UserTokenAccess.java @@ -5,6 +5,7 @@ import com.microsoft.bot.builder.ConnectorClientBuilder; import com.microsoft.bot.builder.TurnContext; +import com.microsoft.bot.builder.UserTokenProvider; import com.microsoft.bot.connector.ConnectorClient; import com.microsoft.bot.connector.authentication.ClaimsIdentity; import com.microsoft.bot.connector.authentication.ConnectorFactory; @@ -33,6 +34,12 @@ public static CompletableFuture getUserToken( settings.getConnectionName(), turnContext.getActivity().getChannelId(), magicCode); + } else if (turnContext.getAdapter() instanceof UserTokenProvider) { + return ((UserTokenProvider) turnContext.getAdapter()).getUserToken( + turnContext, + settings.getOAuthAppCredentials(), + settings.getConnectionName(), + magicCode); } else { throw new UnsupportedOperationException("OAuth prompt is not supported by the current adapter"); } @@ -49,6 +56,13 @@ public static CompletableFuture getSignInResource( settings.getConnectionName(), turnContext.getActivity(), null); + } else if (turnContext.getAdapter() instanceof UserTokenProvider) { + return ((UserTokenProvider) turnContext.getAdapter()).getSignInResource( + turnContext, + settings.getOAuthAppCredentials(), + settings.getConnectionName(), + turnContext.getActivity().getFrom().getId(), + null); } else { throw new UnsupportedOperationException("OAuth prompt is not supported by the current adapter"); } @@ -65,6 +79,12 @@ public static CompletableFuture signOutUser( turnContext.getActivity().getFrom().getId(), settings.getConnectionName(), turnContext.getActivity().getChannelId()); + } else if (turnContext.getAdapter() instanceof UserTokenProvider) { + return ((UserTokenProvider) turnContext.getAdapter()).signOutUser( + turnContext, + settings.getOAuthAppCredentials(), + settings.getConnectionName(), + turnContext.getActivity().getFrom().getId()); } else { throw new UnsupportedOperationException("OAuth prompt is not supported by the current adapter"); } @@ -81,6 +101,12 @@ public static CompletableFuture exchangeToken( String userId = turnContext.getActivity().getFrom().getId(); String channelId = turnContext.getActivity().getChannelId(); return userTokenClient.exchangeToken(userId, settings.getConnectionName(), channelId, tokenExchangeRequest); + } else if (turnContext.getAdapter() instanceof UserTokenProvider) { + return ((UserTokenProvider) turnContext.getAdapter()).exchangeToken( + turnContext, + settings.getConnectionName(), + turnContext.getActivity().getFrom().getId(), + tokenExchangeRequest); } else { throw new UnsupportedOperationException("OAuth prompt is not supported by the current adapter"); } From ff85f8ae87d1e7db23793ce6aeda1f9f93b96ceb Mon Sep 17 00:00:00 2001 From: matiasroldan6 Date: Thu, 12 Aug 2021 14:00:27 -0300 Subject: [PATCH 19/27] Add properties check in signOutUser --- .../com/microsoft/bot/dialogs/prompts/OAuthPrompt.java | 10 +--------- .../microsoft/bot/dialogs/prompts/UserTokenAccess.java | 9 ++++++++- 2 files changed, 9 insertions(+), 10 deletions(-) diff --git a/libraries/bot-dialogs/src/main/java/com/microsoft/bot/dialogs/prompts/OAuthPrompt.java b/libraries/bot-dialogs/src/main/java/com/microsoft/bot/dialogs/prompts/OAuthPrompt.java index 084e656f7..8d71e58c1 100644 --- a/libraries/bot-dialogs/src/main/java/com/microsoft/bot/dialogs/prompts/OAuthPrompt.java +++ b/libraries/bot-dialogs/src/main/java/com/microsoft/bot/dialogs/prompts/OAuthPrompt.java @@ -336,7 +336,7 @@ public static CompletableFuture> recognize try { TokenExchangeRequest tokenExchangeReq = new TokenExchangeRequest(); tokenExchangeReq.setToken(tokenExchangeRequest.getToken()); - tokenExchangeResponse = UserTokenAccess.exchangeToken( + tokenExchangeResponse = UserTokenAccess.exchangeToken( turnContext, settings, tokenExchangeReq).join(); @@ -570,14 +570,6 @@ public CompletableFuture getUserToken(TurnContext turnContext) { * @return A task that represents the work queued to execute. */ public CompletableFuture signOutUser(TurnContext turnContext) { - String id = ""; - if (turnContext.getActivity() != null - && turnContext.getActivity() != null - && turnContext.getActivity().getFrom() != null) { - id = turnContext.getActivity().getFrom().getId(); - } - - // Sign out user return UserTokenAccess.signOutUser(turnContext, settings); } diff --git a/libraries/bot-dialogs/src/main/java/com/microsoft/bot/dialogs/prompts/UserTokenAccess.java b/libraries/bot-dialogs/src/main/java/com/microsoft/bot/dialogs/prompts/UserTokenAccess.java index c90149779..1fac28d2c 100644 --- a/libraries/bot-dialogs/src/main/java/com/microsoft/bot/dialogs/prompts/UserTokenAccess.java +++ b/libraries/bot-dialogs/src/main/java/com/microsoft/bot/dialogs/prompts/UserTokenAccess.java @@ -80,11 +80,18 @@ public static CompletableFuture signOutUser( settings.getConnectionName(), turnContext.getActivity().getChannelId()); } else if (turnContext.getAdapter() instanceof UserTokenProvider) { + String id = ""; + if (turnContext.getActivity() != null + && turnContext.getActivity() != null + && turnContext.getActivity().getFrom() != null) { + id = turnContext.getActivity().getFrom().getId(); + } + return ((UserTokenProvider) turnContext.getAdapter()).signOutUser( turnContext, settings.getOAuthAppCredentials(), settings.getConnectionName(), - turnContext.getActivity().getFrom().getId()); + id); } else { throw new UnsupportedOperationException("OAuth prompt is not supported by the current adapter"); } From 9d360b24e1cf0a5849e46ecc91a0735f05adfebd Mon Sep 17 00:00:00 2001 From: matiasroldan6 Date: Fri, 13 Aug 2021 16:50:15 -0300 Subject: [PATCH 20/27] Add UserTokenAccessTests --- .../dialogs/prompts/UserTokenAccessTests.java | 208 ++++++++++++++++++ 1 file changed, 208 insertions(+) create mode 100644 libraries/bot-dialogs/src/test/java/com/microsoft/bot/dialogs/prompts/UserTokenAccessTests.java diff --git a/libraries/bot-dialogs/src/test/java/com/microsoft/bot/dialogs/prompts/UserTokenAccessTests.java b/libraries/bot-dialogs/src/test/java/com/microsoft/bot/dialogs/prompts/UserTokenAccessTests.java new file mode 100644 index 000000000..35606b27e --- /dev/null +++ b/libraries/bot-dialogs/src/test/java/com/microsoft/bot/dialogs/prompts/UserTokenAccessTests.java @@ -0,0 +1,208 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.bot.dialogs.prompts; + +import com.microsoft.bot.builder.BotFrameworkAdapter; +import com.microsoft.bot.builder.TurnContext; +import com.microsoft.bot.builder.TurnContextStateCollection; +import com.microsoft.bot.connector.ConnectorClient; +import com.microsoft.bot.connector.authentication.AppCredentials; +import com.microsoft.bot.connector.authentication.ClaimsIdentity; +import com.microsoft.bot.connector.authentication.ConnectorFactory; +import com.microsoft.bot.connector.authentication.UserTokenClient; +import com.microsoft.bot.schema.Activity; +import com.microsoft.bot.schema.ChannelAccount; +import com.microsoft.bot.schema.TokenResponse; +import org.junit.Assert; +import org.junit.Test; +import org.mockito.Mockito; + +import java.util.concurrent.CompletableFuture; + +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +public class UserTokenAccessTests { + + @Test + public void getUserToken_ShouldThrowException() { + TurnContext turnContext = mock(TurnContext.class); + + TurnContextStateCollection turnContextStateCollection = mock(TurnContextStateCollection.class); + when(turnContextStateCollection.get(Mockito.any(Class.class))).thenReturn(null); + + when(turnContext.getTurnState()).thenReturn(turnContextStateCollection); + when(turnContext.getAdapter()).thenReturn(null); + + Assert.assertThrows(UnsupportedOperationException.class, () -> UserTokenAccess.getUserToken(turnContext, new OAuthPromptSettings(), "")); + } + + @Test + public void getUserToken_ShouldReturnTokenResponse_WithUserTokenClientNotNull() { + TurnContext turnContext = mock(TurnContext.class); + + UserTokenClient userTokenClient = mock(UserTokenClient.class); + when(userTokenClient.getUserToken(Mockito.anyString(), Mockito.anyString(), Mockito.anyString(), Mockito.anyString())).thenReturn(CompletableFuture.completedFuture(new TokenResponse())); + + TurnContextStateCollection turnContextStateCollection = mock(TurnContextStateCollection.class); + when(turnContextStateCollection.get(Mockito.any(Class.class))).thenReturn(userTokenClient); + + when(turnContext.getTurnState()).thenReturn(turnContextStateCollection); + + OAuthPromptSettings oAuthPromptSettings = mock(OAuthPromptSettings.class); + when(oAuthPromptSettings.getConnectionName()).thenReturn(""); + + Activity activity = mock(Activity.class); + when(activity.getChannelId()).thenReturn(""); + + ChannelAccount channelAccount = mock(ChannelAccount.class); + when(channelAccount.getId()).thenReturn(""); + + when(activity.getFrom()).thenReturn(channelAccount); + + when(turnContext.getActivity()).thenReturn(activity); + + Assert.assertNotNull(UserTokenAccess.getUserToken(turnContext, oAuthPromptSettings, "").join()); + } + + @Test + public void getUserToken_ShouldReturnTokenResponse_WithInstanceOfUserTokenProvider() { + TurnContext turnContext = mock(TurnContext.class); + + TurnContextStateCollection turnContextStateCollection = mock(TurnContextStateCollection.class); + when(turnContextStateCollection.get(Mockito.any(Class.class))).thenReturn(null); + + when(turnContext.getTurnState()).thenReturn(turnContextStateCollection); + + BotFrameworkAdapter adapter = mock(BotFrameworkAdapter.class); + when(adapter.getUserToken(Mockito.any(TurnContext.class), Mockito.any(AppCredentials.class), Mockito.anyString(), Mockito.anyString())).thenReturn(CompletableFuture.completedFuture(new TokenResponse())); + + when(turnContext.getAdapter()).thenReturn(adapter); + + OAuthPromptSettings oAuthPromptSettings = mock(OAuthPromptSettings.class); + when(oAuthPromptSettings.getOAuthAppCredentials()).thenReturn(mock(AppCredentials.class)); + when(oAuthPromptSettings.getConnectionName()).thenReturn(""); + + Assert.assertNotNull(UserTokenAccess.getUserToken(turnContext, oAuthPromptSettings, "").join()); + } + + @Test + public void getSignInResource_ShouldThrowException() { + TurnContext turnContext = mock(TurnContext.class); + + TurnContextStateCollection turnContextStateCollection = mock(TurnContextStateCollection.class); + when(turnContextStateCollection.get(Mockito.any(Class.class))).thenReturn(null); + + when(turnContext.getTurnState()).thenReturn(turnContextStateCollection); + when(turnContext.getAdapter()).thenReturn(null); + + Assert.assertThrows(UnsupportedOperationException.class, () -> UserTokenAccess.getSignInResource(turnContext, new OAuthPromptSettings())); + } + + @Test + public void signOutUser_ShouldThrowException() { + TurnContext turnContext = mock(TurnContext.class); + + TurnContextStateCollection turnContextStateCollection = mock(TurnContextStateCollection.class); + when(turnContextStateCollection.get(Mockito.any(Class.class))).thenReturn(null); + + when(turnContext.getTurnState()).thenReturn(turnContextStateCollection); + when(turnContext.getAdapter()).thenReturn(null); + + Assert.assertThrows(UnsupportedOperationException.class, () -> UserTokenAccess.signOutUser(turnContext, new OAuthPromptSettings())); + } + + @Test + public void signOutUser_ShouldSucceed_WithUserTokenClientNotNull() { + TurnContext turnContext = mock(TurnContext.class); + + UserTokenClient userTokenClient = mock(UserTokenClient.class); + when(userTokenClient.signOutUser(Mockito.anyString(), Mockito.anyString(), Mockito.anyString())).thenReturn(CompletableFuture.completedFuture(null)); + + TurnContextStateCollection turnContextStateCollection = mock(TurnContextStateCollection.class); + when(turnContextStateCollection.get(Mockito.any(Class.class))).thenReturn(userTokenClient); + + when(turnContext.getTurnState()).thenReturn(turnContextStateCollection); + + ChannelAccount channelAccount = mock(ChannelAccount.class); + when(channelAccount.getId()).thenReturn(""); + + Activity activity = mock(Activity.class); + when(activity.getFrom()).thenReturn(channelAccount); + when(activity.getChannelId()).thenReturn(""); + + when(turnContext.getActivity()).thenReturn(activity); + + OAuthPromptSettings oAuthPromptSettings = mock(OAuthPromptSettings.class); + when(oAuthPromptSettings.getConnectionName()).thenReturn(""); + + Assert.assertNull(UserTokenAccess.signOutUser(turnContext, oAuthPromptSettings).join()); + } + + @Test + public void signOutUser_ShouldSucceed_WithInstanceOfUserTokenProvider() { + TurnContext turnContext = mock(TurnContext.class); + + TurnContextStateCollection turnContextStateCollection = mock(TurnContextStateCollection.class); + when(turnContextStateCollection.get(Mockito.any(Class.class))).thenReturn(null); + + when(turnContext.getTurnState()).thenReturn(turnContextStateCollection); + + BotFrameworkAdapter adapter = mock(BotFrameworkAdapter.class); + when(adapter.signOutUser(Mockito.any(TurnContext.class), Mockito.any(AppCredentials.class), Mockito.anyString(), Mockito.anyString())).thenReturn(CompletableFuture.completedFuture(null)); + + when(turnContext.getAdapter()).thenReturn(adapter); + + OAuthPromptSettings oAuthPromptSettings = mock(OAuthPromptSettings.class); + when(oAuthPromptSettings.getOAuthAppCredentials()).thenReturn(mock(AppCredentials.class)); + when(oAuthPromptSettings.getConnectionName()).thenReturn(""); + + UserTokenAccess.signOutUser(turnContext, oAuthPromptSettings).join(); + + Assert.assertNull(UserTokenAccess.signOutUser(turnContext, oAuthPromptSettings).join()); + } + + @Test + public void createConnectorClient_ShouldThrowException() { + TurnContext turnContext = mock(TurnContext.class); + + TurnContextStateCollection turnContextStateCollection = mock(TurnContextStateCollection.class); + when(turnContextStateCollection.get(Mockito.any(Class.class))).thenReturn(null); + + when(turnContext.getTurnState()).thenReturn(turnContextStateCollection); + when(turnContext.getAdapter()).thenReturn(null); + + Assert.assertThrows(UnsupportedOperationException.class, () -> + UserTokenAccess.createConnectorClient(turnContext, "", new ClaimsIdentity("anonymous"), "")); + } + + @Test + public void createConnectorClient_ShouldReturnConnectorClient_WhenConnectorFactoryNotNull() { + TurnContext turnContext = mock(TurnContext.class); + + TurnContextStateCollection turnContextStateCollection = mock(TurnContextStateCollection.class); + when(turnContextStateCollection.get(Mockito.any(Class.class))).thenReturn(mock(ConnectorFactory.class)); + + when(turnContext.getTurnState()).thenReturn(turnContextStateCollection); + + Assert.assertNull(UserTokenAccess.createConnectorClient(turnContext, "", new ClaimsIdentity("anonymous"), "")); + } + + @Test + public void createConnectorClient_ShouldReturnConnectorClient_WhenInstanceOfConnectorClientBuilder() { + TurnContext turnContext = mock(TurnContext.class); + + TurnContextStateCollection turnContextStateCollection = mock(TurnContextStateCollection.class); + when(turnContextStateCollection.get(Mockito.any(Class.class))).thenReturn(null); + + when(turnContext.getTurnState()).thenReturn(turnContextStateCollection); + + BotFrameworkAdapter adapter = mock(BotFrameworkAdapter.class); + when(adapter.createConnectorClient(Mockito.anyString(), Mockito.any(ClaimsIdentity.class), Mockito.anyString())).thenReturn(CompletableFuture.completedFuture(mock(ConnectorClient.class))); + + when(turnContext.getAdapter()).thenReturn(adapter); + + Assert.assertNotNull(UserTokenAccess.createConnectorClient(turnContext, "", new ClaimsIdentity("anonymous"), "")); + } +} From 90ed734e82292e4a09f1f18590d4af7de15c3299 Mon Sep 17 00:00:00 2001 From: matiasroldan6 Date: Tue, 17 Aug 2021 09:27:49 -0300 Subject: [PATCH 21/27] Roll back imports reorder --- .../bot/dialogs/prompts/OAuthPrompt.java | 27 ++++++++++--------- 1 file changed, 15 insertions(+), 12 deletions(-) diff --git a/libraries/bot-dialogs/src/main/java/com/microsoft/bot/dialogs/prompts/OAuthPrompt.java b/libraries/bot-dialogs/src/main/java/com/microsoft/bot/dialogs/prompts/OAuthPrompt.java index 8d71e58c1..1d58a4735 100644 --- a/libraries/bot-dialogs/src/main/java/com/microsoft/bot/dialogs/prompts/OAuthPrompt.java +++ b/libraries/bot-dialogs/src/main/java/com/microsoft/bot/dialogs/prompts/OAuthPrompt.java @@ -3,11 +3,25 @@ package com.microsoft.bot.dialogs.prompts; +import java.net.HttpURLConnection; +import java.time.Duration; +import java.time.OffsetDateTime; +import java.time.ZoneId; +import java.time.temporal.ChronoUnit; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.CompletableFuture; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + import com.microsoft.bot.builder.BotAdapter; import com.microsoft.bot.builder.BotAssert; +import com.microsoft.bot.builder.ConnectorClientBuilder; import com.microsoft.bot.builder.InvokeResponse; import com.microsoft.bot.builder.TurnContext; import com.microsoft.bot.builder.TurnStateConstants; +import com.microsoft.bot.builder.UserTokenProvider; import com.microsoft.bot.connector.Async; import com.microsoft.bot.connector.Channels; import com.microsoft.bot.connector.ConnectorClient; @@ -32,19 +46,8 @@ import com.microsoft.bot.schema.TokenExchangeInvokeResponse; import com.microsoft.bot.schema.TokenExchangeRequest; import com.microsoft.bot.schema.TokenResponse; -import org.apache.commons.lang3.StringUtils; -import java.net.HttpURLConnection; -import java.time.Duration; -import java.time.OffsetDateTime; -import java.time.ZoneId; -import java.time.temporal.ChronoUnit; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.Map; -import java.util.concurrent.CompletableFuture; -import java.util.regex.Matcher; -import java.util.regex.Pattern; +import org.apache.commons.lang3.StringUtils; /** * Creates a new prompt that asks the user to sign in using the Bot Frameworks From 9ab6e89db9f04ad9d6891cde2599b4a5739eb3f4 Mon Sep 17 00:00:00 2001 From: matiasroldan6 Date: Tue, 17 Aug 2021 09:50:35 -0300 Subject: [PATCH 22/27] Remove unused imports --- .../java/com/microsoft/bot/dialogs/prompts/OAuthPrompt.java | 2 -- 1 file changed, 2 deletions(-) diff --git a/libraries/bot-dialogs/src/main/java/com/microsoft/bot/dialogs/prompts/OAuthPrompt.java b/libraries/bot-dialogs/src/main/java/com/microsoft/bot/dialogs/prompts/OAuthPrompt.java index 1d58a4735..922f1858f 100644 --- a/libraries/bot-dialogs/src/main/java/com/microsoft/bot/dialogs/prompts/OAuthPrompt.java +++ b/libraries/bot-dialogs/src/main/java/com/microsoft/bot/dialogs/prompts/OAuthPrompt.java @@ -17,11 +17,9 @@ import com.microsoft.bot.builder.BotAdapter; import com.microsoft.bot.builder.BotAssert; -import com.microsoft.bot.builder.ConnectorClientBuilder; import com.microsoft.bot.builder.InvokeResponse; import com.microsoft.bot.builder.TurnContext; import com.microsoft.bot.builder.TurnStateConstants; -import com.microsoft.bot.builder.UserTokenProvider; import com.microsoft.bot.connector.Async; import com.microsoft.bot.connector.Channels; import com.microsoft.bot.connector.ConnectorClient; From a33f04c879f023ac3e41bf6e3b243b0a77ac1066 Mon Sep 17 00:00:00 2001 From: Leandro Dardick Date: Tue, 17 Aug 2021 11:05:07 -0300 Subject: [PATCH 23/27] Removed unused import --- .../authentication/ParameterizedBotFrameworkAuthentication.java | 1 - 1 file changed, 1 deletion(-) diff --git a/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/authentication/ParameterizedBotFrameworkAuthentication.java b/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/authentication/ParameterizedBotFrameworkAuthentication.java index 9871cb4c4..ffedbba86 100644 --- a/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/authentication/ParameterizedBotFrameworkAuthentication.java +++ b/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/authentication/ParameterizedBotFrameworkAuthentication.java @@ -3,7 +3,6 @@ package com.microsoft.bot.connector.authentication; -import com.microsoft.bot.connector.Async; import com.microsoft.bot.connector.Channels; import com.microsoft.bot.connector.skills.BotFrameworkClient; import com.microsoft.bot.schema.Activity; From 7a0f8615a2827e83b7802f335c931d4194f8d278 Mon Sep 17 00:00:00 2001 From: Leandro Dardick Date: Tue, 17 Aug 2021 11:05:40 -0300 Subject: [PATCH 24/27] Fixed turn state key reference --- .../bot/dialogs/prompts/UserTokenAccess.java | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/libraries/bot-dialogs/src/main/java/com/microsoft/bot/dialogs/prompts/UserTokenAccess.java b/libraries/bot-dialogs/src/main/java/com/microsoft/bot/dialogs/prompts/UserTokenAccess.java index 1fac28d2c..35cbace1e 100644 --- a/libraries/bot-dialogs/src/main/java/com/microsoft/bot/dialogs/prompts/UserTokenAccess.java +++ b/libraries/bot-dialogs/src/main/java/com/microsoft/bot/dialogs/prompts/UserTokenAccess.java @@ -3,6 +3,7 @@ package com.microsoft.bot.dialogs.prompts; +import com.microsoft.bot.builder.CloudAdapterBase; import com.microsoft.bot.builder.ConnectorClientBuilder; import com.microsoft.bot.builder.TurnContext; import com.microsoft.bot.builder.UserTokenProvider; @@ -26,7 +27,7 @@ public static CompletableFuture getUserToken( OAuthPromptSettings settings, String magicCode) { - UserTokenClient userTokenClient = turnContext.getTurnState().get(UserTokenClient.class); + UserTokenClient userTokenClient = turnContext.getTurnState().get(CloudAdapterBase.USER_TOKEN_CLIENT_KEY); if (userTokenClient != null) { return userTokenClient.getUserToken( @@ -49,7 +50,7 @@ public static CompletableFuture getSignInResource( TurnContext turnContext, OAuthPromptSettings settings) { - UserTokenClient userTokenClient = turnContext.getTurnState().get(UserTokenClient.class); + UserTokenClient userTokenClient = turnContext.getTurnState().get(CloudAdapterBase.USER_TOKEN_CLIENT_KEY); if (userTokenClient != null) { return userTokenClient.getSignInResource( @@ -72,7 +73,7 @@ public static CompletableFuture signOutUser( TurnContext turnContext, OAuthPromptSettings settings) { - UserTokenClient userTokenClient = turnContext.getTurnState().get(UserTokenClient.class); + UserTokenClient userTokenClient = turnContext.getTurnState().get(CloudAdapterBase.USER_TOKEN_CLIENT_KEY); if (userTokenClient != null) { return userTokenClient.signOutUser( @@ -102,7 +103,7 @@ public static CompletableFuture exchangeToken( OAuthPromptSettings settings, TokenExchangeRequest tokenExchangeRequest) { - UserTokenClient userTokenClient = turnContext.getTurnState().get(UserTokenClient.class); + UserTokenClient userTokenClient = turnContext.getTurnState().get(CloudAdapterBase.USER_TOKEN_CLIENT_KEY); if (userTokenClient != null) { String userId = turnContext.getActivity().getFrom().getId(); @@ -125,7 +126,7 @@ public static CompletableFuture createConnectorClient( ClaimsIdentity claimsIdentity, String audience) { - ConnectorFactory connectorFactory = turnContext.getTurnState().get(ConnectorFactory.class); + ConnectorFactory connectorFactory = turnContext.getTurnState().get(CloudAdapterBase.CONNECTOR_FACTORY_KEY); if (connectorFactory != null) { return connectorFactory.create(serviceUrl, audience); From 546cb297143b2a0f4cea4458d0746a6db180af29 Mon Sep 17 00:00:00 2001 From: matiasroldan6 Date: Tue, 17 Aug 2021 16:38:11 -0300 Subject: [PATCH 25/27] Change key to String in UserTokenAccessTests --- .../dialogs/prompts/UserTokenAccessTests.java | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/libraries/bot-dialogs/src/test/java/com/microsoft/bot/dialogs/prompts/UserTokenAccessTests.java b/libraries/bot-dialogs/src/test/java/com/microsoft/bot/dialogs/prompts/UserTokenAccessTests.java index 35606b27e..e3c1d6236 100644 --- a/libraries/bot-dialogs/src/test/java/com/microsoft/bot/dialogs/prompts/UserTokenAccessTests.java +++ b/libraries/bot-dialogs/src/test/java/com/microsoft/bot/dialogs/prompts/UserTokenAccessTests.java @@ -30,7 +30,7 @@ public void getUserToken_ShouldThrowException() { TurnContext turnContext = mock(TurnContext.class); TurnContextStateCollection turnContextStateCollection = mock(TurnContextStateCollection.class); - when(turnContextStateCollection.get(Mockito.any(Class.class))).thenReturn(null); + when(turnContextStateCollection.get(Mockito.any(String.class))).thenReturn(null); when(turnContext.getTurnState()).thenReturn(turnContextStateCollection); when(turnContext.getAdapter()).thenReturn(null); @@ -46,7 +46,7 @@ public void getUserToken_ShouldReturnTokenResponse_WithUserTokenClientNotNull() when(userTokenClient.getUserToken(Mockito.anyString(), Mockito.anyString(), Mockito.anyString(), Mockito.anyString())).thenReturn(CompletableFuture.completedFuture(new TokenResponse())); TurnContextStateCollection turnContextStateCollection = mock(TurnContextStateCollection.class); - when(turnContextStateCollection.get(Mockito.any(Class.class))).thenReturn(userTokenClient); + when(turnContextStateCollection.get(Mockito.any(String.class))).thenReturn(userTokenClient); when(turnContext.getTurnState()).thenReturn(turnContextStateCollection); @@ -71,7 +71,7 @@ public void getUserToken_ShouldReturnTokenResponse_WithInstanceOfUserTokenProvid TurnContext turnContext = mock(TurnContext.class); TurnContextStateCollection turnContextStateCollection = mock(TurnContextStateCollection.class); - when(turnContextStateCollection.get(Mockito.any(Class.class))).thenReturn(null); + when(turnContextStateCollection.get(Mockito.any(String.class))).thenReturn(null); when(turnContext.getTurnState()).thenReturn(turnContextStateCollection); @@ -92,7 +92,7 @@ public void getSignInResource_ShouldThrowException() { TurnContext turnContext = mock(TurnContext.class); TurnContextStateCollection turnContextStateCollection = mock(TurnContextStateCollection.class); - when(turnContextStateCollection.get(Mockito.any(Class.class))).thenReturn(null); + when(turnContextStateCollection.get(Mockito.any(String.class))).thenReturn(null); when(turnContext.getTurnState()).thenReturn(turnContextStateCollection); when(turnContext.getAdapter()).thenReturn(null); @@ -105,7 +105,7 @@ public void signOutUser_ShouldThrowException() { TurnContext turnContext = mock(TurnContext.class); TurnContextStateCollection turnContextStateCollection = mock(TurnContextStateCollection.class); - when(turnContextStateCollection.get(Mockito.any(Class.class))).thenReturn(null); + when(turnContextStateCollection.get(Mockito.any(String.class))).thenReturn(null); when(turnContext.getTurnState()).thenReturn(turnContextStateCollection); when(turnContext.getAdapter()).thenReturn(null); @@ -121,7 +121,7 @@ public void signOutUser_ShouldSucceed_WithUserTokenClientNotNull() { when(userTokenClient.signOutUser(Mockito.anyString(), Mockito.anyString(), Mockito.anyString())).thenReturn(CompletableFuture.completedFuture(null)); TurnContextStateCollection turnContextStateCollection = mock(TurnContextStateCollection.class); - when(turnContextStateCollection.get(Mockito.any(Class.class))).thenReturn(userTokenClient); + when(turnContextStateCollection.get(Mockito.any(String.class))).thenReturn(userTokenClient); when(turnContext.getTurnState()).thenReturn(turnContextStateCollection); @@ -145,7 +145,7 @@ public void signOutUser_ShouldSucceed_WithInstanceOfUserTokenProvider() { TurnContext turnContext = mock(TurnContext.class); TurnContextStateCollection turnContextStateCollection = mock(TurnContextStateCollection.class); - when(turnContextStateCollection.get(Mockito.any(Class.class))).thenReturn(null); + when(turnContextStateCollection.get(Mockito.any(String.class))).thenReturn(null); when(turnContext.getTurnState()).thenReturn(turnContextStateCollection); @@ -168,7 +168,7 @@ public void createConnectorClient_ShouldThrowException() { TurnContext turnContext = mock(TurnContext.class); TurnContextStateCollection turnContextStateCollection = mock(TurnContextStateCollection.class); - when(turnContextStateCollection.get(Mockito.any(Class.class))).thenReturn(null); + when(turnContextStateCollection.get(Mockito.any(String.class))).thenReturn(null); when(turnContext.getTurnState()).thenReturn(turnContextStateCollection); when(turnContext.getAdapter()).thenReturn(null); @@ -182,7 +182,7 @@ public void createConnectorClient_ShouldReturnConnectorClient_WhenConnectorFacto TurnContext turnContext = mock(TurnContext.class); TurnContextStateCollection turnContextStateCollection = mock(TurnContextStateCollection.class); - when(turnContextStateCollection.get(Mockito.any(Class.class))).thenReturn(mock(ConnectorFactory.class)); + when(turnContextStateCollection.get(Mockito.any(String.class))).thenReturn(mock(ConnectorFactory.class)); when(turnContext.getTurnState()).thenReturn(turnContextStateCollection); @@ -194,7 +194,7 @@ public void createConnectorClient_ShouldReturnConnectorClient_WhenInstanceOfConn TurnContext turnContext = mock(TurnContext.class); TurnContextStateCollection turnContextStateCollection = mock(TurnContextStateCollection.class); - when(turnContextStateCollection.get(Mockito.any(Class.class))).thenReturn(null); + when(turnContextStateCollection.get(Mockito.any(String.class))).thenReturn(null); when(turnContext.getTurnState()).thenReturn(turnContextStateCollection); From 1c3f5c7edc885b95ac4cfe26e2e1923fe3a6dd2d Mon Sep 17 00:00:00 2001 From: matiasroldan6 Date: Wed, 18 Aug 2021 15:10:35 -0300 Subject: [PATCH 26/27] Add sendMessageToTeamsChannel overload with CloudAdapter support --- .../bot/builder/teams/TeamsInfo.java | 43 +++++ .../bot/builder/teams/TeamsInfoTests.java | 155 ++++++++++++++++++ 2 files changed, 198 insertions(+) diff --git a/libraries/bot-builder/src/main/java/com/microsoft/bot/builder/teams/TeamsInfo.java b/libraries/bot-builder/src/main/java/com/microsoft/bot/builder/teams/TeamsInfo.java index f5d96da44..de080dca4 100644 --- a/libraries/bot-builder/src/main/java/com/microsoft/bot/builder/teams/TeamsInfo.java +++ b/libraries/bot-builder/src/main/java/com/microsoft/bot/builder/teams/TeamsInfo.java @@ -5,6 +5,7 @@ import com.microsoft.bot.builder.BotFrameworkAdapter; import com.microsoft.bot.builder.TurnContext; +import com.microsoft.bot.connector.Channels; import com.microsoft.bot.connector.ConnectorClient; import com.microsoft.bot.connector.authentication.MicrosoftAppCredentials; import com.microsoft.bot.connector.rest.RestTeamsConnectorClient; @@ -429,6 +430,48 @@ public static CompletableFuture> sendMessage ).thenApply(aVoid -> new Pair<>(conversationReference.get(), newActivityId.get())); } + public static CompletableFuture> sendMessageToTeamsChannel( + TurnContext turnContext, + Activity activity, + String teamsChannelId, + String botAppId + ) { + if (turnContext == null) { + return illegalArgument("turnContext is required"); + } + if (turnContext.getActivity() == null) { + return illegalArgument("turnContext.Activity is required"); + } + if (StringUtils.isEmpty(teamsChannelId)) { + return illegalArgument("teamsChannelId is required"); + } + + AtomicReference conversationReference = new AtomicReference<>(); + AtomicReference newActivityId = new AtomicReference<>(); + String serviceUrl = turnContext.getActivity().getServiceUrl(); + + TeamsChannelData teamsChannelData = new TeamsChannelData(); + teamsChannelData.setChannel(new ChannelInfo(teamsChannelId)); + + ConversationParameters conversationParameters = new ConversationParameters(); + conversationParameters.setIsGroup(true); + conversationParameters.setChannelData(teamsChannelData); + conversationParameters.setActivity(activity); + + return turnContext.getAdapter().createConversation( + botAppId, + Channels.MSTEAMS, + serviceUrl, + null, + conversationParameters, + (TurnContext context) -> { + conversationReference.set(context.getActivity().getConversationReference()); + newActivityId.set(context.getActivity().getId()); + return CompletableFuture.completedFuture(null); + } + ).thenApply(aVoid -> new Pair<>(conversationReference.get(), newActivityId.get())); + } + private static CompletableFuture illegalArgument(String message) { CompletableFuture detailResult = new CompletableFuture<>(); detailResult.completeExceptionally(new IllegalArgumentException(message)); diff --git a/libraries/bot-builder/src/test/java/com/microsoft/bot/builder/teams/TeamsInfoTests.java b/libraries/bot-builder/src/test/java/com/microsoft/bot/builder/teams/TeamsInfoTests.java index 942590aa8..7b8dd72f2 100644 --- a/libraries/bot-builder/src/test/java/com/microsoft/bot/builder/teams/TeamsInfoTests.java +++ b/libraries/bot-builder/src/test/java/com/microsoft/bot/builder/teams/TeamsInfoTests.java @@ -5,6 +5,8 @@ import com.fasterxml.jackson.databind.node.JsonNodeFactory; import com.microsoft.bot.builder.ActivityHandler; +import com.microsoft.bot.builder.BotAdapter; +import com.microsoft.bot.builder.BotCallbackHandler; import com.microsoft.bot.builder.BotFrameworkAdapter; import com.microsoft.bot.builder.MessageFactory; import com.microsoft.bot.builder.SimpleAdapter; @@ -27,6 +29,7 @@ import com.microsoft.bot.schema.ConversationReference; import com.microsoft.bot.schema.ConversationResourceResponse; import com.microsoft.bot.schema.Pair; +import com.microsoft.bot.schema.ResourceResponse; import com.microsoft.bot.schema.teams.ChannelInfo; import com.microsoft.bot.schema.teams.ConversationList; import com.microsoft.bot.schema.teams.MeetingDetails; @@ -36,6 +39,7 @@ import com.microsoft.bot.schema.teams.TeamsChannelAccount; import com.microsoft.bot.schema.teams.TeamsChannelData; import com.microsoft.bot.schema.teams.TeamsMeetingInfo; +import org.apache.commons.lang3.NotImplementedException; import org.junit.Assert; import org.junit.Test; import org.junit.runner.RunWith; @@ -81,6 +85,52 @@ public void TestSendMessageToTeamsChannel() { handler.onTurn(turnContext).join(); } + @Test + public void TestSendMessageToTeamsChannelWithCloudAdapterSupport() { + String expectedTeamsChannelId = "teams-channel-id"; + String expectedAppId = "app-id"; + String expectedServiceUrl = "service-url"; + String expectedActivityId = "activity-id"; + String expectedConversationId = "conversation-id"; + + Activity requestActivity = new Activity(ActivityTypes.MESSAGE); + requestActivity.setServiceUrl(expectedServiceUrl); + + TestCreateConversationAdapter adapter = new TestCreateConversationAdapter(expectedActivityId, expectedConversationId); + + TurnContext turnContextMock = Mockito.mock(TurnContext.class); + Mockito.when(turnContextMock.getActivity()).thenReturn(requestActivity); + Mockito.when(turnContextMock.getAdapter()).thenReturn(adapter); + + Activity activity = new Activity(ActivityTypes.MESSAGE); + activity.setText("Test-SendMessageToTeamsChannelAsync"); + activity.setChannelId(Channels.MSTEAMS); + + TeamsChannelData data = new TeamsChannelData(); + + TeamInfo teamInfo = new TeamInfo(); + teamInfo.setId("team-id"); + + data.setTeam(teamInfo); + + activity.setChannelData(data); + + Pair r = TeamsInfo.sendMessageToTeamsChannel(turnContextMock, activity, expectedTeamsChannelId, expectedAppId).join(); + + Assert.assertEquals(expectedConversationId, r.getLeft().getConversation().getId()); + Assert.assertEquals(expectedActivityId, r.getRight()); + Assert.assertEquals(expectedAppId, adapter.getAppId()); + Assert.assertEquals(Channels.MSTEAMS, adapter.getChannelId()); + Assert.assertEquals(expectedServiceUrl, adapter.getServiceUrl()); + Assert.assertNull(adapter.getAudience()); + + Object channelData = adapter.getConversationParameters().getChannelData(); + String id = ((TeamsChannelData) channelData).getChannel().getId(); + + Assert.assertEquals(expectedTeamsChannelId, id); + Assert.assertEquals(adapter.getConversationParameters().getActivity(), activity); + } + @Test public void TestGetTeamDetails() { String baseUri = "https://test.coffee"; @@ -478,4 +528,109 @@ private static TeamsConnectorClient getTeamsConnectorClient( return mockConnectorClient; } + + private static class TestCreateConversationAdapter extends BotAdapter + { + private final String activityId; + private final String conversationId; + + private String appId; + private String channelId; + private String serviceUrl; + private String audience; + private ConversationParameters conversationParameters; + + public String getAppId() { + return appId; + } + + public void setAppId(String appId) { + this.appId = appId; + } + + public String getChannelId() { + return channelId; + } + + public void setChannelId(String channelId) { + this.channelId = channelId; + } + + public String getServiceUrl() { + return serviceUrl; + } + + public void setServiceUrl(String serviceUrl) { + this.serviceUrl = serviceUrl; + } + + public String getAudience() { + return audience; + } + + public void setAudience(String audience) { + this.audience = audience; + } + + public ConversationParameters getConversationParameters() { + return conversationParameters; + } + + public void setConversationParameters(ConversationParameters conversationParameters) { + this.conversationParameters = conversationParameters; + } + + public TestCreateConversationAdapter(String withActivityId, String withConversationId) + { + activityId = withActivityId; + conversationId = withConversationId; + } + + @Override + public CompletableFuture createConversation( + String withBotAppId, + String withChannelId, + String withServiceUrl, + String withAudience, + ConversationParameters withConversationParameters, + BotCallbackHandler callback + ) { + appId = withBotAppId; + channelId = withChannelId; + serviceUrl = withServiceUrl; + audience = withAudience; + conversationParameters = withConversationParameters; + + Activity activity = new Activity(ActivityTypes.MESSAGE); + activity.setId(activityId); + + ConversationAccount conversation = new ConversationAccount(); + conversation.setId(conversationId); + activity.setConversation(conversation); + + TurnContext mockTurnContext = Mockito.mock(TurnContext.class); + Mockito.when(mockTurnContext.getActivity()).thenReturn(activity); + + callback.invoke(mockTurnContext); + return CompletableFuture.completedFuture(null); + } + + @Override + public CompletableFuture deleteActivity(TurnContext turnContext, ConversationReference reference) + { + throw new NotImplementedException("deleteActivity"); + } + + @Override + public CompletableFuture sendActivities(TurnContext turnContext, List activities) + { + throw new NotImplementedException("sendActivities"); + } + + @Override + public CompletableFuture updateActivity(TurnContext turnContext, Activity activity) + { + throw new NotImplementedException("updateActivity"); + } + } } From 248cc89406594ca604cac5676a428461e9ddb69e Mon Sep 17 00:00:00 2001 From: Federico Bernal <64086728+FedericoBernal@users.noreply.github.com> Date: Thu, 5 Aug 2021 14:57:57 -0300 Subject: [PATCH 27/27] Added test cases for ParameterizedBotFrameworkAuthentication --- ...terizedBotFrameworkAuthenticationTest.java | 182 ++++++++++++++++++ 1 file changed, 182 insertions(+) create mode 100644 libraries/bot-connector/src/test/java/com/microsoft/bot/connector/authentication/ParameterizedBotFrameworkAuthenticationTest.java diff --git a/libraries/bot-connector/src/test/java/com/microsoft/bot/connector/authentication/ParameterizedBotFrameworkAuthenticationTest.java b/libraries/bot-connector/src/test/java/com/microsoft/bot/connector/authentication/ParameterizedBotFrameworkAuthenticationTest.java new file mode 100644 index 000000000..a80b1c7d5 --- /dev/null +++ b/libraries/bot-connector/src/test/java/com/microsoft/bot/connector/authentication/ParameterizedBotFrameworkAuthenticationTest.java @@ -0,0 +1,182 @@ +package com.microsoft.bot.connector.authentication; + +import com.microsoft.bot.connector.Channels; +import com.microsoft.bot.schema.Activity; + +import com.microsoft.bot.schema.ChannelAccount; +import com.microsoft.bot.schema.RoleTypes; +import okhttp3.OkHttpClient; +import org.junit.Test; +import org.mockito.Mockito; + +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionException; + +import static org.junit.Assert.*; + +public class ParameterizedBotFrameworkAuthenticationTest { + private final String appId = "123"; + private final String appPassword = "test"; + private final String audienceEmail = "test@example.org"; + private final String callerId = "42"; + private final String tokenIssuer = "ABC123"; + private final String url = "https://example.org/example"; + private final Boolean validateAuth = true; + private final String authHeader = "Auth Header"; + private final String channelIdHeader = "Channel Id Header"; + + @Test + public void getOriginatingAudience_withAuthenticationEnabled_shouldMatch() { + PasswordServiceClientCredentialFactory passwordServiceClientCredentialFactory = new PasswordServiceClientCredentialFactory(appId, appPassword); + + AuthenticationConfiguration authenticationConfiguration = new AuthenticationConfiguration(); + ParameterizedBotFrameworkAuthentication parameterizedBotFrameworkAuthentication = new ParameterizedBotFrameworkAuthentication( + validateAuth, audienceEmail, audienceEmail, tokenIssuer, + url, audienceEmail, audienceEmail, callerId, passwordServiceClientCredentialFactory, + authenticationConfiguration, new OkHttpClient()); + + String originatedAudience = parameterizedBotFrameworkAuthentication.getOriginatingAudience(); + assertEquals(audienceEmail, originatedAudience); + } + + @Test + public void authenticateChannelRequest_withAuthenticationEnabledAndNoHeader_shouldNotBeNull() { + PasswordServiceClientCredentialFactory passwordServiceClientCredentialFactory = new PasswordServiceClientCredentialFactory(); + + AuthenticationConfiguration authenticationConfiguration = new AuthenticationConfiguration(); + ParameterizedBotFrameworkAuthentication parameterizedBotFrameworkAuthentication = new ParameterizedBotFrameworkAuthentication( + validateAuth, audienceEmail, audienceEmail, tokenIssuer, + url, audienceEmail, audienceEmail, callerId, passwordServiceClientCredentialFactory, + authenticationConfiguration, new OkHttpClient()); + ClaimsIdentity claims = parameterizedBotFrameworkAuthentication + .authenticateChannelRequest(null).join(); + + assertNotNull(claims); + assertTrue(claims.isAuthenticated()); + assertEquals(AuthenticationConstants.ANONYMOUS_AUTH_TYPE, claims.getIssuer()); + } + + @Test + public void authenticateChannelRequest_withAuthenticationDisabled_shouldThrowAnError(){ + PasswordServiceClientCredentialFactory passwordServiceClientCredentialFactory = Mockito.mock(PasswordServiceClientCredentialFactory.class); + Mockito.when(passwordServiceClientCredentialFactory.isAuthenticationDisabled()).thenReturn(CompletableFuture.completedFuture(false)); + AuthenticationConfiguration authenticationConfiguration = new AuthenticationConfiguration(); + + ParameterizedBotFrameworkAuthentication parameterizedBotFrameworkAuthentication = new ParameterizedBotFrameworkAuthentication( + validateAuth, audienceEmail, audienceEmail, tokenIssuer, + url, audienceEmail, audienceEmail, callerId, passwordServiceClientCredentialFactory, + authenticationConfiguration, new OkHttpClient()); + + assertThrows(CompletionException.class, () -> { + parameterizedBotFrameworkAuthentication.authenticateChannelRequest(null).join();; + }); + } + + @Test + public void authenticateRequest_withAuthenticationEnabled_shouldNotBeNull(){ + PasswordServiceClientCredentialFactory passwordServiceClientCredentialFactory = new PasswordServiceClientCredentialFactory(appId, appPassword); + + AuthenticationConfiguration authenticationConfiguration = new AuthenticationConfiguration(); + ParameterizedBotFrameworkAuthentication parameterizedBotFrameworkAuthentication = new ParameterizedBotFrameworkAuthentication( + validateAuth, audienceEmail, audienceEmail, tokenIssuer, + url, audienceEmail, audienceEmail, callerId, passwordServiceClientCredentialFactory, + authenticationConfiguration, new OkHttpClient()); + + Activity activity = Activity.createConversationUpdateActivity(); + activity.setChannelId(Channels.EMULATOR); + ChannelAccount channelAccount = new ChannelAccount(); + RoleTypes roleTypes = RoleTypes.SKILL; + channelAccount.setRole(roleTypes); + activity.setRecipient(channelAccount); + + AuthenticateRequestResult result = parameterizedBotFrameworkAuthentication.authenticateRequest(activity, + null).join(); + assertNotNull(result); + assertTrue(result.getClaimsIdentity().isAuthenticated()); + assertEquals(AuthenticationConstants.ANONYMOUS_AUTH_TYPE, result.getClaimsIdentity().getIssuer()); + } + + @Test + public void authenticateRequest_shouldNotBeNull() { + PasswordServiceClientCredentialFactory passwordServiceClientCredentialFactory = new PasswordServiceClientCredentialFactory(appId, appPassword); + + AuthenticationConfiguration authenticationConfiguration = new AuthenticationConfiguration(); + ParameterizedBotFrameworkAuthentication parameterizedBotFrameworkAuthentication = new ParameterizedBotFrameworkAuthentication( + validateAuth, audienceEmail, audienceEmail, tokenIssuer, + url, audienceEmail, audienceEmail, callerId, passwordServiceClientCredentialFactory, + authenticationConfiguration, new OkHttpClient()); + + assertNotNull(parameterizedBotFrameworkAuthentication.authenticateRequest(Activity.createConversationUpdateActivity(), + authHeader)); + } + + @Test + public void authenticateStreamingRequest_withNoChannelId_shouldNotBeNull() { + PasswordServiceClientCredentialFactory passwordServiceClientCredentialFactory = new PasswordServiceClientCredentialFactory(appId, appPassword); + + AuthenticationConfiguration authenticationConfiguration = new AuthenticationConfiguration(); + ParameterizedBotFrameworkAuthentication parameterizedBotFrameworkAuthentication = new ParameterizedBotFrameworkAuthentication( + validateAuth, audienceEmail, audienceEmail, tokenIssuer, + url, audienceEmail, audienceEmail, callerId, passwordServiceClientCredentialFactory, + authenticationConfiguration, new OkHttpClient()); + + assertThrows(CompletionException.class, () -> parameterizedBotFrameworkAuthentication.authenticateStreamingRequest(authHeader, "").join()); + } + + @Test + public void authenticateStreamingRequest_withAuthenticationDisabled_shouldThrowAnError(){ + PasswordServiceClientCredentialFactory passwordServiceClientCredentialFactory = Mockito.mock(PasswordServiceClientCredentialFactory.class); + Mockito.when(passwordServiceClientCredentialFactory.isAuthenticationDisabled()).thenReturn(CompletableFuture.completedFuture(false)); + AuthenticationConfiguration authenticationConfiguration = new AuthenticationConfiguration(); + + ParameterizedBotFrameworkAuthentication parameterizedBotFrameworkAuthentication = new ParameterizedBotFrameworkAuthentication( + validateAuth, audienceEmail, audienceEmail, tokenIssuer, + url, audienceEmail, audienceEmail, callerId, passwordServiceClientCredentialFactory, + authenticationConfiguration, new OkHttpClient()); + + assertThrows(CompletionException.class, () -> { + parameterizedBotFrameworkAuthentication.authenticateStreamingRequest(authHeader, null).join(); + }); + } + + @Test + public void createConnectorFactory_shouldNotBeNull() { + PasswordServiceClientCredentialFactory passwordServiceClientCredentialFactory = new PasswordServiceClientCredentialFactory(appId, appPassword); + + AuthenticationConfiguration authenticationConfiguration = new AuthenticationConfiguration(); + ParameterizedBotFrameworkAuthentication parameterizedBotFrameworkAuthentication = new ParameterizedBotFrameworkAuthentication( + validateAuth, audienceEmail, audienceEmail, tokenIssuer, + url, audienceEmail, audienceEmail, callerId, passwordServiceClientCredentialFactory, + authenticationConfiguration, new OkHttpClient()); + + assertNotNull(parameterizedBotFrameworkAuthentication.createConnectorFactory(SkillValidation.createAnonymousSkillClaim())); + } + + @Test + public void createUserTokenClient_shouldThrowAnError() { + PasswordServiceClientCredentialFactory passwordServiceClientCredentialFactory = new PasswordServiceClientCredentialFactory(appId, appPassword); + + AuthenticationConfiguration authenticationConfiguration = new AuthenticationConfiguration(); + ParameterizedBotFrameworkAuthentication parameterizedBotFrameworkAuthentication = new ParameterizedBotFrameworkAuthentication( + validateAuth, audienceEmail, audienceEmail, tokenIssuer, + url, audienceEmail, audienceEmail, callerId, passwordServiceClientCredentialFactory, + authenticationConfiguration, new OkHttpClient()); + + assertThrows(IllegalArgumentException.class, () -> { + parameterizedBotFrameworkAuthentication.createUserTokenClient(SkillValidation.createAnonymousSkillClaim()).join(); + }); + } + + @Test + public void createBotFrameworkClient_shouldNotBeNull() { + PasswordServiceClientCredentialFactory passwordServiceClientCredentialFactory = new PasswordServiceClientCredentialFactory(appId, appPassword); + + AuthenticationConfiguration authenticationConfiguration = new AuthenticationConfiguration(); + ParameterizedBotFrameworkAuthentication parameterizedBotFrameworkAuthentication = new ParameterizedBotFrameworkAuthentication( + validateAuth, audienceEmail, audienceEmail, tokenIssuer, + url, audienceEmail, audienceEmail, callerId, passwordServiceClientCredentialFactory, + authenticationConfiguration, new OkHttpClient()); + + assertNotNull(parameterizedBotFrameworkAuthentication.createBotFrameworkClient()); + } +}