Beispiel #1
0
def test_multitenant_authentication_not_allowed():
    expected_tenant = "expected-tenant"
    expected_token = "***"

    def fake_check_output(command_line, **_):
        match = re.search("--tenant (.*)", command_line[-1])
        assert match is None or match[1] == expected_tenant
        return json.dumps(
            {
                "expiresOn": datetime.now().strftime("%Y-%m-%d %H:%M:%S.%f"),
                "accessToken": expected_token,
                "subscription": "some-guid",
                "tenant": expected_token,
                "tokenType": "Bearer",
            }
        )

    credential = AzureCliCredential()
    with mock.patch(CHECK_OUTPUT, fake_check_output):
        token = credential.get_token("scope")
        assert token.token == expected_token

        with mock.patch.dict(
            "os.environ", {EnvironmentVariables.AZURE_IDENTITY_DISABLE_MULTITENANTAUTH: "true"}
        ):
            token = credential.get_token("scope", tenant_id="un" + expected_tenant)
        assert token.token == expected_token
Beispiel #2
0
def test_multitenant_authentication():
    default_tenant = "first-tenant"
    first_token = "***"
    second_tenant = "second-tenant"
    second_token = first_token * 2

    def fake_check_output(command_line, **_):
        match = re.search("--tenant (.*)", command_line[-1])
        tenant = match.groups()[0] if match else default_tenant
        assert tenant in (default_tenant, second_tenant), 'unexpected tenant "{}"'.format(tenant)
        return json.dumps(
            {
                "expiresOn": datetime.now().strftime("%Y-%m-%d %H:%M:%S.%f"),
                "accessToken": first_token if tenant == default_tenant else second_token,
                "subscription": "some-guid",
                "tenant": tenant,
                "tokenType": "Bearer",
            }
        )

    credential = AzureCliCredential()
    with mock.patch(CHECK_OUTPUT, fake_check_output):
        token = credential.get_token("scope")
        assert token.token == first_token

        token = credential.get_token("scope", tenant_id=default_tenant)
        assert token.token == first_token

        token = credential.get_token("scope", tenant_id=second_tenant)
        assert token.token == second_token

        # should still default to the first tenant
        token = credential.get_token("scope")
        assert token.token == first_token
Beispiel #3
0
 def _get_azcli_token(self, subscription: str = None) -> str:
     "retrieve token from azcli login"
     token = None
     tenant = self._authority if subscription is None else None
     self._current_authentication_method = self._current_authentication_method = AuthenticationMethod.azcli_login_subscription if subscription is not None else AuthenticationMethod.azcli_login
     try:
         from azure.identity import AzureCliCredential
         try:
             credential = AzureCliCredential()
             access_token = credential.get_token(self._resource)
             expires_datetime = datetime.fromtimestamp(
                 access_token.expires_on)
             token = {
                 'accessToken': access_token.token,
                 'expiresOn':
                 expires_datetime.strftime("%Y-%m-%d %H:%M:%S.%f"),
                 'tokenType': 'Bearer',
             }
         except:
             pass
     except [ImportError, ModuleNotFoundError]:
         raise AuthenticationError(
             "Azure CLI authentication requires 'azure-cli-core' to be installed."
         )
     except:
         pass
     logger().debug(
         f"_MyAadHelper::_get_azcli_token {'failed' if token is None else 'succeeded'} to get token - subscription: '{subscription}', tenant: '{tenant}'"
     )
     return token
class AzCliTokenProvider(CloudInfoTokenProvider):
    """AzCli Token Provider obtains a refresh token from the AzCli cache and uses it to authenticate with MSAL"""
    def __init__(self, kusto_uri: str, is_async: bool = False):
        super().__init__(kusto_uri, is_async)
        self._az_auth_context = None
        self._az_auth_context_async = None
        self._az_token = None

    @staticmethod
    def name() -> str:
        return "AzCliTokenProvider"

    def _context_impl(self) -> dict:
        return {"authority:": self.name()}

    def _init_impl(self):
        pass

    def _get_token_impl(self) -> Optional[dict]:
        try:
            if self._az_auth_context is None:
                self._az_auth_context = AzureCliCredential()

            self._az_token = self._az_auth_context.get_token(self._scopes[0])
            return {
                TokenConstants.AZ_TOKEN_TYPE: TokenConstants.BEARER_TYPE,
                TokenConstants.AZ_ACCESS_TOKEN: self._az_token.token
            }
        except Exception as e:
            raise KustoClientError(
                "Failed to obtain Az Cli token for '{0}'.\nPlease be sure AzCli version 2.3.0 and above is intalled.\n{1}"
                .format(self._kusto_uri, e))

    async def _get_token_impl_async(self) -> Optional[dict]:
        try:
            if self._az_auth_context_async is None:
                self._az_auth_context_async = AsyncAzureCliCredential()

            self._az_token = await self._az_auth_context_async.get_token(
                self._scopes[0])
            return {
                TokenConstants.AZ_TOKEN_TYPE: TokenConstants.BEARER_TYPE,
                TokenConstants.AZ_ACCESS_TOKEN: self._az_token.token
            }
        except Exception as e:
            raise KustoClientError(
                "Failed to obtain Az Cli token for '{0}'.\nPlease be sure AzCli version 2.3.0 and above is installed.\n{1}"
                .format(self._kusto_uri, e))

    def _get_token_from_cache_impl(self) -> Optional[dict]:
        if self._az_token is not None:
            # A token is considered valid if it is due to expire in no less than 10 minutes
            cur_time = time.time()
            if (self._az_token.expires_on - 600) > cur_time:
                return {
                    TokenConstants.MSAL_TOKEN_TYPE: TokenConstants.BEARER_TYPE,
                    TokenConstants.MSAL_ACCESS_TOKEN: self._az_token.token
                }

        return None
