Skip to content
Merged
79 changes: 67 additions & 12 deletions libraries/botbuilder-core/botbuilder/core/bot_framework_adapter.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,17 @@
from botframework.connector import Channels, EmulatorApiClient
from botframework.connector.aio import ConnectorClient
from botframework.connector.auth import (
AuthenticationConfiguration,
AuthenticationConstants,
ChannelValidation,
ChannelProvider,
ClaimsIdentity,
GovernmentChannelValidation,
GovernmentConstants,
MicrosoftAppCredentials,
JwtTokenValidation,
SimpleCredentialProvider,
SkillValidation,
)
from botframework.connector.token_api import TokenApiClient
from botframework.connector.token_api.models import TokenStatus
Expand All @@ -37,6 +41,7 @@
USER_AGENT = f"Microsoft-BotFramework/3.1 (BotBuilder Python/{__version__})"
OAUTH_ENDPOINT = "https://api.botframework.com"
US_GOV_OAUTH_ENDPOINT = "https://api.botframework.azure.us"
BOT_IDENTITY_KEY = "BotIdentity"


class TokenExchangeState(Model):
Expand Down Expand Up @@ -72,13 +77,17 @@ def __init__(
oauth_endpoint: str = None,
open_id_metadata: str = None,
channel_service: str = None,
channel_provider: ChannelProvider = None,
auth_configuration: AuthenticationConfiguration = None,
):
self.app_id = app_id
self.app_password = app_password
self.channel_auth_tenant = channel_auth_tenant
self.oauth_endpoint = oauth_endpoint
self.open_id_metadata = open_id_metadata
self.channel_service = channel_service
self.channel_provider = channel_provider
self.auth_configuration = auth_configuration or AuthenticationConfiguration()


class BotFrameworkAdapter(BotAdapter, UserTokenProvider):
Expand All @@ -90,6 +99,7 @@ def __init__(self, settings: BotFrameworkAdapterSettings):
self.settings.channel_service = self.settings.channel_service or os.environ.get(
AuthenticationConstants.CHANNEL_SERVICE
)

