Skip to content

build: add user-based managed identity #122

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 9 commits into
base: dev
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
Expand Up @@ -31,3 +31,27 @@ updates:
commit-message:
# Prefix all commit messages with "build: "
prefix: "build"
- package-ecosystem: "pip"
# Files stored in `app` directory
directory: "/azurefunctions-extensions-bindings-cosmosdb"
schedule:
interval: "weekly"
commit-message:
# Prefix all commit messages with "build: "
prefix: "build"
- package-ecosystem: "pip"
# Files stored in `app` directory
directory: "/azurefunctions-extensions-bindings-eventhub"
schedule:
interval: "weekly"
commit-message:
# Prefix all commit messages with "build: "
prefix: "build"
- package-ecosystem: "pip"
# Files stored in `app` directory
directory: "/azurefunctions-extensions-bindings-servicebus"
schedule:
interval: "weekly"
commit-message:
# Prefix all commit messages with "build: "
prefix: "build"
Original file line number Diff line number Diff line change
@@ -1,59 +1,22 @@
# Copyright (c) Microsoft Corporation. All rights reserved.
# Licensed under the MIT License.

import json
from azurefunctions.extensions.base import Datum
from .blobSdkType import BlobSdkType

from azure.identity import DefaultAzureCredential
from azure.storage.blob import BlobServiceClient
from azurefunctions.extensions.base import Datum, SdkType
from .utils import get_connection_string, using_managed_identity


class BlobClient(SdkType):
class BlobClient(BlobSdkType):
def __init__(self, *, data: Datum) -> None:
# model_binding_data properties
self._data = data
self._using_managed_identity = False
self._version = None
self._source = None
self._content_type = None
self._connection = None
self._containerName = None
self._blobName = None
if self._data:
self._version = data.version
self._source = data.source
self._content_type = data.content_type
content_json = json.loads(data.content)
self._connection = get_connection_string(content_json.get("Connection"))
self._using_managed_identity = using_managed_identity(
content_json.get("Connection")
)
self._containerName = content_json.get("ContainerName")
self._blobName = content_json.get("BlobName")
super().__init__(data=data)

# Returns a BlobClient
def get_sdk_type(self):
"""
When using Managed Identity, the only way to create a BlobClient is
through a BlobServiceClient. There are two ways to create a
BlobServiceClient:
1. Through the constructor: this is the only option when using Managed Identity
2. Through from_connection_string: this is the only option when
not using Managed Identity

We track if Managed Identity is being used through a flag.
"""
if self._data:
blob_service_client = (
BlobServiceClient(
account_url=self._connection, credential=DefaultAzureCredential()
)
if self._using_managed_identity
else BlobServiceClient.from_connection_string(self._connection)
)
blob_service_client = super().get_sdk_type()
try:
return blob_service_client.get_blob_client(
container=self._containerName,
blob=self._blobName,
)
else:
raise ValueError(f"Unable to create {self.__class__.__name__} SDK type.")
except Exception as e:
raise ValueError(f"Unable to create {self.__class__.__name__} SDK type."
f"Exception: {e}")
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
# Copyright (c) Microsoft Corporation. All rights reserved.
# Licensed under the MIT License.
import json

from azurefunctions.extensions.base import Datum, SdkType
from .utils import (validate_connection_name,
service_client_factory)


class BlobSdkType(SdkType):
def __init__(self, *, data: Datum) -> None:
# model_binding_data properties
self._data = data
self._version = None
self._source = None
self._content_type = None
self._connection = None
self._containerName = None
self._blobName = None
if self._data:
self._version = data.version
self._source = data.source
self._content_type = data.content_type
content_json = json.loads(data.content)
self._connection = validate_connection_name(
content_json.get("Connection"))
self._containerName = content_json.get("ContainerName")
self._blobName = content_json.get("BlobName")

