Skip to content

Commit

Permalink
[AppConfig] Support import/export of keyvault ref from appservice (#1…
Browse files Browse the repository at this point in the history
…1773)

* Support import/export of keyvault from appservice

* Centralize reserved keywords and constants

* Add connection string to all test commands

* Remove shell_safe_json_parse

* Making appservice test LiveScenarioTest

* Updating history

* Resolving comments

* Reuse appsvc_value_dict variable

* Remove test file from credscansuppression

Co-authored-by: Zunli Hu <[email protected]>
  • Loading branch information
avanigupta and Juliehzl committed Jan 17, 2020
1 parent 9624744 commit a68970c
Show file tree
Hide file tree
Showing 17 changed files with 3,271 additions and 3,105 deletions.
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

**AppService**

* Azure Stack: surface commands under the profile of 2019-03-01-hybrid
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

0 comments on commit a68970c

Please sign in to comment.