self.settings.open_id_metadata = (
self.settings.open_id_metadata
or os.environ.get(AuthenticationConstants.BOT_OPEN_ID_METADATA_KEY)
Expand Down Expand Up @@ -163,7 +173,7 @@ async def create_conversation(

# Create conversation
parameters = ConversationParameters(bot=reference.bot)
client = self.create_connector_client(reference.service_url)
client = await self.create_connector_client(reference.service_url)

# Mix in the tenant ID if specified. This is required for MS Teams.
if reference.conversation is not None and reference.conversation.tenant_id:
Expand Down Expand Up @@ -207,8 +217,9 @@ async def process_activity(self, req, auth_header: str, logic: Callable):
activity = await self.parse_request(req)
auth_header = auth_header or ""

await self.authenticate_request(activity, auth_header)
identity = await self.authenticate_request(activity, auth_header)
context = self.create_context(activity)
context.turn_state[BOT_IDENTITY_KEY] = identity

# Fix to assign tenant_id from channelData to Conversation.tenant_id.
# MS Teams currently sends the tenant ID in channelData and the correct behavior is to expose
Expand All @@ -228,7 +239,9 @@ async def process_activity(self, req, auth_header: str, logic: Callable):

return await self.run_pipeline(context, logic)

async def authenticate_request(self, request: Activity, auth_header: str):
async def authenticate_request(
self, request: Activity, auth_header: str
) -> ClaimsIdentity:
"""
Allows for the overriding of authentication in unit tests.
:param request:
Expand All @@ -240,11 +253,14 @@ async def authenticate_request(self, request: Activity, auth_header: str):
auth_header,
self._credential_provider,
self.settings.channel_service,
self.settings.auth_configuration,
)

if not claims.is_authenticated:
raise Exception("Unauthorized Access. Request is not authorized")

return claims

def create_context(self, activity):
"""
Allows for the overriding of the context object in unit tests and derived adapters.
Expand Down Expand Up @@ -306,7 +322,8 @@ async def update_activity(self, context: TurnContext, activity: Activity):
:return:
"""
try:
client = self.create_connector_client(activity.service_url)
identity: ClaimsIdentity = context.turn_state.get(BOT_IDENTITY_KEY)
client = await self.create_connector_client(activity.service_url, identity)
return await client.conversations.update_activity(
activity.conversation.id, activity.id, activity
)
Expand All @@ -324,7 +341,8 @@ async def delete_activity(
:return:
"""
try:
client = self.create_connector_client(reference.service_url)
identity: ClaimsIdentity = context.turn_state.get(BOT_IDENTITY_KEY)
client = await self.create_connector_client(reference.service_url, identity)
await client.conversations.delete_activity(
reference.conversation.id, reference.activity_id
)
Expand Down Expand Up @@ -365,7 +383,10 @@ async def send_activities(
"BotFrameworkAdapter.send_activity(): conversation.id can not be None."
)

client = self.create_connector_client(activity.service_url)
identity: ClaimsIdentity = context.turn_state.get(BOT_IDENTITY_KEY)
client = await self.create_connector_client(
activity.service_url, identity
)
if activity.type == "trace" and activity.channel_id != "emulator":
pass
elif activity.reply_to_id:
Expand Down Expand Up @@ -409,7 +430,8 @@ async def delete_conversation_member(
)
service_url = context.activity.service_url
conversation_id = context.activity.conversation.id
client = self.create_connector_client(service_url)
identity: ClaimsIdentity = context.turn_state.get(BOT_IDENTITY_KEY)
client = await self.create_connector_client(service_url, identity)
return await client.conversations.delete_conversation_member(
conversation_id, member_id
)
Expand Down Expand Up @@ -446,7 +468,8 @@ async def get_activity_members(self, context: TurnContext, activity_id: str):
)
service_url = context.activity.service_url
conversation_id = context.activity.conversation.id
client = self.create_connector_client(service_url)
identity: ClaimsIdentity = context.turn_state.get(BOT_IDENTITY_KEY)
client = await self.create_connector_client(service_url, identity)
return await client.conversations.get_activity_members(
conversation_id, activity_id
)
Expand Down Expand Up @@ -474,7 +497,8 @@ async def get_conversation_members(self, context: TurnContext):
)
service_url = context.activity.service_url
conversation_id = context.activity.conversation.id
client = self.create_connector_client(service_url)
identity: ClaimsIdentity = context.turn_state.get(BOT_IDENTITY_KEY)
client = await self.create_connector_client(service_url, identity)
return await client.conversations.get_conversation_members(conversation_id)
except Exception as error:
raise error
Expand All @@ -488,7 +512,7 @@ async def get_conversations(self, service_url: str, continuation_token: str = No
:param continuation_token:
:return:
"""
client = self.create_connector_client(service_url)
client = await self.create_connector_client(service_url)
return await client.conversations.get_conversations(continuation_token)

async def get_user_token(
Expand Down Expand Up @@ -595,13 +619,44 @@ async def get_aad_tokens(
user_id, connection_name, context.activity.channel_id, resource_urls
)

def create_connector_client(self, service_url: str) -> ConnectorClient:
async def create_connector_client(
self, service_url: str, identity: ClaimsIdentity = None
) -> ConnectorClient:
"""
Allows for mocking of the connector client in unit tests.
:param service_url:
:param identity:
:return:
"""
client = ConnectorClient(self._credentials, base_url=service_url)
if identity:
bot_app_id_claim = identity.claims.get(
AuthenticationConstants.AUDIENCE_CLAIM
) or identity.claims.get(AuthenticationConstants.APP_ID_CLAIM)

credentials = None
if bot_app_id_claim and SkillValidation.is_skill_claim(identity.claims):
scope = JwtTokenValidation.get_app_id_from_claims(identity.claims)

password = await self._credential_provider.get_app_password(
bot_app_id_claim
)
credentials = MicrosoftAppCredentials(
bot_app_id_claim, password, oauth_scope=scope
)
if (
self.settings.channel_provider
and self.settings.channel_provider.is_government()
):
credentials.oauth_endpoint = (
GovernmentConstants.TO_CHANNEL_FROM_BOT_LOGIN_URL
)
credentials.oauth_scope = (
GovernmentConstants.TO_CHANNEL_FROM_BOT_OAUTH_SCOPE
)
else:
credentials = self._credentials

client = ConnectorClient(credentials, base_url=service_url)
client.config.add_user_agent(USER_AGENT)
return client

Expand Down
16 changes: 16 additions & 0 deletions libraries/botbuilder-core/botbuilder/core/integration/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# coding=utf-8
# --------------------------------------------------------------------------
# Copyright (c) Microsoft Corporation. All rights reserved.
# Licensed under the MIT License. See License.txt in the project root for
# license information.
# --------------------------------------------------------------------------

from .aiohttp_channel_service import aiohttp_channel_service_routes
from .bot_framework_http_client import BotFrameworkHttpClient
from .channel_service_handler import ChannelServiceHandler

__all__ = [
"aiohttp_channel_service_routes",
"BotFrameworkHttpClient",
"ChannelServiceHandler",
]
Loading