1# --------------------------------------------------------------------------------------------
2# Copyright (c) Microsoft Corporation. All rights reserved.
3# Licensed under the MIT License. See License.txt in the project root for license information.
4# --------------------------------------------------------------------------------------------
5
6# pylint: disable=line-too-long
7from knack.log import get_logger
8from knack.util import CLIError
9from azure.appconfiguration import AzureAppConfigurationClient
10from azure.core.exceptions import HttpResponseError
11
12from ._client_factory import cf_configstore
13from ._constants import HttpHeaders
14
15logger = get_logger(__name__)
16
17
18def construct_connection_string(cmd, config_store_name):
19    connection_string_template = 'Endpoint={};Id={};Secret={}'
20    # If the logged in user/Service Principal does not have 'Reader' or 'Contributor' role
21    # assigned for the requested AppConfig, resolve_store_metadata will raise CLI error
22    resource_group_name, endpoint = resolve_store_metadata(cmd, config_store_name)
23
24    try:
25        config_store_client = cf_configstore(cmd.cli_ctx)
26        access_keys = config_store_client.list_keys(resource_group_name, config_store_name)
27        for entry in access_keys:
28            if not entry.read_only:
29                return connection_string_template.format(endpoint, entry.id, entry.value)
30    except HttpResponseError as ex:
31        raise CLIError('Failed to get access keys for the App Configuration "{}". Make sure that the account that logged in has sufficient permissions to access the App Configuration store.\n{}'.format(config_store_name, str(ex)))
32
33    raise CLIError('Cannot find a read write access key for the App Configuration {}'.format(config_store_name))
34
35
36def resolve_store_metadata(cmd, config_store_name):
37    try:
38        config_store_client = cf_configstore(cmd.cli_ctx)
39        all_stores = config_store_client.list()
40        for store in all_stores:
41            if store.name.lower() == config_store_name.lower():
42                # Id has a fixed structure /subscriptions/subscriptionName/resourceGroups/groupName/providers/providerName/configurationStores/storeName"
43                return store.id.split('/')[4], store.endpoint
44    except HttpResponseError as ex:
45        raise CLIError("Failed to get the list of App Configuration stores for the current user. Make sure that the account that logged in has sufficient permissions to access the App Configuration store.\n{}".format(str(ex)))
46
47    raise CLIError("Failed to find the App Configuration store '{}'.".format(config_store_name))
48
49
50def resolve_connection_string(cmd, config_store_name=None, connection_string=None):
51    string = ''
52    error_message = '''You may have specified both store name and connection string, which is a conflict.
53Please specify exactly ONE (suggest connection string) in one of the following options:\n
541 pass in App Configuration store name as a parameter\n
552 pass in connection string as a parameter\n
563 preset App Configuration store name using 'az configure --defaults app_configuration_store=xxxx'\n
574 preset connection string using 'az configure --defaults appconfig_connection_string=xxxx'\n
585 preset connection in environment variable like set AZURE_APPCONFIG_CONNECTION_STRING=xxxx'''
59
60    if config_store_name:
61        string = construct_connection_string(cmd, config_store_name)
62
63    if connection_string:
64        if string and ';'.join(sorted(connection_string.split(';'))) != string:
65            raise CLIError(error_message)
66        string = connection_string
67
68    connection_string_env = cmd.cli_ctx.config.get(
69        'appconfig', 'connection_string', None)
70
71    if connection_string_env:
72        if not is_valid_connection_string(connection_string_env):
73            raise CLIError(
74                "The environment variable connection string is invalid. Correct format should be Endpoint=https://example.appconfig.io;Id=xxxxx;Secret=xxxx")
75
76        if string and ';'.join(sorted(connection_string_env.split(';'))) != string:
77            raise CLIError(error_message)
78        string = connection_string_env
79
80    if not string:
81        raise CLIError(
82            'Please specify config store name or connection string(suggested).')
83    return string
84
85
86def is_valid_connection_string(connection_string):
87    if connection_string is not None:
88        segments = connection_string.split(';')
89        if len(segments) != 3:
90            return False
91
92        segments.sort()
93        if segments[0].startswith('Endpoint=') and segments[1].startswith('Id=') and segments[2].startswith('Secret='):
94            return True
95    return False
96
97
98def get_store_name_from_connection_string(connection_string):
99    if is_valid_connection_string(connection_string):
100        segments = dict(seg.split("=", 1) for seg in connection_string.split(";"))
101        endpoint = segments.get("Endpoint")
102        if endpoint:
103            return endpoint.split("//")[1].split('.')[0]
104    return None
105
106
107def prep_label_filter_for_url_encoding(label=None):
108    if label is not None:
109        import ast
110        # ast library requires quotes around string
111        label = '"{0}"'.format(label)
112        label = ast.literal_eval(label)
113    return label
114
115
116def get_appconfig_data_client(cmd, name, connection_string, auth_mode, endpoint):
117    azconfig_client = None
118    if auth_mode == "key":
119        connection_string = resolve_connection_string(cmd, name, connection_string)
120        try:
121            azconfig_client = AzureAppConfigurationClient.from_connection_string(connection_string=connection_string,
122                                                                                 user_agent=HttpHeaders.USER_AGENT)
123        except ValueError as ex:
124            raise CLIError("Failed to initialize AzureAppConfigurationClient due to an exception: {}".format(str(ex)))
125
126    if auth_mode == "login":
127        if not endpoint:
128            try:
129                if name:
130                    _, endpoint = resolve_store_metadata(cmd, name)
131                else:
132                    raise CLIError("App Configuration endpoint or name should be provided if auth mode is 'login'.")
133            except Exception as ex:
134                raise CLIError(str(ex) + "\nYou may be able to resolve this issue by providing App Configuration endpoint instead of name.")
135
136        from azure.cli.core._profile import Profile
137        profile = Profile(cli_ctx=cmd.cli_ctx)
138        cred, _, _ = profile.get_login_credentials()
139        try:
140            azconfig_client = AzureAppConfigurationClient(credential=cred,
141                                                          base_url=endpoint,
142                                                          user_agent=HttpHeaders.USER_AGENT)
143        except (ValueError, TypeError) as ex:
144            raise CLIError("Failed to initialize AzureAppConfigurationClient due to an exception: {}".format(str(ex)))
145
146    return azconfig_client
147