Beispiel #5
0
    def _get_token(self):
        try:
            credential = AzureCliCredential()
            azureToken = credential.get_token(self.scope)
        except Exception as ex:
            raise RuntimeError(str(ex))

        return azureToken.token
Beispiel #6
0
def test_multitenant_authentication_not_allowed():
    """get_token(tenant_id=...) should raise when allow_multitenant_authentication is False (the default)"""

    expected_tenant = "expected-tenant"
    expected_token = "***"

    def fake_check_output(command_line, **_):
        match = re.search("--tenant (.*)", command_line[-1])
        assert match is None or match[1] == expected_tenant
        return json.dumps({
            "expiresOn":
            datetime.now().strftime("%Y-%m-%d %H:%M:%S.%f"),
            "accessToken":
            expected_token,
            "subscription":
            "some-guid",
            "tenant":
            expected_token,
            "tokenType":
            "Bearer",
        })

    credential = AzureCliCredential()
    with mock.patch(CHECK_OUTPUT, fake_check_output):
        token = credential.get_token("scope")
        assert token.token == expected_token

        # specifying a tenant should get an error
        with pytest.raises(ClientAuthenticationError,
                           match="allow_multitenant_authentication"):
            credential.get_token("scope", tenant_id="un" + expected_tenant)

        # ...unless the compat switch is enabled
        with mock.patch.dict(
                "os.environ",
            {
                EnvironmentVariables.AZURE_IDENTITY_ENABLE_LEGACY_TENANT_SELECTION:
                "true"
            }):
            token = credential.get_token("scope",
                                         tenant_id="un" + expected_tenant)
        assert (
            token.token == expected_token
        ), "credential should ignore tenant_id kwarg when the compat switch is enabled"
Beispiel #7
0
class CredentialWrapper(BasicTokenAuthentication):
    def __init__(self, credential=None, resource_id="https://management.azure.com/.default", **kwargs):
        """Wrap any azure-identity credential to work with SDK that needs azure.common.credentials/msrestazure.
        Default resource is ARM (syntax of endpoint v2)
        :param credential: Any azure-identity credential (DefaultAzureCredential by default)
        :param str resource_id: The scope to use to get the token (default ARM)
        """
        super(CredentialWrapper, self).__init__(None)
        self.credential = credential
        if credential is None:
            self.credential = AzureCliCredential()
        self._policy = BearerTokenCredentialPolicy(self.credential, resource_id, **kwargs)

    def _make_request(self):
        return PipelineRequest(
            HttpRequest(
                "CredentialWrapper",
                "https://fakeurl"
            ),
            PipelineContext(None)
        )

    def set_token(self):
        """Ask the azure-core BearerTokenCredentialPolicy policy to get a token.
        Using the policy gives us for free the caching system of azure-core.
        We could make this code simpler by using private method, but by definition
        I can't assure they will be there forever, so mocking a fake call to the policy
        to extract the token, using 100% public API."""
        request = self._make_request()
        self._policy.on_request(request)
        # Read Authorization, and get the second part after Bearer
        token = request.http_request.headers["Authorization"].split(" ", 1)[1]
        self.token = {"access_token": token}

    def signed_session(self, session=None):
        self.set_token()
        return super(CredentialWrapper, self).signed_session(session)

    def get_token(self, *scopes, **kwargs):
        # Pass get_token call to credential
        return self.credential.get_token(*scopes, **kwargs)
