Skip to content

Commit be0dc1a

Browse files
build: add user-based managed identity (#122)
* Initial changes for supporting user-based MI * fix tests * fix user assigned check & tests * Working user-assigned MI * add unit tests * update dependabot * feedback * skip cosmosdb tests --------- Co-authored-by: Gavin Aguiar <[email protected]>
1 parent 5da034a commit be0dc1a

File tree

11 files changed

+309
-173
lines changed

11 files changed

+309
-173
lines changed

.github/dependabot.yml

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,3 +31,27 @@ updates:
3131
commit-message:
3232
# Prefix all commit messages with "build: "
3333
prefix: "build"
34+
- package-ecosystem: "pip"
35+
# Files stored in `app` directory
36+
directory: "/azurefunctions-extensions-bindings-cosmosdb"
37+
schedule:
38+
interval: "weekly"
39+
commit-message:
40+
# Prefix all commit messages with "build: "
41+
prefix: "build"
42+
- package-ecosystem: "pip"
43+
# Files stored in `app` directory
44+
directory: "/azurefunctions-extensions-bindings-eventhub"
45+
schedule:
46+
interval: "weekly"
47+
commit-message:
48+
# Prefix all commit messages with "build: "
49+
prefix: "build"
50+
- package-ecosystem: "pip"
51+
# Files stored in `app` directory
52+
directory: "/azurefunctions-extensions-bindings-servicebus"
53+
schedule:
54+
interval: "weekly"
55+
commit-message:
56+
# Prefix all commit messages with "build: "
57+
prefix: "build"
Lines changed: 10 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -1,59 +1,22 @@
11
# Copyright (c) Microsoft Corporation. All rights reserved.
22
# Licensed under the MIT License.
33

4-
import json
4+
from azurefunctions.extensions.base import Datum
5+
from .blobSdkType import BlobSdkType
56

6-
from azure.identity import DefaultAzureCredential
7-
from azure.storage.blob import BlobServiceClient
8-
from azurefunctions.extensions.base import Datum, SdkType
9-
from .utils import get_connection_string, using_managed_identity
107

11-
12-
class BlobClient(SdkType):
8+
class BlobClient(BlobSdkType):
139
def __init__(self, *, data: Datum) -> None:
14-
# model_binding_data properties
15-
self._data = data
16-
self._using_managed_identity = False
17-
self._version = None
18-
self._source = None
19-
self._content_type = None
20-
self._connection = None
21-
self._containerName = None
22-
self._blobName = None
23-
if self._data:
24-
self._version = data.version
25-
self._source = data.source
26-
self._content_type = data.content_type
27-
content_json = json.loads(data.content)
28-
self._connection = get_connection_string(content_json.get("Connection"))
29-
self._using_managed_identity = using_managed_identity(
30-
content_json.get("Connection")
31-
)
32-
self._containerName = content_json.get("ContainerName")
33-
self._blobName = content_json.get("BlobName")
10+
super().__init__(data=data)
3411

12+
# Returns a BlobClient
3513
def get_sdk_type(self):
36-
"""
37-
When using Managed Identity, the only way to create a BlobClient is
38-
through a BlobServiceClient. There are two ways to create a
39-
BlobServiceClient:
40-
1. Through the constructor: this is the only option when using Managed Identity
41-
2. Through from_connection_string: this is the only option when
42-
not using Managed Identity
43-
44-
We track if Managed Identity is being used through a flag.
45-
"""
46-
if self._data:
47-
blob_service_client = (
48-
BlobServiceClient(
49-
account_url=self._connection, credential=DefaultAzureCredential()
50-
)
51-
if self._using_managed_identity
52-
else BlobServiceClient.from_connection_string(self._connection)
53-
)
14+
blob_service_client = super().get_sdk_type()
15+
try:
5416
return blob_service_client.get_blob_client(
5517
container=self._containerName,
5618
blob=self._blobName,
5719
)
58-
else:
59-
raise ValueError(f"Unable to create {self.__class__.__name__} SDK type.")
20+
except Exception as e:
21+
raise ValueError(f"Unable to create {self.__class__.__name__} SDK type."
22+
f"Exception: {e}")
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
# Copyright (c) Microsoft Corporation. All rights reserved.
2+
# Licensed under the MIT License.
3+
import json
4+
5+
from azurefunctions.extensions.base import Datum, SdkType
6+
from .utils import (validate_connection_name,
7+
service_client_factory)
8+
9+
10+
class BlobSdkType(SdkType):
11+
def __init__(self, *, data: Datum) -> None:
12+
# model_binding_data properties
13+
self._data = data
14+
self._version = None
15+
self._source = None
16+
self._content_type = None
17+
self._connection = None
18+
self._containerName = None
19+
self._blobName = None
20+
if self._data:
21+
self._version = data.version
22+
self._source = data.source
23+
self._content_type = data.content_type
24+
content_json = json.loads(data.content)
25+
self._connection = validate_connection_name(
26+
content_json.get("Connection"))
27+
self._containerName = content_json.get("ContainerName")
28+
self._blobName = content_json.get("BlobName")
29+
30+
def get_sdk_type(self):
31+
"""
32+
When using Managed Identity, the only way to create a BlobClient is
33+
through a BlobServiceClient. There are two ways to create a
34+
BlobServiceClient:
35+
1. Through the constructor: this is the only option when using Managed Identity
36+
1a. If system-based MI, the credential is DefaultAzureCredential
37+
1b. If user-based MI, the credential is ManagedIdentityCredential
38+
2. Through from_connection_string: this is the only option when
39+
not using Managed Identity
40+
41+
We track if Managed Identity is being used through a flag.
42+
"""
43+
if self._data:
44+
blob_service_client = service_client_factory(self._connection)
45+
return blob_service_client
46+
else:
47+
raise ValueError("Unable to create Blob Service Client type.")
Lines changed: 9 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -1,49 +1,21 @@
11
# Copyright (c) Microsoft Corporation. All rights reserved.
22
# Licensed under the MIT License.
33

4-
import json
4+
from azurefunctions.extensions.base import Datum
5+
from .blobSdkType import BlobSdkType
56

6-
from azure.identity import DefaultAzureCredential
7-
from azure.storage.blob import BlobServiceClient
8-
from azurefunctions.extensions.base import Datum, SdkType
9-
from .utils import get_connection_string, using_managed_identity
107

11-
12-
class ContainerClient(SdkType):
8+
class ContainerClient(BlobSdkType):
139
def __init__(self, *, data: Datum) -> None:
14-
# model_binding_data properties
15-
self._data = data
16-
self._using_managed_identity = False
17-
self._version = ""
18-
self._source = ""
19-
self._content_type = ""
20-
self._connection = ""
21-
self._containerName = ""
22-
self._blobName = ""
23-
if self._data:
24-
self._version = data.version
25-
self._source = data.source
26-
self._content_type = data.content_type
27-
content_json = json.loads(data.content)
28-
self._connection = get_connection_string(content_json.get("Connection"))
29-
self._using_managed_identity = using_managed_identity(
30-
content_json.get("Connection")
31-
)
32-
self._containerName = content_json.get("ContainerName")
33-
self._blobName = content_json.get("BlobName")
10+
super().__init__(data=data)
3411

3512
# Returns a ContainerClient
3613
def get_sdk_type(self):
37-
if self._data:
38-
blob_service_client = (
39-
BlobServiceClient(
40-
account_url=self._connection, credential=DefaultAzureCredential()
41-
)
42-
if self._using_managed_identity
43-
else BlobServiceClient.from_connection_string(self._connection)
44-
)
14+
blob_service_client = super().get_sdk_type()
15+
try:
4516
return blob_service_client.get_container_client(
4617
container=self._containerName
4718
)
48-
else:
49-
raise ValueError(f"Unable to create {self.__class__.__name__} SDK type.")
19+
except Exception as e:
20+
raise ValueError(f"Unable to create {self.__class__.__name__} SDK type."
21+
f"Exception: {e}")
Lines changed: 10 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -1,51 +1,23 @@
11
# Copyright (c) Microsoft Corporation. All rights reserved.
22
# Licensed under the MIT License.
33

4-
import json
4+
from azurefunctions.extensions.base import Datum
5+
from .blobSdkType import BlobSdkType
56

6-
from azure.identity import DefaultAzureCredential
7-
from azure.storage.blob import BlobServiceClient
8-
from azurefunctions.extensions.base import Datum, SdkType
9-
from .utils import get_connection_string, using_managed_identity
107

11-
12-
class StorageStreamDownloader(SdkType):
8+
class StorageStreamDownloader(BlobSdkType):
139
def __init__(self, *, data: Datum) -> None:
14-
# model_binding_data properties
15-
self._data = data
16-
self._using_managed_identity = False
17-
self._version = ""
18-
self._source = ""
19-
self._content_type = ""
20-
self._connection = ""
21-
self._containerName = ""
22-
self._blobName = ""
23-
if self._data:
24-
self._version = data.version
25-
self._source = data.source
26-
self._content_type = data.content_type
27-
content_json = json.loads(data.content)
28-
self._connection = get_connection_string(content_json.get("Connection"))
29-
self._using_managed_identity = using_managed_identity(
30-
content_json.get("Connection")
31-
)
32-
self._containerName = content_json.get("ContainerName")
33-
self._blobName = content_json.get("BlobName")
10+
super().__init__(data=data)
3411

3512
# Returns a StorageStreamDownloader
3613
def get_sdk_type(self):
37-
if self._data:
38-
blob_service_client = (
39-
BlobServiceClient(
40-
account_url=self._connection, credential=DefaultAzureCredential()
41-
)
42-
if self._using_managed_identity
43-
else BlobServiceClient.from_connection_string(self._connection)
44-
)
45-
# download_blob() returns a StorageStreamDownloader object
14+
blob_service_client = super().get_sdk_type()
15+
16+
try:
4617
return blob_service_client.get_blob_client(
4718
container=self._containerName,
4819
blob=self._blobName,
4920
).download_blob()
50-
else:
51-
raise ValueError(f"Unable to create {self.__class__.__name__} SDK type.")
21+
except Exception as e:
22+
raise ValueError(f"Unable to create {self.__class__.__name__} SDK type."
23+
f"Exception: {e}")

azurefunctions-extensions-bindings-blob/azurefunctions/extensions/bindings/blob/utils.py

Lines changed: 67 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,27 @@
22
# Licensed under the MIT License.
33
import os
44

5+
from azure.identity import DefaultAzureCredential, ManagedIdentityCredential
6+
from azure.storage.blob import BlobServiceClient
57

6-
def get_connection_string(connection_string: str) -> str:
8+
9+
def validate_connection_name(connection_name: str) -> str:
10+
"""
11+
Validates and returns the connection name. The setting must
12+
not be None - if it is, a ValueError will be raised.
713
"""
8-
Validates and returns the connection string. If the connection string is
9-
not an App Setting, an error will be thrown.
14+
if connection_name is None:
15+
raise ValueError(
16+
"Storage account connection name cannot be None. "
17+
"Please provide a connection setting."
18+
)
19+
else:
20+
return connection_name
21+
22+
23+
def get_connection_string(connection_name: str) -> str:
24+
"""
25+
Returns the connection string.
1026
1127
When using managed identity, the connection string variable name is formatted
1228
like so:
@@ -21,30 +37,63 @@ def get_connection_string(connection_string: str) -> str:
2137
3. Using managed identity for blob trigger: __blobServiceUri must be appended
2238
4. None of these cases existed, so the connection variable is invalid.
2339
"""
24-
if connection_string is None:
25-
raise ValueError(
26-
"Storage account connection string cannot be None. "
27-
"Please provide a connection string."
28-
)
29-
elif connection_string in os.environ:
30-
return os.getenv(connection_string)
31-
elif connection_string + "__serviceUri" in os.environ:
32-
return os.getenv(connection_string + "__serviceUri")
33-
elif connection_string + "__blobServiceUri" in os.environ:
34-
return os.getenv(connection_string + "__blobServiceUri")
40+
if connection_name in os.environ:
41+
return os.getenv(connection_name)
42+
elif connection_name + "__serviceUri" in os.environ:
43+
return os.getenv(connection_name + "__serviceUri")
44+
elif connection_name + "__blobServiceUri" in os.environ:
45+
return os.getenv(connection_name + "__blobServiceUri")
3546
else:
3647
raise ValueError(
37-
f"Storage account connection string {connection_string} does not exist. "
48+
f"Storage account connection name {connection_name} does not exist. "
3849
f"Please make sure that it is a defined App Setting."
3950
)
4051

4152

42-
def using_managed_identity(connection_name: str) -> bool:
53+
def using_system_managed_identity(connection_name: str) -> bool:
4354
"""
44-
To determine if managed identity is being used, we check if the provided
45-
connection string has either of the two suffixes:
55+
To determine if system-assigned managed identity is being used, we check if
56+
the provided connection string has either of the two suffixes:
4657
__serviceUri or __blobServiceUri.
4758
"""
4859
return (os.getenv(connection_name + "__serviceUri") is not None) or (
4960
os.getenv(connection_name + "__blobServiceUri") is not None
5061
)
62+
63+
64+
def using_user_managed_identity(connection_name: str) -> bool:
65+
"""
66+
To determine if user-assigned managed identity is being used, we check if
67+
the provided connection string has the following suffixes:
68+
__credential AND __clientId
69+
"""
70+
return (os.getenv(connection_name + "__credential") is not None) and (
71+
os.getenv(connection_name + "__clientId") is not None
72+
)
73+
74+
75+
def service_client_factory(connection: str):
76+
"""
77+
Returns the BlobServiceClient.
78+
79+
How the BlobServiceClient is created depends on the authentication
80+
strategy of the customer.
81+
82+
There are 3 cases:
83+
1. The customer is using user-assigned managed identity -> the BlobServiceClient
84+
must be created using a ManagedIdentityCredential.
85+
2. The customer is using system based managed identity -> the BlobServiceClient
86+
must be created using a DefaultAzureCredential.
87+
3. The customer is not using managed identity -> the BlobServiceClient must
88+
be created using a connection string.
89+
"""
90+
connection_string = get_connection_string(connection)
91+
if using_user_managed_identity(connection):
92+
return BlobServiceClient(account_url=connection_string,
93+
credential=ManagedIdentityCredential(
94+
client_id=os.getenv(connection + "__clientId")))
95+
elif using_system_managed_identity(connection):
96+
return BlobServiceClient(account_url=connection_string,
97+
credential=DefaultAzureCredential())
98+
else:
99+
return BlobServiceClient.from_connection_string(connection_string)

0 commit comments

Comments
 (0)