8000 UserAssigned MSI support (#2129) · sahilparekh/botbuilder-python@579888d · GitHub
[go: up one dir, main page]

Skip to content

Commit 579888d

Browse files
tracyboehrerTracy Boehrer
and
Tracy Boehrer
authored
UserAssigned MSI support (microsoft#2129)
* Added ManagedIdentity * Missing ConfigurationServiceClientCredential 8000 Factory awaits * ManagedIdentityAppCredentials needs ManagedIdentity dict * Added missing PermissionError descriptions * Black reformatting in botbuilder-core --------- Co-authored-by: Tracy Boehrer <trboehre@microsoft.com>
1 parent d7cd937 commit 579888d

File tree

10 files changed

+170
-19
lines changed
  • 10 files changed

    +170
    -19
    lines changed

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

    Lines changed: 3 additions & 1 deletion
    Original file line numberDiff line numberDiff line change
    @@ -504,7 +504,9 @@ async def _authenticate(self, auth_header: str) -> ClaimsIdentity:
    504504
    )
    505505
    if not is_auth_disabled:
    506506
    # No auth header. Auth is required. Request is not authorized.
    507-
    raise PermissionError()
    507+
    raise PermissionError(
    508+
    "Authorization is required but has been disabled."
    509+
    )
    508510

    509511
    # In the scenario where Auth is disabled, we still want to have the
    510512
    # IsAuthenticated flag set in the ClaimsIdentity. To do this requires

    libraries/botbuilder-integration-aiohttp/botbuilder/integration/aiohttp/cloud_adapter.py

    Lines changed: 1 addition & 1 deletion
    Original file line numberDiff line numberDiff line change
    @@ -107,7 +107,7 @@ async def process(
    107107
    return Response(status=201)
    108108
    else:
    109109
    raise HTTPMethodNotAllowed
    110-
    except (HTTPUnauthorized, PermissionError) as _:
    110+
    except PermissionError:
    111111
    raise HTTPUnauthorized
    112112

    113113
    async def _connect(

    libraries/botbuilder-integration-aiohttp/botbuilder/integration/aiohttp/configuration_service_client_credential_factory.py

    Lines changed: 60 additions & 8 deletions
    Original file line numberDiff line numberDiff line change
    @@ -4,18 +4,23 @@
    44
    from logging import Logger
    55
    from typing import Any
    66

    7+
    from msrest.authentication import Authentication
    8+
    79
    from botframework.connector.auth import PasswordServiceClientCredentialFactory
    10+
    from botframework.connector.auth import ManagedIdentityServiceClientCredentialsFactory
    11+
    from botframework.connector.auth import ServiceClientCredentialsFactory
    812

    913

    10-
    class ConfigurationServiceClientCredentialFactory(
    11-
    PasswordServiceClientCredentialFactory
    12-
    ):
    14+
    class ConfigurationServiceClientCredentialFactory(ServiceClientCredentialsFactory):
    1315
    def __init__(self, configuration: Any, *, logger: Logger = None) -> None:
    16+
    self._inner = None
    17+
    1418
    app_type = (
    1519
    configuration.APP_TYPE
    1620
    if hasattr(configuration, "APP_TYPE")
    1721
    else "MultiTenant"
    18-
    )
    22+
    ).lower()
    23+
    1924
    app_id = configuration.APP_ID if hasattr(configuration, "APP_ID") else None
    2025
    app_password = (
    2126
    configuration.APP_PASSWORD
    @@ -24,10 +29,25 @@ def __init__(self, configuration: Any, *, logger: Logger = None) -> None:
    2429
    )
    2530
    app_tenantid = None
    2631

    27-
    if app_type == "UserAssignedMsi":
    28-
    raise Exception("UserAssignedMsi APP_TYPE is not supported")
    32+
    if app_type == "userassignedmsi":
    33+
    if not app_id:
    34+
    raise Exception("Property 'APP_ID' is expected in configuration object")
    35+
    36+
    app_tenantid = (
    37+
    configuration.APP_TENANTID< 9E81 /span>
    38+
    if hasattr(configuration, "APP_TENANTID")
    39+
    else None
    40+
    )
    41+
    if not app_tenantid:
    42+
    raise Exception(
    43+
    "Property 'APP_TENANTID' is expected in configuration object"
    44+
    )
    45+
    46+
    self._inner = ManagedIdentityServiceClientCredentialsFactory(
    47+
    app_id, logger=logger
    48+
    )
    2949

    30-
    if app_type == "SingleTenant":
    50+
    elif app_type == "singletenant":
    3151
    app_tenantid = (
    3252
    configuration.APP_TENANTID
    3353
    if hasattr(configuration, "APP_TENANTID")
    @@ -45,4 +65,36 @@ def __init__(self, configuration: Any, *, logger: Logger = None) -> None:
    4565
    "Property 'APP_TENANTID' is expected in configuration object"
    4666
    )
    4767

    48-
    super().__init__(app_id, app_password, app_tenantid, logger=logger)
    68+
    self._inner = PasswordServiceClientCredentialFactory(
    69+
    app_id, app_password, app_tenantid, logger=logger
    70+
    )
    71+
    72+
    # Default to MultiTenant
    73+
    else:
    74+
    if not app_id:
    75+
    raise Exception("Property 'APP_ID' is expected in configuration object")
    76+
    if not app_password:
    77+
    raise Exception(
    78+
    "Property 'APP_PASSWORD' is expected in configuration object"
    79+
    )
    80+
    81+
    self._inner = PasswordServiceClientCredentialFactory(
    82+
    app_id, app_password, None, logger=logger
    83+
    )
    84+
    85+
    async def is_valid_app_id(self, app_id: str) -> bool:
    86+
    return await self._inner.is_valid_app_id(app_id)
    87+
    88+
    async def is_authentication_disabled(self) -> bool:
    89+
    return await self._inner.is_authentication_disabled()
    90+
    91+
    async def create_credentials(
    92+
    self,
    93+
    app_id: str,
    94+
    oauth_scope: str,
    95+
    login_endpoint: str,
    96+
    validate_authority: bool,
    97+
    ) -> Authentication:
    98+
    return await self._inner.create_credentials(
    99+
    app_id, oauth_scope, login_endpoint, validate_authority
    100+
    )

    libraries/botframework-connector/botframework/connector/auth/__init__.py

    Lines changed: 2 additions & 0 deletions
    Original file line numberDiff line numberDiff line change
    @@ -27,3 +27,5 @@
    2727
    from .service_client_credentials_factory import *
    2828
    from .user_token_client import *
    2929
    from .authentication_configuration import *
    30+
    from .managedidentity_app_credentials import *
    31+
    from .managedidentity_service_client_credential_factory import *

    libraries/botframework-connector/botframework/connector/auth/_parameterized_bot_framework_authentication.py

    Lines changed: 6 additions & 6 deletions
    Original file line numberDiff line numberDiff line change
    @@ -473,11 +473,11 @@ async def _government_channel_validation_validate_identity(
    473473
    ):
    474474
    if identity is None:
    475475
    # No valid identity. Not Authorized.
    476-
    raise PermissionError()
    476+
    raise PermissionError("Identity missing")
    477477

    478478
    if not identity.is_authenticated:
    479479
    # The token is in some way invalid. Not Authorized.
    480-
    raise PermissionError()
    480+
    raise PermissionError("Invalid token")
    481481

    482482
    # Now check that the AppID in the claim set matches
    483483
    # what we're looking for. Note that in a multi-tenant bot, this value
    @@ -487,12 +487,12 @@ async def _government_channel_validation_validate_identity(
    487487
    # Look for the "aud" claim, but only if issued from the Bot Framework
    488488
    issuer = identity.get_claim_value(AuthenticationConstants.ISSUER_CLAIM)
    489489
    if issuer != self._to_bot_from_channel_token_issuer:
    490-
    raise PermissionError()
    490+
    raise PermissionError("'iss' claim missing")
    491491

    492492
    app_id = identity.get_claim_value(AuthenticationConstants.AUDIENCE_CLAIM)
    493493
    if not app_id:
    494494
    # The relevant audience Claim MUST be present. Not Authorized.
    495-
    raise PermissionError()
    495+
    raise PermissionError("'aud' claim missing")
    496496

    497497
    # The AppId from the claim in the token must match the AppId specified by the developer.
    498498
    # In this case, the token is destined for the app, so we find the app ID in the audience claim.
    @@ -507,8 +507,8 @@ async def _government_channel_validation_validate_identity(
    507507
    )
    508508
    if not service_url_claim:
    509509
    # Claim must be present. Not Authorized.
    510-
    raise PermissionError()
    510+
    raise PermissionError("'serviceurl' claim missing")
    511511

    512512
    if service_url_claim != service_url:
    513513
    # Claim must match. Not Authorized.
    514-
    raise PermissionError()
    514+
    raise PermissionError("Invalid 'serviceurl' claim")

    libraries/botframework-connector/botframework/connector/auth/jwt_token_validation.py

    Lines changed: 1 addition & 1 deletion
    Original file line numberDiff line numberDiff line change
    @@ -46,7 +46,7 @@ async def authenticate_request(
    4646
    auth_is_disabled = await credentials.is_authentication_disabled()
    4747
    if not auth_is_disabled:
    4848
    # No Auth Header. Auth is required. Request is not authorized.
    49-
    raise PermissionError("Unauthorized Access. Request is not authorized")
    49+
    raise PermissionError("Required Authorization token was not supplied")
    5050

    5151
    # Check if the activity is for a skill call and is coming from the Emulator.
    5252
    try:
    Lines changed: 56 additions & 0 deletions
    Original file line numberDiff line numberDiff line change
    @@ -0,0 +1,56 @@
    1+
    # Copyright (c) Microsoft Corporation. All rights reserved.
    2+
    # Licensed under the MIT License.
    3+
    4+
    from abc import ABC
    5+
    6+
    import msal
    7+
    import requests
    8+
    9+
    from .app_credentials import AppCredentials
    10+
    from .microsoft_app_credentials import MicrosoftAppCredentials
    11+
    12+
    13+
    class ManagedIdentityAppCredentials(AppCredentials, ABC):
    14+
    """
    15+
    AppCredentials implementation using application ID and password.
    16+
    """
    17+
    18+
    global_token_cache = msal.TokenCache()
    19+
    20+
    def __init__(self, app_id: str, oauth_scope: str = None):
    21+
    # super will set proper scope and endpoint.
    22+
    super().__init__(
    23+
    app_id=app_id,
    24+
    oauth_scope=oauth_scope,
    25+
    )
    26+
    27+
    self._managed_identity = {"ManagedIdentityIdType": "ClientId", "Id": app_id}
    28+
    29+
    self.app = None
    30+
    31+
    @staticmethod
    32+
    def empty():
    33+
    return MicrosoftAppCredentials("", "")
    34+
    35+
    def get_access_token(self, force_refresh: bool = False) -> str:
    36+
    """
    37+
    Implementation of AppCredentials.get_token.
    38+
    :return: The access token for the given app id and password.
    39+
    """
    40+
    41+
    # Firstly, looks up a token from cache
    42+
    # Since we are looking for token for the current app, NOT for an end user,
    43+
    # notice we give account parameter as None.
    44+
    auth_token = self.__get_msal_app().acquire_token_for_client(
    45+
    resource=self.oauth_scope
    46+
    )
    47+
    return auth_token["access_token"]
    48+
    49+
    def __get_msal_app(self):
    50+
    if not self.app:
    51+
    self.app = msal.ManagedIdentityClient(
    52+
    self._managed_identity,
    53+
    http_client=requests.Session(),
    54+
    token_cache=ManagedIdentityAppCredentials.global_token_cache,
    55+
    )
    56+
    return self.app
    Original file line numberDiff line numberDiff line change
    @@ -0,0 +1,39 @@
    1+
    # Copyright (c) Microsoft Corporation. All rights reserved.
    2+
    # Licensed under the MIT License.
    3+
    4+
    from logging import Logger
    5+
    6+
    from msrest.authentication import Authentication
    7+
    8+
    from .managedidentity_app_credentials import ManagedIdentityAppCredentials
    9+
    from .microsoft_app_credentials import MicrosoftAppCredentials
    10+
    from .service_client_credentials_factory import ServiceClientCredentialsFactory
    11+
    12+
    13+
    class ManagedIdentityServiceClientCredentialsFactory(ServiceClientCredentialsFactory):
    14+
    def __init__(self, app_id: str = None, *, logger: Logger = None) -> None:
    15+
    self.app_id = app_id
    16+
    self._logger = logger
    17+
    18+
    async def is_valid_app_id(self, app_id: str) -> bool:
    19+
    return app_id == self.app_id
    20+
    21+
    async def is_authentication_disabled(self) -> bool:
    22+
    return not self.app_id
    23+
    24+
    async def create_credentials(
    25+
    self,
    26+
    app_id: str,
    27+
    oauth_scope: str,
    28+
    login_endpoint: str,
    29+
    validate_authority: bool,
    30+
    ) -> Authentication:
    31+
    if await self.is_authentication_disabled():
    32+
    return MicrosoftAppCredentials.empty()
    33+
    34+
    if not await self.is_valid_app_id(app_id):
    35+
    raise Exception("Invalid app_id")
    36+
    37+
    credentials = ManagedIdentityAppCredentials(app_id, oauth_scope)
    38+
    39+
    return credentials

    libraries/botframework-connector/requirements.txt

    Lines changed: 1 addition & 1 deletion
    Original file line numberDiff line numberDiff line change
    @@ -3,4 +3,4 @@ botbuilder-schema==4.16.0
    33
    requests==2.32.0
    44
    PyJWT==2.4.0
    55
    cryptography==42.0.4
    6-
    msal==1.*
    6+
    msal>=1.29.0

    libraries/botframework-connector/setup.py

    Lines changed: 1 addition & 1 deletion
    Original file line numberDiff line numberDiff line change
    @@ -11,7 +11,7 @@
    1111
    # "requests>=2.23.0,<2.26",
    1212
    "PyJWT>=2.4.0",
    1313
    "botbuilder-schema==4.16.0",
    14-
    "msal==1.*",
    14+
    "msal>=1.29.0",
    1515
    ]
    1616

    1717
    root = os.path.abspath(os.path.dirname(__file__))

    0 commit comments

    Comments
     (0)
    0