Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -164,11 +164,12 @@ def load_arguments(self, _):
c.argument('top', arg_type=top_arg_type)
c.argument('all_', options_list=['--all'], action='store_true', help="List all items.")
c.argument('fields', arg_type=fields_arg_type)
c.argument('endpoint', help='If auth mode is "login", provide endpoint URL of the App Configuration store. The endpoint can be retrieved using "az appconfig show" command. You can configure the default endpoint using `az configure --defaults appconfig_endpoint=<endpoint>`', configured_default='appconfig_endpoint')
c.argument('auth_mode', arg_type=get_enum_type(['login', 'key']), configured_default='appconfig_auth_mode', validator=validate_auth_mode,
c.argument('endpoint', help='If auth mode is "login" or "anonymous", provide endpoint URL of the App Configuration store. The endpoint can be retrieved using "az appconfig show" command. You can configure the default endpoint using `az configure --defaults appconfig_endpoint=<endpoint>`', configured_default='appconfig_endpoint')
Copy link
Member

Choose a reason for hiding this comment

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

Is saying anonymous useful here as the simulator is also a requirement. It could mislead someone to thinking our service allows it.

Copy link
Contributor Author

@ChristineWanjau ChristineWanjau Jan 19, 2026

Choose a reason for hiding this comment

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

Maybe we can mention in our help texts anonymous is only used with the app configuration emulator?

c.argument('auth_mode', arg_type=get_enum_type(['login', 'key', 'anonymous']), configured_default='appconfig_auth_mode', validator=validate_auth_mode,
help='This parameter can be used for indicating how a data operation is to be authorized. ' +
'If the auth mode is "key", provide connection string or store name and your account access keys will be retrieved for authorization. ' +
'If the auth mode is "login", provide the `--endpoint` or `--name` and your "az login" credentials will be used for authorization. ' +
'If the auth mode is "anonymous", provide the `--endpoint` that will be used for authorization. Anonymous mode only allows HTTP endpoints. ' +
'You can configure the default auth mode using `az configure --defaults appconfig_auth_mode=<auth_mode>`. ' +
'For more information, see https://learn.microsoft.com/azure/azure-app-configuration/concept-enable-rbac')

Expand Down Expand Up @@ -256,7 +257,7 @@ def load_arguments(self, _):
c.argument('src_label', help="Only keys with this label in source AppConfig will be imported. If no value specified, import keys with null label by default. Support star sign as filters, for instance * means all labels, abc* means labels with abc as prefix.")
c.argument('preserve_labels', arg_type=get_three_state_flag(), help="Flag to preserve labels from source AppConfig. This argument should NOT be specified along with --label.")
c.argument('src_endpoint', help='If --src-auth-mode is "login", provide endpoint URL of the source App Configuration store.')
c.argument('src_auth_mode', arg_type=get_enum_type(['login', 'key']),
c.argument('src_auth_mode', arg_type=get_enum_type(['login', 'key', 'anonymous']),
help='Auth mode for connecting to source App Configuration store. For details, refer to "--auth-mode" argument.')
c.argument('src_snapshot', validator=validate_snapshot_import,
help='Import all keys in a given snapshot of the source App Configuration store. If no snapshot is specified, the keys currently in the store are imported based on the specified key and label filters.')
Expand Down Expand Up @@ -299,7 +300,7 @@ def load_arguments(self, _):
c.argument('dest_label', help="Exported KVs will be labeled with this destination label. If neither --dest-label nor --preserve-labels is specified, will assign null label.")
c.argument('preserve_labels', arg_type=get_three_state_flag(), help="Flag to preserve labels from source AppConfig. This argument should NOT be specified along with --dest-label.")
c.argument('dest_endpoint', help='If --dest-auth-mode is "login", provide endpoint URL of the destination App Configuration store.')
c.argument('dest_auth_mode', arg_type=get_enum_type(['login', 'key']),
c.argument('dest_auth_mode', arg_type=get_enum_type(['login', 'key', 'anonymous']),
help='Auth mode for connecting to the destination App Configuration store. For details, refer to "--auth-mode" argument.')
c.argument('dest_tags', nargs="*", help="Exported KVs and feature flags will be assigned with these tags. If no tags are specified, exported KVs and features will retain existing tags. Support space-separated tags: key[=value] [key[=value] ...]. Use "" to clear existing tags.")

Expand Down
35 changes: 34 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 @@ -8,13 +8,14 @@
from knack.util import CLIError
from azure.appconfiguration import AzureAppConfigurationClient
from azure.core.exceptions import HttpResponseError
from azure.core.credentials import AzureKeyCredential
from azure.cli.core.azclierror import (ValidationError,
AzureResponseError,
InvalidArgumentValueError,
ResourceNotFoundError,
RequiredArgumentMissingError,
MutuallyExclusiveArgumentError)

from azure.core.pipeline.transport import RequestsTransport # pylint: disable=no-name-in-module
from ._client_factory import cf_configstore
from ._constants import HttpHeaders, FeatureFlagConstants

Expand Down Expand Up @@ -156,8 +157,33 @@ def prep_filter_for_url_encoding(filter_value=None):
return filter_value


class AuthHeaderRequestsTransport(RequestsTransport): # pylint: disable=too-few-public-methods
def send(self, request, **kwargs): # pylint: disable=arguments-differ
# Strip any auth/signature headers to allow anonymous access
if 'Authorization' in request.headers:
del request.headers['Authorization']

# Also remove HMAC signature header if present
if 'x-ms-content-sha256' in request.headers:
del request.headers['x-ms-content-sha256']

return super().send(request, **kwargs)


def get_appconfig_data_client(cmd, name, connection_string, auth_mode, endpoint):
azconfig_client = None

if auth_mode == "anonymous":
try:
azconfig_client = AzureAppConfigurationClient(
base_url=endpoint,
credential=AzureKeyCredential(key=""),
id_credential="",
user_agent=HttpHeaders.USER_AGENT,
transport=AuthHeaderRequestsTransport())
except (ValueError, TypeError) as ex:
raise CLIError("Failed to initialize AzureAppConfigurationClient due to an exception: {}".format(str(ex)))

if auth_mode == "key":
connection_string = resolve_connection_string(cmd, name, connection_string)
try:
Expand Down Expand Up @@ -254,3 +280,10 @@ def parse_tags_to_dict(tags):
tags_dict[tag_key] = tag_value
return tags_dict
return tags


def is_http_endpoint(endpoint):
if not endpoint:
return False

return str(endpoint).lower().startswith('http://')
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,10 @@
from ._utils import (is_valid_connection_string,
resolve_store_metadata,
get_store_name_from_connection_string,
get_store_endpoint_from_connection_string,
validate_feature_flag_name,
validate_feature_flag_key)
validate_feature_flag_key,
is_http_endpoint)
from ._models import QueryFields
from ._constants import ImportExportProfiles
from ._featuremodels import FeatureQueryFields
Expand Down Expand Up @@ -64,12 +66,31 @@ def validate_connection_string(cmd, namespace):

def validate_auth_mode(namespace):
auth_mode = namespace.auth_mode
endpoint = getattr(namespace, 'endpoint', None)
connection_string = getattr(namespace, 'connection_string', None)

if auth_mode != "anonymous":
# Disallow HTTP endpoints unless explicitly using anonymous mode.
if endpoint and is_http_endpoint(endpoint):
raise CLIError("HTTP endpoint is only supported when auth mode is 'anonymous'.")

if connection_string:
conn_endpoint = get_store_endpoint_from_connection_string(connection_string)
if is_http_endpoint(conn_endpoint):
raise CLIError("HTTP endpoint is only supported when auth mode is 'anonymous'.")
Comment on lines +74 to +80
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
if endpoint and is_http_endpoint(endpoint):
raise CLIError("HTTP endpoint is only supported when auth mode is 'anonymous'.")
if connection_string:
conn_endpoint = get_store_endpoint_from_connection_string(connection_string)
if is_http_endpoint(conn_endpoint):
raise CLIError("HTTP endpoint is only supported when auth mode is 'anonymous'.")
if connection_string:
endpoint = get_store_endpoint_from_connection_string(connection_string)
if endpoint and is_http_endpoint(endpoint):
raise CLIError("HTTP endpoint is only supported when auth mode is 'anonymous'.")

I'm not sure if this works with like 83 as I'm not familiar with namespace.name but you could always add a line before to get a temp value so we don't have this twice.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Sorry, I am not sure I understand. Getting the endpoint parameter twice?


if auth_mode == "login":
if not namespace.name and not namespace.endpoint:
if not namespace.name and not endpoint:
raise CLIError("App Configuration name or endpoint should be provided if auth mode is 'login'.")
if namespace.connection_string:
if connection_string:
raise CLIError("Auth mode should be 'key' when connection string is provided.")

if auth_mode == "anonymous":
if not endpoint:
raise RequiredArgumentMissingError("App Configuration endpoint should be provided if auth mode is 'anonymous'.")
if connection_string:
raise CLIError("Auth mode 'anonymous' only supports the '--endpoint' argument. Connection string is not supported.")


def validate_import_depth(namespace):
depth = namespace.depth
Expand Down
Loading