Skip to content

Commit 07e0273

Browse files
committed
Squash commit: Protocol layer with auth, unit testing pending.
1 parent 32b633c commit 07e0273

File tree

78 files changed

+6812
-100
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

78 files changed

+6812
-100
lines changed

libraries/botbuilder-core/botbuilder/core/bot_framework_adapter.py

Lines changed: 68 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -12,19 +12,23 @@
1212
ConversationAccount,
1313
ConversationParameters,
1414
ConversationReference,
15-
ResourceResponse,
1615
TokenResponse,
16+
ResourceResponse,
1717
)
1818
from botframework.connector import Channels, EmulatorApiClient
1919
from botframework.connector.aio import ConnectorClient
2020
from botframework.connector.auth import (
21+
AuthenticationConfiguration,
2122
AuthenticationConstants,
2223
ChannelValidation,
24+
ChannelProvider,
25+
ClaimsIdentity,
2326
GovernmentChannelValidation,
2427
GovernmentConstants,
2528
MicrosoftAppCredentials,
2629
JwtTokenValidation,
2730
SimpleCredentialProvider,
31+
SkillValidation,
2832
)
2933
from botframework.connector.token_api import TokenApiClient
3034
from botframework.connector.token_api.models import TokenStatus
@@ -37,6 +41,7 @@
3741
USER_AGENT = f"Microsoft-BotFramework/3.1 (BotBuilder Python/{__version__})"
3842
OAUTH_ENDPOINT = "https://api.botframework.com"
3943
US_GOV_OAUTH_ENDPOINT = "https://api.botframework.azure.us"
44+
BOT_IDENTITY_KEY = "BotIdentity"
4045

4146

4247
class TokenExchangeState(Model):
@@ -72,13 +77,17 @@ def __init__(
7277
oauth_endpoint: str = None,
7378
open_id_metadata: str = None,
7479
channel_service: str = None,
80+
channel_provider: ChannelProvider = None,
81+
auth_configuration: AuthenticationConfiguration = None,
7582
):
7683
self.app_id = app_id
7784
self.app_password = app_password
7885
self.channel_auth_tenant = channel_auth_tenant
7986
self.oauth_endpoint = oauth_endpoint
8087
self.open_id_metadata = open_id_metadata
8188
self.channel_service = channel_service
89+
self.channel_provider = channel_provider
90+
self.auth_configuration = auth_configuration or AuthenticationConfiguration()
8291

8392

