From ff1e3a6f617bb14b6007daff6ed8296d4a78c20d Mon Sep 17 00:00:00 2001 From: David Blain Date: Sun, 26 Jan 2025 07:42:02 +0100 Subject: [PATCH] Added support for certificate authentication with MSGraphAsyncOperator (#45935) * refactor: Added support for certificate authentication in KiotaRequestAdapterHook * refactor: Fixed label for allowed hosts in MS Graph connection form --------- Co-authored-by: David Blain --- .../connections/images/msgraph.png | Bin 0 -> 71754 bytes .../connections/msgraph.rst | 135 ++++++++++++++++++ .../microsoft/azure/hooks/msgraph.py | 83 ++++++++--- .../microsoft/azure/operators/msgraph.py | 4 + .../microsoft/azure/sensors/msgraph.py | 4 + .../microsoft/azure/triggers/msgraph.py | 4 + .../microsoft/azure/hooks/test_msgraph.py | 31 ++++ .../microsoft/azure/triggers/test_msgraph.py | 9 +- 8 files changed, 247 insertions(+), 23 deletions(-) create mode 100644 docs/apache-airflow-providers-microsoft-azure/connections/images/msgraph.png create mode 100644 docs/apache-airflow-providers-microsoft-azure/connections/msgraph.rst diff --git a/docs/apache-airflow-providers-microsoft-azure/connections/images/msgraph.png b/docs/apache-airflow-providers-microsoft-azure/connections/images/msgraph.png new file mode 100644 index 0000000000000000000000000000000000000000..36c36fa783164d9e2647290caf5e94018a68898b GIT binary patch literal 71754 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 oW`nHvP>fMfshD~9H!eGKzeYs=KlhY#n*aa+ literal 0 HcmV?d00001 diff --git a/docs/apache-airflow-providers-microsoft-azure/connections/msgraph.rst b/docs/apache-airflow-providers-microsoft-azure/connections/msgraph.rst new file mode 100644 index 0000000000000..8c81741616288 --- /dev/null +++ b/docs/apache-airflow-providers-microsoft-azure/connections/msgraph.rst @@ -0,0 +1,135 @@ +.. Licensed to the Apache Software Foundation (ASF) under one + or more contributor license agreements. See the NOTICE file + distributed with this work for additional information + regarding copyright ownership. The ASF licenses this file + to you under the Apache License, Version 2.0 (the + "License"); you may not use this file except in compliance + with the License. You may obtain a copy of the License at + + .. http://www.apache.org/licenses/LICENSE-2.0 + + .. Unless required by applicable law or agreed to in writing, + software distributed under the License is distributed on an + "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + KIND, either express or implied. See the License for the + specific language governing permissions and limitations + under the License. + + + +.. _howto/connection:msgraph: + +Microsoft Graph API Connection +============================== + +The Microsoft Graph API connection type enables Microsoft Graph API Integrations. + +The :class:`~airflow.providers.microsoft.azure.hooks.msgraph.KiotaRequestAdapterHook` and :class:`~airflow.providers.microsoft.azure.operators.msgraph.MSGraphAsyncOperator` requires a connection of type ``msgraph`` to authenticate with Microsoft Graph API. + +Authenticating to Microsoft Graph API +------------------------------------- + +1. Use `token credentials + `_ + i.e. add specific credentials (client_id, client_secret, tenant_id) to the Airflow connection. + +Default Connection IDs +---------------------- + +All hooks and operators related to Microsoft Graph API use ``msgraph_default`` by default. + +Configuring the Connection +-------------------------- + +Client ID + Specify the ``client_id`` used for the initial connection. + This is needed for *token credentials* authentication mechanism. + + +Client Secret + Specify the ``client_secret`` used for the initial connection. + This is needed for *token credentials* authentication mechanism unless a certificate is used. + + +Tenant ID + Specify the ``tenant_id`` used for the initial connection. + This is needed for *token credentials* authentication mechanism. + + +API Version + Specify the ``api_version`` used for the initial connection. + Default value is ``v1.0``. + + +Authority + The ``authority`` parameter defines the endpoint (or tenant) that MSAL uses to authenticate requests. + It determines which identity provider will handle authentication. + Default value is ``login.microsoftonline.com``. + + +Scopes + The ``scopes`` parameter specifies the permissions or access rights that your application is requesting for a connection. + These permissions define what resources or data your application can access on behalf of the user or application. + Default value is ``https://graph.microsoft.com/.default``. + + +Certificate path + The ``certificate_path`` parameter specifies the filepath where the certificate is located. + Both ``certificate_path`` and ``certificate_data`` parameter cannot be used together, they should be mutually exclusive. + Default value is None. + + +Certificate data + The ``certificate_date`` parameter specifies the certificate as a string. + Both ``certificate_path`` and ``certificate_data`` parameter cannot be used together, they should be mutually exclusive. + Default value is None. + + +Disable instance discovery + The ``disable_instance_discovery`` parameter determines whether MSAL should validate and discover Azure AD endpoints dynamically during runtime. + Default value is False (e.g. disabled). + + +Allowed hosts + The ``allowed_hosts`` parameter is used to define a list of acceptable hosts that the authentication provider will trust when making requests. + This parameter is particularly useful for enhancing security and controlling which endpoints the authentication provider interacts with. + + +Proxies + The ``proxies`` parameter is used to define a dict for the ``http`` and ``https`` schema, the ``no`` key can be use to define hosts not to be used by the proxy. + Default value is None. + + +Verify environment + The ``verify`` parameter specifies whether SSL certificates should be verified when making HTTPS requests. + By default, ``verify`` parameter is set to True. This means that the `httpx `_ library will verify the SSL certificate presented by the server to ensure: + + - The certificate is valid and trusted. + - The certificate matches the hostname of the server. + - The certificate has not expired or been revoked. + + Setting ``verify`` to False disables SSL certificate verification. This is typically used in development or testing environments when working with self-signed certificates or servers without valid certificates. + + +Trust environment + The ``trust_env`` parameter determines whether or not the library should use environment variables for configuration when making HTTP/HTTPS requests. + By default, ``trust_env`` parameter is set to True. This means the `httpx `_ library will automatically trust and use environment variables for proxy configuration, SSL settings, and authentication. + + +Base URL + The ``base_url`` parameter allows you to override the default base url used to make it requests, namely ``https://graph.microsoft.com/``. + This can be useful if you want to use the MSGraphAsyncOperator to call other Microsoft REST API's like Sharepoint or PowerBI. + Default value is None. + + +.. raw:: html + +
+ Microsoft Graph API connection form +
+ + +.. spelling:word-list:: + + Entra diff --git a/providers/src/airflow/providers/microsoft/azure/hooks/msgraph.py b/providers/src/airflow/providers/microsoft/azure/hooks/msgraph.py index f01fa1c585837..1754d04b10356 100644 --- a/providers/src/airflow/providers/microsoft/azure/hooks/msgraph.py +++ b/providers/src/airflow/providers/microsoft/azure/hooks/msgraph.py @@ -26,7 +26,7 @@ from urllib.parse import quote, urljoin, urlparse import httpx -from azure.identity import ClientSecretCredential +from azure.identity import CertificateCredential, ClientSecretCredential from httpx import AsyncHTTPTransport, Timeout from kiota_abstractions.api_error import APIError from kiota_abstractions.method import Method @@ -47,6 +47,7 @@ from airflow.hooks.base import BaseHook if TYPE_CHECKING: + from azure.identity._internal.client_credential_base import ClientCredentialBase from kiota_abstractions.request_adapter import RequestAdapter from kiota_abstractions.request_information import QueryParams from kiota_abstractions.response_handler import NativeResponseType @@ -107,6 +108,7 @@ class KiotaRequestAdapterHook(BaseHook): """ DEFAULT_HEADERS = {"Accept": "application/json;q=1"} + DEFAULT_SCOPE = "https://graph.microsoft.com/.default" cached_request_adapters: dict[str, tuple[APIVersion, RequestAdapter]] = {} conn_type: str = "msgraph" conn_name_attr: str = "conn_id" @@ -119,7 +121,7 @@ def __init__( timeout: float | None = None, proxies: dict | None = None, host: str = NationalClouds.Global.value, - scopes: list[str] | None = None, + scopes: str | list[str] | None = None, api_version: APIVersion | str | None = None, ): super().__init__() @@ -127,7 +129,10 @@ def __init__( self.timeout = timeout self.proxies = proxies self.host = host - self.scopes = scopes or ["https://graph.microsoft.com/.default"] + if isinstance(scopes, str): + self.scopes = [scopes] + else: + self.scopes = scopes or [self.DEFAULT_SCOPE] self._api_version = self.resolve_api_version_from_value(api_version) @classmethod @@ -140,20 +145,21 @@ def get_connection_form_widgets(cls) -> dict[str, Any]: return { "tenant_id": StringField(lazy_gettext("Tenant ID"), widget=BS3TextFieldWidget()), "api_version": StringField( - lazy_gettext("API Version"), widget=BS3TextFieldWidget(), default="v1.0" + lazy_gettext("API Version"), widget=BS3TextFieldWidget(), default=APIVersion.v1.value ), "authority": StringField(lazy_gettext("Authority"), widget=BS3TextFieldWidget()), + "certificate_path": StringField(lazy_gettext("Certificate path"), widget=BS3TextFieldWidget()), + "certificate_data": StringField(lazy_gettext("Certificate data"), widget=BS3TextFieldWidget()), "scopes": StringField( lazy_gettext("Scopes"), widget=BS3TextFieldWidget(), - default="https://graph.microsoft.com/.default", + default=cls.DEFAULT_SCOPE, ), "disable_instance_discovery": BooleanField( lazy_gettext("Disable instance discovery"), default=False ), - "allowed_hosts": StringField(lazy_gettext("Allowed"), widget=BS3TextFieldWidget()), + "allowed_hosts": StringField(lazy_gettext("Allowed hosts"), widget=BS3TextFieldWidget()), "proxies": StringField(lazy_gettext("Proxies"), widget=BS3TextAreaFieldWidget()), - "stream": BooleanField(lazy_gettext("Stream"), default=False), "verify": BooleanField(lazy_gettext("Verify"), default=True), "trust_env": BooleanField(lazy_gettext("Trust environment"), default=True), "base_url": StringField(lazy_gettext("Base URL"), widget=BS3TextFieldWidget()), @@ -241,18 +247,17 @@ def get_conn(self) -> RequestAdapter: client_id = connection.login client_secret = connection.password config = connection.extra_dejson if connection.extra else {} - tenant_id = config.get("tenant_id") or config.get("tenantId") api_version = self.get_api_version(config) host = self.get_host(connection) base_url = config.get("base_url", urljoin(host, api_version)) authority = config.get("authority") proxies = self.proxies or config.get("proxies", {}) - msal_proxies = self.to_msal_proxies(authority=authority, proxies=proxies) httpx_proxies = self.to_httpx_proxies(proxies=proxies) scopes = config.get("scopes", self.scopes) + if isinstance(scopes, str): + scopes = scopes.split(",") verify = config.get("verify", True) trust_env = config.get("trust_env", False) - disable_instance_discovery = config.get("disable_instance_discovery", False) allowed_hosts = (config.get("allowed_hosts", authority) or "").split(",") self.log.info( @@ -262,7 +267,6 @@ def get_conn(self) -> RequestAdapter: ) self.log.info("Host: %s", host) self.log.info("Base URL: %s", base_url) - self.log.info("Tenant id: %s", tenant_id) self.log.info("Client id: %s", client_id) self.log.info("Client secret: %s", client_secret) self.log.info("API version: %s", api_version) @@ -271,19 +275,16 @@ def get_conn(self) -> RequestAdapter: self.log.info("Timeout: %s", self.timeout) self.log.info("Trust env: %s", trust_env) self.log.info("Authority: %s", authority) - self.log.info("Disable instance discovery: %s", disable_instance_discovery) self.log.info("Allowed hosts: %s", allowed_hosts) self.log.info("Proxies: %s", proxies) - self.log.info("MSAL Proxies: %s", msal_proxies) self.log.info("HTTPX Proxies: %s", httpx_proxies) - credentials = ClientSecretCredential( - tenant_id=tenant_id, # type: ignore - client_id=connection.login, - client_secret=connection.password, + credentials = self.get_credentials( + login=connection.login, + password=connection.password, + config=config, authority=authority, - proxies=msal_proxies, - disable_instance_discovery=disable_instance_discovery, - connection_verify=verify, + verify=verify, + proxies=proxies, ) http_client = GraphClientFactory.create_with_default_middleware( api_version=api_version, # type: ignore @@ -313,6 +314,48 @@ def get_conn(self) -> RequestAdapter: self._api_version = api_version return request_adapter + def get_credentials( + self, + login: str | None, + password: str | None, + config, + authority: str | None, + verify: bool, + proxies: dict, + ) -> ClientCredentialBase: + tenant_id = config.get("tenant_id") or config.get("tenantId") + certificate_path = config.get("certificate_path") + certificate_data = config.get("certificate_data") + disable_instance_discovery = config.get("disable_instance_discovery", False) + msal_proxies = self.to_msal_proxies(authority=authority, proxies=proxies) + self.log.info("Tenant id: %s", tenant_id) + self.log.info("Certificate path: %s", certificate_path) + self.log.info("Certificate data: %s", certificate_data is not None) + self.log.info("Authority: %s", authority) + self.log.info("Disable instance discovery: %s", disable_instance_discovery) + self.log.info("MSAL Proxies: %s", msal_proxies) + if certificate_path or certificate_data: + return CertificateCredential( + tenant_id=tenant_id, # type: ignore + client_id=login, # type: ignore + password=password, + certificate_path=certificate_path, + certificate_data=certificate_data.encode() if certificate_data else None, + authority=authority, + proxies=msal_proxies, + disable_instance_discovery=disable_instance_discovery, + connection_verify=verify, + ) + return ClientSecretCredential( + tenant_id=tenant_id, # type: ignore + client_id=login, # type: ignore + client_secret=password, # type: ignore + authority=authority, + proxies=msal_proxies, + disable_instance_discovery=disable_instance_discovery, + connection_verify=verify, + ) + def test_connection(self): """Test HTTP Connection.""" try: diff --git a/providers/src/airflow/providers/microsoft/azure/operators/msgraph.py b/providers/src/airflow/providers/microsoft/azure/operators/msgraph.py index 9a3fc197d4486..7aefd2971d4e6 100644 --- a/providers/src/airflow/providers/microsoft/azure/operators/msgraph.py +++ b/providers/src/airflow/providers/microsoft/azure/operators/msgraph.py @@ -71,6 +71,7 @@ class MSGraphAsyncOperator(BaseOperator): :param timeout: The HTTP timeout being used by the `KiotaRequestAdapter` (default is None). When no timeout is specified or set to None then there is no HTTP timeout on each request. :param proxies: A dict defining the HTTP proxies to be used (default is None). + :param scopes: The scopes to be used (default is ["https://graph.microsoft.com/.default"]). :param api_version: The API version of the Microsoft Graph API to be used (default is v1). You can pass an enum named APIVersion which has 2 possible members v1 and beta, or you can pass a string as `v1.0` or `beta`. @@ -110,6 +111,7 @@ def __init__( key: str = XCOM_RETURN_KEY, timeout: float | None = None, proxies: dict | None = None, + scopes: str | list[str] | None = None, api_version: APIVersion | str | None = None, pagination_function: Callable[[MSGraphAsyncOperator, dict, Context], tuple[str, dict]] | None = None, result_processor: Callable[[Context, Any], Any] = lambda context, result: result, @@ -130,6 +132,7 @@ def __init__( self.key = key self.timeout = timeout self.proxies = proxies + self.scopes = scopes self.api_version = api_version self.pagination_function = pagination_function or self.paginate self.result_processor = result_processor @@ -150,6 +153,7 @@ def execute(self, context: Context) -> None: conn_id=self.conn_id, timeout=self.timeout, proxies=self.proxies, + scopes=self.scopes, api_version=self.api_version, serializer=type(self.serializer), ), diff --git a/providers/src/airflow/providers/microsoft/azure/sensors/msgraph.py b/providers/src/airflow/providers/microsoft/azure/sensors/msgraph.py index 6b5622e2d7ae2..ecad1a34f16a9 100644 --- a/providers/src/airflow/providers/microsoft/azure/sensors/msgraph.py +++ b/providers/src/airflow/providers/microsoft/azure/sensors/msgraph.py @@ -47,6 +47,7 @@ class MSGraphSensor(BaseSensorOperator): :param method: The HTTP method being used to do the REST call (default is GET). :param conn_id: The HTTP Connection ID to run the operator against (templated). :param proxies: A dict defining the HTTP proxies to be used (default is None). + :param scopes: The scopes to be used (default is ["https://graph.microsoft.com/.default"]). :param api_version: The API version of the Microsoft Graph API to be used (default is v1). You can pass an enum named APIVersion which has 2 possible members v1 and beta, or you can pass a string as `v1.0` or `beta`. @@ -83,6 +84,7 @@ def __init__( data: dict[str, Any] | str | BytesIO | None = None, conn_id: str = KiotaRequestAdapterHook.default_conn_name, proxies: dict | None = None, + scopes: str | list[str] | None = None, api_version: APIVersion | str | None = None, event_processor: Callable[[Context, Any], bool] = lambda context, e: e.get("status") == "Succeeded", result_processor: Callable[[Context, Any], Any] = lambda context, result: result, @@ -101,6 +103,7 @@ def __init__( self.data = data self.conn_id = conn_id self.proxies = proxies + self.scopes = scopes self.api_version = api_version self.event_processor = event_processor self.result_processor = result_processor @@ -120,6 +123,7 @@ def execute(self, context: Context): conn_id=self.conn_id, timeout=self.timeout, proxies=self.proxies, + scopes=self.scopes, api_version=self.api_version, serializer=type(self.serializer), ), diff --git a/providers/src/airflow/providers/microsoft/azure/triggers/msgraph.py b/providers/src/airflow/providers/microsoft/azure/triggers/msgraph.py index 076f2f493ea89..4006ee6c3c0bb 100644 --- a/providers/src/airflow/providers/microsoft/azure/triggers/msgraph.py +++ b/providers/src/airflow/providers/microsoft/azure/triggers/msgraph.py @@ -90,6 +90,7 @@ class MSGraphTrigger(BaseTrigger): :param timeout: The HTTP timeout being used by the `KiotaRequestAdapter` (default is None). When no timeout is specified or set to None then there is no HTTP timeout on each request. :param proxies: A dict defining the HTTP proxies to be used (default is None). + :param scopes: The scopes to be used (default is ["https://graph.microsoft.com/.default"]). :param api_version: The API version of the Microsoft Graph API to be used (default is v1). You can pass an enum named APIVersion which has 2 possible members v1 and beta, or you can pass a string as `v1.0` or `beta`. @@ -121,6 +122,7 @@ def __init__( conn_id: str = KiotaRequestAdapterHook.default_conn_name, timeout: float | None = None, proxies: dict | None = None, + scopes: str | list[str] | None = None, api_version: APIVersion | str | None = None, serializer: type[ResponseSerializer] = ResponseSerializer, ): @@ -129,6 +131,7 @@ def __init__( conn_id=conn_id, timeout=timeout, proxies=proxies, + scopes=scopes, api_version=api_version, ) self.url = url @@ -157,6 +160,7 @@ def serialize(self) -> tuple[str, dict[str, Any]]: "conn_id": self.conn_id, "timeout": self.timeout, "proxies": self.proxies, + "scopes": self.hook.scopes, "api_version": self.api_version, "serializer": f"{self.serializer.__class__.__module__}.{self.serializer.__class__.__name__}", "url": self.url, diff --git a/providers/tests/microsoft/azure/hooks/test_msgraph.py b/providers/tests/microsoft/azure/hooks/test_msgraph.py index aff5d0226a1c4..3dbf8b9bf645b 100644 --- a/providers/tests/microsoft/azure/hooks/test_msgraph.py +++ b/providers/tests/microsoft/azure/hooks/test_msgraph.py @@ -86,6 +86,37 @@ def test_get_conn_with_custom_base_url(self): assert isinstance(actual, HttpxRequestAdapter) assert actual.base_url == "https://api.fabric.microsoft.com/v1" + def test_scopes_when_default(self): + with patch( + "airflow.hooks.base.BaseHook.get_connection", + side_effect=get_airflow_connection, + ): + hook = KiotaRequestAdapterHook(conn_id="msgraph_api") + + assert hook.scopes == [KiotaRequestAdapterHook.DEFAULT_SCOPE] + + def test_scopes_when_passed_as_string(self): + with patch( + "airflow.hooks.base.BaseHook.get_connection", + side_effect=get_airflow_connection, + ): + hook = KiotaRequestAdapterHook( + conn_id="msgraph_api", scopes="https://microsoft.sharepoint.com/.default" + ) + + assert hook.scopes == ["https://microsoft.sharepoint.com/.default"] + + def test_scopes_when_passed_as_list(self): + with patch( + "airflow.hooks.base.BaseHook.get_connection", + side_effect=get_airflow_connection, + ): + hook = KiotaRequestAdapterHook( + conn_id="msgraph_api", scopes=["https://microsoft.sharepoint.com/.default"] + ) + + assert hook.scopes == ["https://microsoft.sharepoint.com/.default"] + def test_api_version(self): with patch( "airflow.hooks.base.BaseHook.get_connection", diff --git a/providers/tests/microsoft/azure/triggers/test_msgraph.py b/providers/tests/microsoft/azure/triggers/test_msgraph.py index 0784d8d83177c..ce5a554fe1d52 100644 --- a/providers/tests/microsoft/azure/triggers/test_msgraph.py +++ b/providers/tests/microsoft/azure/triggers/test_msgraph.py @@ -24,8 +24,10 @@ from uuid import uuid4 import pendulum +from msgraph_core import APIVersion from airflow.exceptions import AirflowException +from airflow.providers.microsoft.azure.hooks.msgraph import KiotaRequestAdapterHook from airflow.providers.microsoft.azure.triggers.msgraph import ( MSGraphTrigger, ResponseSerializer, @@ -108,7 +110,7 @@ def test_serialize(self): actual = trigger.serialize() assert isinstance(actual, tuple) - assert actual[0] == "airflow.providers.microsoft.azure.triggers.msgraph.MSGraphTrigger" + assert actual[0] == f"{MSGraphTrigger.__module__}.{MSGraphTrigger.__name__}" assert actual[1] == { "url": "https://graph.microsoft.com/v1.0/me/drive/items", "path_parameters": None, @@ -121,8 +123,9 @@ def test_serialize(self): "conn_id": "msgraph_api", "timeout": None, "proxies": None, - "api_version": "v1.0", - "serializer": "airflow.providers.microsoft.azure.triggers.msgraph.ResponseSerializer", + "scopes": [KiotaRequestAdapterHook.DEFAULT_SCOPE], + "api_version": APIVersion.v1.value, + "serializer": f"{ResponseSerializer.__module__}.{ResponseSerializer.__name__}", } def test_template_fields(self):