def get_sdk_type(self):
"""
When using Managed Identity, the only way to create a BlobClient is
through a BlobServiceClient. There are two ways to create a
BlobServiceClient:
1. Through the constructor: this is the only option when using Managed Identity
1a. If system-based MI, the credential is DefaultAzureCredential
1b. If user-based MI, the credential is ManagedIdentityCredential
2. Through from_connection_string: this is the only option when
not using Managed Identity

We track if Managed Identity is being used through a flag.
"""
if self._data:
blob_service_client = service_client_factory(self._connection)
return blob_service_client
else:
raise ValueError("Unable to create Blob Service Client type.")
Original file line number Diff line number Diff line change
@@ -1,49 +1,21 @@
# Copyright (c) Microsoft Corporation. All rights reserved.
# Licensed under the MIT License.

import json
from azurefunctions.extensions.base import Datum
from .blobSdkType import BlobSdkType

from azure.identity import DefaultAzureCredential
from azure.storage.blob import BlobServiceClient
from azurefunctions.extensions.base import Datum, SdkType
from .utils import get_connection_string, using_managed_identity


class ContainerClient(SdkType):
class ContainerClient(BlobSdkType):
def __init__(self, *, data: Datum) -> None:
# model_binding_data properties
self._data = data
self._using_managed_identity = False
self._version = ""
self._source = ""
self._content_type = ""
self._connection = ""
self._containerName = ""
self._blobName = ""
if self._data:
self._version = data.version
self._source = data.source
self._content_type = data.content_type
content_json = json.loads(data.content)
self._connection = get_connection_string(content_json.get("Connection"))
self._using_managed_identity = using_managed_identity(
content_json.get("Connection")
)
self._containerName = content_json.get("ContainerName")
self._blobName = content_json.get("BlobName")
super().__init__(data=data)

# Returns a ContainerClient
def get_sdk_type(self):
if self._data:
blob_service_client = (
BlobServiceClient(
account_url=self._connection, credential=DefaultAzureCredential()
)
if self._using_managed_identity
else BlobServiceClient.from_connection_string(self._connection)
)
blob_service_client = super().get_sdk_type()
try:
return blob_service_client.get_container_client(
container=self._containerName
)
else:
raise ValueError(f"Unable to create {self.__class__.__name__} SDK type.")
except Exception as e:
raise ValueError(f"Unable to create {self.__class__.__name__} SDK type."
f"Exception: {e}")
Original file line number Diff line number Diff line change
@@ -1,51 +1,23 @@
# Copyright (c) Microsoft Corporation. All rights reserved.
# Licensed under the MIT License.

import json
from azurefunctions.extensions.base import Datum
from .blobSdkType import BlobSdkType

from azure.identity import DefaultAzureCredential
from azure.storage.blob import BlobServiceClient
from azurefunctions.extensions.base import Datum, SdkType
from .utils import get_connection_string, using_managed_identity


class StorageStreamDownloader(SdkType):
class StorageStreamDownloader(BlobSdkType):
def __init__(self, *, data: Datum) -> None:
# model_binding_data properties
self._data = data
self._using_managed_identity = False
self._version = ""
self._source = ""
self._content_type = ""
self._connection = ""
self._containerName = ""
self._blobName = ""
if self._data:
self._version = data.version
self._source = data.source
self._content_type = data.content_type
content_json = json.loads(data.content)
self._connection = get_connection_string(content_json.get("Connection"))
self._using_managed_identity = using_managed_identity(
content_json.get("Connection")
)
self._containerName = content_json.get("ContainerName")
self._blobName = content_json.get("BlobName")
super().__init__(data=data)

# Returns a StorageStreamDownloader
def get_sdk_type(self):
if self._data:
blob_service_client = (
BlobServiceClient(
account_url=self._connection, credential=DefaultAzureCredential()
)
if self._using_managed_identity
else BlobServiceClient.from_connection_string(self._connection)
)
# download_blob() returns a StorageStreamDownloader object
blob_service_client = super().get_sdk_type()

