diff --git a/sdk/appconfiguration/azure-appconfiguration-provider/CHANGELOG.md b/sdk/appconfiguration/azure-appconfiguration-provider/CHANGELOG.md new file mode 100644 index 000000000000..c3f16119c9f4 --- /dev/null +++ b/sdk/appconfiguration/azure-appconfiguration-provider/CHANGELOG.md @@ -0,0 +1,20 @@ +# Release History + +## 1.0.0b1 (Unreleased) + +New Azure App Configuration Provider + +Provides additional support above the Azure App Configuration SDK. Enables: +* Connecting to an Azure App Configuration store +* Selecting multiple keys using Setting Selector +* Resolve Key Vault References when supplied AzureAppConfigurationKeyVaultOptions + +The Azure App Configuration Provider once loaded returns a dictionary of key/value pairs to use in configuration. + +```python +endpoint = "https://.azconfig.io" +default_credential = DefaultAzureCredential() +config = AzureAppConfigurationProvider.load( + endpoint=endpoint, credential=default_credential) +print(config["message"]) +``` \ No newline at end of file diff --git a/sdk/appconfiguration/azure-appconfiguration-provider/LICENSE b/sdk/appconfiguration/azure-appconfiguration-provider/LICENSE new file mode 100644 index 000000000000..63447fd8bbbf --- /dev/null +++ b/sdk/appconfiguration/azure-appconfiguration-provider/LICENSE @@ -0,0 +1,21 @@ +Copyright (c) Microsoft Corporation. + +MIT License + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/sdk/appconfiguration/azure-appconfiguration-provider/MANIFEST.in b/sdk/appconfiguration/azure-appconfiguration-provider/MANIFEST.in new file mode 100644 index 000000000000..29c30ac7f9c3 --- /dev/null +++ b/sdk/appconfiguration/azure-appconfiguration-provider/MANIFEST.in @@ -0,0 +1,9 @@ +recursive-include tests *.py +include *.md +include LICENSE +include azure/__init__.py +include azure/appconfiguration/__init__.py +include azure/appconfiguration/provider/*.py +recursive-include samples *.py *.md +recursive-include doc *.rst +include azure/appconfiguration/provider/py.typed \ No newline at end of file diff --git a/sdk/appconfiguration/azure-appconfiguration-provider/README.md b/sdk/appconfiguration/azure-appconfiguration-provider/README.md new file mode 100644 index 000000000000..1a762bfbefd8 --- /dev/null +++ b/sdk/appconfiguration/azure-appconfiguration-provider/README.md @@ -0,0 +1,142 @@ +# Azure App Configuration Python Provider client library for Python + +Azure App Configuration is a managed service that helps developers centralize their application configurations simply and securely. This provider adds additional functionality above the azure-sdk-for-python. + +Using the provider enables loading sets of configurations from an Azure App Configuration store in a managed way. + +## Getting started + +### Get credentials + +Use the [Azure CLI][azure_cli] snippet below to get the connection string from the Configuration Store. + +```Powershell +az appconfig credential list --name +``` + +Alternatively, get the connection string from the Azure Portal. + +### Creating a provider + +You can create a client with a connection string: + +```python +config = AzureAppConfigurationProvider.load(connection_string="your-connection-string") +``` + +or with AAD: + +```python +config = AzureAppConfigurationProvider.load(endpoint="your-endpoint", credential=DefaultAzureCredential()) +``` + +these providers will by default load all configurations with `(No Label)` from your configuration store. + +### Features + +Currently the Azure App Configuration Provider enables: + +* Connecting to an App Configuration Store using a connection string or Azure Active Directory. +* Selecting multiple sets of configurations using `SettingSelector`. +* Trim prefixes off key names. +* Resolving Key Vault References, requires AAD. +* Secret Resolver, resolve Key Vault References locally without connecting to Key Vault. +* Json Content Type + +#### Future Features + +List of features we are going to add to the Python Provider in the future. + +* Geo-Replication support +* Feature Management +* Dynamic Refresh +* Configuration Placeholders + +## Examples + +### Selecting configurations + +You can refine or expand the configurations loaded from your store by using `SettingSelector`s. Setting selectors provide a way to pass a key filter and label filter into the provider. + +```python +selects = {SettingSelector(key_filter="*", label_filter="\0"), SettingSelector(key_filter="*", label_filter="dev")} +config = AzureAppConfigurationProvider.load( + endpoint=endpoint, credential=default_credential, selects=selects) +``` +In this example all configuration with empty label and the dev label are loaded. Because the dev selector is listed last, any configurations from dev take priority over those with `(No Label)` when duplicates are found. + +### Trimming Keys + +You can trim the prefix off of keys by providing a list of trimmed key prefixes to the provider. For example, if you have the key(s) like `/application/message` in your configuration store, you could trim `/application/` from them. + +```python +trimmed_key_prefixes={"/application/"} +config = AzureAppConfigurationProvider.load( + endpoint=endpoint, credential=default_credential, trimmed_key_prefixes=trimmed_key_prefixes) +print(config["message"]) +``` + +### Resolving Key Vault References + +Key Vault References can be resolved by providing credentials to your key vault to the provider using `AzureAppConfigurationKeyVaultOptions`. + +#### With Credentials + +You can provide `AzureAppConfigurationKeyVaultOptions` with a credential and all key vault references will be resolved with it. The provider will attempt to connect to any key vault referenced with the credential provided. + +```python +key_vault_options = AzureAppConfigurationKeyVaultOptions(credential=default_credential) +config = AzureAppConfigurationProvider.load(endpoint=endpoint, credential=default_credential, key_vault_options=key_vault_options) +``` +### With Clients + +You can provide `AzureAppConfigurationKeyVaultOptions` with a list of `SecretClients`. + +```python +key_vault_options = AzureAppConfigurationKeyVaultOptions( + secret_clients={SecretClient( + vault_url=key_vault_uri, credential=default_credential)}) +config = AzureAppConfigurationProvider.load(endpoint=endpoint, credential=default_credential, key_vault_options=key_vault_options) +``` + +### Secret Resolver + +If no Credentials or Clients are provided a secret resolver can be used. Secret resolver provides a way to return any value you want to a key vault reference. + +```python +def secret_resolver(uri): + return "From Secret Resolver" + +key_vault_options = AzureAppConfigurationKeyVaultOptions( + secret_resolver=secret_resolver) +config = AzureAppConfigurationProvider.load( + endpoint=endpoint, credential=default_credential, key_vault_options=key_vault_options) +``` + +## Key concepts + +## Troubleshooting + +## Next steps + +## Contributing + +This project welcomes contributions and suggestions. Most contributions require +you to agree to a Contributor License Agreement (CLA) declaring that you have +the right to, and actually do, grant us the rights to use your contribution. +For details, visit https://cla.microsoft.com. + +When you submit a pull request, a CLA-bot will automatically determine whether +you need to provide a CLA and decorate the PR appropriately (e.g., label, +comment). Simply follow the instructions provided by the bot. You will only +need to do this once across all repos using our CLA. + +This project has adopted the +[Microsoft Open Source Code of Conduct][code_of_conduct]. For more information, +see the Code of Conduct FAQ or contact opencode@microsoft.com with any +additional questions or comments. + +[cla]: https://cla.microsoft.com +[code_of_conduct]: https://opensource.microsoft.com/codeofconduct/ +[coc_faq]: https://opensource.microsoft.com/codeofconduct/faq/ +[coc_contact]: mailto:opencode@microsoft.com \ No newline at end of file diff --git a/sdk/appconfiguration/azure-appconfiguration-provider/azure/__init__.py b/sdk/appconfiguration/azure-appconfiguration-provider/azure/__init__.py new file mode 100644 index 000000000000..d55ccad1f573 --- /dev/null +++ b/sdk/appconfiguration/azure-appconfiguration-provider/azure/__init__.py @@ -0,0 +1 @@ +__path__ = __import__("pkgutil").extend_path(__path__, __name__) # type: ignore diff --git a/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/__init__.py b/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/__init__.py new file mode 100644 index 000000000000..d55ccad1f573 --- /dev/null +++ b/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/__init__.py @@ -0,0 +1 @@ +__path__ = __import__("pkgutil").extend_path(__path__, __name__) # type: ignore diff --git a/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/__init__.py b/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/__init__.py new file mode 100644 index 000000000000..a29ac854e457 --- /dev/null +++ b/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/__init__.py @@ -0,0 +1,14 @@ +# ------------------------------------------------------------------------ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# ------------------------------------------------------------------------- + +from ._azureappconfigurationprovider import AzureAppConfigurationProvider +from ._azureappconfigurationkeyvaultoptions import AzureAppConfigurationKeyVaultOptions +from ._settingselector import SettingSelector + +from ._version import VERSION + +__version__ = VERSION +__all__ = ["AzureAppConfigurationProvider", "AzureAppConfigurationKeyVaultOptions", "SettingSelector"] diff --git a/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_azure_appconfiguration_provider_error.py b/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_azure_appconfiguration_provider_error.py new file mode 100644 index 000000000000..3c7021cb71ba --- /dev/null +++ b/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_azure_appconfiguration_provider_error.py @@ -0,0 +1,9 @@ +# ------------------------------------------------------------------------ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# ------------------------------------------------------------------------- + + +class KeyVaultReferenceError(ValueError): + """Raised when a Key Vault reference is invalid.""" diff --git a/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_azureappconfigurationkeyvaultoptions.py b/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_azureappconfigurationkeyvaultoptions.py new file mode 100644 index 000000000000..7a6519e1b9be --- /dev/null +++ b/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_azureappconfigurationkeyvaultoptions.py @@ -0,0 +1,29 @@ +# ------------------------------------------------------------------------ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# ------------------------------------------------------------------------- + + +class AzureAppConfigurationKeyVaultOptions: + """ + Options for connecting to Key Vault. + + :param credential: A credential for authenticating with the key vault. This is optional if secret_clients is + provided. + :type credential: ~azure.core.credentials.TokenCredential + :param secret_clients: A list of SecretClient from azure-keyvault-secrets. This is optional if credential is + provided. + :type secret_clients: list[~azure.keyvault.secrets.SecretClient] + :param secret_resolver: A function that takes a URI and returns a value. + :type secret_resolver: callable + """ + + def __init__(self, credential=None, secret_clients=None, secret_resolver=None): + # type: (TokenCredential, List[SecretClient], Callable) -> None + self.credential = credential + self.secret_clients = secret_clients + self.secret_resolver = secret_resolver + + if self.secret_clients is None: + self.secret_clients = {} diff --git a/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_azureappconfigurationprovider.py b/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_azureappconfigurationprovider.py new file mode 100644 index 000000000000..203ebf6165ca --- /dev/null +++ b/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_azureappconfigurationprovider.py @@ -0,0 +1,211 @@ +# ------------------------------------------------------------------------ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# ------------------------------------------------------------------------- + +import json +from azure.appconfiguration import AzureAppConfigurationClient +from azure.keyvault.secrets import SecretClient, KeyVaultSecretIdentifier +from azure.core.exceptions import ResourceNotFoundError +from ._settingselector import SettingSelector +from ._azure_appconfiguration_provider_error import KeyVaultReferenceError +from ._constants import KEY_VAULT_REFERENCE_CONTENT_TYPE + +from ._user_agent import USER_AGENT + + +class AzureAppConfigurationProvider: + """ + Provides a dictionary-like interface to Azure App Configuration settings. Enables loading of sets of configuration + settings from Azure App Configuration into a Python application. Enables trimming of prefixes from configuration + keys. Enables resolution of Key Vault references in configuration settings. + """ + + def __init__(self): + # type: () -> None + self._dict = {} + self._trim_prefixes = [] + self._client = None + + @classmethod + def load(cls, connection_string=None, endpoint=None, credential=None, **kwargs): + """ + Loads configuration settings from Azure App Configuration into a Python application. + + :param connection_string: Connection string (one of connection_string or endpoint and credential must be set) + :type connection_string: str + :param endpoint: Endpoint (one of connection_string or endpoint and credential must be set) + :type endpoint: str + :param credential: Credential (one of connection_string or endpoint and credential must be set) + :type credential: Union[AppConfigConnectionStringCredential, TokenCredential] + :keyword selectors: List of setting selectors to filter configuration settings + :type selectors: list[~azure.appconfigurationprovider.SettingSelector] + :keyword trim_prefixes: List of prefixes to trim from configuration keys + :type trim_prefixes: list[str] + :keyword key_vault_options: Options for resolving Key Vault references + :type key_vault_options: ~azure.appconfigurationprovider.KeyVaultOptions + """ + provider = AzureAppConfigurationProvider() + + key_vault_options = kwargs.pop("key_vault_options", None) + + provider.__buildprovider(connection_string, endpoint, credential, key_vault_options) + + selects = kwargs.pop("selects", {SettingSelector("*", "\0")}) + + provider._trim_prefixes = sorted(kwargs.pop("trimmed_key_prefixes", []), key=len, reverse=True) + + provider._dict = {} + + secret_clients = key_vault_options.secret_clients if key_vault_options else {} + + for select in selects: + configurations = provider._client.list_configuration_settings( + key_filter=select.key_filter, label_filter=select.label_filter + ) + for config in configurations: + + trimmed_key = config.key + # Trim the key if it starts with one of the prefixes provided + for trim in provider._trim_prefixes: + if config.key.startswith(trim): + trimmed_key = config.key[len(trim) :] + break + + if config.content_type == KEY_VAULT_REFERENCE_CONTENT_TYPE: + secret = provider.__resolve_keyvault_reference(config, key_vault_options, secret_clients) + provider._dict[trimmed_key] = secret + elif provider.__is_json_content_type(config.content_type): + try: + j_object = json.loads(config.value) + provider._dict[trimmed_key] = j_object + except json.JSONDecodeError: + # If the value is not a valid JSON, treat it like regular string value + provider._dict[trimmed_key] = config.value + else: + provider._dict[trimmed_key] = config.value + return provider + + def __buildprovider(self, connection_string, endpoint, credential, key_vault_options): + headers = {} + correlation_context = "RequestType=Startup" + + if key_vault_options and ( + key_vault_options.credential or key_vault_options.secret_clients or key_vault_options.secret_resolver + ): + correlation_context += ",UsesKeyVault" + + headers["Correlation-Context"] = correlation_context + useragent = USER_AGENT + + if connection_string and endpoint: + raise AttributeError("Both connection_string and endpoint are set. Only one of these should be set.") + + if connection_string: + self._client = AzureAppConfigurationClient.from_connection_string( + connection_string, user_agent=useragent, headers=headers + ) + return + self._client = AzureAppConfigurationClient(endpoint, credential, user_agent=useragent, headers=headers) + + @staticmethod + def __resolve_keyvault_reference(config, key_vault_options, secret_clients): + if key_vault_options is None: + raise AttributeError("Key Vault options must be set to resolve Key Vault references.") + + if config.secret_id is None: + raise AttributeError("Key Vault reference must have a uri value.") + + key_vault_identifier = KeyVaultSecretIdentifier(config.secret_id) + + referenced_client = next( + (client for client in secret_clients if client.vault_url == key_vault_identifier.vault_url), None + ) + + if referenced_client is None and key_vault_options.credential is not None: + referenced_client = SecretClient( + vault_url=key_vault_identifier.vault_url, credential=key_vault_options.credential + ) + secret_clients[key_vault_identifier.vault_url] = referenced_client + + if referenced_client: + try: + return referenced_client.get_secret( + key_vault_identifier.name, version=key_vault_identifier.version + ).value + except ResourceNotFoundError: + raise KeyVaultReferenceError( + "Key Vault %s does not contain secret %s" + % (key_vault_identifier.vault_url, key_vault_identifier.name) + ) + + if key_vault_options.secret_resolver is not None: + return key_vault_options.secret_resolver(config.secret_id) + raise KeyVaultReferenceError( + "No Secret Client found for Key Vault reference %s" % (key_vault_identifier.vault_url) + ) + + @staticmethod + def __is_json_content_type(content_type): + if not content_type: + return False + + content_type = content_type.strip().lower() + mime_type = content_type.split(";")[0].strip() + + type_parts = mime_type.split("/") + if len(type_parts) != 2: + return False + + (main_type, sub_type) = type_parts + if main_type != "application": + return False + + sub_types = sub_type.split("+") + if "json" in sub_types: + return True + + return False + + def __getitem__(self, key): + return self._dict[key] + + def __repr__(self): + return repr(self._dict) + + def __len__(self): + return len(self._dict) + + def copy(self): + """ + Returns a copy of the configuration settings + + type: () -> dict + """ + return self._dict.copy() + + def __contains__(self, __x: object): + """ + Returns True if the configuration settings contains the specified key + + type: (object) -> bool + """ + return self._dict.__contains__(__x) + + def keys(self): + """ + Returns a list of keys loaded from Azure App Configuration. + + type: () -> list + """ + return self._dict.keys() + + def values(self): + """ + Returns a list of values loaded from Azure App Configuration. Any values that are Key Vault references will be + resolved. + + type: () -> list + """ + return self._dict.values() diff --git a/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_constants.py b/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_constants.py new file mode 100644 index 000000000000..eb9425333a1d --- /dev/null +++ b/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_constants.py @@ -0,0 +1,7 @@ +# ------------------------------------------------------------------------ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# ------------------------------------------------------------------------- + +KEY_VAULT_REFERENCE_CONTENT_TYPE = "application/vnd.microsoft.appconfig.keyvaultref+json;charset=utf-8" diff --git a/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_settingselector.py b/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_settingselector.py new file mode 100644 index 000000000000..bef1b4668a6b --- /dev/null +++ b/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_settingselector.py @@ -0,0 +1,21 @@ +# ------------------------------------------------------------------------ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# ------------------------------------------------------------------------- + + +class SettingSelector: + """ + Selects a set of configuration settings from Azure App Configuration. + + :param key_filter: A filter to select configuration settings based on their keys. + :type key_filter: str + :param label_filter: A filter to select configuration settings based on their labels. Default is value is '\0' + :type label_filter: str + """ + + def __init__(self, key_filter, label_filter="\0"): + # type: (str, str) -> None + self.key_filter = key_filter + self.label_filter = label_filter diff --git a/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_user_agent.py b/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_user_agent.py new file mode 100644 index 000000000000..62961551d751 --- /dev/null +++ b/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_user_agent.py @@ -0,0 +1,9 @@ +# ------------------------------------------------------------------------ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# ------------------------------------------------------------------------- + +from ._version import VERSION + +USER_AGENT = "python-appconfiguration-provider/{}".format(VERSION) diff --git a/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_version.py b/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_version.py new file mode 100644 index 000000000000..839ee5fc6576 --- /dev/null +++ b/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_version.py @@ -0,0 +1,7 @@ +# ------------------------------------------------------------------------ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# ------------------------------------------------------------------------- + +VERSION = "1.0.0b1" diff --git a/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/py.typed b/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/py.typed new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/sdk/appconfiguration/azure-appconfiguration-provider/dev_requirements.txt b/sdk/appconfiguration/azure-appconfiguration-provider/dev_requirements.txt new file mode 100644 index 000000000000..8ca0b23f04e5 --- /dev/null +++ b/sdk/appconfiguration/azure-appconfiguration-provider/dev_requirements.txt @@ -0,0 +1,7 @@ +-e ../../../tools/azure-devtools +azure-core +azure-appconfiguration +azure-identity +azure-keyvault-secrets +aiohttp>=3.0 +-e ../../../tools/azure-sdk-tools diff --git a/sdk/appconfiguration/azure-appconfiguration-provider/mypy.ini b/sdk/appconfiguration/azure-appconfiguration-provider/mypy.ini new file mode 100644 index 000000000000..a90acaba8d0e --- /dev/null +++ b/sdk/appconfiguration/azure-appconfiguration-provider/mypy.ini @@ -0,0 +1,12 @@ +[mypy] +python_version = 3.6 +warn_unused_configs = True +ignore_missing_imports = True + +# Per-module options: + +[mypy-azure.appconfiguration._generated.*] +ignore_errors = True + +[mypy-azure.core.*] +ignore_errors = True \ No newline at end of file diff --git a/sdk/appconfiguration/azure-appconfiguration-provider/samples/README.md b/sdk/appconfiguration/azure-appconfiguration-provider/samples/README.md new file mode 100644 index 000000000000..6eed2a4059e4 --- /dev/null +++ b/sdk/appconfiguration/azure-appconfiguration-provider/samples/README.md @@ -0,0 +1,56 @@ +--- +page_type: sample +languages: + - python +products: + - azure + - azure-app-configuration +--- + +# Azure App Configuration Provider Library Python Samples + +## Prerequisites + +You must have an [Azure subscription][azure_sub], and a [Configuration Store][configuration_store] to use this package. + +To create a Configuration Store, you can either use [Azure Portal](https://ms.portal.azure.com/#create/Microsoft.Azconfig) or if you are using [Azure CLI][azure_cli] you can simply run the following snippet in your console: + +```Powershell +az appconfig create --name --resource-group --location eastus +``` + +### Create Keys + +```Powershell +az appconfig kv set --name --key message --value "hi" +az appconfig kv set --name --key test.message --value "Hi with test Prefix" +``` + +### Create Key Vault Reference + +Requires Key Vault with Secret already created. + +```Powershell +az appconfig kv set-keyvault --name --key secret --secret-identifier +``` + +## Setup + +Install the Azure App Configuration Provider client library for Python with pip: + +```commandline +pip install azure.appconfiguration.provider +``` + +## Contents + +| File | Description | +|-------------|-------------| +| aad_sample.py | demos connecting to app configuration with Azure Active Directory | +| connection_string_sample.py | demos connecting to app configuration with a Connection String | +| key_vault_reference_sample.py | demos resolving key vault references with App Configuration | + + +[azure_sub]: https://azure.microsoft.com/free/ +[azure_cli]: https://docs.microsoft.com/cli/azure +[configuration_store]: https://azure.microsoft.com/services/app-configuration/ \ No newline at end of file diff --git a/sdk/appconfiguration/azure-appconfiguration-provider/samples/aad_sample.py b/sdk/appconfiguration/azure-appconfiguration-provider/samples/aad_sample.py new file mode 100644 index 000000000000..4c875ad53b86 --- /dev/null +++ b/sdk/appconfiguration/azure-appconfiguration-provider/samples/aad_sample.py @@ -0,0 +1,36 @@ +# ------------------------------------------------------------------------ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# ------------------------------------------------------------------------- + +from azure.appconfiguration.provider import ( + AzureAppConfigurationProvider, + SettingSelector +) +from azure.identity import DefaultAzureCredential +import os + +endpoint = os.environ.get("AZURE_APPCONFIG_ENDPOINT") +credential = DefaultAzureCredential() + +# Connecting to Azure App Configuration using AAD +config = AzureAppConfigurationProvider.load( + endpoint=endpoint, credential=credential) + +print(config["message"]) + +# Connecting to Azure App Configuration using AAD and trimmed key prefixes +trimmed = {"test."} +config = AzureAppConfigurationProvider.load( + endpoint=endpoint, credential=credential, trimmed_key_prefixes=trimmed) + +print(config["message"]) + +# Connection to Azure App Configuration using SettingSelector +selects = {SettingSelector("message*", "\0")} +config = AzureAppConfigurationProvider.load( + endpoint=endpoint, credential=credential, selects=selects) + +print("message found: " + str("message" in config)) +print("test.message found: " + str("test.message" in config)) diff --git a/sdk/appconfiguration/azure-appconfiguration-provider/samples/connection_string_sample.py b/sdk/appconfiguration/azure-appconfiguration-provider/samples/connection_string_sample.py new file mode 100644 index 000000000000..c18a5f989fbb --- /dev/null +++ b/sdk/appconfiguration/azure-appconfiguration-provider/samples/connection_string_sample.py @@ -0,0 +1,35 @@ +# ------------------------------------------------------------------------ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# ------------------------------------------------------------------------- + +from azure.appconfiguration.provider import ( + AzureAppConfigurationProvider, + SettingSelector +) +import os + +connection_string = os.environ.get("AZURE_APPCONFIG_CONNECTION_STRING") + +# Connecting to Azure App Configuration using connection string +config = AzureAppConfigurationProvider.load( + connection_string=connection_string) + +print(config["message"]) +print(config["my_json"]["key"]) + +# Connecting to Azure App Configuration using connection string and trimmed key prefixes +trimmed = {"test."} +config = AzureAppConfigurationProvider.load( + connection_string=connection_string, trimmed_key_prefixes=trimmed) + +print(config["message"]) + +# Connection to Azure App Configuration using SettingSelector +selects = {SettingSelector("message*", "\0")} +config = AzureAppConfigurationProvider.load( + connection_string=connection_string, selects=selects) + +print("message found: " + str("message" in config)) +print("test.message found: " + str("test.message" in config)) diff --git a/sdk/appconfiguration/azure-appconfiguration-provider/samples/key_vault_reference_provided_clients_sample.py b/sdk/appconfiguration/azure-appconfiguration-provider/samples/key_vault_reference_provided_clients_sample.py new file mode 100644 index 000000000000..8d67f617c34c --- /dev/null +++ b/sdk/appconfiguration/azure-appconfiguration-provider/samples/key_vault_reference_provided_clients_sample.py @@ -0,0 +1,28 @@ +# ------------------------------------------------------------------------ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# ------------------------------------------------------------------------- + +from azure.appconfiguration.provider import ( + AzureAppConfigurationProvider, + AzureAppConfigurationKeyVaultOptions, + SettingSelector +) +from azure.keyvault.secrets import SecretClient +from azure.identity import DefaultAzureCredential +import os + +endpoint = os.environ.get("AZURE_APPCONFIG_ENDPOINT") +key_vault_uri = os.environ.get("AZURE_KEYVAULT_URI") +credential = DefaultAzureCredential() + +# Connection to Azure App Configuration using AAD with Provided Client +secret_client = SecretClient(vault_url=key_vault_uri, credential=credential) +selects = {SettingSelector("*", "prod")} +key_vault_options = AzureAppConfigurationKeyVaultOptions(secret_clients=[ + secret_client]) +config = AzureAppConfigurationProvider.load( + endpoint=endpoint, credential=credential, key_vault_options=key_vault_options, selects=selects) + +print(config["secret"]) diff --git a/sdk/appconfiguration/azure-appconfiguration-provider/samples/key_vault_reference_sample.py b/sdk/appconfiguration/azure-appconfiguration-provider/samples/key_vault_reference_sample.py new file mode 100644 index 000000000000..4e02cb434197 --- /dev/null +++ b/sdk/appconfiguration/azure-appconfiguration-provider/samples/key_vault_reference_sample.py @@ -0,0 +1,25 @@ +# ------------------------------------------------------------------------ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# ------------------------------------------------------------------------- + +from azure.appconfiguration.provider import ( + AzureAppConfigurationProvider, + AzureAppConfigurationKeyVaultOptions, + SettingSelector +) +from azure.identity import DefaultAzureCredential +import os + +endpoint = os.environ.get("AZURE_APPCONFIG_ENDPOINT") +credential = DefaultAzureCredential() + +# Connection to Azure App Configuration using AAD and Resolving Key Vault References +key_vault_options = AzureAppConfigurationKeyVaultOptions(credential=credential) +selects = {SettingSelector("*", "prod")} + +config = AzureAppConfigurationProvider.load( + endpoint=endpoint, credential=credential, key_vault_options=key_vault_options, selects=selects) + +print(config["secret"]) diff --git a/sdk/appconfiguration/azure-appconfiguration-provider/sdk_packaging.toml b/sdk/appconfiguration/azure-appconfiguration-provider/sdk_packaging.toml new file mode 100644 index 000000000000..7d93f77a90ba --- /dev/null +++ b/sdk/appconfiguration/azure-appconfiguration-provider/sdk_packaging.toml @@ -0,0 +1,9 @@ +[packaging] +package_name = "azure-appconfiguration-provider" +package_nspkg = "azure-data-nspkg" +package_pprint_name = "App Configuration Provider" +package_doc_id = "" +is_stable = false +is_arm = false +need_msrestazure = false +auto_update = false \ No newline at end of file diff --git a/sdk/appconfiguration/azure-appconfiguration-provider/setup.py b/sdk/appconfiguration/azure-appconfiguration-provider/setup.py new file mode 100644 index 000000000000..6e2821a58357 --- /dev/null +++ b/sdk/appconfiguration/azure-appconfiguration-provider/setup.py @@ -0,0 +1,80 @@ +#!/usr/bin/env python + +# ------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# -------------------------------------------------------------------------- + +import sys +import re +import os.path +from io import open +from setuptools import find_packages, setup + +# Change the PACKAGE_NAME only to change folder and different name +PACKAGE_NAME = "azure-appconfiguration-provider" +PACKAGE_PPRINT_NAME = "App Configuration Provider" + +# a-b-c => a/b/c +package_folder_path = PACKAGE_NAME.replace("-", "/") +# a-b-c => a.b.c +namespace_name = PACKAGE_NAME.replace("-", ".") + +# Version extraction inspired from 'requests' +with open(os.path.join(package_folder_path, "_version.py"), "r") as fd: + version = re.search( + r'^VERSION\s*=\s*[\'"]([^\'"]*)[\'"]', fd.read(), re.MULTILINE + ).group(1) + +if not version: + raise RuntimeError("Cannot find version information") + +with open("README.md", encoding="utf-8") as f: + readme = f.read() +with open("CHANGELOG.md", encoding="utf-8") as f: + changelog = f.read() + +exclude_packages = [ + "tests", + "tests.*", + "samples", + # Exclude packages that will be covered by PEP420 or nspkg + "azure", + "azure.appconfiguration" +] +if sys.version_info < (3, 5, 3): + exclude_packages.extend(["*.aio", "*.aio.*"]) + +setup( + name=PACKAGE_NAME, + version=version, + include_package_data=True, + description="Microsoft {} Library for Python".format(PACKAGE_PPRINT_NAME), + long_description=readme + "\n\n" + changelog, + long_description_content_type="text/markdown", + license="MIT License", + author="Microsoft Corporation", + author_email="azpysdkhelp@microsoft.com", + url="https://github.com/Azure/azure-sdk-for-python/tree/main/sdk/appconfiguration/azure-appconfiguration-provider", + classifiers=[ + "Development Status :: 4 - Beta", + "Programming Language :: Python", + "Programming Language :: Python :: 3 :: Only", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.7", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "License :: OSI Approved :: MIT License", + ], + zip_safe=False, + packages=find_packages(exclude=exclude_packages), + python_requires=">=3.6", + install_requires=[ + "msrest>=0.6.21", + "azure-core<2.0.0,>=1.2.2", + "azure-appconfiguration<2.0.0,>=1.3.0", + "azure-keyvault-secrets<5.0.0,>=4.3.0", + ], +) diff --git a/sdk/appconfiguration/azure-appconfiguration-provider/tests/conftest.py b/sdk/appconfiguration/azure-appconfiguration-provider/tests/conftest.py new file mode 100644 index 000000000000..eb633dd0f1e8 --- /dev/null +++ b/sdk/appconfiguration/azure-appconfiguration-provider/tests/conftest.py @@ -0,0 +1,25 @@ +import os +from devtools_testutils import ( + add_general_regex_sanitizer, + add_general_string_sanitizer, + add_oauth_response_sanitizer, +) +import pytest + +# autouse=True will trigger this fixture on each pytest run, even if it's not explicitly used by a test method + + +@pytest.fixture(scope="session", autouse=True) +def add_sanitizers(test_proxy): + add_general_regex_sanitizer(value="https://fake-endpoint.azconfig.io", + regex= os.environ.get('APPCONFIGURATION_ENDPOINT_STRING', "https://fake-endpoint.azconfig.io")) + add_general_regex_sanitizer(value="fake-connection-string", + regex=os.environ.get('APPCONFIGURATION_CONNECTION_STRING', "fake-connection-string")) + add_general_regex_sanitizer( + value="fake-client-id", regex=os.environ.get('APPCONFIGURATION_CLIENT_ID', "fake-client-id")) + add_general_regex_sanitizer( + value="fake-client-secret", regex=os.environ.get('APPCONFIGURATION_CLIENT_SECRET', "fake-client-secret")) + add_general_regex_sanitizer( + value="fake-tenant-id", regex=os.environ.get('APPCONFIGURATION_TENANT_ID', "fake-tenant-id")) + add_general_string_sanitizer(value="https://fake-key-vault.vault.azure.net/", target=os.environ.get("APPCONFIGURATION_KEY_VAULT_REFERENCE", "https://fake-key-vault.vault.azure.net/")) + add_oauth_response_sanitizer() diff --git a/sdk/appconfiguration/azure-appconfiguration-provider/tests/preparers.py b/sdk/appconfiguration/azure-appconfiguration-provider/tests/preparers.py new file mode 100644 index 000000000000..5febb75fb5f8 --- /dev/null +++ b/sdk/appconfiguration/azure-appconfiguration-provider/tests/preparers.py @@ -0,0 +1,60 @@ +# ------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# -------------------------------------------------------------------------- +import functools +from devtools_testutils import EnvironmentVariableLoader +import inspect + +AppConfigProviderPreparer = functools.partial( + EnvironmentVariableLoader, + "appconfiguration", + appconfiguration_connection_string="Endpoint=https://fake-endpoint.azconfig.io;Id=0-l4-s0:h5htBaY5Z1LwFz50bIQv;Secret=lamefakesecretlamefakesecretlamefakesecrett=", + appconfiguration_endpoint_string="https://fake-endpoint.azconfig.io", + appconfiguration_client_id="fake-client-id", + appconfiguration_client_secret="fake-client-secret", + appconfiguration_tenant_id="fake-tenant-id", +) + + +def app_config_decorator(func, **kwargs): + @AppConfigProviderPreparer() + def wrapper(*args, **kwargs): + appconfiguration_connection_string = kwargs.pop( + "appconfiguration_connection_string") + kwargs['appconfiguration_connection_string'] = appconfiguration_connection_string + + trimmed_kwargs = {k: v for k, v in kwargs.items()} + trim_kwargs_from_test_function(func, trimmed_kwargs) + + func(*args, **trimmed_kwargs) + return wrapper + + +def app_config_decorator_aad(func, **kwargs): + @AppConfigProviderPreparer() + def wrapper(*args, **kwargs): + appconfiguration_endpoint_string = kwargs.pop( + "appconfiguration_endpoint_string") + kwargs['appconfiguration_endpoint_string'] = appconfiguration_endpoint_string + + trimmed_kwargs = {k: v for k, v in kwargs.items()} + trim_kwargs_from_test_function(func, trimmed_kwargs) + + func(*args, **trimmed_kwargs) + return wrapper + +def trim_kwargs_from_test_function(fn, kwargs): + # the next function is the actual test function. the kwargs need to be trimmed so + # that parameters which are not required will not be passed to it. + if not getattr(fn, '__is_preparer', False): + try: + args, _, kw, _, _, _, _ = inspect.getfullargspec(fn) + except AttributeError: + args, _, kw, _ = inspect.getargspec( + fn) # pylint: disable=deprecated-method + if kw is None: + args = set(args) + for key in [k for k in kwargs if k not in args]: + del kwargs[key] \ No newline at end of file diff --git a/sdk/appconfiguration/azure-appconfiguration-provider/tests/recordings/test_provider.pyTestAppConfigurationProvidertest_provider_creation.json b/sdk/appconfiguration/azure-appconfiguration-provider/tests/recordings/test_provider.pyTestAppConfigurationProvidertest_provider_creation.json new file mode 100644 index 000000000000..fd5b1bf899c6 --- /dev/null +++ b/sdk/appconfiguration/azure-appconfiguration-provider/tests/recordings/test_provider.pyTestAppConfigurationProvidertest_provider_creation.json @@ -0,0 +1,67 @@ +{ + "Entries": [ + { + "RequestUri": "https://fake-endpoint.azconfig.io/kv?key=%2A\u0026label=%00\u0026api-version=1.0", + "RequestMethod": "GET", + "RequestHeaders": { + "Accept": "application/vnd.microsoft.appconfig.kvset\u002Bjson, application/json, application/problem\u002Bjson", + "Accept-Encoding": "gzip, deflate", + "Connection": "keep-alive", + "Correlation-Context": "RequestType=Startup", + "User-Agent": "python-appconfiguration-provider/1.0.0b1 azsdk-python-appconfiguration/1.4.0 Python/3.9.13 (Windows-10-10.0.19044-SP0)", + "x-ms-content-sha256": "47DEQpj8HBSa\u002B/TImW\u002B5JCeuQeRkm5NMpJWZG3hSuFU=", + "x-ms-date": "Sep, 27 2022 22:42:40.104313 GMT" + }, + "RequestBody": null, + "StatusCode": 200, + "ResponseHeaders": { + "Access-Control-Allow-Credentials": "true", + "Access-Control-Allow-Origin": "*", + "Access-Control-Expose-Headers": "DNT, X-CustomHeader, Keep-Alive, User-Agent, X-Requested-With, If-Modified-Since, Cache-Control, Content-Type, Authorization, x-ms-client-request-id, x-ms-useragent, x-ms-content-sha256, x-ms-date, host, Accept, Accept-Datetime, Date, If-Match, If-None-Match, Sync-Token, x-ms-return-client-request-id, ETag, Last-Modified, Link, Memento-Datetime, retry-after-ms, x-ms-request-id, x-ms-client-session-id, x-ms-effective-locale, WWW-Authenticate, traceparent, tracestate", + "Connection": "keep-alive", + "Content-Type": "application/vnd.microsoft.appconfig.kvset\u002Bjson; charset=utf-8", + "Date": "Tue, 27 Sep 2022 22:42:40 GMT", + "Server": "openresty/1.17.8.2", + "Strict-Transport-Security": "max-age=15724800; includeSubDomains", + "Sync-Token": "zAJw6V16=NzoxNyMxOTgyNzIxMg==;sn=19827212", + "Transfer-Encoding": "chunked", + "x-ms-correlation-request-id": "c7ce670d-847a-4232-93fc-a4efa00562d4" + }, + "ResponseBody": { + "items": [ + { + "etag": "RX6Uzn3YdpSnDk7SX9kzxXwDFdYhR8dGGLEXBZ6i_nM", + "key": "message", + "label": null, + "content_type": "", + "value": "hi", + "tags": {}, + "locked": false, + "last_modified": "2022-09-27T19:41:40\u002B00:00" + }, + { + "etag": "dYgyN_oxYWzXEQN8K8d2q-4rKQJwftuzR75Jagqq_pw", + "key": "my_json", + "label": null, + "content_type": "application/json", + "value": "{\u0022key\u0022:\u0022value\u0022}", + "tags": {}, + "locked": false, + "last_modified": "2022-09-27T20:35:17\u002B00:00" + }, + { + "etag": "7vw5ma2Cc0-eE1cnRaaCv2_VtAS8BQ5jbHP9eNS_MFA", + "key": "test.trimmed", + "label": null, + "content_type": "key", + "value": "key", + "tags": {}, + "locked": false, + "last_modified": "2022-09-27T19:58:41\u002B00:00" + } + ] + } + } + ], + "Variables": {} +} \ No newline at end of file diff --git a/sdk/appconfiguration/azure-appconfiguration-provider/tests/recordings/test_provider.pyTestAppConfigurationProvidertest_provider_selectors.json b/sdk/appconfiguration/azure-appconfiguration-provider/tests/recordings/test_provider.pyTestAppConfigurationProvidertest_provider_selectors.json new file mode 100644 index 000000000000..525dccf8aaab --- /dev/null +++ b/sdk/appconfiguration/azure-appconfiguration-provider/tests/recordings/test_provider.pyTestAppConfigurationProvidertest_provider_selectors.json @@ -0,0 +1,47 @@ +{ + "Entries": [ + { + "RequestUri": "https://fake-endpoint.azconfig.io/kv?key=message%2A\u0026label=dev\u0026api-version=1.0", + "RequestMethod": "GET", + "RequestHeaders": { + "Accept": "application/vnd.microsoft.appconfig.kvset\u002Bjson, application/json, application/problem\u002Bjson", + "Accept-Encoding": "gzip, deflate", + "Connection": "keep-alive", + "Correlation-Context": "RequestType=Startup", + "User-Agent": "python-appconfiguration-provider/1.0.0b1 azsdk-python-appconfiguration/1.4.0 Python/3.9.13 (Windows-10-10.0.19044-SP0)", + "x-ms-content-sha256": "47DEQpj8HBSa\u002B/TImW\u002B5JCeuQeRkm5NMpJWZG3hSuFU=", + "x-ms-date": "Sep, 27 2022 22:42:40.650526 GMT" + }, + "RequestBody": null, + "StatusCode": 200, + "ResponseHeaders": { + "Access-Control-Allow-Credentials": "true", + "Access-Control-Allow-Origin": "*", + "Access-Control-Expose-Headers": "DNT, X-CustomHeader, Keep-Alive, User-Agent, X-Requested-With, If-Modified-Since, Cache-Control, Content-Type, Authorization, x-ms-client-request-id, x-ms-useragent, x-ms-content-sha256, x-ms-date, host, Accept, Accept-Datetime, Date, If-Match, If-None-Match, Sync-Token, x-ms-return-client-request-id, ETag, Last-Modified, Link, Memento-Datetime, retry-after-ms, x-ms-request-id, x-ms-client-session-id, x-ms-effective-locale, WWW-Authenticate, traceparent, tracestate", + "Connection": "keep-alive", + "Content-Type": "application/vnd.microsoft.appconfig.kvset\u002Bjson; charset=utf-8", + "Date": "Tue, 27 Sep 2022 22:42:40 GMT", + "Server": "openresty/1.17.8.2", + "Strict-Transport-Security": "max-age=15724800; includeSubDomains", + "Sync-Token": "zAJw6V16=NzoxNyMxOTgyNzIxMg==;sn=19827212", + "Transfer-Encoding": "chunked", + "x-ms-correlation-request-id": "06221105-78da-4e5e-8239-20d1d53ea669" + }, + "ResponseBody": { + "items": [ + { + "etag": "yEiVkIl-IHlB26quPXo_Z2GbB5yQm_ENTWI2c1k1J4E", + "key": "message", + "label": "dev", + "content_type": "", + "value": "test", + "tags": {}, + "locked": false, + "last_modified": "2022-09-27T20:06:21\u002B00:00" + } + ] + } + } + ], + "Variables": {} +} \ No newline at end of file diff --git a/sdk/appconfiguration/azure-appconfiguration-provider/tests/recordings/test_provider.pyTestAppConfigurationProvidertest_provider_trimmed_key_prefixes.json b/sdk/appconfiguration/azure-appconfiguration-provider/tests/recordings/test_provider.pyTestAppConfigurationProvidertest_provider_trimmed_key_prefixes.json new file mode 100644 index 000000000000..6faf86a93e30 --- /dev/null +++ b/sdk/appconfiguration/azure-appconfiguration-provider/tests/recordings/test_provider.pyTestAppConfigurationProvidertest_provider_trimmed_key_prefixes.json @@ -0,0 +1,67 @@ +{ + "Entries": [ + { + "RequestUri": "https://fake-endpoint.azconfig.io/kv?key=%2A\u0026label=%00\u0026api-version=1.0", + "RequestMethod": "GET", + "RequestHeaders": { + "Accept": "application/vnd.microsoft.appconfig.kvset\u002Bjson, application/json, application/problem\u002Bjson", + "Accept-Encoding": "gzip, deflate", + "Connection": "keep-alive", + "Correlation-Context": "RequestType=Startup", + "User-Agent": "python-appconfiguration-provider/1.0.0b1 azsdk-python-appconfiguration/1.4.0 Python/3.9.13 (Windows-10-10.0.19044-SP0)", + "x-ms-content-sha256": "47DEQpj8HBSa\u002B/TImW\u002B5JCeuQeRkm5NMpJWZG3hSuFU=", + "x-ms-date": "Sep, 27 2022 22:42:40.490235 GMT" + }, + "RequestBody": null, + "StatusCode": 200, + "ResponseHeaders": { + "Access-Control-Allow-Credentials": "true", + "Access-Control-Allow-Origin": "*", + "Access-Control-Expose-Headers": "DNT, X-CustomHeader, Keep-Alive, User-Agent, X-Requested-With, If-Modified-Since, Cache-Control, Content-Type, Authorization, x-ms-client-request-id, x-ms-useragent, x-ms-content-sha256, x-ms-date, host, Accept, Accept-Datetime, Date, If-Match, If-None-Match, Sync-Token, x-ms-return-client-request-id, ETag, Last-Modified, Link, Memento-Datetime, retry-after-ms, x-ms-request-id, x-ms-client-session-id, x-ms-effective-locale, WWW-Authenticate, traceparent, tracestate", + "Connection": "keep-alive", + "Content-Type": "application/vnd.microsoft.appconfig.kvset\u002Bjson; charset=utf-8", + "Date": "Tue, 27 Sep 2022 22:42:40 GMT", + "Server": "openresty/1.17.8.2", + "Strict-Transport-Security": "max-age=15724800; includeSubDomains", + "Sync-Token": "zAJw6V16=NzoxNyMxOTgyNzIxMg==;sn=19827212", + "Transfer-Encoding": "chunked", + "x-ms-correlation-request-id": "55e5698f-c69d-4645-81aa-3f25371d56a4" + }, + "ResponseBody": { + "items": [ + { + "etag": "RX6Uzn3YdpSnDk7SX9kzxXwDFdYhR8dGGLEXBZ6i_nM", + "key": "message", + "label": null, + "content_type": "", + "value": "hi", + "tags": {}, + "locked": false, + "last_modified": "2022-09-27T19:41:40\u002B00:00" + }, + { + "etag": "dYgyN_oxYWzXEQN8K8d2q-4rKQJwftuzR75Jagqq_pw", + "key": "my_json", + "label": null, + "content_type": "application/json", + "value": "{\u0022key\u0022:\u0022value\u0022}", + "tags": {}, + "locked": false, + "last_modified": "2022-09-27T20:35:17\u002B00:00" + }, + { + "etag": "7vw5ma2Cc0-eE1cnRaaCv2_VtAS8BQ5jbHP9eNS_MFA", + "key": "test.trimmed", + "label": null, + "content_type": "key", + "value": "key", + "tags": {}, + "locked": false, + "last_modified": "2022-09-27T19:58:41\u002B00:00" + } + ] + } + } + ], + "Variables": {} +} \ No newline at end of file diff --git a/sdk/appconfiguration/azure-appconfiguration-provider/tests/recordings/test_provider_aad.pyTestAppConfigurationProvidertest_provider_creation_aad.json b/sdk/appconfiguration/azure-appconfiguration-provider/tests/recordings/test_provider_aad.pyTestAppConfigurationProvidertest_provider_creation_aad.json new file mode 100644 index 000000000000..2d4e0036340f --- /dev/null +++ b/sdk/appconfiguration/azure-appconfiguration-provider/tests/recordings/test_provider_aad.pyTestAppConfigurationProvidertest_provider_creation_aad.json @@ -0,0 +1,240 @@ +{ + "Entries": [ + { + "RequestUri": "https://login.microsoftonline.com/fake-tenant-id/v2.0/.well-known/openid-configuration", + "RequestMethod": "GET", + "RequestHeaders": { + "Accept": "*/*", + "Accept-Encoding": "gzip, deflate", + "Connection": "keep-alive", + "User-Agent": "azsdk-python-identity/1.12.0b2 Python/3.9.13 (Windows-10-10.0.19044-SP0)" + }, + "RequestBody": null, + "StatusCode": 200, + "ResponseHeaders": { + "Access-Control-Allow-Methods": "GET, OPTIONS", + "Access-Control-Allow-Origin": "*", + "Cache-Control": "max-age=86400, private", + "Content-Length": "1599", + "Content-Type": "application/json; charset=utf-8", + "Date": "Tue, 27 Sep 2022 22:42:40 GMT", + "P3P": "CP=\u0022DSP CUR OTPi IND OTRi ONL FIN\u0022", + "Set-Cookie": [ + "fpc=Aih2w7HdW0RLsLtcNXW6NJo; expires=Thu, 27-Oct-2022 22:42:41 GMT; path=/; secure; HttpOnly; SameSite=None", + "esctx=AQABAAAAAAD--DLA3VO7QrddgJg7WevrkYxHw6mf13fuPRCu6v488gBdmaagfm2C8Q8ZiC9pB0nYYkXjS9U5w5mUTT51HcOESfBkGL6zDFmEHCECrlz_k72p8NUOMuj8OHKqWYGUIMnrKLpjfjH0i6lBviGMxCSFlnfEbzZfWev4InHJwQ2u2shZYtE7Y5Hb_K_3kYBp1RkgAA; domain=.login.microsoftonline.com; path=/; secure; HttpOnly; SameSite=None", + "x-ms-gateway-slice=estsfd; path=/; secure; samesite=none; httponly", + "stsservicecookie=estsfd; path=/; secure; samesite=none; httponly" + ], + "Strict-Transport-Security": "max-age=31536000; includeSubDomains", + "X-Content-Type-Options": "nosniff", + "x-ms-ests-server": "2.1.13672.11 - NCUS ProdSlices", + "X-XSS-Protection": "0" + }, + "ResponseBody": { + "token_endpoint": "https://login.microsoftonline.com/fake-tenant-id/oauth2/v2.0/token", + "token_endpoint_auth_methods_supported": [ + "client_secret_post", + "private_key_jwt", + "client_secret_basic" + ], + "jwks_uri": "https://login.microsoftonline.com/fake-tenant-id/discovery/v2.0/keys", + "response_modes_supported": [ + "query", + "fragment", + "form_post" + ], + "subject_types_supported": [ + "pairwise" + ], + "id_token_signing_alg_values_supported": [ + "RS256" + ], + "response_types_supported": [ + "code", + "id_token", + "code id_token", + "id_token token" + ], + "scopes_supported": [ + "openid", + "profile", + "email", + "offline_access" + ], + "issuer": "https://login.microsoftonline.com/fake-tenant-id/v2.0", + "request_uri_parameter_supported": false, + "userinfo_endpoint": "https://graph.microsoft.com/oidc/userinfo", + "authorization_endpoint": "https://login.microsoftonline.com/fake-tenant-id/oauth2/v2.0/authorize", + "device_authorization_endpoint": "https://login.microsoftonline.com/fake-tenant-id/oauth2/v2.0/devicecode", + "http_logout_supported": true, + "frontchannel_logout_supported": true, + "end_session_endpoint": "https://login.microsoftonline.com/fake-tenant-id/oauth2/v2.0/logout", + "claims_supported": [ + "sub", + "iss", + "cloud_instance_name", + "cloud_instance_host_name", + "cloud_graph_host_name", + "msgraph_host", + "aud", + "exp", + "iat", + "auth_time", + "acr", + "nonce", + "preferred_username", + "name", + "tid", + "ver", + "at_hash", + "c_hash", + "email" + ], + "kerberos_endpoint": "https://login.microsoftonline.com/fake-tenant-id/kerberos", + "tenant_region_scope": "WW", + "cloud_instance_name": "microsoftonline.com", + "cloud_graph_host_name": "graph.windows.net", + "msgraph_host": "graph.microsoft.com", + "rbac_url": "https://pas.windows.net" + } + }, + { + "RequestUri": "https://login.microsoftonline.com/common/discovery/instance?api-version=1.1\u0026authorization_endpoint=https://login.microsoftonline.com/common/oauth2/authorize", + "RequestMethod": "GET", + "RequestHeaders": { + "Accept": "application/json", + "Accept-Encoding": "gzip, deflate", + "Connection": "keep-alive", + "Cookie": "fpc=Aih2w7HdW0RLsLtcNXW6NJo; stsservicecookie=estsfd; x-ms-gateway-slice=estsfd", + "User-Agent": "azsdk-python-identity/1.12.0b2 Python/3.9.13 (Windows-10-10.0.19044-SP0)" + }, + "RequestBody": null, + "StatusCode": 200, + "ResponseHeaders": { + "Access-Control-Allow-Methods": "GET, OPTIONS", + "Access-Control-Allow-Origin": "*", + "Cache-Control": "max-age=86400, private", + "Content-Length": "945", + "Content-Type": "application/json; charset=utf-8", + "Date": "Tue, 27 Sep 2022 22:42:40 GMT", + "P3P": "CP=\u0022DSP CUR OTPi IND OTRi ONL FIN\u0022", + "Set-Cookie": [ + "fpc=Aih2w7HdW0RLsLtcNXW6NJo; expires=Thu, 27-Oct-2022 22:42:41 GMT; path=/; secure; HttpOnly; SameSite=None", + "x-ms-gateway-slice=estsfd; path=/; secure; samesite=none; httponly", + "stsservicecookie=estsfd; path=/; secure; samesite=none; httponly" + ], + "Strict-Transport-Security": "max-age=31536000; includeSubDomains", + "X-Content-Type-Options": "nosniff", + "x-ms-ests-server": "2.1.13672.11 - SCUS ProdSlices", + "X-XSS-Protection": "0" + }, + "ResponseBody": { + "tenant_discovery_endpoint": "https://login.microsoftonline.com/common/.well-known/openid-configuration", + "api-version": "1.1", + "metadata": [ + { + "preferred_network": "login.microsoftonline.com", + "preferred_cache": "login.windows.net", + "aliases": [ + "login.microsoftonline.com", + "login.windows.net", + "login.microsoft.com", + "sts.windows.net" + ] + }, + { + "preferred_network": "login.partner.microsoftonline.cn", + "preferred_cache": "login.partner.microsoftonline.cn", + "aliases": [ + "login.partner.microsoftonline.cn", + "login.chinacloudapi.cn" + ] + }, + { + "preferred_network": "login.microsoftonline.de", + "preferred_cache": "login.microsoftonline.de", + "aliases": [ + "login.microsoftonline.de" + ] + }, + { + "preferred_network": "login.microsoftonline.us", + "preferred_cache": "login.microsoftonline.us", + "aliases": [ + "login.microsoftonline.us", + "login.usgovcloudapi.net" + ] + }, + { + "preferred_network": "login-us.microsoftonline.com", + "preferred_cache": "login-us.microsoftonline.com", + "aliases": [ + "login-us.microsoftonline.com" + ] + } + ] + } + }, + { + "RequestUri": "https://fake-endpoint.azconfig.io/kv?key=%2A\u0026label=%00\u0026api-version=1.0", + "RequestMethod": "GET", + "RequestHeaders": { + "Accept": "application/vnd.microsoft.appconfig.kvset\u002Bjson, application/json, application/problem\u002Bjson", + "Accept-Encoding": "gzip, deflate", + "Connection": "keep-alive", + "Correlation-Context": "RequestType=Startup", + "User-Agent": "python-appconfiguration-provider/1.0.0b1 azsdk-python-appconfiguration/1.4.0 Python/3.9.13 (Windows-10-10.0.19044-SP0)" + }, + "RequestBody": null, + "StatusCode": 200, + "ResponseHeaders": { + "Access-Control-Allow-Credentials": "true", + "Access-Control-Allow-Origin": "*", + "Access-Control-Expose-Headers": "DNT, X-CustomHeader, Keep-Alive, User-Agent, X-Requested-With, If-Modified-Since, Cache-Control, Content-Type, Authorization, x-ms-client-request-id, x-ms-useragent, x-ms-content-sha256, x-ms-date, host, Accept, Accept-Datetime, Date, If-Match, If-None-Match, Sync-Token, x-ms-return-client-request-id, ETag, Last-Modified, Link, Memento-Datetime, retry-after-ms, x-ms-request-id, x-ms-client-session-id, x-ms-effective-locale, WWW-Authenticate, traceparent, tracestate", + "Connection": "keep-alive", + "Content-Type": "application/vnd.microsoft.appconfig.kvset\u002Bjson; charset=utf-8", + "Date": "Tue, 27 Sep 2022 22:42:41 GMT", + "Server": "openresty/1.17.8.2", + "Strict-Transport-Security": "max-age=15724800; includeSubDomains", + "Sync-Token": "zAJw6V16=NzoxNyMxOTgyNzIxMg==;sn=19827212", + "Transfer-Encoding": "chunked", + "x-ms-correlation-request-id": "30054af8-b684-4033-b1b5-9c071f491287" + }, + "ResponseBody": { + "items": [ + { + "etag": "RX6Uzn3YdpSnDk7SX9kzxXwDFdYhR8dGGLEXBZ6i_nM", + "key": "message", + "label": null, + "content_type": "", + "value": "hi", + "tags": {}, + "locked": false, + "last_modified": "2022-09-27T19:41:40\u002B00:00" + }, + { + "etag": "dYgyN_oxYWzXEQN8K8d2q-4rKQJwftuzR75Jagqq_pw", + "key": "my_json", + "label": null, + "content_type": "application/json", + "value": "{\u0022key\u0022:\u0022value\u0022}", + "tags": {}, + "locked": false, + "last_modified": "2022-09-27T20:35:17\u002B00:00" + }, + { + "etag": "7vw5ma2Cc0-eE1cnRaaCv2_VtAS8BQ5jbHP9eNS_MFA", + "key": "test.trimmed", + "label": null, + "content_type": "key", + "value": "key", + "tags": {}, + "locked": false, + "last_modified": "2022-09-27T19:58:41\u002B00:00" + } + ] + } + } + ], + "Variables": {} +} \ No newline at end of file diff --git a/sdk/appconfiguration/azure-appconfiguration-provider/tests/recordings/test_provider_aad.pyTestAppConfigurationProvidertest_provider_selectors.json b/sdk/appconfiguration/azure-appconfiguration-provider/tests/recordings/test_provider_aad.pyTestAppConfigurationProvidertest_provider_selectors.json new file mode 100644 index 000000000000..692dc3e00582 --- /dev/null +++ b/sdk/appconfiguration/azure-appconfiguration-provider/tests/recordings/test_provider_aad.pyTestAppConfigurationProvidertest_provider_selectors.json @@ -0,0 +1,219 @@ +{ + "Entries": [ + { + "RequestUri": "https://login.microsoftonline.com/fake-tenant-id/v2.0/.well-known/openid-configuration", + "RequestMethod": "GET", + "RequestHeaders": { + "Accept": "*/*", + "Accept-Encoding": "gzip, deflate", + "Connection": "keep-alive", + "User-Agent": "azsdk-python-identity/1.12.0b2 Python/3.9.13 (Windows-10-10.0.19044-SP0)" + }, + "RequestBody": null, + "StatusCode": 200, + "ResponseHeaders": { + "Access-Control-Allow-Methods": "GET, OPTIONS", + "Access-Control-Allow-Origin": "*", + "Cache-Control": "max-age=86400, private", + "Content-Length": "1599", + "Content-Type": "application/json; charset=utf-8", + "Date": "Tue, 27 Sep 2022 22:42:41 GMT", + "P3P": "CP=\u0022DSP CUR OTPi IND OTRi ONL FIN\u0022", + "Set-Cookie": [ + "fpc=Aih2w7HdW0RLsLtcNXW6NJri05sNAgAAAGBzxdoOAAAA; expires=Thu, 27-Oct-2022 22:42:41 GMT; path=/; secure; HttpOnly; SameSite=None", + "x-ms-gateway-slice=estsfd; path=/; secure; samesite=none; httponly", + "stsservicecookie=estsfd; path=/; secure; samesite=none; httponly" + ], + "Strict-Transport-Security": "max-age=31536000; includeSubDomains", + "X-Content-Type-Options": "nosniff", + "x-ms-ests-server": "2.1.13672.11 - EUS ProdSlices", + "X-XSS-Protection": "0" + }, + "ResponseBody": { + "token_endpoint": "https://login.microsoftonline.com/fake-tenant-id/oauth2/v2.0/token", + "token_endpoint_auth_methods_supported": [ + "client_secret_post", + "private_key_jwt", + "client_secret_basic" + ], + "jwks_uri": "https://login.microsoftonline.com/fake-tenant-id/discovery/v2.0/keys", + "response_modes_supported": [ + "query", + "fragment", + "form_post" + ], + "subject_types_supported": [ + "pairwise" + ], + "id_token_signing_alg_values_supported": [ + "RS256" + ], + "response_types_supported": [ + "code", + "id_token", + "code id_token", + "id_token token" + ], + "scopes_supported": [ + "openid", + "profile", + "email", + "offline_access" + ], + "issuer": "https://login.microsoftonline.com/fake-tenant-id/v2.0", + "request_uri_parameter_supported": false, + "userinfo_endpoint": "https://graph.microsoft.com/oidc/userinfo", + "authorization_endpoint": "https://login.microsoftonline.com/fake-tenant-id/oauth2/v2.0/authorize", + "device_authorization_endpoint": "https://login.microsoftonline.com/fake-tenant-id/oauth2/v2.0/devicecode", + "http_logout_supported": true, + "frontchannel_logout_supported": true, + "end_session_endpoint": "https://login.microsoftonline.com/fake-tenant-id/oauth2/v2.0/logout", + "claims_supported": [ + "sub", + "iss", + "cloud_instance_name", + "cloud_instance_host_name", + "cloud_graph_host_name", + "msgraph_host", + "aud", + "exp", + "iat", + "auth_time", + "acr", + "nonce", + "preferred_username", + "name", + "tid", + "ver", + "at_hash", + "c_hash", + "email" + ], + "kerberos_endpoint": "https://login.microsoftonline.com/fake-tenant-id/kerberos", + "tenant_region_scope": "WW", + "cloud_instance_name": "microsoftonline.com", + "cloud_graph_host_name": "graph.windows.net", + "msgraph_host": "graph.microsoft.com", + "rbac_url": "https://pas.windows.net" + } + }, + { + "RequestUri": "https://login.microsoftonline.com/common/discovery/instance?api-version=1.1\u0026authorization_endpoint=https://login.microsoftonline.com/common/oauth2/authorize", + "RequestMethod": "GET", + "RequestHeaders": { + "Accept": "application/json", + "Accept-Encoding": "gzip, deflate", + "Connection": "keep-alive", + "Cookie": "fpc=Aih2w7HdW0RLsLtcNXW6NJri05sNAgAAAGBzxdoOAAAA; stsservicecookie=estsfd; x-ms-gateway-slice=estsfd", + "User-Agent": "azsdk-python-identity/1.12.0b2 Python/3.9.13 (Windows-10-10.0.19044-SP0)" + }, + "RequestBody": null, + "StatusCode": 200, + "ResponseHeaders": { + "Access-Control-Allow-Methods": "GET, OPTIONS", + "Access-Control-Allow-Origin": "*", + "Cache-Control": "max-age=86400, private", + "Content-Length": "945", + "Content-Type": "application/json; charset=utf-8", + "Date": "Tue, 27 Sep 2022 22:42:41 GMT", + "P3P": "CP=\u0022DSP CUR OTPi IND OTRi ONL FIN\u0022", + "Set-Cookie": [ + "fpc=Aih2w7HdW0RLsLtcNXW6NJri05sNAgAAAGBzxdoOAAAA; expires=Thu, 27-Oct-2022 22:42:41 GMT; path=/; secure; HttpOnly; SameSite=None", + "x-ms-gateway-slice=estsfd; path=/; secure; samesite=none; httponly", + "stsservicecookie=estsfd; path=/; secure; samesite=none; httponly" + ], + "Strict-Transport-Security": "max-age=31536000; includeSubDomains", + "X-Content-Type-Options": "nosniff", + "x-ms-ests-server": "2.1.13672.11 - SCUS ProdSlices", + "X-XSS-Protection": "0" + }, + "ResponseBody": { + "tenant_discovery_endpoint": "https://login.microsoftonline.com/common/.well-known/openid-configuration", + "api-version": "1.1", + "metadata": [ + { + "preferred_network": "login.microsoftonline.com", + "preferred_cache": "login.windows.net", + "aliases": [ + "login.microsoftonline.com", + "login.windows.net", + "login.microsoft.com", + "sts.windows.net" + ] + }, + { + "preferred_network": "login.partner.microsoftonline.cn", + "preferred_cache": "login.partner.microsoftonline.cn", + "aliases": [ + "login.partner.microsoftonline.cn", + "login.chinacloudapi.cn" + ] + }, + { + "preferred_network": "login.microsoftonline.de", + "preferred_cache": "login.microsoftonline.de", + "aliases": [ + "login.microsoftonline.de" + ] + }, + { + "preferred_network": "login.microsoftonline.us", + "preferred_cache": "login.microsoftonline.us", + "aliases": [ + "login.microsoftonline.us", + "login.usgovcloudapi.net" + ] + }, + { + "preferred_network": "login-us.microsoftonline.com", + "preferred_cache": "login-us.microsoftonline.com", + "aliases": [ + "login-us.microsoftonline.com" + ] + } + ] + } + }, + { + "RequestUri": "https://fake-endpoint.azconfig.io/kv?key=message%2A\u0026label=dev\u0026api-version=1.0", + "RequestMethod": "GET", + "RequestHeaders": { + "Accept": "application/vnd.microsoft.appconfig.kvset\u002Bjson, application/json, application/problem\u002Bjson", + "Accept-Encoding": "gzip, deflate", + "Connection": "keep-alive", + "Correlation-Context": "RequestType=Startup", + "User-Agent": "python-appconfiguration-provider/1.0.0b1 azsdk-python-appconfiguration/1.4.0 Python/3.9.13 (Windows-10-10.0.19044-SP0)" + }, + "RequestBody": null, + "StatusCode": 200, + "ResponseHeaders": { + "Access-Control-Allow-Credentials": "true", + "Access-Control-Allow-Origin": "*", + "Access-Control-Expose-Headers": "DNT, X-CustomHeader, Keep-Alive, User-Agent, X-Requested-With, If-Modified-Since, Cache-Control, Content-Type, Authorization, x-ms-client-request-id, x-ms-useragent, x-ms-content-sha256, x-ms-date, host, Accept, Accept-Datetime, Date, If-Match, If-None-Match, Sync-Token, x-ms-return-client-request-id, ETag, Last-Modified, Link, Memento-Datetime, retry-after-ms, x-ms-request-id, x-ms-client-session-id, x-ms-effective-locale, WWW-Authenticate, traceparent, tracestate", + "Connection": "keep-alive", + "Content-Type": "application/vnd.microsoft.appconfig.kvset\u002Bjson; charset=utf-8", + "Date": "Tue, 27 Sep 2022 22:42:42 GMT", + "Server": "openresty/1.17.8.2", + "Strict-Transport-Security": "max-age=15724800; includeSubDomains", + "Sync-Token": "zAJw6V16=NzoxNyMxOTgyNzIxMg==;sn=19827212", + "Transfer-Encoding": "chunked", + "x-ms-correlation-request-id": "98fc8948-d720-480a-a4f8-837e65f57cd0" + }, + "ResponseBody": { + "items": [ + { + "etag": "yEiVkIl-IHlB26quPXo_Z2GbB5yQm_ENTWI2c1k1J4E", + "key": "message", + "label": "dev", + "content_type": "", + "value": "test", + "tags": {}, + "locked": false, + "last_modified": "2022-09-27T20:06:21\u002B00:00" + } + ] + } + } + ], + "Variables": {} +} \ No newline at end of file diff --git a/sdk/appconfiguration/azure-appconfiguration-provider/tests/recordings/test_provider_aad.pyTestAppConfigurationProvidertest_provider_trimmed_key_prefixes.json b/sdk/appconfiguration/azure-appconfiguration-provider/tests/recordings/test_provider_aad.pyTestAppConfigurationProvidertest_provider_trimmed_key_prefixes.json new file mode 100644 index 000000000000..ae777e18c7df --- /dev/null +++ b/sdk/appconfiguration/azure-appconfiguration-provider/tests/recordings/test_provider_aad.pyTestAppConfigurationProvidertest_provider_trimmed_key_prefixes.json @@ -0,0 +1,239 @@ +{ + "Entries": [ + { + "RequestUri": "https://login.microsoftonline.com/fake-tenant-id/v2.0/.well-known/openid-configuration", + "RequestMethod": "GET", + "RequestHeaders": { + "Accept": "*/*", + "Accept-Encoding": "gzip, deflate", + "Connection": "keep-alive", + "User-Agent": "azsdk-python-identity/1.12.0b2 Python/3.9.13 (Windows-10-10.0.19044-SP0)" + }, + "RequestBody": null, + "StatusCode": 200, + "ResponseHeaders": { + "Access-Control-Allow-Methods": "GET, OPTIONS", + "Access-Control-Allow-Origin": "*", + "Cache-Control": "max-age=86400, private", + "Content-Length": "1599", + "Content-Type": "application/json; charset=utf-8", + "Date": "Tue, 27 Sep 2022 22:42:41 GMT", + "P3P": "CP=\u0022DSP CUR OTPi IND OTRi ONL FIN\u0022", + "Set-Cookie": [ + "fpc=Aih2w7HdW0RLsLtcNXW6NJri05sNAQAAAGBzxdoOAAAA; expires=Thu, 27-Oct-2022 22:42:41 GMT; path=/; secure; HttpOnly; SameSite=None", + "x-ms-gateway-slice=estsfd; path=/; secure; samesite=none; httponly", + "stsservicecookie=estsfd; path=/; secure; samesite=none; httponly" + ], + "Strict-Transport-Security": "max-age=31536000; includeSubDomains", + "X-Content-Type-Options": "nosniff", + "x-ms-ests-server": "2.1.13672.11 - NCUS ProdSlices", + "X-XSS-Protection": "0" + }, + "ResponseBody": { + "token_endpoint": "https://login.microsoftonline.com/fake-tenant-id/oauth2/v2.0/token", + "token_endpoint_auth_methods_supported": [ + "client_secret_post", + "private_key_jwt", + "client_secret_basic" + ], + "jwks_uri": "https://login.microsoftonline.com/fake-tenant-id/discovery/v2.0/keys", + "response_modes_supported": [ + "query", + "fragment", + "form_post" + ], + "subject_types_supported": [ + "pairwise" + ], + "id_token_signing_alg_values_supported": [ + "RS256" + ], + "response_types_supported": [ + "code", + "id_token", + "code id_token", + "id_token token" + ], + "scopes_supported": [ + "openid", + "profile", + "email", + "offline_access" + ], + "issuer": "https://login.microsoftonline.com/fake-tenant-id/v2.0", + "request_uri_parameter_supported": false, + "userinfo_endpoint": "https://graph.microsoft.com/oidc/userinfo", + "authorization_endpoint": "https://login.microsoftonline.com/fake-tenant-id/oauth2/v2.0/authorize", + "device_authorization_endpoint": "https://login.microsoftonline.com/fake-tenant-id/oauth2/v2.0/devicecode", + "http_logout_supported": true, + "frontchannel_logout_supported": true, + "end_session_endpoint": "https://login.microsoftonline.com/fake-tenant-id/oauth2/v2.0/logout", + "claims_supported": [ + "sub", + "iss", + "cloud_instance_name", + "cloud_instance_host_name", + "cloud_graph_host_name", + "msgraph_host", + "aud", + "exp", + "iat", + "auth_time", + "acr", + "nonce", + "preferred_username", + "name", + "tid", + "ver", + "at_hash", + "c_hash", + "email" + ], + "kerberos_endpoint": "https://login.microsoftonline.com/fake-tenant-id/kerberos", + "tenant_region_scope": "WW", + "cloud_instance_name": "microsoftonline.com", + "cloud_graph_host_name": "graph.windows.net", + "msgraph_host": "graph.microsoft.com", + "rbac_url": "https://pas.windows.net" + } + }, + { + "RequestUri": "https://login.microsoftonline.com/common/discovery/instance?api-version=1.1\u0026authorization_endpoint=https://login.microsoftonline.com/common/oauth2/authorize", + "RequestMethod": "GET", + "RequestHeaders": { + "Accept": "application/json", + "Accept-Encoding": "gzip, deflate", + "Connection": "keep-alive", + "Cookie": "fpc=Aih2w7HdW0RLsLtcNXW6NJri05sNAQAAAGBzxdoOAAAA; stsservicecookie=estsfd; x-ms-gateway-slice=estsfd", + "User-Agent": "azsdk-python-identity/1.12.0b2 Python/3.9.13 (Windows-10-10.0.19044-SP0)" + }, + "RequestBody": null, + "StatusCode": 200, + "ResponseHeaders": { + "Access-Control-Allow-Methods": "GET, OPTIONS", + "Access-Control-Allow-Origin": "*", + "Cache-Control": "max-age=86400, private", + "Content-Length": "945", + "Content-Type": "application/json; charset=utf-8", + "Date": "Tue, 27 Sep 2022 22:42:41 GMT", + "P3P": "CP=\u0022DSP CUR OTPi IND OTRi ONL FIN\u0022", + "Set-Cookie": [ + "fpc=Aih2w7HdW0RLsLtcNXW6NJri05sNAQAAAGBzxdoOAAAA; expires=Thu, 27-Oct-2022 22:42:41 GMT; path=/; secure; HttpOnly; SameSite=None", + "x-ms-gateway-slice=estsfd; path=/; secure; samesite=none; httponly", + "stsservicecookie=estsfd; path=/; secure; samesite=none; httponly" + ], + "Strict-Transport-Security": "max-age=31536000; includeSubDomains", + "X-Content-Type-Options": "nosniff", + "x-ms-ests-server": "2.1.13672.11 - EUS ProdSlices", + "X-XSS-Protection": "0" + }, + "ResponseBody": { + "tenant_discovery_endpoint": "https://login.microsoftonline.com/common/.well-known/openid-configuration", + "api-version": "1.1", + "metadata": [ + { + "preferred_network": "login.microsoftonline.com", + "preferred_cache": "login.windows.net", + "aliases": [ + "login.microsoftonline.com", + "login.windows.net", + "login.microsoft.com", + "sts.windows.net" + ] + }, + { + "preferred_network": "login.partner.microsoftonline.cn", + "preferred_cache": "login.partner.microsoftonline.cn", + "aliases": [ + "login.partner.microsoftonline.cn", + "login.chinacloudapi.cn" + ] + }, + { + "preferred_network": "login.microsoftonline.de", + "preferred_cache": "login.microsoftonline.de", + "aliases": [ + "login.microsoftonline.de" + ] + }, + { + "preferred_network": "login.microsoftonline.us", + "preferred_cache": "login.microsoftonline.us", + "aliases": [ + "login.microsoftonline.us", + "login.usgovcloudapi.net" + ] + }, + { + "preferred_network": "login-us.microsoftonline.com", + "preferred_cache": "login-us.microsoftonline.com", + "aliases": [ + "login-us.microsoftonline.com" + ] + } + ] + } + }, + { + "RequestUri": "https://fake-endpoint.azconfig.io/kv?key=%2A\u0026label=%00\u0026api-version=1.0", + "RequestMethod": "GET", + "RequestHeaders": { + "Accept": "application/vnd.microsoft.appconfig.kvset\u002Bjson, application/json, application/problem\u002Bjson", + "Accept-Encoding": "gzip, deflate", + "Connection": "keep-alive", + "Correlation-Context": "RequestType=Startup", + "User-Agent": "python-appconfiguration-provider/1.0.0b1 azsdk-python-appconfiguration/1.4.0 Python/3.9.13 (Windows-10-10.0.19044-SP0)" + }, + "RequestBody": null, + "StatusCode": 200, + "ResponseHeaders": { + "Access-Control-Allow-Credentials": "true", + "Access-Control-Allow-Origin": "*", + "Access-Control-Expose-Headers": "DNT, X-CustomHeader, Keep-Alive, User-Agent, X-Requested-With, If-Modified-Since, Cache-Control, Content-Type, Authorization, x-ms-client-request-id, x-ms-useragent, x-ms-content-sha256, x-ms-date, host, Accept, Accept-Datetime, Date, If-Match, If-None-Match, Sync-Token, x-ms-return-client-request-id, ETag, Last-Modified, Link, Memento-Datetime, retry-after-ms, x-ms-request-id, x-ms-client-session-id, x-ms-effective-locale, WWW-Authenticate, traceparent, tracestate", + "Connection": "keep-alive", + "Content-Type": "application/vnd.microsoft.appconfig.kvset\u002Bjson; charset=utf-8", + "Date": "Tue, 27 Sep 2022 22:42:41 GMT", + "Server": "openresty/1.17.8.2", + "Strict-Transport-Security": "max-age=15724800; includeSubDomains", + "Sync-Token": "zAJw6V16=NzoxNyMxOTgyNzIxMg==;sn=19827212", + "Transfer-Encoding": "chunked", + "x-ms-correlation-request-id": "902e6b7c-5cfa-45b1-a567-c8ca2469af77" + }, + "ResponseBody": { + "items": [ + { + "etag": "RX6Uzn3YdpSnDk7SX9kzxXwDFdYhR8dGGLEXBZ6i_nM", + "key": "message", + "label": null, + "content_type": "", + "value": "hi", + "tags": {}, + "locked": false, + "last_modified": "2022-09-27T19:41:40\u002B00:00" + }, + { + "etag": "dYgyN_oxYWzXEQN8K8d2q-4rKQJwftuzR75Jagqq_pw", + "key": "my_json", + "label": null, + "content_type": "application/json", + "value": "{\u0022key\u0022:\u0022value\u0022}", + "tags": {}, + "locked": false, + "last_modified": "2022-09-27T20:35:17\u002B00:00" + }, + { + "etag": "7vw5ma2Cc0-eE1cnRaaCv2_VtAS8BQ5jbHP9eNS_MFA", + "key": "test.trimmed", + "label": null, + "content_type": "key", + "value": "key", + "tags": {}, + "locked": false, + "last_modified": "2022-09-27T19:58:41\u002B00:00" + } + ] + } + } + ], + "Variables": {} +} \ No newline at end of file diff --git a/sdk/appconfiguration/azure-appconfiguration-provider/tests/test_provider.py b/sdk/appconfiguration/azure-appconfiguration-provider/tests/test_provider.py new file mode 100644 index 000000000000..ec14c4a5bad8 --- /dev/null +++ b/sdk/appconfiguration/azure-appconfiguration-provider/tests/test_provider.py @@ -0,0 +1,48 @@ +# ------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# -------------------------------------------------------------------------- +from azure.appconfiguration.provider import ( + AzureAppConfigurationProvider, + SettingSelector +) +from devtools_testutils import ( + AzureRecordedTestCase, + recorded_by_proxy +) +from azure.appconfiguration import AzureAppConfigurationClient +from preparers import app_config_decorator + +class TestAppConfigurationProvider(AzureRecordedTestCase): + + def build_provider(self, connection_string, trimmed_key_prefixes=[], selects={SettingSelector("*", "\0")}): + return AzureAppConfigurationProvider.load(connection_string=connection_string, trimmed_key_prefixes=trimmed_key_prefixes, selects=selects) + + # method: provider_creation + @recorded_by_proxy + @app_config_decorator + def test_provider_creation(self, appconfiguration_connection_string): + client = self.build_provider(appconfiguration_connection_string) + assert client["message"] == "hi" + assert client["my_json"]["key"] == "value" + + # method: provider_trimmed_key_prefixes + @recorded_by_proxy + @app_config_decorator + def test_provider_trimmed_key_prefixes(self, appconfiguration_connection_string): + trimmed = {"test."} + client = self.build_provider(appconfiguration_connection_string, trimmed_key_prefixes=trimmed) + assert client["message"] == "hi" + assert client["my_json"]["key"] == "value" + assert client["trimmed"] == "key" + assert "test.trimmed" not in client + + # method: provider_selectors + @recorded_by_proxy + @app_config_decorator + def test_provider_selectors(self, appconfiguration_connection_string): + selects = {SettingSelector("message*", "dev")} + client = self.build_provider(appconfiguration_connection_string, selects=selects) + assert client["message"] == "test" + assert "test.trimmed" not in client diff --git a/sdk/appconfiguration/azure-appconfiguration-provider/tests/test_provider_aad.py b/sdk/appconfiguration/azure-appconfiguration-provider/tests/test_provider_aad.py new file mode 100644 index 000000000000..5ab9351a2828 --- /dev/null +++ b/sdk/appconfiguration/azure-appconfiguration-provider/tests/test_provider_aad.py @@ -0,0 +1,49 @@ +# ------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# -------------------------------------------------------------------------- +from azure.appconfiguration.provider import ( + AzureAppConfigurationProvider, + SettingSelector +) +from devtools_testutils import ( + AzureRecordedTestCase, + recorded_by_proxy +) +from azure.appconfiguration import AzureAppConfigurationClient +from preparers import app_config_decorator_aad + +class TestAppConfigurationProvider(AzureRecordedTestCase): + + def build_provider_aad(self, endpoint, trimmed_key_prefixes=[], selects={SettingSelector("*", "\0")}): + cred = self.get_credential(AzureAppConfigurationClient) + return AzureAppConfigurationProvider.load(endpoint=endpoint,credential=cred, trimmed_key_prefixes=trimmed_key_prefixes, selects=selects) + + # method: provider_creation_aad + @recorded_by_proxy + @app_config_decorator_aad + def test_provider_creation_aad(self, appconfiguration_endpoint_string): + client = self.build_provider_aad(appconfiguration_endpoint_string) + assert client["message"] == "hi" + assert client["my_json"]["key"] == "value" + + # method: provider_trimmed_key_prefixes + @recorded_by_proxy + @app_config_decorator_aad + def test_provider_trimmed_key_prefixes(self, appconfiguration_endpoint_string): + trimmed = {"test."} + client = self.build_provider_aad(appconfiguration_endpoint_string, trimmed_key_prefixes=trimmed) + assert client["message"] == "hi" + assert client["my_json"]["key"] == "value" + assert client["trimmed"] == "key" + assert "test.trimmed" not in client + + # method: provider_selectors + @recorded_by_proxy + @app_config_decorator_aad + def test_provider_selectors(self, appconfiguration_endpoint_string): + selects = {SettingSelector("message*", "dev")} + client = self.build_provider_aad(appconfiguration_endpoint_string, selects=selects) + assert client["message"] == "test" + assert "test.trimmed" not in client \ No newline at end of file diff --git a/sdk/appconfiguration/ci.yml b/sdk/appconfiguration/ci.yml index c6b6d395385b..05db4ccc96ca 100644 --- a/sdk/appconfiguration/ci.yml +++ b/sdk/appconfiguration/ci.yml @@ -3,27 +3,27 @@ trigger: branches: include: - - main - - hotfix/* - - release/* - - restapi* + - main + - hotfix/* + - release/* + - restapi* paths: include: - - sdk/appconfiguration/ - - sdk/core/ + - sdk/appconfiguration/ + - sdk/core/ pr: branches: include: - - main - - feature/* - - hotfix/* - - release/* - - restapi* + - main + - feature/* + - hotfix/* + - release/* + - restapi* paths: include: - - sdk/appconfiguration/ - - sdk/core/ + - sdk/appconfiguration/ + - sdk/core/ extends: template: ../../eng/pipelines/templates/stages/archetype-sdk-client.yml @@ -35,5 +35,7 @@ extends: Artifacts: - name: azure-appconfiguration safeName: azureappconfiguration + - name: azure-appconfiguration-provider + safeName: azureappconfigurationprovider - name: azure-mgmt-appconfiguration - safeName: azuremgmtappconfiguration \ No newline at end of file + safeName: azuremgmtappconfiguration diff --git a/shared_requirements.txt b/shared_requirements.txt index cd9469a29bf9..19f88b6cf136 100644 --- a/shared_requirements.txt +++ b/shared_requirements.txt @@ -3,6 +3,7 @@ azure-ai-translation-nspkg azure-ai-language-nspkg azure-iot-nspkg azure-monitor-nspkg +azure-appconfiguration<2.0.0,>=1.2.0 azure-applicationinsights~=0.1.0 azure-batch~=4.1 azure-cognitiveservices-nspkg @@ -185,6 +186,8 @@ opentelemetry-sdk<2.0.0,>=1.5.0,!=1.10a0 #override azure-eventhub-checkpointstoretable azure-core<2.0.0,>=1.14.0 #override azure-eventhub uamqp>=1.6.0,<2.0.0 #override azure-appconfiguration azure-core<2.0.0,>=1.24.0 +#override azure-appconfiguration-provider azure-appconfiguration<2.0.0,>=1.3.0 +#override azure-appconfiguration-provider azure-keyvault-secrets<5.0.0,>=4.3.0 #override azure-servicebus uamqp>=1.5.1,<2.0.0 #override azure-servicebus msrest>=0.6.17,<2.0.0 #override azure-servicebus azure-core<2.0.0,>=1.14.0