8493
class BotFrameworkAdapter(BotAdapter, UserTokenProvider):
@@ -90,6 +99,7 @@ def __init__(self, settings: BotFrameworkAdapterSettings):
9099
self.settings.channel_service = self.settings.channel_service or os.environ.get(
91100
AuthenticationConstants.CHANNEL_SERVICE
92101
)
102+
93103
self.settings.open_id_metadata = (
94104
self.settings.open_id_metadata
95105
or os.environ.get(AuthenticationConstants.BOT_OPEN_ID_METADATA_KEY)
@@ -163,7 +173,7 @@ async def create_conversation(
163173

164174
# Create conversation
165175
parameters = ConversationParameters(bot=reference.bot)
166-
client = self.create_connector_client(reference.service_url)
176+
client = await self.create_connector_client(reference.service_url)
167177

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

210-
await self.authenticate_request(activity, auth_header)
220+
identity = await self.authenticate_request(activity, auth_header)
211221
context = self.create_context(activity)
222+
context.turn_state[BOT_IDENTITY_KEY] = identity
212223

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

229240
return await self.run_pipeline(context, logic)
230241

231-
async def authenticate_request(self, request: Activity, auth_header: str):
242+
async def authenticate_request(
243+
self, request: Activity, auth_header: str
244+
) -> ClaimsIdentity:
232245
"""
233246
Allows for the overriding of authentication in unit tests.
234247
:param request:
@@ -240,11 +253,14 @@ async def authenticate_request(self, request: Activity, auth_header: str):
240253
auth_header,
241254
self._credential_provider,
242255
self.settings.channel_service,
256+
self.settings.auth_configuration,
243257
)
244258

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

262+
return claims
263+
248264
def create_context(self, activity):
249265
"""
250266
Allows for the overriding of the context object in unit tests and derived adapters.
@@ -306,7 +322,8 @@ async def update_activity(self, context: TurnContext, activity: Activity):
306322
:return:
307323
"""
308324
try:
309-
client = self.create_connector_client(activity.service_url)
325+
identity: ClaimsIdentity = context.turn_state.get(BOT_IDENTITY_KEY)
326+
client = await self.create_connector_client(activity.service_url, identity)
310327
return await client.conversations.update_activity(
311328
activity.conversation.id, activity.id, activity
312329
)
@@ -324,7 +341,8 @@ async def delete_activity(
324341
:return:
325342
"""
326343
try:
327-
client = self.create_connector_client(reference.service_url)
344+
identity: ClaimsIdentity = context.turn_state.get(BOT_IDENTITY_KEY)
345+
client = await self.create_connector_client(reference.service_url, identity)
328346
await client.conversations.delete_activity(
329347
reference.conversation.id, reference.activity_id
330348
)
@@ -365,7 +383,10 @@ async def send_activities(
365383
"BotFrameworkAdapter.send_activity(): conversation.id can not be None."
366384
)
367385

368-
client = self.create_connector_client(activity.service_url)
386+
identity: ClaimsIdentity = context.turn_state.get(BOT_IDENTITY_KEY)
387+
client = await self.create_connector_client(
388+
activity.service_url, identity
389+
)
369390
if activity.type == "trace" and activity.channel_id != "emulator":
370391
pass
371392
elif activity.reply_to_id:
@@ -409,7 +430,8 @@ async def delete_conversation_member(
409430
)
410431
service_url = context.activity.service_url
411432
conversation_id = context.activity.conversation.id
412-
client = self.create_connector_client(service_url)
433+
identity: ClaimsIdentity = context.turn_state.get(BOT_IDENTITY_KEY)
434+
client = await self.create_connector_client(service_url, identity)
413435
return await client.conversations.delete_conversation_member(
414436
conversation_id, member_id
415437
)
@@ -446,7 +468,8 @@ async def get_activity_members(self, context: TurnContext, activity_id: str):
446468
)
447469
service_url = context.activity.service_url
448470
conversation_id = context.activity.conversation.id
449-
client = self.create_connector_client(service_url)
471+
identity: ClaimsIdentity = context.turn_state.get(BOT_IDENTITY_KEY)
472+
client = await self.create_connector_client(service_url, identity)
450473
return await client.conversations.get_activity_members(
451474
conversation_id, activity_id
452475
)
@@ -474,7 +497,8 @@ async def get_conversation_members(self, context: TurnContext):
474497
)
475498
service_url = context.activity.service_url
476499
conversation_id = context.activity.conversation.id
477-
client = self.create_connector_client(service_url)
500+
identity: ClaimsIdentity = context.turn_state.get(BOT_IDENTITY_KEY)
501+
client = await self.create_connector_client(service_url, identity)
478502
return await client.conversations.get_conversation_members(conversation_id)
479503
except Exception as error:
480504
raise error
@@ -488,7 +512,7 @@ async def get_conversations(self, service_url: str, continuation_token: str = No
488512
:param continuation_token:
489513
:return:
490514
"""
491-
client = self.create_connector_client(service_url)
515+
client = await self.create_connector_client(service_url)
492516
return await client.conversations.get_conversations(continuation_token)
493517

494518
async def get_user_token(
@@ -595,13 +619,44 @@ async def get_aad_tokens(
595619
user_id, connection_name, context.activity.channel_id, resource_urls
596620
)
597621

598-
def create_connector_client(self, service_url: str) -> ConnectorClient:
622+
async def create_connector_client(
623+
self, service_url: str, identity: ClaimsIdentity = None
624+
) -> ConnectorClient:
599625
"""
600626
Allows for mocking of the connector client in unit tests.
601627
:param service_url:
628+
:param identity:
602629
:return:
603630
"""
604-
client = ConnectorClient(self._credentials, base_url=service_url)
631+
if identity:
632+
bot_app_id_claim = identity.claims.get(
633+
AuthenticationConstants.AUDIENCE_CLAIM
634+
) or identity.claims.get(AuthenticationConstants.APP_ID_CLAIM)
635+
636+
credentials = None
637+
if bot_app_id_claim and SkillValidation.is_skill_claim(identity.claims):
638+
scope = JwtTokenValidation.get_app_id_from_claims(identity.claims)
639+
640+
password = await self._credential_provider.get_app_password(
641+
bot_app_id_claim
642+
)
643+
credentials = MicrosoftAppCredentials(
644+
bot_app_id_claim, password, oauth_scope=scope
645+
)
646+
if (
647+
self.settings.channel_provider
648+
and self.settings.channel_provider.is_government()
649+
):
650+
credentials.oauth_endpoint = (
651+
GovernmentConstants.TO_CHANNEL_FROM_BOT_LOGIN_URL
652+
)
653+
credentials.oauth_scope = (
654+
GovernmentConstants.TO_CHANNEL_FROM_BOT_OAUTH_SCOPE
655+
)
656+
else:
657+
credentials = self._credentials
658+
659+
client = ConnectorClient(credentials, base_url=service_url)
605660
client.config.add_user_agent(USER_AGENT)
606661
return client
607662

libraries/botbuilder-core/botbuilder/core/bot_state.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
from abc import abstractmethod
55
from copy import deepcopy
66
from typing import Callable, Dict, Union
7+
from jsonpickle.pickler import Pickler
78
from botbuilder.core.state_property_accessor import StatePropertyAccessor
89
from .turn_context import TurnContext
910
from .storage import Storage
@@ -24,8 +25,7 @@ def is_changed(self) -> bool:
2425
return self.hash != self.compute_hash(self.state)
2526

2627
def compute_hash(self, obj: object) -> str:
27-
# TODO: Should this be compatible with C# JsonConvert.SerializeObject ?
28-
return str(obj)
28+
return str(Pickler().flatten(obj))
2929

3030

3131
class BotState(PropertyManager):

libraries/botbuilder-core/botbuilder/core/integration/bot_framework_http_client.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,7 @@ async def post_activity(
7676
}
7777
if token:
7878
headers_dict.update(
79-
{"Authorization": f"Bearer:{token}",}
79+
{"Authorization": f"Bearer {token}",}
8080
)
8181

8282
json_content = json.dumps(activity.serialize())
@@ -111,7 +111,7 @@ async def _get_app_credentials(
111111
app_credentials = MicrosoftAppCredentials(
112112
app_id, app_password, oauth_scope=oauth_scope
113113
)
114-
if self._channel_provider.is_government():
114+
if self._channel_provider and self._channel_provider.is_government():
115115
app_credentials.oauth_endpoint = (
116116
GovernmentConstants.TO_CHANNEL_FROM_BOT_LOGIN_URL
117117
)

libraries/botbuilder-core/botbuilder/core/integration/channel_service_handler.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -454,7 +454,7 @@ async def _authenticate(self, auth_header: str) -> ClaimsIdentity:
454454
return await JwtTokenValidation.validate_auth_header(
455455
auth_header,
456456
self._credential_provider,
457-
self._channel_provider.channel_service,
457+
self._channel_provider,
458458
"unknown",
459-
self._auth_config,
459+
auth_configuration=self._auth_config,
460460
)
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
# coding=utf-8
2+
# --------------------------------------------------------------------------
3+
# Copyright (c) Microsoft Corporation. All rights reserved.
4+
# Licensed under the MIT License. See License.txt in the project root for
5+
# license information.
6+
# --------------------------------------------------------------------------
7+
8+
from .teams_activity_handler import TeamsActivityHandler
9+
from .teams_info import TeamsInfo
10+
11+
__all__ = [
12+
"TeamsActivityHandler",
13+
"TeamsInfo",
14+
]

0 commit comments

Comments
 (0)