try:
return blob_service_client.get_blob_client(
container=self._containerName,
blob=self._blobName,
).download_blob()
else:
raise ValueError(f"Unable to create {self.__class__.__name__} SDK type.")
except Exception as e:
raise ValueError(f"Unable to create {self.__class__.__name__} SDK type."
f"Exception: {e}")
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,27 @@
# Licensed under the MIT License.
import os

from azure.identity import DefaultAzureCredential, ManagedIdentityCredential
from azure.storage.blob import BlobServiceClient

def get_connection_string(connection_string: str) -> str:

def validate_connection_name(connection_name: str) -> str:
"""
Validates and returns the connection name. The setting must
not be None - if it is, a ValueError will be raised.
"""
Validates and returns the connection string. If the connection string is
not an App Setting, an error will be thrown.
if connection_name is None:
raise ValueError(
"Storage account connection name cannot be None. "
"Please provide a connection setting."
)
else:
return connection_name


def get_connection_string(connection_name: str) -> str:
"""
Returns the connection string.

When using managed identity, the connection string variable name is formatted
like so:
Expand All @@ -21,30 +37,63 @@ def get_connection_string(connection_string: str) -> str:
3. Using managed identity for blob trigger: __blobServiceUri must be appended
4. None of these cases existed, so the connection variable is invalid.
"""
if connection_string is None:
raise ValueError(
"Storage account connection string cannot be None. "
"Please provide a connection string."
)
elif connection_string in os.environ:
return os.getenv(connection_string)
elif connection_string + "__serviceUri" in os.environ:
return os.getenv(connection_string + "__serviceUri")
elif connection_string + "__blobServiceUri" in os.environ:
return os.getenv(connection_string + "__blobServiceUri")
if connection_name in os.environ:
return os.getenv(connection_name)
elif connection_name + "__serviceUri" in os.environ:
return os.getenv(connection_name + "__serviceUri")
elif connection_name + "__blobServiceUri" in os.environ:
return os.getenv(connection_name + "__blobServiceUri")
else:
raise ValueError(
f"Storage account connection string {connection_string} does not exist. "
f"Storage account connection name {connection_name} does not exist. "
f"Please make sure that it is a defined App Setting."
)


def using_managed_identity(connection_name: str) -> bool:
def using_system_managed_identity(connection_name: str) -> bool:
"""
To determine if managed identity is being used, we check if the provided
connection string has either of the two suffixes:
To determine if system-assigned managed identity is being used, we check if
the provided connection string has either of the two suffixes:
__serviceUri or __blobServiceUri.
"""
return (os.getenv(connection_name + "__serviceUri") is not None) or (
os.getenv(connection_name + "__blobServiceUri") is not None
)


def using_user_managed_identity(connection_name: str) -> bool:
"""
To determine if user-assigned managed identity is being used, we check if
the provided connection string has the following suffixes:
__credential AND __clientId
"""
return (os.getenv(connection_name + "__credential") is not None) and (
os.getenv(connection_name + "__clientId") is not None
)


def service_client_factory(connection: str):
"""
Returns the BlobServiceClient.

How the BlobServiceClient is created depends on the authentication
strategy of the customer.

There are 3 cases:
1. The customer is using user-assigned managed identity -> the BlobServiceClient
must be created using a ManagedIdentityCredential.
2. The customer is using system based managed identity -> the BlobServiceClient
must be created using a DefaultAzureCredential.
3. The customer is not using managed identity -> the BlobServiceClient must
be created using a connection string.
"""
connection_string = get_connection_string(connection)
if using_user_managed_identity(connection):
return BlobServiceClient(account_url=connection_string,
credential=ManagedIdentityCredential(
client_id=os.getenv(connection + "__clientId")))
elif using_system_managed_identity(connection):
return BlobServiceClient(account_url=connection_string,
credential=DefaultAzureCredential())
else:
return BlobServiceClient.from_connection_string(connection_string)
Loading