async def logic(context: TurnContext): if test_case != SkillFlowTestCase.root_bot_only: # Create a skill ClaimsIdentity and put it in turn_state so isSkillClaim() returns True. claims_identity = ClaimsIdentity({}, False) claims_identity.claims[ "ver" ] = "2.0" # AuthenticationConstants.VersionClaim claims_identity.claims[ "aud" ] = ( SimpleComponentDialog.skill_bot_id ) # AuthenticationConstants.AudienceClaim claims_identity.claims[ "azp" ] = ( SimpleComponentDialog.parent_bot_id ) # AuthenticationConstants.AuthorizedParty context.turn_state[BotAdapter.BOT_IDENTITY_KEY] = claims_identity if test_case == SkillFlowTestCase.root_bot_consuming_skill: # Simulate the SkillConversationReference with a channel OAuthScope stored in turn_state. # This emulates a response coming to a root bot through SkillHandler. context.turn_state[ SkillHandler.SKILL_CONVERSATION_REFERENCE_KEY ] = SkillConversationReference( None, AuthenticationConstants.TO_CHANNEL_FROM_BOT_OAUTH_SCOPE ) if test_case == SkillFlowTestCase.middle_skill: # Simulate the SkillConversationReference with a parent Bot ID stored in turn_state. # This emulates a response coming to a skill from another skill through SkillHandler. context.turn_state[ SkillHandler.SKILL_CONVERSATION_REFERENCE_KEY ] = SkillConversationReference( None, SimpleComponentDialog.parent_bot_id ) async def aux( turn_context: TurnContext, # pylint: disable=unused-argument activities: List[Activity], next: Callable, ): for activity in activities: if activity.type == ActivityTypes.end_of_conversation: SimpleComponentDialog.eoc_sent = activity break return await next() # Interceptor to capture the EoC activity if it was sent so we can assert it in the tests. context.on_send_activities(aux) SimpleComponentDialog.dm_turn_result = await dialog_manager.on_turn(context)
async def __process_activity_creates_correct_creds_and_client( self, bot_app_id: str, expected_caller_id: str, channel_service: str, expected_scope: str, expected_app_credentials_count: int, expected_client_credentials_count: int, ): identity = ClaimsIdentity({}, True) if bot_app_id: identity.claims = { AuthenticationConstants.AUDIENCE_CLAIM: bot_app_id, AuthenticationConstants.APP_ID_CLAIM: bot_app_id, AuthenticationConstants.VERSION_CLAIM: "1.0", } credential_provider = SimpleCredentialProvider(bot_app_id, None) service_url = "https://smba.trafficmanager.net/amer/" async def callback(context: TurnContext): TestBotFrameworkAdapter.get_creds_and_assert_values( context, bot_app_id, expected_scope, expected_app_credentials_count, ) TestBotFrameworkAdapter.get_client_and_assert_values( context, bot_app_id, expected_scope, service_url, expected_client_credentials_count, ) assert context.activity.caller_id == expected_caller_id settings = BotFrameworkAdapterSettings( bot_app_id, credential_provider=credential_provider, channel_provider=SimpleChannelProvider(channel_service), ) sut = BotFrameworkAdapter(settings) await sut.process_activity_with_identity( Activity( channel_id="emulator", service_url=service_url, text="test", ), identity, callback, )
async def test_process_activity_for_forwarded_activity(self): bot_app_id = "00000000-0000-0000-0000-000000000001" skill_1_app_id = "00000000-0000-0000-0000-000000skill1" identity = ClaimsIdentity( claims={ AuthenticationConstants.AUDIENCE_CLAIM: skill_1_app_id, AuthenticationConstants.APP_ID_CLAIM: bot_app_id, AuthenticationConstants.VERSION_CLAIM: "1.0", }, is_authenticated=True, ) service_url = "https://root-bot.test.azurewebsites.net/" async def callback(context: TurnContext): TestBotFrameworkAdapter.get_creds_and_assert_values( context, skill_1_app_id, bot_app_id, 1, ) TestBotFrameworkAdapter.get_client_and_assert_values( context, skill_1_app_id, bot_app_id, service_url, 1, ) scope = context.turn_state[BotFrameworkAdapter.BOT_OAUTH_SCOPE_KEY] assert bot_app_id == scope settings = BotFrameworkAdapterSettings(bot_app_id) sut = BotFrameworkAdapter(settings) await sut.process_activity_with_identity( Activity(channel_id="emulator", service_url=service_url, text="test",), identity, callback, )
def setUpClass(cls): cls.bot_id = str(uuid4()) cls.skill_id = str(uuid4()) cls._test_id_factory = ConversationIdFactoryForTest() cls._claims_identity = ClaimsIdentity({}, False) cls._claims_identity.claims[AuthenticationConstants.AUDIENCE_CLAIM] = cls.bot_id cls._claims_identity.claims[AuthenticationConstants.APP_ID_CLAIM] = cls.skill_id cls._claims_identity.claims[ AuthenticationConstants.SERVICE_URL_CLAIM ] = "http://testbot.com/api/messages" cls._conversation_reference = ConversationReference( conversation=ConversationAccount(id=str(uuid4())), service_url="http://testbot.com/api/messages", ) activity = Activity.create_message_activity() activity.apply_conversation_reference(cls._conversation_reference) skill = BotFrameworkSkill( app_id=cls.skill_id, id="skill", skill_endpoint="http://testbot.com/api/messages", ) cls._options = SkillConversationIdFactoryOptions( from_bot_oauth_scope=cls.bot_id, from_bot_id=cls.bot_id, activity=activity, bot_framework_skill=skill, )
async def test_enterprise_channel_validation_wrong_issuer_fails(self): credentials = SimpleCredentialProvider( "2cd87869-38a0-4182-9251-d056e8f0ac24", "2.30Vs3VQLKt974F") with pytest.raises(Exception) as excinfo: await EnterpriseChannelValidation.validate_identity( ClaimsIdentity({"iss": "peanut"}, True), credentials) assert "Unauthorized" in str(excinfo.value)
async def _ensure_channel_connector_client_is_created( self, service_url: str, claims_identity: ClaimsIdentity): # Ensure we have a default ConnectorClient and MSAppCredentials instance for the audience. audience = claims_identity.claims.get( AuthenticationConstants.AUDIENCE_CLAIM) if (not audience or AuthenticationConstants.TO_BOT_FROM_CHANNEL_TOKEN_ISSUER != audience): # We create a default connector for audiences that are not coming from # the default https://api.botframework.com audience. # We create a default claim that contains only the desired audience. default_connector_claims = { AuthenticationConstants.AUDIENCE_CLAIM: audience } connector_claims_identity = ClaimsIdentity( claims=default_connector_claims, is_authenticated=True) await self.create_connector_client(service_url, connector_claims_identity) if SkillValidation.is_skill_claim(claims_identity.claims): # Add the channel service URL to the trusted services list so we can send messages back. # the service URL for skills is trusted because it is applied by the # SkillHandler based on the original request received by the root bot MicrosoftAppCredentials.trust_service_url(service_url)
async def test_enterprise_channel_validation_no_audience_fails(self): credentials = SimpleCredentialProvider("", "") with pytest.raises(Exception) as excinfo: await GovernmentChannelValidation.validate_identity( ClaimsIdentity({"iss": "https://api.botframework.com"}, True), credentials, ) assert "Unauthorized" in str(excinfo.value)
def create_claims_identity(self, bot_app_id: str = "") -> ClaimsIdentity: return ClaimsIdentity( { AuthenticationConstants.AUDIENCE_CLAIM: bot_app_id, AuthenticationConstants.APP_ID_CLAIM: bot_app_id, }, True, )
async def test_process_activity_with_identity_token_exchange_invoke_response( self): mock_credential_provider = unittest.mock.create_autospec( CredentialProvider) settings = BotFrameworkAdapterSettings( app_id="bot_id", credential_provider=mock_credential_provider, ) adapter = AdapterUnderTest(settings) identity = ClaimsIdentity( claims={ AuthenticationConstants.AUDIENCE_CLAIM: "bot_id", AuthenticationConstants.APP_ID_CLAIM: "bot_id", AuthenticationConstants.VERSION_CLAIM: "1.0", }, is_authenticated=True, ) inbound_activity = Activity( type=ActivityTypes.invoke, name=SignInConstants.token_exchange_operation_name, service_url="http://tempuri.org/whatever", delivery_mode=DeliveryModes.normal, conversation=ConversationAccount(id="conversationId"), value=TokenExchangeInvokeRequest( id="token_exchange_id", token="token", connection_name="connection_name", ), ) async def callback(context: TurnContext): activity = Activity( type=ActivityTypes.invoke_response, value=InvokeResponse( status=200, body=TokenExchangeInvokeResponse( id=context.activity.value.id, connection_name=context.activity.value.connection_name, ), ), ) await context.send_activity(activity) invoke_response = await adapter.process_activity_with_identity( inbound_activity, identity, callback, ) assert invoke_response assert invoke_response.status == 200 assert invoke_response.body["id"] == inbound_activity.value.id assert (invoke_response.body["connectionName"] == inbound_activity.value.connection_name)
async def test_continue_conversation_with_audience(self): mock_credential_provider = unittest.mock.create_autospec(CredentialProvider) settings = BotFrameworkAdapterSettings( app_id="bot_id", credential_provider=mock_credential_provider ) adapter = BotFrameworkAdapter(settings) skill_1_app_id = "00000000-0000-0000-0000-000000skill1" skill_2_app_id = "00000000-0000-0000-0000-000000skill2" skills_identity = ClaimsIdentity( claims={ AuthenticationConstants.AUDIENCE_CLAIM: skill_1_app_id, AuthenticationConstants.APP_ID_CLAIM: skill_2_app_id, AuthenticationConstants.VERSION_CLAIM: "1.0", }, is_authenticated=True, ) skill_2_service_url = "https://skill2.com/api/skills/" async def callback(context: TurnContext): TestBotFrameworkAdapter.get_creds_and_assert_values( context, skill_1_app_id, skill_2_app_id, 1, ) TestBotFrameworkAdapter.get_client_and_assert_values( context, skill_1_app_id, skill_2_app_id, skill_2_service_url, 1, ) # pylint: disable=protected-access client_cache = context.adapter._connector_client_cache client = client_cache.get( BotFrameworkAdapter.key_for_connector_client( skill_2_service_url, skill_1_app_id, skill_2_app_id, ) ) assert client turn_state_client = context.turn_state.get( BotFrameworkAdapter.BOT_CONNECTOR_CLIENT_KEY ) assert turn_state_client client_creds = turn_state_client.config.credentials assert skill_1_app_id == client_creds.microsoft_app_id assert skill_2_app_id == client_creds.oauth_scope assert client.config.base_url == turn_state_client.config.base_url scope = context.turn_state[BotFrameworkAdapter.BOT_OAUTH_SCOPE_KEY] assert skill_2_app_id == scope refs = ConversationReference(service_url=skill_2_service_url) await adapter.continue_conversation( refs, callback, claims_identity=skills_identity, audience=skill_2_app_id )
async def test_government_channel_validation_no_audience_fails(self): credentials = SimpleCredentialProvider( "2cd87869-38a0-4182-9251-d056e8f0ac24", "2.30Vs3VQLKt974F") with pytest.raises(Exception) as excinfo: await GovernmentChannelValidation.validate_identity( ClaimsIdentity({"iss": "https://api.botframework.us"}, True), credentials, ) assert "Unauthorized" in str(excinfo.value)
async def test_enterprise_channel_validation_succeeds(self): credentials = SimpleCredentialProvider("", "") await EnterpriseChannelValidation.validate_identity( ClaimsIdentity( { "iss": "https://api.botframework.com", "aud": credentials.app_id }, True), credentials, )
async def test_enterprise_channel_validation_succeeds(self): credentials = SimpleCredentialProvider( "2cd87869-38a0-4182-9251-d056e8f0ac24", "2.30Vs3VQLKt974F" ) await EnterpriseChannelValidation.validate_identity( ClaimsIdentity( {"iss": "https://api.botframework.com", "aud": credentials.app_id}, True ), credentials, )
async def continue_conversation( self, reference: ConversationReference, callback: Callable, bot_id: str = None, claims_identity: ClaimsIdentity = None, # pylint: disable=unused-argument ): """ Continues a conversation with a user. :param reference: A reference to the conversation to continue :type reference: :class:`botbuilder.schema.ConversationReference :param callback: The method to call for the resulting bot turn :type callback: :class:`typing.Callable` :param bot_id: The application Id of the bot. This is the appId returned by the Azure portal registration, and is generally found in the `MicrosoftAppId` parameter in `config.py`. :type bot_id: :class:`typing.str` :param claims_identity: The bot claims identity :type claims_identity: :class:`botframework.connector.auth.ClaimsIdentity` :raises: It raises an argument null exception. :return: A task that represents the work queued to execute. .. remarks:: This is often referred to as the bots *proactive messaging* flow as it lets the bot proactively send messages to a conversation or user that are already in a communication. Scenarios such as sending notifications or coupons to a user are enabled by this function. """ # TODO: proactive messages if not claims_identity: if not bot_id: raise TypeError("Expected bot_id: str but got None instead") claims_identity = ClaimsIdentity( claims={ AuthenticationConstants.AUDIENCE_CLAIM: bot_id, AuthenticationConstants.APP_ID_CLAIM: bot_id, }, is_authenticated=True, ) context = TurnContext(self, get_continuation_activity(reference)) context.turn_state[BOT_IDENTITY_KEY] = claims_identity context.turn_state["BotCallbackHandler"] = callback await self._ensure_channel_connector_client_is_created( reference.service_url, claims_identity ) return await self.run_pipeline(context, callback)
def create_turn_context(self, activity: Activity) -> TurnContext: turn_context = super().create_turn_context(activity) claims_identity = ClaimsIdentity( claims={ AuthenticationConstants.VERSION_CLAIM: "2.0", AuthenticationConstants.AUDIENCE_CLAIM: str(uuid4()), AuthenticationConstants.AUTHORIZED_PARTY: str(uuid4()), }, is_authenticated=True, ) turn_context.turn_state[self.BOT_IDENTITY_KEY] = claims_identity return turn_context
async def create_connector_client(self, service_url: str, identity: ClaimsIdentity = None, audience: str = None) -> ConnectorClient: """ Creates the connector client :param service_url: The service URL :param identity: The claims identity :param audience: :return: An instance of the :class:`ConnectorClient` class """ if not identity: # This is different from C# where an exception is raised. In this case # we are creating a ClaimsIdentity to retain compatibility with this # method. identity = ClaimsIdentity( claims={ AuthenticationConstants.AUDIENCE_CLAIM: self.settings.app_id, AuthenticationConstants.APP_ID_CLAIM: self.settings.app_id, }, is_authenticated=True, ) # 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. bot_app_id = identity.claims.get( AuthenticationConstants.AUDIENCE_CLAIM) or identity.claims.get( AuthenticationConstants.APP_ID_CLAIM) # Anonymous claims and non-skill claims should fall through without modifying the scope. credentials = None if bot_app_id: scope = audience if not scope: scope = (JwtTokenValidation.get_app_id_from_claims( identity.claims) if SkillValidation.is_skill_claim(identity.claims) else self.__get_botframework_oauth_scope()) credentials = await self.__get_app_credentials(bot_app_id, scope) return self._get_or_create_connector_client(service_url, credentials)
async def test_delivery_mode_expect_replies(self): mock_credential_provider = unittest.mock.create_autospec(CredentialProvider) settings = BotFrameworkAdapterSettings( app_id="bot_id", credential_provider=mock_credential_provider ) adapter = AdapterUnderTest(settings) async def callback(context: TurnContext): await context.send_activity("activity 1") await context.send_activity("activity 2") await context.send_activity("activity 3") inbound_activity = Activity( type=ActivityTypes.message, channel_id="emulator", service_url="http://tempuri.org/whatever", delivery_mode=DeliveryModes.expect_replies, text="hello world", ) identity = ClaimsIdentity( claims={ AuthenticationConstants.AUDIENCE_CLAIM: "bot_id", AuthenticationConstants.APP_ID_CLAIM: "bot_id", AuthenticationConstants.VERSION_CLAIM: "1.0", }, is_authenticated=True, ) invoke_response = await adapter.process_activity_with_identity( inbound_activity, identity, callback ) assert invoke_response assert invoke_response.status == 200 activities = ExpectedReplies().deserialize(invoke_response.body).activities assert len(activities) == 3 assert activities[0].text == "activity 1" assert activities[1].text == "activity 2" assert activities[2].text == "activity 3" assert ( adapter.connector_client_mock.conversations.send_to_conversation.call_count == 0 )
def setUpClass(cls): cls.bot_id = str(uuid4()) cls.skill_id = str(uuid4()) cls._test_id_factory = ConversationIdFactoryForTest() cls._claims_identity = ClaimsIdentity({}, False) cls._claims_identity.claims[ AuthenticationConstants.AUDIENCE_CLAIM] = cls.bot_id cls._claims_identity.claims[ AuthenticationConstants.APP_ID_CLAIM] = cls.skill_id cls._claims_identity.claims[ AuthenticationConstants. SERVICE_URL_CLAIM] = "http://testbot.com/api/messages" cls._conversation_reference = ConversationReference( conversation=ConversationAccount(id=str(uuid4())), service_url="http://testbot.com/api/messages", )
async def _authenticate(self, auth_header: str) -> ClaimsIdentity: if not auth_header: is_auth_disabled = ( await self._credential_provider.is_authentication_disabled()) if is_auth_disabled: # 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 ClaimsIdentity({}, True) raise PermissionError() return await JwtTokenValidation.validate_auth_header( auth_header, self._credential_provider, self._channel_provider, "unknown", auth_configuration=self._auth_config, )
async def test_process_activity_creates_correct_creds_and_client(self): bot_app_id = "00000000-0000-0000-0000-000000000001" identity = ClaimsIdentity( claims={ AuthenticationConstants.AUDIENCE_CLAIM: bot_app_id, AuthenticationConstants.APP_ID_CLAIM: bot_app_id, AuthenticationConstants.VERSION_CLAIM: "1.0", }, is_authenticated=True, ) service_url = "https://smba.trafficmanager.net/amer/" async def callback(context: TurnContext): TestBotFrameworkAdapter.get_creds_and_assert_values( context, bot_app_id, AuthenticationConstants.TO_CHANNEL_FROM_BOT_OAUTH_SCOPE, 1, ) TestBotFrameworkAdapter.get_client_and_assert_values( context, bot_app_id, AuthenticationConstants.TO_CHANNEL_FROM_BOT_OAUTH_SCOPE, service_url, 1, ) scope = context.turn_state[BotFrameworkAdapter.BOT_OAUTH_SCOPE_KEY] assert AuthenticationConstants.TO_CHANNEL_FROM_BOT_OAUTH_SCOPE == scope settings = BotFrameworkAdapterSettings(bot_app_id) sut = BotFrameworkAdapter(settings) await sut.process_activity_with_identity( Activity( channel_id="emulator", service_url=service_url, text="test", ), identity, callback, )
async def logic(context: TurnContext): if test_case != FlowTestCase.root_bot_only: claims_identity = ClaimsIdentity( { AuthenticationConstants.VERSION_CLAIM: "2.0", AuthenticationConstants.AUDIENCE_CLAIM: self.skill_bot_id, AuthenticationConstants.AUTHORIZED_PARTY: self.parent_bot_id, }, True, ) context.turn_state[ BotAdapter.BOT_IDENTITY_KEY] = claims_identity if test_case == FlowTestCase.root_bot_consuming_skill: context.turn_state[ SkillHandler. SKILL_CONVERSATION_REFERENCE_KEY] = SkillConversationReference( None, AuthenticationConstants. TO_CHANNEL_FROM_BOT_OAUTH_SCOPE) if test_case == FlowTestCase.middle_skill: context.turn_state[ SkillHandler. SKILL_CONVERSATION_REFERENCE_KEY] = SkillConversationReference( None, self.parent_bot_id) async def capture_eoc(inner_context: TurnContext, activities: List[Activity], next): # pylint: disable=unused-argument for activity in activities: if activity.type == ActivityTypes.end_of_conversation: self.eoc_sent = activity break return await next() context.on_send_activities(capture_eoc) await DialogExtensions.run_dialog( dialog, context, convo_state.create_property("DialogState"))
async def _authenticate_request(self, request: Activity, auth_header: str) -> ClaimsIdentity: self.tester.assertIsNotNone( request, "authenticate_request() not passed request.") self.tester.assertEqual( auth_header, self.expect_auth_header, "authenticateRequest() not passed expected authHeader.", ) if self.fail_auth: raise PermissionError( "Unauthorized Access. Request is not authorized") return ClaimsIdentity( claims={ AuthenticationConstants.AUDIENCE_CLAIM: self.settings.app_id, AuthenticationConstants.APP_ID_CLAIM: self.settings.app_id, }, is_authenticated=True, )
async def test_delivery_mode_normal(self): mock_credential_provider = unittest.mock.create_autospec(CredentialProvider) settings = BotFrameworkAdapterSettings( app_id="bot_id", credential_provider=mock_credential_provider ) adapter = AdapterUnderTest(settings) async def callback(context: TurnContext): await context.send_activity("activity 1") await context.send_activity("activity 2") await context.send_activity("activity 3") inbound_activity = Activity( type=ActivityTypes.message, channel_id="emulator", service_url="http://tempuri.org/whatever", delivery_mode=DeliveryModes.normal, text="hello world", conversation=ConversationAccount(id="conversationId"), ) identity = ClaimsIdentity( claims={ AuthenticationConstants.AUDIENCE_CLAIM: "bot_id", AuthenticationConstants.APP_ID_CLAIM: "bot_id", AuthenticationConstants.VERSION_CLAIM: "1.0", }, is_authenticated=True, ) invoke_response = await adapter.process_activity_with_identity( inbound_activity, identity, callback ) assert not invoke_response assert ( adapter.connector_client_mock.conversations.send_to_conversation.call_count == 3 )
async def continue_conversation( self, reference: ConversationReference, callback: Callable, bot_id: str = None, claims_identity: ClaimsIdentity = None, # pylint: disable=unused-argument ): """ Continues a conversation with a user. This is often referred to as the bots "Proactive Messaging" flow as its lets the bot proactively send messages to a conversation or user that its already communicated with. Scenarios like sending notifications or coupons to a user are enabled by this method. :param bot_id: :param reference: :param callback: :param claims_identity: :return: """ # TODO: proactive messages if not claims_identity: if not bot_id: raise TypeError("Expected bot_id: str but got None instead") claims_identity = ClaimsIdentity( claims={ AuthenticationConstants.AUDIENCE_CLAIM: bot_id, AuthenticationConstants.APP_ID_CLAIM: bot_id, }, is_authenticated=True, ) context = TurnContext(self, get_continuation_activity(reference)) context.turn_state[BOT_IDENTITY_KEY] = claims_identity context.turn_state["BotCallbackHandler"] = callback return await self.run_pipeline(context, callback)
async def continue_conversation( self, reference: ConversationReference, callback: Callable, bot_id: str = None, claims_identity: ClaimsIdentity = None, audience: str = None, ): """ Continues a conversation with a user. :param reference: A reference to the conversation to continue :type reference: :class:`botbuilder.schema.ConversationReference :param callback: The method to call for the resulting bot turn :type callback: :class:`typing.Callable` :param bot_id: The application Id of the bot. This is the appId returned by the Azure portal registration, and is generally found in the `MicrosoftAppId` parameter in `config.py`. :type bot_id: :class:`typing.str` :param claims_identity: The bot claims identity :type claims_identity: :class:`botframework.connector.auth.ClaimsIdentity` :param audience: :type audience: :class:`typing.str` :raises: It raises an argument null exception. :return: A task that represents the work queued to execute. .. remarks:: This is often referred to as the bots *proactive messaging* flow as it lets the bot proactively send messages to a conversation or user that are already in a communication. Scenarios such as sending notifications or coupons to a user are enabled by this function. """ if not reference: raise TypeError( "Expected reference: ConversationReference but got None instead" ) if not callback: raise TypeError("Expected callback: Callable but got None instead") # This has to have either a bot_id, in which case a ClaimsIdentity will be created, or # a ClaimsIdentity. In either case, if an audience isn't supplied one will be created. if not (bot_id or claims_identity): raise TypeError("Expected bot_id or claims_identity") if bot_id and not claims_identity: claims_identity = ClaimsIdentity( claims={ AuthenticationConstants.AUDIENCE_CLAIM: bot_id, AuthenticationConstants.APP_ID_CLAIM: bot_id, }, is_authenticated=True, ) if not audience: audience = self.__get_botframework_oauth_scope() context = TurnContext(self, get_continuation_activity(reference)) context.turn_state[BotAdapter.BOT_IDENTITY_KEY] = claims_identity context.turn_state[BotAdapter.BOT_CALLBACK_HANDLER_KEY] = callback context.turn_state[BotAdapter.BOT_OAUTH_SCOPE_KEY] = audience # Add the channel service URL to the trusted services list so we can send messages back. # the service URL for skills is trusted because it is applied by the SkillHandler based # on the original request received by the root bot AppCredentials.trust_service_url(reference.service_url) client = await self.create_connector_client(reference.service_url, claims_identity, audience) context.turn_state[BotAdapter.BOT_CONNECTOR_CLIENT_KEY] = client return await self.run_pipeline(context, callback)
async def test_enterprise_channel_validation_no_authentication_fails(self): with pytest.raises(Exception) as excinfo: await EnterpriseChannelValidation.validate_identity( ClaimsIdentity({}, False), None) assert "Unauthorized" in str(excinfo.value)
async def test_enterprise_channel_validation_wrong_issuer_fails(self): credentials = SimpleCredentialProvider("", "") with pytest.raises(Exception) as excinfo: await EnterpriseChannelValidation.validate_identity( ClaimsIdentity({"iss": "peanut"}, True), credentials) assert "Unauthorized" in str(excinfo.value)
async def test_continue_conversation_without_audience(self): mock_credential_provider = unittest.mock.create_autospec(CredentialProvider) settings = BotFrameworkAdapterSettings( app_id="bot_id", credential_provider=mock_credential_provider ) adapter = BotFrameworkAdapter(settings) skill_1_app_id = "00000000-0000-0000-0000-000000skill1" skill_2_app_id = "00000000-0000-0000-0000-000000skill2" skills_identity = ClaimsIdentity( claims={ AuthenticationConstants.AUDIENCE_CLAIM: skill_1_app_id, AuthenticationConstants.APP_ID_CLAIM: skill_2_app_id, AuthenticationConstants.VERSION_CLAIM: "1.0", }, is_authenticated=True, ) channel_service_url = "https://smba.trafficmanager.net/amer/" async def callback(context: TurnContext): TestBotFrameworkAdapter.get_creds_and_assert_values( context, skill_1_app_id, AuthenticationConstants.TO_CHANNEL_FROM_BOT_OAUTH_SCOPE, 1, ) TestBotFrameworkAdapter.get_client_and_assert_values( context, skill_1_app_id, AuthenticationConstants.TO_CHANNEL_FROM_BOT_OAUTH_SCOPE, channel_service_url, 1, ) # pylint: disable=protected-access client_cache = context.adapter._connector_client_cache client = client_cache.get( BotFrameworkAdapter.key_for_connector_client( channel_service_url, skill_1_app_id, AuthenticationConstants.TO_CHANNEL_FROM_BOT_OAUTH_SCOPE, ) ) assert client turn_state_client = context.turn_state.get( BotFrameworkAdapter.BOT_CONNECTOR_CLIENT_KEY ) assert turn_state_client client_creds = turn_state_client.config.credentials assert skill_1_app_id == client_creds.microsoft_app_id assert ( AuthenticationConstants.TO_CHANNEL_FROM_BOT_OAUTH_SCOPE == client_creds.oauth_scope ) assert client.config.base_url == turn_state_client.config.base_url scope = context.turn_state[BotFrameworkAdapter.BOT_OAUTH_SCOPE_KEY] assert AuthenticationConstants.TO_CHANNEL_FROM_BOT_OAUTH_SCOPE == scope # Ensure the serviceUrl was added to the trusted hosts assert AppCredentials.is_trusted_service(channel_service_url) refs = ConversationReference(service_url=channel_service_url) # Ensure the serviceUrl is NOT in the trusted hosts assert not AppCredentials.is_trusted_service(channel_service_url) await adapter.continue_conversation( refs, callback, claims_identity=skills_identity )
async def create_conversation( self, reference: ConversationReference, logic: Callable[[TurnContext], Awaitable] = None, conversation_parameters: ConversationParameters = None, channel_id: str = None, service_url: str = None, credentials: AppCredentials = None, ): """ Starts a new conversation with a user. Used to direct message to a member of a group. :param reference: The conversation reference that contains the tenant :type reference: :class:`botbuilder.schema.ConversationReference` :param logic: The logic to use for the creation of the conversation :type logic: :class:`typing.Callable` :param conversation_parameters: The information to use to create the conversation :type conversation_parameters: :param channel_id: The ID for the channel. :type channel_id: :class:`typing.str` :param service_url: The channel's service URL endpoint. :type service_url: :class:`typing.str` :param credentials: The application credentials for the bot. :type credentials: :class:`botframework.connector.auth.AppCredentials` :raises: It raises a generic exception error. :return: A task representing the work queued to execute. .. remarks:: 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 conversation update activity through its middleware pipeline to the the callback method. If the conversation is established with the specified users, the ID of the activity will contain the ID of the new conversation. """ try: if not service_url: service_url = reference.service_url if not service_url: raise TypeError( "BotFrameworkAdapter.create_conversation(): service_url or reference.service_url is required." ) if not channel_id: channel_id = reference.channel_id if not channel_id: raise TypeError( "BotFrameworkAdapter.create_conversation(): channel_id or reference.channel_id is required." ) parameters = (conversation_parameters if conversation_parameters else ConversationParameters(bot=reference.bot, members=[reference.user], is_group=False)) # Mix in the tenant ID if specified. This is required for MS Teams. if reference.conversation and reference.conversation.tenant_id: # Putting tenant_id in channel_data is a temporary while we wait for the Teams API to be updated parameters.channel_data = { "tenant": { "id": reference.conversation.tenant_id } } # Permanent solution is to put tenant_id in parameters.tenant_id parameters.tenant_id = reference.conversation.tenant_id # This is different from C# where credentials are required in the method call. # Doing this for compatibility. app_credentials = (credentials if credentials else await self.__get_app_credentials( self.settings.app_id, self.__get_botframework_oauth_scope())) # Create conversation client = self._get_or_create_connector_client( service_url, app_credentials) resource_response = await client.conversations.create_conversation( parameters) event_activity = Activity( type=ActivityTypes.event, name="CreateConversation", channel_id=channel_id, service_url=service_url, id=resource_response.activity_id if resource_response.activity_id else str(uuid.uuid4()), conversation=ConversationAccount( id=resource_response.id, tenant_id=parameters.tenant_id, ), channel_data=parameters.channel_data, recipient=parameters.bot, ) context = self._create_context(event_activity) context.turn_state[BotAdapter.BOT_CONNECTOR_CLIENT_KEY] = client claims_identity = ClaimsIdentity( claims={ AuthenticationConstants.AUDIENCE_CLAIM: app_credentials.microsoft_app_id, AuthenticationConstants.APP_ID_CLAIM: app_credentials.microsoft_app_id, AuthenticationConstants.SERVICE_URL_CLAIM: service_url, }, is_authenticated=True, ) context.turn_state[BotAdapter.BOT_IDENTITY_KEY] = claims_identity return await self.run_pipeline(context, logic) except Exception as error: raise error