Beispiel #8
0
class AzureCredential:
    def __init__(self,
                 cloud_endpoints,
                 authorization_file=None,
                 subscription_id_override=None):
        # type: (*str, *str) -> None

        if authorization_file:
            with open(authorization_file) as json_file:
                self._auth_params = json.load(json_file)
        else:
            self._auth_params = {
                'client_id':
                os.environ.get(constants.ENV_CLIENT_ID),
                'client_secret':
                os.environ.get(constants.ENV_CLIENT_SECRET),
                'access_token':
                os.environ.get(constants.ENV_ACCESS_TOKEN),
                'tenant_id':
                os.environ.get(constants.ENV_TENANT_ID),
                'use_msi':
                bool(os.environ.get(constants.ENV_USE_MSI)),
                'subscription_id':
                os.environ.get(constants.ENV_SUB_ID),
                'keyvault_client_id':
                os.environ.get(constants.ENV_KEYVAULT_CLIENT_ID),
                'keyvault_secret_id':
                os.environ.get(constants.ENV_KEYVAULT_SECRET_ID),
                'enable_cli_auth':
                True
            }

        self._auth_params[
            'authority'] = cloud_endpoints.endpoints.active_directory

        keyvault_client_id = self._auth_params.get('keyvault_client_id')
        keyvault_secret_id = self._auth_params.get('keyvault_secret_id')

        # If user provided KeyVault secret, we will pull auth params information from it
        try:
            if keyvault_secret_id:
                self._auth_params.update(
                    json.loads(
                        get_keyvault_secret(keyvault_client_id,
                                            keyvault_secret_id)))
        except HTTPError as e:
            e.message = 'Failed to retrieve SP credential ' \
                        'from Key Vault with client id: {0}'.format(keyvault_client_id)
            raise

        self._credential = None
        if self._auth_params.get('access_token') is not None:
            auth_name = 'Access Token'
            pass
        elif (self._auth_params.get('client_id')
              and self._auth_params.get('client_secret')
              and self._auth_params.get('tenant_id')):
            auth_name = 'Principal'
            self._credential = ClientSecretCredential(
                client_id=self._auth_params['client_id'],
                client_secret=self._auth_params['client_secret'],
                tenant_id=self._auth_params['tenant_id'],
                authority=self._auth_params['authority'])
        elif self._auth_params.get('use_msi'):
            auth_name = 'MSI'
            self._credential = ManagedIdentityCredential(
                client_id=self._auth_params.get('client_id'))
        elif self._auth_params.get('enable_cli_auth'):
            auth_name = 'Azure CLI'
            self._credential = AzureCliCredential()
            account_info, error = _run_command('az account show --output json')
            account_json = json.loads(account_info)
            self._auth_params['subscription_id'] = account_json['id']
            self._auth_params['tenant_id'] = account_json['tenantId']
            if error is not None:
                raise Exception('Unable to query TenantId and SubscriptionId')

        if subscription_id_override is not None:
            self._auth_params['subscription_id'] = subscription_id_override

        self._subscription_id = self._auth_params['subscription_id']
        self._tenant_id = self._auth_params['tenant_id']
        log.info('Authenticated [%s | %s%s]', auth_name, self.subscription_id,
                 ' | Authorization File' if authorization_file else '')

    def get_token(self, *scopes, **kwargs):
        # Access Token is used only in tests realistically because
        # KeyVault, Storage and mgmt plane requires separate tokens.
        # TODO: Should we scope this to tests only?
        if (self._auth_params.get('access_token')):
            return AccessToken(self._auth_params['access_token'], expires_on=0)
        try:
            return self._credential.get_token(*scopes, **kwargs)
        except Exception as e:
            log.error('Failed to authenticate.\nMessage: {}'.format(e))
            exit(1)

    # This is temporary until all SDKs we use are upgraded to Track 2
    # List of legacy users:
    #  - DNS
    #  - Record Set (uses DNS SDK)
    #  - Azure Graph
    def legacy_credentials(self, scope):
        # Track 2 SDKs use tuple
        token = self.get_token((scope + '.default'))
        return BasicTokenAuthentication(token={'access_token': token.token})

    @property
    def tenant_id(self):
        # type: (None) -> str
        return self._tenant_id

    @property
    def auth_params(self):
        # type: (None) -> str
        return self._auth_params

    @property
    def subscription_id(self):
        # type: (None) -> str
        return self._subscription_id
Beispiel #9
0
class AzureCliAuthenticationContext(AzmetaAuthenticationContext):
    def __init__(self):
        self._credential = AzureCliCredential()

    def get_token(self, scopes: Union[Sequence[str], str]) -> AccessToken:
        return self._credential.get_token(scopes)