Skip to content
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CredScanSuppressions.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
},
{
"file": [
"src\\azure-cli\\azure\\cli\\command_modules\\appconfig\\tests\\latest\\recordings\\test_appconfig_to_appservice_import_export.yaml",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you tell which secret/password is against the CredScan?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

my bad, cred scan suppression is not needed for this test because its a livescenario test. I'll remove it.

"src\\azure-cli\\azure\\cli\\command_modules\\appconfig\\tests\\latest\\recordings\\test_azconfig_credential.yaml",
"src\\azure-cli\\azure\\cli\\command_modules\\appconfig\\tests\\latest\\recordings\\test_azconfig_feature.yaml",
"src\\azure-cli\\azure\\cli\\command_modules\\appconfig\\tests\\latest\\recordings\\test_azconfig_feature_filter.yaml",
Expand Down
4 changes: 4 additions & 0 deletions src/azure-cli/HISTORY.rst
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,10 @@
Release History
===============

**AppConfig**

* Support import/export of keyvault references from/to appservice

2.0.79
++++++

Expand Down
19 changes: 19 additions & 0 deletions src/azure-cli/azure/cli/command_modules/appconfig/_constants.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# --------------------------------------------------------------------------------------------
# Copyright (c) Microsoft Corporation. All rights reserved.
# Licensed under the MIT License. See License.txt in the project root for license information.
# --------------------------------------------------------------------------------------------

# pylint: disable=too-few-public-methods

"""Reserved keywords in the AppConfiguration service.
"""


class FeatureFlagConstants:
FEATURE_FLAG_PREFIX = ".appconfig.featureflag/"
FEATURE_FLAG_CONTENT_TYPE = "application/vnd.microsoft.appconfig.ff+json;charset=utf-8"


class KeyVaultConstants:
KEYVAULT_CONTENT_TYPE = "application/vnd.microsoft.appconfig.keyvaultref+json;charset=utf-8"
APPSVC_KEYVAULT_PREFIX = "@Microsoft.KeyVault"
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,12 @@
from knack.log import get_logger
from azure.cli.core.util import shell_safe_json_parse
from ._azconfig.models import KeyValue
from ._constants import FeatureFlagConstants

# pylint: disable=too-few-public-methods
# pylint: disable=too-many-instance-attributes

logger = get_logger(__name__)
FEATURE_FLAG_PREFIX = ".appconfig.featureflag/"
FEATURE_FLAG_CONTENT_TYPE = "application/vnd.microsoft.appconfig.ff+json;charset=utf-8"

# Feature Flag Models #

Expand Down Expand Up @@ -191,12 +190,12 @@ def map_featureflag_to_keyvalue(featureflag):
enabled=enabled,
conditions=featureflag.conditions)

set_kv = KeyValue(key=FEATURE_FLAG_PREFIX + featureflag.key,
set_kv = KeyValue(key=FeatureFlagConstants.FEATURE_FLAG_PREFIX + featureflag.key,
label=featureflag.label,
value=json.dumps(feature_flag_value,
default=lambda o: o.__dict__,
ensure_ascii=False),
content_type=FEATURE_FLAG_CONTENT_TYPE,
content_type=FeatureFlagConstants.FEATURE_FLAG_CONTENT_TYPE,
tags={})

set_kv.locked = featureflag.locked
Expand Down Expand Up @@ -224,7 +223,7 @@ def map_keyvalue_to_featureflag(keyvalue, show_conditions=True):
Return:
FeatureFlag object
'''
feature_name = keyvalue.key[len(FEATURE_FLAG_PREFIX):]
feature_name = keyvalue.key[len(FeatureFlagConstants.FEATURE_FLAG_PREFIX):]

feature_flag_value = map_keyvalue_to_featureflagvalue(keyvalue)

Expand Down Expand Up @@ -272,7 +271,7 @@ def map_keyvalue_to_featureflagvalue(keyvalue):
try:
# Make sure value string is a valid json
feature_flag_dict = shell_safe_json_parse(keyvalue.value)
feature_name = keyvalue.key[len(FEATURE_FLAG_PREFIX):]
feature_name = keyvalue.key[len(FeatureFlagConstants.FEATURE_FLAG_PREFIX):]

# Make sure value json has all the fields we support in the backend
valid_fields = {
Expand Down
65 changes: 55 additions & 10 deletions src/azure-cli/azure/cli/command_modules/appconfig/_kv_helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
from knack.log import get_logger
from knack.util import CLIError

from ._constants import FeatureFlagConstants, KeyVaultConstants
from ._utils import resolve_connection_string, user_confirmation
from ._azconfig.azconfig_client import AzconfigClient
from ._azconfig.models import (KeyValue,
Expand All @@ -26,8 +27,6 @@
FeatureFlagValue)

logger = get_logger(__name__)
FEATURE_FLAG_PREFIX = ".appconfig.featureflag/"
FEATURE_FLAG_CONTENT_TYPE = "application/vnd.microsoft.appconfig.ff+json;charset=utf-8"
FEATURE_MANAGEMENT_KEYWORDS = ["FeatureManagement", "featureManagement", "feature_management", "feature-management"]
ENABLED_FOR_KEYWORDS = ["EnabledFor", "enabledFor", "enabled_for", "enabled-for"]

Expand Down Expand Up @@ -248,7 +247,7 @@ def __write_kv_and_features_to_config_store(cmd, key_values, features=None, name

def __is_feature_flag(kv):
if kv and kv.key and kv.content_type:
return kv.key.startswith(FEATURE_FLAG_PREFIX) and kv.content_type == FEATURE_FLAG_CONTENT_TYPE
return kv.key.startswith(FeatureFlagConstants.FEATURE_FLAG_PREFIX) and kv.content_type == FeatureFlagConstants.FEATURE_FLAG_CONTENT_TYPE
return False


Expand All @@ -269,14 +268,48 @@ def __read_kv_from_app_service(cmd, appservice_account, prefix_to_add=""):
cmd, resource_group_name=appservice_account["resource_group"], name=appservice_account["name"], slot=None)
for item in settings:
key = prefix_to_add + item['name']
value = item['value']
tags = {'AppService:SlotSetting': str(item['slotSetting']).lower()} if item['slotSetting'] else {}
value = item['value']

# Value will look like one of the following if it is a KeyVault reference:
# @Microsoft.KeyVault(SecretUri=https://myvault.vault.azure.net/secrets/mysecret/ec96f02080254f109c51a1f14cdb1931)
# @Microsoft.KeyVault(VaultName=myvault;SecretName=mysecret;SecretVersion=ec96f02080254f109c51a1f14cdb1931)
if value and value.strip().lower().startswith(KeyVaultConstants.APPSVC_KEYVAULT_PREFIX.lower()):
try:
# Strip all whitespaces from value string.
# Valid values of SecretUri, VaultName, SecretName or SecretVersion will never have whitespaces.
value = value.replace(" ", "")
appsvc_value_dict = dict(x.split('=') for x in value[len(KeyVaultConstants.APPSVC_KEYVAULT_PREFIX) + 1: -1].split(';'))
appsvc_value_dict = {k.lower(): v for k, v in appsvc_value_dict.items()}
secret_identifier = appsvc_value_dict.get('secreturi')
if not secret_identifier:
# Construct secreturi
vault_name = appsvc_value_dict.get('vaultname')
secret_name = appsvc_value_dict.get('secretname')
secret_version = appsvc_value_dict.get('secretversion')
secret_identifier = "https://{0}.vault.azure.net/secrets/{1}/{2}".format(vault_name, secret_name, secret_version)
try:
from azure.keyvault.key_vault_id import KeyVaultIdentifier
# this throws an exception for invalid format of secret identifier
KeyVaultIdentifier(uri=secret_identifier)
kv = KeyValue(key=key,
value=json.dumps({"uri": secret_identifier}, ensure_ascii=False, separators=(',', ':')),
tags=tags,
content_type=KeyVaultConstants.KEYVAULT_CONTENT_TYPE)
key_values.append(kv)
continue
except (TypeError, ValueError) as e:
logger.debug(
'Exception while validating the format of KeyVault identifier. Key "%s" with value "%s" will be treated like a regular key-value.\n%s', key, value, str(e))
except (AttributeError, TypeError, ValueError) as e:
logger.debug(
'Key "%s" with value "%s" is not a well-formatted KeyVault reference. It will be treated like a regular key-value.\n%s', key, value, str(e))

kv = KeyValue(key=key, value=value, tags=tags)
key_values.append(kv)
return key_values
except Exception as exception:
raise CLIError(
"Fail to read key-values from appservice." + str(exception))
raise CLIError("Failed to read key-values from appservice." + str(exception))


def __write_kv_to_app_service(cmd, key_values, appservice_account):
Expand All @@ -286,6 +319,19 @@ def __write_kv_to_app_service(cmd, key_values, appservice_account):
for kv in key_values:
name = kv.key
value = kv.value
# If its a KeyVault ref, convert the format to AppService KeyVault ref format
if kv.content_type and kv.content_type.lower() == KeyVaultConstants.KEYVAULT_CONTENT_TYPE:
try:
secret_uri = json.loads(value).get("uri")
if secret_uri:
value = KeyVaultConstants.APPSVC_KEYVAULT_PREFIX + '(SecretUri={0})'.format(secret_uri)
else:
logger.debug(
'Key "%s" with value "%s" is not a well-formatted KeyVault reference. It will be treated like a regular key-value.\n', name, value)
except (AttributeError, TypeError, ValueError) as e:
logger.debug(
'Key "%s" with value "%s" is not a well-formatted KeyVault reference. It will be treated like a regular key-value.\n%s', name, value, str(e))

if 'AppService:SlotSetting' in kv.tags and kv.tags['AppService:SlotSetting'] == 'true':
slot_settings.append(name + '=' + value)
else:
Expand All @@ -295,8 +341,7 @@ def __write_kv_to_app_service(cmd, key_values, appservice_account):
update_app_settings(cmd, resource_group_name=appservice_account["resource_group"],
name=appservice_account["name"], settings=non_slot_settings, slot_settings=slot_settings)
except Exception as exception:
raise CLIError(
"Fail to write key-values to appservice: " + str(exception))
raise CLIError("Failed to write key-values to appservice: " + str(exception))


# Helper functions
Expand Down Expand Up @@ -649,7 +694,7 @@ def __convert_feature_dict_to_keyvalue_list(features_dict, enabled_for_keyword):

try:
for k, v in features_dict.items():
key = FEATURE_FLAG_PREFIX + str(k)
key = FeatureFlagConstants.FEATURE_FLAG_PREFIX + str(k)
feature_flag_value = FeatureFlagValue(id_=str(k))

if isinstance(v, dict):
Expand Down Expand Up @@ -688,7 +733,7 @@ def __convert_feature_dict_to_keyvalue_list(features_dict, enabled_for_keyword):

set_kv = KeyValue(key=key,
value=json.dumps(feature_flag_value, default=lambda o: o.__dict__, ensure_ascii=False),
content_type=FEATURE_FLAG_CONTENT_TYPE)
content_type=FeatureFlagConstants.FEATURE_FLAG_CONTENT_TYPE)
key_values.append(set_kv)

except Exception as exception:
Expand Down
11 changes: 10 additions & 1 deletion src/azure-cli/azure/cli/command_modules/appconfig/_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ def resolve_resource_group(cmd, config_store_name):
config_store_client = cf_configstore(cmd.cli_ctx)
all_stores = config_store_client.list()
for store in all_stores:
if store.name == config_store_name:
if store.name.lower() == config_store_name.lower():
# Id has a fixed structure /subscriptions/subscriptionName/resourceGroups/groupName/providers/providerName/configurationStores/storeName"
return store.id.split('/')[4], store.endpoint
raise CLIError(
Expand Down Expand Up @@ -99,3 +99,12 @@ def is_valid_connection_string(connection_string):
return False
return True
return False


def get_store_name_from_connection_string(connection_string):
if is_valid_connection_string(connection_string):
segments = dict(seg.split("=", 1) for seg in connection_string.split(";"))
endpoint = segments.get("Endpoint")
if endpoint:
return endpoint.split("//")[1].split('.')[0]
return None
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
from knack.log import get_logger
from knack.util import CLIError

from ._utils import is_valid_connection_string, resolve_resource_group
from ._utils import is_valid_connection_string, resolve_resource_group, get_store_name_from_connection_string
from ._azconfig.models import QueryFields
from ._featuremodels import FeatureQueryFields

Expand Down Expand Up @@ -85,7 +85,10 @@ def validate_appservice_name_or_id(cmd, namespace):
from msrestazure.tools import is_valid_resource_id, parse_resource_id
if namespace.appservice_account:
if not is_valid_resource_id(namespace.appservice_account):
resource_group, _ = resolve_resource_group(cmd, namespace.name)
config_store_name = namespace.name
if not config_store_name:
config_store_name = get_store_name_from_connection_string(namespace.connection_string)
resource_group, _ = resolve_resource_group(cmd, config_store_name)
namespace.appservice_account = {
"subscription": get_subscription_id(cmd.cli_ctx),
"resource_group": resource_group,
Expand Down
Loading