MicrosoftAzureStorageApiModule

Common Microsoft Azure Storage code that will be appended into each Microsoft Azure Storage integration.

python · ApiModules

Source

import defusedxml.ElementTree as defused_ET
import demistomock as demisto  # noqa: F401
from CommonServerPython import *  # noqa: F401

MANAGED_IDENTITIES_TOKEN_URL = (
    "http://169.254.169.254/metadata/identity/oauth2/token?api-version=2018-02-01&resource=https://storage.azure.com/"
)
MANAGED_IDENTITIES_SYSTEM_ASSIGNED = "SYSTEM_ASSIGNED"


class MicrosoftStorageClient(BaseClient):  # type: ignore[name-defined]
    """
    Microsoft Azure Storage API Client
    """

    def __init__(
        self,
        server_url,
        verify,
        proxy,
        account_sas_token,
        storage_account_name,
        api_version,
        managed_identities_client_id: Optional[str] = None,  # type: ignore[name-defined]
    ):
        super().__init__(base_url=server_url, verify=verify, proxy=proxy)
        self._account_sas_token = account_sas_token or ""
        self._storage_account_name = storage_account_name
        self._api_version = api_version
        self._base_url = server_url

        self._managed_identities_client_id = managed_identities_client_id
        if self._managed_identities_client_id:
            token, _ = self._get_managed_identities_token()
            self._headers = {"Authorization": f"Bearer {token}"}

    def http_request(
        self,
        *args,
        url_suffix="",
        params=None,
        resp_type="response",
        headers=None,
        return_empty_response=False,
        full_url="",
        **kwargs,
    ):
        """
        Overrides Base client request function.
        Create and adds to the headers the Authorization Header component before sending the request.
        Parse Azure XML response.
        Args:
            url_suffix (str): Request URL suffix.
            params (dict): Request Params.
            resp_type (str): Determines which data format to return from the HTTP request.
            headers (dict): Request Header.
            return_empty_response (bool): Return the response itself if the return_code is 201 or 204.
            full_url (str): Request full URL.
        Returns:
            Response from API according to resp_type.
        """

        if "ok_codes" not in kwargs and not self._ok_codes:
            kwargs["ok_codes"] = (200, 201, 202, 204, 206, 404)

        if not full_url:
            # This logic will chain the SAS token along with the params
            # in order to create a valid URL for requests in Microsoft Azure Storage.
            # For example:
            # SAS token = '?sv=2020-08-04&ss=bt&spr=https&sig=t8'
            # params = '{'restype': 'directory', 'comp': 'list'}'
            # url_suffix = 'container'
            # The updated url_suffix after performing this logic will be:
            # url_suffix = 'container?sv=2020-08-04&ss=ay&spr=https&sig=s5&restype=directory&comp=list'
            params_query = self.params_dict_to_query_string(params, prefix="")
            uri_token_part = self._account_sas_token if self._account_sas_token.startswith("?") else f"?{self._account_sas_token}"
            url_suffix = f"{url_suffix}{uri_token_part}{params_query}"
            params = None

        default_headers = {"x-ms-version": self._api_version}

        if headers:
            default_headers.update(headers)

        if self._headers:
            default_headers.update(self._headers)

        response = super()._http_request(  # type: ignore[misc]
            *args,
            url_suffix=url_suffix,
            params=params,
            resp_type="response",
            headers=default_headers,
            full_url=full_url,
            **kwargs,
        )

        # 206 indicates Partial Content, reason will be in the warning header.
        # In that case, logs with the warning header will be written.
        if response.status_code == 206:
            demisto.debug(str(response.headers))

        is_response_empty_and_successful = response.status_code == 204 or response.status_code == 201
        if is_response_empty_and_successful and return_empty_response:
            return response

        # Handle 404 errors instead of raising them as exceptions:
        if response.status_code == 404:
            try:
                error_message = response.json()
            except Exception:
                error_message = f"Not Found - 404 Response \nContent: {response.content}"
            raise NotFoundError(error_message)

        try:
            if resp_type == "json":
                return response.json()
            if resp_type == "text":
                return response.text
            if resp_type == "content":
                return response.content
            if resp_type == "xml":
                defused_ET.parse(response.text)
            return response
        except ValueError as exception:
            raise DemistoException(
                f"Failed to parse json object from response:"  # type: ignore[name-defined]
                f" {response.content}",
                exception,
            )  # type: ignore[name-defined]

    def _get_managed_identities_token(self):
        """
        Gets a token based on the Azure Managed Identities mechanism
        in case user was configured the Azure VM and the other Azure resource correctly
        """
        try:
            # system assigned are restricted to one per resource and is tied to the lifecycle of the Azure resource
            # see https://learn.microsoft.com/en-us/azure/active-directory/managed-identities-azure-resources/overview
            demisto.debug("try to get token based on the Managed Identities")
            use_system_assigned = self._managed_identities_client_id == MANAGED_IDENTITIES_SYSTEM_ASSIGNED
            params = {}
            if not use_system_assigned:
                params["client_id"] = self._managed_identities_client_id
            response_json = requests.get(
                MANAGED_IDENTITIES_TOKEN_URL,  # type: ignore[name-defined]
                params=params,
                headers={"Metadata": "True"},
            ).json()  # type: ignore[name-defined]
            access_token = response_json.get("access_token")
            expires_in = int(response_json.get("expires_in", 3595))
            if access_token:
                return access_token, expires_in

            err = response_json.get("error_description")
        except Exception as e:
            err = f"{e!s}"  # type: ignore[name-defined]

        return_error(f"Error in Microsoft authorization with Azure Managed Identities: {err}")
        return None

    def params_dict_to_query_string(self, params: dict = None, prefix: str = "") -> str:
        """
        Convert request params to string query.
        Args:
            params (dict): Request Params.
            prefix (str): String prefix.

        Returns:
            str: String query.

        """
        if not params:
            return ""
        query = prefix
        for key, value in params.items():
            query += f"&{key}={value}"

        return query


class NotFoundError(Exception):
    """Exception raised for 404 - Not Found errors.
    Attributes:
        message -- explanation of the error
    """

    def __init__(self, message):
        self.message = message


def get_azure_managed_identities_client_id(params: dict) -> Optional[str]:  # type: ignore[name-defined]
    """ "extract the Azure Managed Identities from the demisto params

    Args:
        params (dict): the demisto params

    Returns:
        Optional[str]: if the use_managed_identities are True
        the managed_identities_client_id or MANAGED_IDENTITIES_SYSTEM_ASSIGNED
        will return, otherwise - None

    """
    auth_type = params.get("auth_type") or params.get("authentication_type")
    if params and (
        argToBoolean(
            params.get("use_managed_identities")  # type: ignore[name-defined]
            or auth_type == "Azure Managed Identities"
        )
    ):
        client_id = params.get("managed_identities_client_id", {}).get("password")
        return client_id or MANAGED_IDENTITIES_SYSTEM_ASSIGNED
    return None

README

To use the common Microsoft Azure Storage API logic , run the following command to import the MicrosoftAzureStorageApiModule.

def main():
    ...


from MicrosoftAzureStorageApiModule import *  # noqa: E402

if __name__ in ["builtins", "__main__"]:
    main()

Then, the MicrosoftStorageClient will be available for usage. For examples, see the Azure Storage Queue or Azure Storage Table integrations.