diff --git a/src/containerapp/HISTORY.rst b/src/containerapp/HISTORY.rst index 8e61aac719d..12807a2bb9a 100644 --- a/src/containerapp/HISTORY.rst +++ b/src/containerapp/HISTORY.rst @@ -9,6 +9,7 @@ Release History * Added 'az containerapp env certificate' to manage certificates in a container app environment * Added 'az containerapp hostname' to manage hostnames in a container app * Added 'az containerapp ssl upload' to upload a certificate, add a hostname and the binding to a container app +* Added 'az containerapp auth' to manage AuthConfigs for a containerapp 0.3.4 ++++++ diff --git a/src/containerapp/azext_containerapp/_constants.py b/src/containerapp/azext_containerapp/_constants.py index ca0a0afd70e..1ba84998feb 100644 --- a/src/containerapp/azext_containerapp/_constants.py +++ b/src/containerapp/azext_containerapp/_constants.py @@ -14,4 +14,13 @@ MAX_ENV_PER_LOCATION = 2 +MICROSOFT_SECRET_SETTING_NAME = "microsoft-provider-authentication-secret" +FACEBOOK_SECRET_SETTING_NAME = "facebook-provider-authentication-secret" +GITHUB_SECRET_SETTING_NAME = "github-provider-authentication-secret" +GOOGLE_SECRET_SETTING_NAME = "google-provider-authentication-secret" +MSA_SECRET_SETTING_NAME = "msa-provider-authentication-secret" +TWITTER_SECRET_SETTING_NAME = "twitter-provider-authentication-secret" +APPLE_SECRET_SETTING_NAME = "apple-provider-authentication-secret" +UNAUTHENTICATED_CLIENT_ACTION = ['RedirectToLoginPage', 'AllowAnonymous', 'RejectWith401', 'RejectWith404'] +FORWARD_PROXY_CONVENTION = ['NoProxy', 'Standard', 'Custom'] CHECK_CERTIFICATE_NAME_AVAILABILITY_TYPE = "Microsoft.App/managedEnvironments/certificates" diff --git a/src/containerapp/azext_containerapp/_help.py b/src/containerapp/azext_containerapp/_help.py index 4f19429f41f..bbe07960a82 100644 --- a/src/containerapp/azext_containerapp/_help.py +++ b/src/containerapp/azext_containerapp/_help.py @@ -807,3 +807,224 @@ text: | az containerapp hostname list -n MyContainerapp -g MyResourceGroup """ + +# Auth commands +helps['containerapp auth'] = """ +type: group +short-summary: Manage containerapp authentication and authorization. +""" + +helps['containerapp auth show'] = """ +type: command +short-summary: Show the authentication settings for the containerapp. +examples: + - name: Show the authentication settings for the containerapp. + text: az containerapp auth show --name MyContainerapp --resource-group MyResourceGroup +""" + +helps['containerapp auth update'] = """ +type: command +short-summary: Update the authentication settings for the containerapp. +examples: + - name: Update the client ID of the AAD provider already configured. + text: | + az containerapp auth update -g myResourceGroup --name MyContainerapp --set identityProviders.azureActiveDirectory.registration.clientId=my-client-id + - name: Configure the app with file based authentication by setting the config file path. + text: | + az containerapp auth update -g myResourceGroup --name MyContainerapp --config-file-path D:\\home\\site\\wwwroot\\auth.json + - name: Configure the app to allow unauthenticated requests to hit the app. + text: | + az containerapp auth update -g myResourceGroup --name MyContainerapp --unauthenticated-client-action AllowAnonymous + - name: Configure the app to redirect unauthenticated requests to the Facebook provider. + text: | + az containerapp auth update -g myResourceGroup --name MyContainerapp --redirect-provider Facebook + - name: Configure the app to listen to the forward headers X-FORWARDED-HOST and X-FORWARDED-PROTO. + text: | + az containerapp auth update -g myResourceGroup --name MyContainerapp --proxy-convention Standard +""" + +helps['containerapp auth apple'] = """ +type: group +short-summary: Manage containerapp authentication and authorization of the Apple identity provider. +""" + +helps['containerapp auth apple show'] = """ +type: command +short-summary: Show the authentication settings for the Apple identity provider. +examples: + - name: Show the authentication settings for the Apple identity provider. + text: az containerapp auth apple show --name MyContainerapp --resource-group MyResourceGroup +""" + +helps['containerapp auth apple update'] = """ +type: command +short-summary: Update the client id and client secret for the Apple identity provider. +examples: + - name: Update the client id and client secret for the Apple identity provider. + text: | + az containerapp auth apple update -g myResourceGroup --name MyContainerapp \\ + --client-id my-client-id --client-secret very_secret_password +""" + +helps['containerapp auth facebook'] = """ +type: group +short-summary: Manage containerapp authentication and authorization of the Facebook identity provider. +""" + +helps['containerapp auth facebook show'] = """ +type: command +short-summary: Show the authentication settings for the Facebook identity provider. +examples: + - name: Show the authentication settings for the Facebook identity provider. + text: az containerapp auth facebook show --name MyContainerapp --resource-group MyResourceGroup +""" + +helps['containerapp auth facebook update'] = """ +type: command +short-summary: Update the app id and app secret for the Facebook identity provider. +examples: + - name: Update the app id and app secret for the Facebook identity provider. + text: | + az containerapp auth facebook update -g myResourceGroup --name MyContainerapp \\ + --app-id my-client-id --app-secret very_secret_password +""" + +helps['containerapp auth github'] = """ +type: group +short-summary: Manage containerapp authentication and authorization of the GitHub identity provider. +""" + +helps['containerapp auth github show'] = """ +type: command +short-summary: Show the authentication settings for the GitHub identity provider. +examples: + - name: Show the authentication settings for the GitHub identity provider. + text: az containerapp auth github show --name MyContainerapp --resource-group MyResourceGroup +""" + +helps['containerapp auth github update'] = """ +type: command +short-summary: Update the client id and client secret for the GitHub identity provider. +examples: + - name: Update the client id and client secret for the GitHub identity provider. + text: | + az containerapp auth github update -g myResourceGroup --name MyContainerapp \\ + --client-id my-client-id --client-secret very_secret_password +""" + +helps['containerapp auth google'] = """ +type: group +short-summary: Manage containerapp authentication and authorization of the Google identity provider. +""" + +helps['containerapp auth google show'] = """ +type: command +short-summary: Show the authentication settings for the Google identity provider. +examples: + - name: Show the authentication settings for the Google identity provider. + text: az containerapp auth google show --name MyContainerapp --resource-group MyResourceGroup +""" + +helps['containerapp auth google update'] = """ +type: command +short-summary: Update the client id and client secret for the Google identity provider. +examples: + - name: Update the client id and client secret for the Google identity provider. + text: | + az containerapp auth google update -g myResourceGroup --name MyContainerapp \\ + --client-id my-client-id --client-secret very_secret_password +""" + +helps['containerapp auth microsoft'] = """ +type: group +short-summary: Manage containerapp authentication and authorization of the Microsoft identity provider. +""" + +helps['containerapp auth microsoft show'] = """ +type: command +short-summary: Show the authentication settings for the Azure Active Directory identity provider. +examples: + - name: Show the authentication settings for the Azure Active Directory identity provider. + text: az containerapp auth microsoft show --name MyContainerapp --resource-group MyResourceGroup +""" + +helps['containerapp auth microsoft update'] = """ +type: command +short-summary: Update the client id and client secret for the Azure Active Directory identity provider. +examples: + - name: Update the open id issuer, client id and client secret for the Azure Active Directory identity provider. + text: | + az containerapp auth microsoft update -g myResourceGroup --name MyContainerapp \\ + --client-id my-client-id --client-secret very_secret_password \\ + --issuer https://sts.windows.net/54826b22-38d6-4fb2-bad9-b7983a3e9c5a/ +""" + +helps['containerapp auth openid-connect'] = """ +type: group +short-summary: Manage containerapp authentication and authorization of the custom OpenID Connect identity providers. +""" + +helps['containerapp auth openid-connect show'] = """ +type: command +short-summary: Show the authentication settings for the custom OpenID Connect identity provider. +examples: + - name: Show the authentication settings for the custom OpenID Connect identity provider. + text: az containerapp auth openid-connect show --name MyContainerapp --resource-group MyResourceGroup \\ + --provider-name myOpenIdConnectProvider +""" + +helps['containerapp auth openid-connect add'] = """ +type: command +short-summary: Configure a new custom OpenID Connect identity provider. +examples: + - name: Configure a new custom OpenID Connect identity provider. + text: | + az containerapp auth openid-connect add -g myResourceGroup --name MyContainerapp \\ + --provider-name myOpenIdConnectProvider --client-id my-client-id \\ + --client-secret-name MY_SECRET_APP_SETTING \\ + --openid-configuration https://myopenidprovider.net/.well-known/openid-configuration +""" + +helps['containerapp auth openid-connect update'] = """ +type: command +short-summary: Update the client id and client secret setting name for an existing custom OpenID Connect identity provider. +examples: + - name: Update the client id and client secret setting name for an existing custom OpenID Connect identity provider. + text: | + az containerapp auth openid-connect update -g myResourceGroup --name MyContainerapp \\ + --provider-name myOpenIdConnectProvider --client-id my-client-id \\ + --client-secret-name MY_SECRET_APP_SETTING +""" + +helps['containerapp auth openid-connect remove'] = """ +type: command +short-summary: Removes an existing custom OpenID Connect identity provider. +examples: + - name: Removes an existing custom OpenID Connect identity provider. + text: | + az containerapp auth openid-connect remove --name MyContainerapp --resource-group MyResourceGroup \\ + --provider-name myOpenIdConnectProvider +""" + +helps['containerapp auth twitter'] = """ +type: group +short-summary: Manage containerapp authentication and authorization of the Twitter identity provider. +""" + +helps['containerapp auth twitter show'] = """ +type: command +short-summary: Show the authentication settings for the Twitter identity provider. +examples: + - name: Show the authentication settings for the Twitter identity provider. + text: az containerapp auth twitter show --name MyContainerapp --resource-group MyResourceGroup +""" + +helps['containerapp auth twitter update'] = """ +type: command +short-summary: Update the consumer key and consumer secret for the Twitter identity provider. +examples: + - name: Update the consumer key and consumer secret for the Twitter identity provider. + text: | + az containerapp auth twitter update -g myResourceGroup --name MyContainerapp \\ + --consumer-key my-client-id --consumer-secret very_secret_password +""" diff --git a/src/containerapp/azext_containerapp/_params.py b/src/containerapp/azext_containerapp/_params.py index ff4d5112f2b..adcfaddf145 100644 --- a/src/containerapp/azext_containerapp/_params.py +++ b/src/containerapp/azext_containerapp/_params.py @@ -13,6 +13,7 @@ from ._validators import (validate_memory, validate_cpu, validate_managed_env_name_or_id, validate_registry_server, validate_registry_user, validate_registry_pass, validate_target_port, validate_ingress) +from ._constants import UNAUTHENTICATED_CLIENT_ACTION, FORWARD_PROXY_CONVENTION def load_arguments(self, _): @@ -223,6 +224,7 @@ def load_arguments(self, _): c.argument('secret_name', help="The name of the secret to show.") c.argument('secret_names', nargs='+', help="A list of secret(s) for the container app. Space-separated secret values names.") c.argument('show_values', help='Show the secret values.') + c.ignore('disable_max_length') with self.argument_context('containerapp env dapr-component') as c: c.argument('dapr_app_id', help="The Dapr app ID.") @@ -272,6 +274,42 @@ def load_arguments(self, _): c.argument('service_principal_client_secret', help='The service principal client secret. Used by Github Actions to authenticate with Azure.', options_list=["--service-principal-client-secret", "--sp-sec"]) c.argument('service_principal_tenant_id', help='The service principal tenant ID. Used by Github Actions to authenticate with Azure.', options_list=["--service-principal-tenant-id", "--sp-tid"]) + with self.argument_context('containerapp auth') as c: + # subgroup update + c.argument('client_id', options_list=['--client-id'], help='The Client ID of the app used for login.') + c.argument('client_secret', options_list=['--client-secret'], help='The client secret.') + c.argument('client_secret_setting_name', options_list=['--client-secret-name'], help='The app secret name that contains the client secret of the relying party application.') + c.argument('issuer', options_list=['--issuer'], help='The OpenID Connect Issuer URI that represents the entity which issues access tokens for this application.') + c.argument('allowed_token_audiences', options_list=['--allowed-token-audiences', '--allowed-audiences'], help='The configuration settings of the allowed list of audiences from which to validate the JWT token.') + c.argument('client_secret_certificate_thumbprint', options_list=['--thumbprint', '--client-secret-certificate-thumbprint'], help='Alternative to AAD Client Secret, thumbprint of a certificate used for signing purposes') + c.argument('client_secret_certificate_san', options_list=['--san', '--client-secret-certificate-san'], help='Alternative to AAD Client Secret and thumbprint, subject alternative name of a certificate used for signing purposes') + c.argument('client_secret_certificate_issuer', options_list=['--certificate-issuer', '--client-secret-certificate-issuer'], help='Alternative to AAD Client Secret and thumbprint, issuer of a certificate used for signing purposes') + c.argument('yes', options_list=['--yes', '-y'], help='Do not prompt for confirmation.', action='store_true') + c.argument('tenant_id', options_list=['--tenant-id'], help='The tenant id of the application.') + c.argument('app_id', options_list=['--app-id'], help='The App ID of the app used for login.') + c.argument('app_secret', options_list=['--app-secret'], help='The app secret.') + c.argument('app_secret_setting_name', options_list=['--app-secret-name', '--secret-name'], help='The app secret name that contains the app secret.') + c.argument('graph_api_version', options_list=['--graph-api-version'], help='The version of the Facebook api to be used while logging in.') + c.argument('scopes', options_list=['--scopes'], help='A list of the scopes that should be requested while authenticating.') + c.argument('consumer_key', options_list=['--consumer-key'], help='The OAuth 1.0a consumer key of the Twitter application used for sign-in.') + c.argument('consumer_secret', options_list=['--consumer-secret'], help='The consumer secret.') + c.argument('consumer_secret_setting_name', options_list=['--consumer-secret-name', '--secret-name'], help='The consumer secret name that contains the app secret.') + c.argument('provider_name', options_list=['--provider-name'], required=True, help='The name of the custom OpenID Connect provider.') + c.argument('openid_configuration', options_list=['--openid-configuration'], help='The endpoint that contains all the configuration endpoints for the provider.') + # auth update + c.argument('set_string', options_list=['--set'], help='Value of a specific field within the configuration settings for the Azure App Service Authentication / Authorization feature.') + c.argument('config_file_path', options_list=['--config-file-path'], help='The path of the config file containing auth settings if they come from a file.') + c.argument('unauthenticated_client_action', options_list=['--unauthenticated-client-action', '--action'], arg_type=get_enum_type(UNAUTHENTICATED_CLIENT_ACTION), help='The action to take when an unauthenticated client attempts to access the app.') + c.argument('redirect_provider', options_list=['--redirect-provider'], help='The default authentication provider to use when multiple providers are configured.') + c.argument('enable_token_store', options_list=['--enable-token-store'], arg_type=get_three_state_flag(return_label=True), help='true to durably store platform-specific security tokens that are obtained during login flows; otherwise, false.') + c.argument('require_https', options_list=['--require-https'], arg_type=get_three_state_flag(return_label=True), help='false if the authentication/authorization responses not having the HTTPS scheme are permissible; otherwise, true.') + c.argument('proxy_convention', options_list=['--proxy-convention'], arg_type=get_enum_type(FORWARD_PROXY_CONVENTION), help='The convention used to determine the url of the request made.') + c.argument('proxy_custom_host_header', options_list=['--proxy-custom-host-header', '--custom-host-header'], help='The name of the header containing the host of the request.') + c.argument('proxy_custom_proto_header', options_list=['--proxy-custom-proto-header', '--custom-proto-header'], help='The name of the header containing the scheme of the request.') + c.argument('excluded_paths', options_list=['--excluded-paths'], help='The list of paths that should be excluded from authentication rules.') + c.argument('enabled', options_list=['--enabled'], arg_type=get_three_state_flag(return_label=True), help='true if the Authentication / Authorization feature is enabled for the current app; otherwise, false.') + c.argument('runtime_version', options_list=['--runtime-version'], help='The RuntimeVersion of the Authentication / Authorization feature in use for the current app.') + with self.argument_context('containerapp ssl upload') as c: c.argument('hostname', help='The custom domain name.') c.argument('environment', options_list=['--environment', '-e'], help='Name or resource id of the Container App environment.') diff --git a/src/containerapp/azext_containerapp/_utils.py b/src/containerapp/azext_containerapp/_utils.py index 401ad0caade..5b27d3581c4 100644 --- a/src/containerapp/azext_containerapp/_utils.py +++ b/src/containerapp/azext_containerapp/_utils.py @@ -1193,6 +1193,70 @@ def create_new_acr(cmd, registry_name, resource_group_name, location=None, sku=" return acr +def set_field_in_auth_settings(auth_settings, set_string): + if set_string is not None: + split1 = set_string.split("=") + fieldName = split1[0] + fieldValue = split1[1] + split2 = fieldName.split(".") + auth_settings = set_field_in_auth_settings_recursive(split2, fieldValue, auth_settings) + return auth_settings + + +def set_field_in_auth_settings_recursive(field_name_split, field_value, auth_settings): + if len(field_name_split) == 1: + if not field_value.startswith('[') or not field_value.endswith(']'): + auth_settings[field_name_split[0]] = field_value + else: + field_value_list_string = field_value[1:-1] + auth_settings[field_name_split[0]] = field_value_list_string.split(",") + return auth_settings + + remaining_field_names = field_name_split[1:] + if field_name_split[0] not in auth_settings: + auth_settings[field_name_split[0]] = {} + auth_settings[field_name_split[0]] = set_field_in_auth_settings_recursive(remaining_field_names, + field_value, + auth_settings[field_name_split[0]]) + return auth_settings + + +def update_http_settings_in_auth_settings(auth_settings, require_https, proxy_convention, + proxy_custom_host_header, proxy_custom_proto_header): + if require_https is not None: + if "httpSettings" not in auth_settings: + auth_settings["httpSettings"] = {} + auth_settings["httpSettings"]["requireHttps"] = require_https + + if proxy_convention is not None: + if "httpSettings" not in auth_settings: + auth_settings["httpSettings"] = {} + if "forwardProxy" not in auth_settings["httpSettings"]: + auth_settings["httpSettings"]["forwardProxy"] = {} + auth_settings["httpSettings"]["forwardProxy"]["convention"] = proxy_convention + + if proxy_custom_host_header is not None: + if "httpSettings" not in auth_settings: + auth_settings["httpSettings"] = {} + if "forwardProxy" not in auth_settings["httpSettings"]: + auth_settings["httpSettings"]["forwardProxy"] = {} + auth_settings["httpSettings"]["forwardProxy"]["customHostHeaderName"] = proxy_custom_host_header + + if proxy_custom_proto_header is not None: + if "httpSettings" not in auth_settings: + auth_settings["httpSettings"] = {} + if "forwardProxy" not in auth_settings["httpSettings"]: + auth_settings["httpSettings"]["forwardProxy"] = {} + auth_settings["httpSettings"]["forwardProxy"]["customProtoHeaderName"] = proxy_custom_proto_header + + return auth_settings + + +def get_oidc_client_setting_app_setting_name(provider_name): + provider_name_prefix = provider_name.lower() + return provider_name_prefix + "-provider-authentication-secret" + + # only accept .pfx or .pem file def load_cert_file(file_path, cert_password=None): from base64 import b64encode @@ -1224,7 +1288,7 @@ def load_cert_file(file_path, cert_password=None): else: raise FileOperationError('Not a valid file type. Only .PFX and .PEM files are supported.') except Exception as e: - raise CLIInternalError(e) + raise CLIInternalError(e) from e return blob, thumbprint diff --git a/src/containerapp/azext_containerapp/commands.py b/src/containerapp/azext_containerapp/commands.py index 43b57720e47..c1c355cdd5f 100644 --- a/src/containerapp/azext_containerapp/commands.py +++ b/src/containerapp/azext_containerapp/commands.py @@ -8,6 +8,7 @@ # from msrestazure.tools import is_valid_resource_id, parse_resource_id from azext_containerapp._client_factory import ex_handler_factory from ._validators import validate_ssh +from ._clients import STABLE_API_VERSION def transform_containerapp_output(app): @@ -43,8 +44,16 @@ def transform_revision_list_output(revs): return [transform_revision_output(r) for r in revs] +def auth_config_client_factory(cli_ctx, *_): + from azure.cli.core.commands.client_factory import get_mgmt_service_client + from azure.cli.core.profiles import CustomResourceType + MGMT_APPCONTAINERS = CustomResourceType(import_prefix='azure.mgmt.appcontainers', client_name='ContainerAppsAPIClient') + return get_mgmt_service_client(cli_ctx, MGMT_APPCONTAINERS, api_version=STABLE_API_VERSION).container_apps_auth_configs + + def load_command_table(self, _): - with self.command_group('containerapp', is_preview=True) as g: + + with self.command_group('containerapp') as g: g.custom_show_command('show', 'show_containerapp', table_transformer=transform_containerapp_output) g.custom_command('list', 'list_containerapp', table_transformer=transform_containerapp_list_output) g.custom_command('create', 'create_containerapp', supports_no_wait=True, exception_handler=ex_handler_factory(), table_transformer=transform_containerapp_output) @@ -54,11 +63,11 @@ def load_command_table(self, _): g.custom_command('up', 'containerapp_up', supports_no_wait=False, exception_handler=ex_handler_factory()) g.custom_command('browse', 'open_containerapp_in_browser') - with self.command_group('containerapp replica', is_preview=True) as g: + with self.command_group('containerapp replica') as g: g.custom_show_command('show', 'get_replica') # TODO implement the table transformer g.custom_command('list', 'list_replicas') - with self.command_group('containerapp logs', is_preview=True) as g: + with self.command_group('containerapp logs') as g: g.custom_show_command('show', 'stream_containerapp_logs', validator=validate_ssh) with self.command_group('containerapp env') as g: @@ -78,7 +87,7 @@ def load_command_table(self, _): g.custom_command('upload', 'upload_certificate') g.custom_command('delete', 'delete_certificate', confirmation=True, exception_handler=ex_handler_factory()) - with self.command_group('containerapp env storage') as g: + with self.command_group('containerapp env storage', is_preview=True) as g: g.custom_show_command('show', 'show_storage') g.custom_command('list', 'list_storage') g.custom_command('set', 'create_or_update_storage', supports_no_wait=True, exception_handler=ex_handler_factory()) @@ -132,6 +141,40 @@ def load_command_table(self, _): g.custom_command('enable', 'enable_dapr', exception_handler=ex_handler_factory()) g.custom_command('disable', 'disable_dapr', exception_handler=ex_handler_factory()) + with self.command_group('containerapp auth', client_factory=auth_config_client_factory) as g: + g.custom_show_command('show', 'show_auth_config', exception_handler=ex_handler_factory()) + g.custom_command('update', 'update_auth_config', exception_handler=ex_handler_factory()) + + with self.command_group('containerapp auth microsoft', client_factory=auth_config_client_factory) as g: + g.custom_show_command('show', 'get_aad_settings') + g.custom_command('update', 'update_aad_settings', exception_handler=ex_handler_factory()) + + with self.command_group('containerapp auth facebook', client_factory=auth_config_client_factory) as g: + g.custom_show_command('show', 'get_facebook_settings') + g.custom_command('update', 'update_facebook_settings', exception_handler=ex_handler_factory()) + + with self.command_group('containerapp auth github', client_factory=auth_config_client_factory) as g: + g.custom_show_command('show', 'get_github_settings') + g.custom_command('update', 'update_github_settings') + + with self.command_group('containerapp auth google', client_factory=auth_config_client_factory) as g: + g.custom_show_command('show', 'get_google_settings') + g.custom_command('update', 'update_google_settings', exception_handler=ex_handler_factory()) + + with self.command_group('containerapp auth twitter', client_factory=auth_config_client_factory) as g: + g.custom_show_command('show', 'get_twitter_settings') + g.custom_command('update', 'update_twitter_settings', exception_handler=ex_handler_factory()) + + with self.command_group('containerapp auth apple', client_factory=auth_config_client_factory) as g: + g.custom_show_command('show', 'get_apple_settings') + g.custom_command('update', 'update_apple_settings', exception_handler=ex_handler_factory()) + + with self.command_group('containerapp auth openid-connect', client_factory=auth_config_client_factory) as g: + g.custom_show_command('show', 'get_openid_connect_provider_settings') + g.custom_command('add', 'add_openid_connect_provider_settings', exception_handler=ex_handler_factory()) + g.custom_command('update', 'update_openid_connect_provider_settings', exception_handler=ex_handler_factory()) + g.custom_command('remove', 'remove_openid_connect_provider_settings') + with self.command_group('containerapp ssl') as g: g.custom_command('upload', 'upload_ssl', exception_handler=ex_handler_factory()) diff --git a/src/containerapp/azext_containerapp/custom.py b/src/containerapp/azext_containerapp/custom.py index ef558eb21ce..c71570e4230 100644 --- a/src/containerapp/azext_containerapp/custom.py +++ b/src/containerapp/azext_containerapp/custom.py @@ -16,11 +16,13 @@ ResourceNotFoundError, CLIError, CLIInternalError, - InvalidArgumentValueError) + InvalidArgumentValueError, + ArgumentUsageError) from azure.cli.core.commands.client_factory import get_subscription_id from azure.cli.core.util import open_page_in_browser from azure.cli.command_modules.appservice.utils import _normalize_location from knack.log import get_logger +from knack.prompting import prompt_y_n from msrestazure.tools import parse_resource_id, is_valid_resource_id from msrest.exceptions import DeserializationError @@ -64,7 +66,8 @@ from ._ssh_utils import (SSH_DEFAULT_ENCODING, WebSocketConnection, read_ssh, get_stdin_writer, SSH_CTRL_C_MSG, SSH_BACKUP_ENCODING) -from ._constants import MAXIMUM_SECRET_LENGTH, CONTAINER_APPS_RP +from ._constants import (MAXIMUM_SECRET_LENGTH, MICROSOFT_SECRET_SETTING_NAME, FACEBOOK_SECRET_SETTING_NAME, GITHUB_SECRET_SETTING_NAME, + GOOGLE_SECRET_SETTING_NAME, TWITTER_SECRET_SETTING_NAME, APPLE_SECRET_SETTING_NAME, CONTAINER_APPS_RP) logger = get_logger(__name__) @@ -1852,6 +1855,7 @@ def remove_secrets(cmd, name, resource_group_name, secret_names, no_wait=False): def set_secrets(cmd, name, resource_group_name, secrets, # yaml=None, + disable_max_length=False, no_wait=False): _validate_subscription_registered(cmd, CONTAINER_APPS_RP) @@ -1859,7 +1863,7 @@ def set_secrets(cmd, name, resource_group_name, secrets, if s: parsed = s.split("=") if parsed: - if len(parsed[0]) > MAXIMUM_SECRET_LENGTH: + if len(parsed[0]) > MAXIMUM_SECRET_LENGTH and not disable_max_length: raise ValidationError(f"Secret names cannot be longer than {MAXIMUM_SECRET_LENGTH}. " f"Please shorten {parsed[0]}") @@ -2374,7 +2378,6 @@ def upload_certificate(cmd, name, resource_group_name, certificate_file, certifi cert_name = None if certificate_name: if not check_cert_name_availability(cmd, resource_group_name, name, certificate_name): - from knack.prompting import prompt_y_n msg = 'A certificate with the name {} already exists in {}. If continue with this name, it will be overwritten by the new certificate file.\nOverwrite?' overwrite = prompt_y_n(msg.format(certificate_name, name)) if overwrite: @@ -2536,7 +2539,7 @@ def create_or_update_storage(cmd, storage_name, resource_group_name, name, azure pass if r: - logger.warning("Only AzureFile account keys can be updated. In order to change the AzureFile share name or account name, please delete this storage and create a new one.") + logger.warning("Only AzureFile account can be updated. In order to change the AzureFile share name or account name, please delete this storage and create a new one.") storage_def = AzureFilePropertiesModel storage_def["accountKey"] = azure_file_account_key @@ -2560,3 +2563,760 @@ def remove_storage(cmd, storage_name, name, resource_group_name, no_wait=False): return StorageClient.delete(cmd, resource_group_name, name, storage_name, no_wait) except CLIError as e: handle_raw_exception(e) + + +# TODO: Refactor provider code to make it cleaner +def update_aad_settings(cmd, client, resource_group_name, name, + client_id=None, client_secret_setting_name=None, + issuer=None, allowed_token_audiences=None, client_secret=None, + client_secret_certificate_thumbprint=None, + client_secret_certificate_san=None, + client_secret_certificate_issuer=None, + yes=False, tenant_id=None): + + try: + show_ingress(cmd, name, resource_group_name) + except Exception as e: + raise ValidationError("Authentication requires ingress to be enabled for your containerapp.") from e + + if client_secret is not None and client_secret_setting_name is not None: + raise ArgumentUsageError('Usage Error: --client-secret and --client-secret-setting-name cannot both be ' + 'configured to non empty strings') + + if client_secret_setting_name is not None and client_secret_certificate_thumbprint is not None: + raise ArgumentUsageError('Usage Error: --client-secret-setting-name and --thumbprint cannot both be ' + 'configured to non empty strings') + + if client_secret is not None and client_secret_certificate_thumbprint is not None: + raise ArgumentUsageError('Usage Error: --client-secret and --thumbprint cannot both be ' + 'configured to non empty strings') + + if client_secret is not None and client_secret_certificate_san is not None: + raise ArgumentUsageError('Usage Error: --client-secret and --san cannot both be ' + 'configured to non empty strings') + + if client_secret_setting_name is not None and client_secret_certificate_san is not None: + raise ArgumentUsageError('Usage Error: --client-secret-setting-name and --san cannot both be ' + 'configured to non empty strings') + + if client_secret_certificate_thumbprint is not None and client_secret_certificate_san is not None: + raise ArgumentUsageError('Usage Error: --thumbprint and --san cannot both be ' + 'configured to non empty strings') + + if ((client_secret_certificate_san is not None and client_secret_certificate_issuer is None) or + (client_secret_certificate_san is None and client_secret_certificate_issuer is not None)): + raise ArgumentUsageError('Usage Error: --san and --certificate-issuer must both be ' + 'configured to non empty strings') + + if issuer is not None and (tenant_id is not None): + raise ArgumentUsageError('Usage Error: --issuer and --tenant-id cannot be configured ' + 'to non empty strings at the same time.') + + is_new_aad_app = False + existing_auth = {} + try: + existing_auth = client.get(resource_group_name=resource_group_name, container_app_name=name, auth_config_name="current").serialize()["properties"] + except: + existing_auth = {} + existing_auth["platform"] = {} + existing_auth["platform"]["enabled"] = True + existing_auth["globalValidation"] = {} + existing_auth["login"] = {} + + registration = {} + validation = {} + if "identityProviders" not in existing_auth: + existing_auth["identityProviders"] = {} + if "azureActiveDirectory" not in existing_auth["identityProviders"]: + existing_auth["identityProviders"]["azureActiveDirectory"] = {} + is_new_aad_app = True + + if is_new_aad_app and issuer is None and tenant_id is None: + raise ArgumentUsageError('Usage Error: Either --issuer or --tenant-id must be specified when configuring the ' + 'Microsoft auth registration.') + + if client_secret is not None and not yes: + msg = 'Configuring --client-secret will add a secret to the containerapp. Are you sure you want to continue?' + if not prompt_y_n(msg, default="n"): + raise ArgumentUsageError('Usage Error: --client-secret cannot be used without agreeing to add secret ' + 'to the containerapp.') + + openid_issuer = issuer + if openid_issuer is None: + # cmd.cli_ctx.cloud resolves to whichever cloud the customer is currently logged into + authority = cmd.cli_ctx.cloud.endpoints.active_directory + + if tenant_id is not None: + openid_issuer = authority + "/" + tenant_id + "/v2.0" + + registration = {} + validation = {} + if "identityProviders" not in existing_auth: + existing_auth["identityProviders"] = {} + if "azureActiveDirectory" not in existing_auth["identityProviders"]: + existing_auth["identityProviders"]["azureActiveDirectory"] = {} + if (client_id is not None or client_secret is not None or + client_secret_setting_name is not None or openid_issuer is not None or + client_secret_certificate_thumbprint is not None or + client_secret_certificate_san is not None or + client_secret_certificate_issuer is not None): + if "registration" not in existing_auth["identityProviders"]["azureActiveDirectory"]: + existing_auth["identityProviders"]["azureActiveDirectory"]["registration"] = {} + registration = existing_auth["identityProviders"]["azureActiveDirectory"]["registration"] + if allowed_token_audiences is not None: + if "validation" not in existing_auth["identityProviders"]["azureActiveDirectory"]: + existing_auth["identityProviders"]["azureActiveDirectory"]["validation"] = {} + validation = existing_auth["identityProviders"]["azureActiveDirectory"]["validation"] + + if client_id is not None: + registration["clientId"] = client_id + if client_secret_setting_name is not None: + registration["clientSecretSettingName"] = client_secret_setting_name + if client_secret is not None: + registration["clientSecretSettingName"] = MICROSOFT_SECRET_SETTING_NAME + set_secrets(cmd, name, resource_group_name, secrets=[f"{MICROSOFT_SECRET_SETTING_NAME}={client_secret}"], no_wait=True, disable_max_length=True) + if client_secret_setting_name is not None or client_secret is not None: + fields = ["clientSecretCertificateThumbprint", "clientSecretCertificateSubjectAlternativeName", "clientSecretCertificateIssuer"] + for field in [f for f in fields if registration.get(f)]: + registration[field] = None + if client_secret_certificate_thumbprint is not None: + registration["clientSecretCertificateThumbprint"] = client_secret_certificate_thumbprint + fields = ["clientSecretSettingName", "clientSecretCertificateSubjectAlternativeName", "clientSecretCertificateIssuer"] + for field in [f for f in fields if registration.get(f)]: + registration[field] = None + if client_secret_certificate_san is not None: + registration["clientSecretCertificateSubjectAlternativeName"] = client_secret_certificate_san + if client_secret_certificate_issuer is not None: + registration["clientSecretCertificateIssuer"] = client_secret_certificate_issuer + if client_secret_certificate_san is not None and client_secret_certificate_issuer is not None: + if "clientSecretSettingName" in registration: + registration["clientSecretSettingName"] = None + if "clientSecretCertificateThumbprint" in registration: + registration["clientSecretCertificateThumbprint"] = None + if openid_issuer is not None: + registration["openIdIssuer"] = openid_issuer + if allowed_token_audiences is not None: + validation["allowedAudiences"] = allowed_token_audiences.split(",") + existing_auth["identityProviders"]["azureActiveDirectory"]["validation"] = validation + if (client_id is not None or client_secret is not None or + client_secret_setting_name is not None or issuer is not None or + client_secret_certificate_thumbprint is not None or + client_secret_certificate_san is not None or + client_secret_certificate_issuer is not None): + existing_auth["identityProviders"]["azureActiveDirectory"]["registration"] = registration + + updated_auth_settings = client.create_or_update(resource_group_name=resource_group_name, container_app_name=name, auth_config_name="current", auth_config_envelope=existing_auth).serialize()["properties"] + return updated_auth_settings["identityProviders"]["azureActiveDirectory"] + + +def get_aad_settings(client, resource_group_name, name): + auth_settings = {} + try: + auth_settings = client.get(resource_group_name=resource_group_name, container_app_name=name, auth_config_name="current").serialize()["properties"] + except: + pass + if "identityProviders" not in auth_settings: + return {} + if "azureActiveDirectory" not in auth_settings["identityProviders"]: + return {} + return auth_settings["identityProviders"]["azureActiveDirectory"] + + +def get_facebook_settings(client, resource_group_name, name): + auth_settings = {} + try: + auth_settings = client.get(resource_group_name=resource_group_name, container_app_name=name, auth_config_name="current").serialize()["properties"] + except: + pass + if "identityProviders" not in auth_settings(): + return {} + if "facebook" not in auth_settings["identityProviders"]: + return {} + return auth_settings["identityProviders"]["facebook"] + + +def update_facebook_settings(cmd, client, resource_group_name, name, + app_id=None, app_secret_setting_name=None, + graph_api_version=None, scopes=None, app_secret=None, yes=False): + try: + show_ingress(cmd, name, resource_group_name) + except Exception as e: + raise ValidationError("Authentication requires ingress to be enabled for your containerapp.") from e + + if app_secret is not None and app_secret_setting_name is not None: + raise ArgumentUsageError('Usage Error: --app-secret and --app-secret-setting-name cannot both be configured ' + 'to non empty strings') + + if app_secret is not None and not yes: + msg = 'Configuring --client-secret will add a secret to the containerapp. Are you sure you want to continue?' + if not prompt_y_n(msg, default="n"): + raise ArgumentUsageError('Usage Error: --client-secret cannot be used without agreeing to add secret ' + 'to the containerapp.') + + existing_auth = {} + try: + existing_auth = client.get(resource_group_name=resource_group_name, container_app_name=name, auth_config_name="current").serialize()["properties"] + except: + existing_auth = {} + existing_auth["platform"] = {} + existing_auth["platform"]["enabled"] = True + existing_auth["globalValidation"] = {} + existing_auth["login"] = {} + + registration = {} + if "identityProviders" not in existing_auth: + existing_auth["identityProviders"] = {} + if "facebook" not in existing_auth["identityProviders"]: + existing_auth["identityProviders"]["facebook"] = {} + if app_id is not None or app_secret is not None or app_secret_setting_name is not None: + if "registration" not in existing_auth["identityProviders"]["facebook"]: + existing_auth["identityProviders"]["facebook"]["registration"] = {} + registration = existing_auth["identityProviders"]["facebook"]["registration"] + if scopes is not None: + if "login" not in existing_auth["identityProviders"]["facebook"]: + existing_auth["identityProviders"]["facebook"]["login"] = {} + + if app_id is not None: + registration["appId"] = app_id + if app_secret_setting_name is not None: + registration["appSecretSettingName"] = app_secret_setting_name + if app_secret is not None: + registration["appSecretSettingName"] = FACEBOOK_SECRET_SETTING_NAME + set_secrets(cmd, name, resource_group_name, secrets=[f"{FACEBOOK_SECRET_SETTING_NAME}={app_secret}"], no_wait=True, disable_max_length=True) + if graph_api_version is not None: + existing_auth["identityProviders"]["facebook"]["graphApiVersion"] = graph_api_version + if scopes is not None: + existing_auth["identityProviders"]["facebook"]["login"]["scopes"] = scopes.split(",") + if app_id is not None or app_secret is not None or app_secret_setting_name is not None: + existing_auth["identityProviders"]["facebook"]["registration"] = registration + + updated_auth_settings = client.create_or_update(resource_group_name=resource_group_name, container_app_name=name, auth_config_name="current", auth_config_envelope=existing_auth).serialize()["properties"] + return updated_auth_settings["identityProviders"]["facebook"] + + +def get_github_settings(client, resource_group_name, name): + auth_settings = {} + try: + auth_settings = client.get(resource_group_name=resource_group_name, container_app_name=name, auth_config_name="current").serialize()["properties"] + except: + pass + if "identityProviders" not in auth_settings: + return {} + if "gitHub" not in auth_settings["identityProviders"]: + return {} + return auth_settings["identityProviders"]["gitHub"] + + +def update_github_settings(cmd, client, resource_group_name, name, + client_id=None, client_secret_setting_name=None, + scopes=None, client_secret=None, yes=False): + try: + show_ingress(cmd, name, resource_group_name) + except Exception as e: + raise ValidationError("Authentication requires ingress to be enabled for your containerapp.") from e + + if client_secret is not None and client_secret_setting_name is not None: + raise ArgumentUsageError('Usage Error: --client-secret and --client-secret-setting-name cannot ' + 'both be configured to non empty strings') + + if client_secret is not None and not yes: + msg = 'Configuring --client-secret will add a secret to the containerapp. Are you sure you want to continue?' + if not prompt_y_n(msg, default="n"): + raise ArgumentUsageError('Usage Error: --client-secret cannot be used without agreeing to add secret ' + 'to the containerapp.') + + existing_auth = {} + try: + existing_auth = client.get(resource_group_name=resource_group_name, container_app_name=name, auth_config_name="current").serialize()["properties"] + except: + existing_auth = {} + existing_auth["platform"] = {} + existing_auth["platform"]["enabled"] = True + existing_auth["globalValidation"] = {} + existing_auth["login"] = {} + + registration = {} + if "identityProviders" not in existing_auth: + existing_auth["identityProviders"] = {} + if "gitHub" not in existing_auth["identityProviders"]: + existing_auth["identityProviders"]["gitHub"] = {} + if client_id is not None or client_secret is not None or client_secret_setting_name is not None: + if "registration" not in existing_auth["identityProviders"]["gitHub"]: + existing_auth["identityProviders"]["gitHub"]["registration"] = {} + registration = existing_auth["identityProviders"]["gitHub"]["registration"] + if scopes is not None: + if "login" not in existing_auth["identityProviders"]["gitHub"]: + existing_auth["identityProviders"]["gitHub"]["login"] = {} + + if client_id is not None: + registration["clientId"] = client_id + if client_secret_setting_name is not None: + registration["clientSecretSettingName"] = client_secret_setting_name + if client_secret is not None: + registration["clientSecretSettingName"] = GITHUB_SECRET_SETTING_NAME + set_secrets(cmd, name, resource_group_name, secrets=[f"{GITHUB_SECRET_SETTING_NAME}={client_secret}"], no_wait=True, disable_max_length=True) + if scopes is not None: + existing_auth["identityProviders"]["gitHub"]["login"]["scopes"] = scopes.split(",") + if client_id is not None or client_secret is not None or client_secret_setting_name is not None: + existing_auth["identityProviders"]["gitHub"]["registration"] = registration + + updated_auth_settings = client.create_or_update(resource_group_name=resource_group_name, container_app_name=name, auth_config_name="current", auth_config_envelope=existing_auth).serialize()["properties"] + return updated_auth_settings["identityProviders"]["gitHub"] + + +def get_google_settings(client, resource_group_name, name): + auth_settings = {} + try: + auth_settings = client.get(resource_group_name=resource_group_name, container_app_name=name, auth_config_name="current").serialize()["properties"] + except: + pass + if "identityProviders" not in auth_settings: + return {} + if "google" not in auth_settings["identityProviders"]: + return {} + return auth_settings["identityProviders"]["google"] + + +def update_google_settings(cmd, client, resource_group_name, name, + client_id=None, client_secret_setting_name=None, + scopes=None, allowed_token_audiences=None, client_secret=None, yes=False): + try: + show_ingress(cmd, name, resource_group_name) + except Exception as e: + raise ValidationError("Authentication requires ingress to be enabled for your containerapp.") from e + + if client_secret is not None and client_secret_setting_name is not None: + raise ArgumentUsageError('Usage Error: --client-secret and --client-secret-setting-name cannot ' + 'both be configured to non empty strings') + + if client_secret is not None and not yes: + msg = 'Configuring --client-secret will add a secret to the containerapp. Are you sure you want to continue?' + if not prompt_y_n(msg, default="n"): + raise ArgumentUsageError('Usage Error: --client-secret cannot be used without agreeing to add secret ' + 'to the containerapp.') + + existing_auth = {} + try: + existing_auth = client.get(resource_group_name=resource_group_name, container_app_name=name, auth_config_name="current").serialize()["properties"] + except: + existing_auth = {} + existing_auth["platform"] = {} + existing_auth["platform"]["enabled"] = True + existing_auth["globalValidation"] = {} + existing_auth["login"] = {} + + registration = {} + validation = {} + if "identityProviders" not in existing_auth: + existing_auth["identityProviders"] = {} + if "google" not in existing_auth["identityProviders"]: + existing_auth["identityProviders"]["google"] = {} + if client_id is not None or client_secret is not None or client_secret_setting_name is not None: + if "registration" not in existing_auth["identityProviders"]["google"]: + existing_auth["identityProviders"]["google"]["registration"] = {} + registration = existing_auth["identityProviders"]["google"]["registration"] + if scopes is not None: + if "login" not in existing_auth["identityProviders"]["google"]: + existing_auth["identityProviders"]["google"]["login"] = {} + if allowed_token_audiences is not None: + if "validation" not in existing_auth["identityProviders"]["google"]: + existing_auth["identityProviders"]["google"]["validation"] = {} + + if client_id is not None: + registration["clientId"] = client_id + if client_secret_setting_name is not None: + registration["clientSecretSettingName"] = client_secret_setting_name + if client_secret is not None: + registration["clientSecretSettingName"] = GOOGLE_SECRET_SETTING_NAME + set_secrets(cmd, name, resource_group_name, secrets=[f"{GOOGLE_SECRET_SETTING_NAME}={client_secret}"], no_wait=True, disable_max_length=True) + if scopes is not None: + existing_auth["identityProviders"]["google"]["login"]["scopes"] = scopes.split(",") + if allowed_token_audiences is not None: + validation["allowedAudiences"] = allowed_token_audiences.split(",") + existing_auth["identityProviders"]["google"]["validation"] = validation + if client_id is not None or client_secret is not None or client_secret_setting_name is not None: + existing_auth["identityProviders"]["google"]["registration"] = registration + + updated_auth_settings = client.create_or_update(resource_group_name=resource_group_name, container_app_name=name, auth_config_name="current", auth_config_envelope=existing_auth).serialize()["properties"] + return updated_auth_settings["identityProviders"]["google"] + + +def get_twitter_settings(client, resource_group_name, name): + auth_settings = {} + try: + auth_settings = client.get(resource_group_name=resource_group_name, container_app_name=name, auth_config_name="current").serialize()["properties"] + except: + pass + if "identityProviders" not in auth_settings: + return {} + if "twitter" not in auth_settings["identityProviders"]: + return {} + return auth_settings["identityProviders"]["twitter"] + + +def update_twitter_settings(cmd, client, resource_group_name, name, + consumer_key=None, consumer_secret_setting_name=None, + consumer_secret=None, yes=False): + try: + show_ingress(cmd, name, resource_group_name) + except Exception as e: + raise ValidationError("Authentication requires ingress to be enabled for your containerapp.") from e + + if consumer_secret is not None and consumer_secret_setting_name is not None: + raise ArgumentUsageError('Usage Error: --consumer-secret and --consumer-secret-setting-name cannot ' + 'both be configured to non empty strings') + + if consumer_secret is not None and not yes: + msg = 'Configuring --client-secret will add a secret to the containerapp. Are you sure you want to continue?' + if not prompt_y_n(msg, default="n"): + raise ArgumentUsageError('Usage Error: --client-secret cannot be used without agreeing to add secret ' + 'to the containerapp.') + + existing_auth = {} + try: + existing_auth = client.get(resource_group_name=resource_group_name, container_app_name=name, auth_config_name="current").serialize()["properties"] + except: + existing_auth = {} + existing_auth["platform"] = {} + existing_auth["platform"]["enabled"] = True + existing_auth["globalValidation"] = {} + existing_auth["login"] = {} + + registration = {} + if "identityProviders" not in existing_auth: + existing_auth["identityProviders"] = {} + if "twitter" not in existing_auth["identityProviders"]: + existing_auth["identityProviders"]["twitter"] = {} + if consumer_key is not None or consumer_secret is not None or consumer_secret_setting_name is not None: + if "registration" not in existing_auth["identityProviders"]["twitter"]: + existing_auth["identityProviders"]["twitter"]["registration"] = {} + registration = existing_auth["identityProviders"]["twitter"]["registration"] + + if consumer_key is not None: + registration["consumerKey"] = consumer_key + if consumer_secret_setting_name is not None: + registration["consumerSecretSettingName"] = consumer_secret_setting_name + if consumer_secret is not None: + registration["consumerSecretSettingName"] = TWITTER_SECRET_SETTING_NAME + set_secrets(cmd, name, resource_group_name, secrets=[f"{TWITTER_SECRET_SETTING_NAME}={consumer_secret}"], no_wait=True, disable_max_length=True) + if consumer_key is not None or consumer_secret is not None or consumer_secret_setting_name is not None: + existing_auth["identityProviders"]["twitter"]["registration"] = registration + updated_auth_settings = client.create_or_update(resource_group_name=resource_group_name, container_app_name=name, auth_config_name="current", auth_config_envelope=existing_auth).serialize()["properties"] + return updated_auth_settings["identityProviders"]["twitter"] + + +def get_apple_settings(client, resource_group_name, name): + auth_settings = {} + try: + auth_settings = client.get(resource_group_name=resource_group_name, container_app_name=name, auth_config_name="current").serialize()["properties"] + except: + pass + if "identityProviders" not in auth_settings: + return {} + if "apple" not in auth_settings["identityProviders"]: + return {} + return auth_settings["identityProviders"]["apple"] + + +def update_apple_settings(cmd, client, resource_group_name, name, + client_id=None, client_secret_setting_name=None, + scopes=None, client_secret=None, yes=False): + try: + show_ingress(cmd, name, resource_group_name) + except Exception as e: + raise ValidationError("Authentication requires ingress to be enabled for your containerapp.") from e + + if client_secret is not None and client_secret_setting_name is not None: + raise ArgumentUsageError('Usage Error: --client-secret and --client-secret-setting-name ' + 'cannot both be configured to non empty strings') + + if client_secret is not None and not yes: + msg = 'Configuring --client-secret will add a secret to the containerapp. Are you sure you want to continue?' + if not prompt_y_n(msg, default="n"): + raise ArgumentUsageError('Usage Error: --client-secret cannot be used without agreeing to add secret ' + 'to the containerapp.') + + existing_auth = {} + try: + existing_auth = client.get(resource_group_name=resource_group_name, container_app_name=name, auth_config_name="current").serialize()["properties"] + except: + existing_auth = {} + existing_auth["platform"] = {} + existing_auth["platform"]["enabled"] = True + existing_auth["globalValidation"] = {} + existing_auth["login"] = {} + + registration = {} + if "identityProviders" not in existing_auth: + existing_auth["identityProviders"] = {} + if "apple" not in existing_auth["identityProviders"]: + existing_auth["identityProviders"]["apple"] = {} + if client_id is not None or client_secret is not None or client_secret_setting_name is not None: + if "registration" not in existing_auth["identityProviders"]["apple"]: + existing_auth["identityProviders"]["apple"]["registration"] = {} + registration = existing_auth["identityProviders"]["apple"]["registration"] + if scopes is not None: + if "login" not in existing_auth["identityProviders"]["apple"]: + existing_auth["identityProviders"]["apple"]["login"] = {} + + if client_id is not None: + registration["clientId"] = client_id + if client_secret_setting_name is not None: + registration["clientSecretSettingName"] = client_secret_setting_name + if client_secret is not None: + registration["clientSecretSettingName"] = APPLE_SECRET_SETTING_NAME + set_secrets(cmd, name, resource_group_name, secrets=[f"{APPLE_SECRET_SETTING_NAME}={client_secret}"], no_wait=True, disable_max_length=True) + if scopes is not None: + existing_auth["identityProviders"]["apple"]["login"]["scopes"] = scopes.split(",") + if client_id is not None or client_secret is not None or client_secret_setting_name is not None: + existing_auth["identityProviders"]["apple"]["registration"] = registration + + updated_auth_settings = client.create_or_update(resource_group_name=resource_group_name, container_app_name=name, auth_config_name="current", auth_config_envelope=existing_auth).serialize()["properties"] + return updated_auth_settings["identityProviders"]["apple"] + + +def get_openid_connect_provider_settings(client, resource_group_name, name, provider_name): + auth_settings = {} + try: + auth_settings = client.get(resource_group_name=resource_group_name, container_app_name=name, auth_config_name="current").serialize()["properties"] + except: + pass + if "identityProviders" not in auth_settings: + raise ArgumentUsageError('Usage Error: The following custom OpenID Connect provider ' + 'has not been configured: ' + provider_name) + if "customOpenIdConnectProviders" not in auth_settings["identityProviders"]: + raise ArgumentUsageError('Usage Error: The following custom OpenID Connect provider ' + 'has not been configured: ' + provider_name) + if provider_name not in auth_settings["identityProviders"]["customOpenIdConnectProviders"]: + raise ArgumentUsageError('Usage Error: The following custom OpenID Connect provider ' + 'has not been configured: ' + provider_name) + return auth_settings["identityProviders"]["customOpenIdConnectProviders"][provider_name] + + +def add_openid_connect_provider_settings(cmd, client, resource_group_name, name, provider_name, + client_id=None, client_secret_setting_name=None, + openid_configuration=None, scopes=None, + client_secret=None, yes=False): + from ._utils import get_oidc_client_setting_app_setting_name + try: + show_ingress(cmd, name, resource_group_name) + except Exception as e: + raise ValidationError("Authentication requires ingress to be enabled for your containerapp.") from e + + if client_secret is not None and not yes: + msg = 'Configuring --client-secret will add a secret to the containerapp. Are you sure you want to continue?' + if not prompt_y_n(msg, default="n"): + raise ArgumentUsageError('Usage Error: --client-secret cannot be used without agreeing to add secret ' + 'to the containerapp.') + + auth_settings = {} + try: + auth_settings = client.get(resource_group_name=resource_group_name, container_app_name=name, auth_config_name="current").serialize()["properties"] + except: + auth_settings = {} + auth_settings["platform"] = {} + auth_settings["platform"]["enabled"] = True + auth_settings["globalValidation"] = {} + auth_settings["login"] = {} + + if "identityProviders" not in auth_settings: + auth_settings["identityProviders"] = {} + if "customOpenIdConnectProviders" not in auth_settings["identityProviders"]: + auth_settings["identityProviders"]["customOpenIdConnectProviders"] = {} + if provider_name in auth_settings["identityProviders"]["customOpenIdConnectProviders"]: + raise ArgumentUsageError('Usage Error: The following custom OpenID Connect provider has already been ' + 'configured: ' + provider_name + '. Please use `az containerapp auth oidc update` to ' + 'update the provider.') + + final_client_secret_setting_name = client_secret_setting_name + if client_secret is not None: + final_client_secret_setting_name = get_oidc_client_setting_app_setting_name(provider_name) + set_secrets(cmd, name, resource_group_name, secrets=[f"{final_client_secret_setting_name}={client_secret}"], no_wait=True, disable_max_length=True) + + auth_settings["identityProviders"]["customOpenIdConnectProviders"][provider_name] = { + "registration": { + "clientId": client_id, + "clientCredential": { + "clientSecretSettingName": final_client_secret_setting_name + }, + "openIdConnectConfiguration": { + "wellKnownOpenIdConfiguration": openid_configuration + } + } + } + login = {} + if scopes is not None: + login["scopes"] = scopes.split(',') + else: + login["scopes"] = ["openid"] + + auth_settings["identityProviders"]["customOpenIdConnectProviders"][provider_name]["login"] = login + + updated_auth_settings = client.create_or_update(resource_group_name=resource_group_name, container_app_name=name, auth_config_name="current", auth_config_envelope=auth_settings).serialize()["properties"] + return updated_auth_settings["identityProviders"]["customOpenIdConnectProviders"][provider_name] + + +def update_openid_connect_provider_settings(cmd, client, resource_group_name, name, provider_name, + client_id=None, client_secret_setting_name=None, + openid_configuration=None, scopes=None, + client_secret=None, yes=False): + from ._utils import get_oidc_client_setting_app_setting_name + try: + show_ingress(cmd, name, resource_group_name) + except Exception as e: + raise ValidationError("Authentication requires ingress to be enabled for your containerapp.") from e + + if client_secret is not None and not yes: + msg = 'Configuring --client-secret will add a secret to the containerapp. Are you sure you want to continue?' + if not prompt_y_n(msg, default="n"): + raise ArgumentUsageError('Usage Error: --client-secret cannot be used without agreeing to add secret ' + 'to the containerapp.') + + auth_settings = {} + try: + auth_settings = client.get(resource_group_name=resource_group_name, container_app_name=name, auth_config_name="current").serialize()["properties"] + except: + auth_settings = {} + auth_settings["platform"] = {} + auth_settings["platform"]["enabled"] = True + auth_settings["globalValidation"] = {} + auth_settings["login"] = {} + + if "identityProviders" not in auth_settings: + raise ArgumentUsageError('Usage Error: The following custom OpenID Connect provider ' + 'has not been configured: ' + provider_name) + if "customOpenIdConnectProviders" not in auth_settings["identityProviders"]: + raise ArgumentUsageError('Usage Error: The following custom OpenID Connect provider ' + 'has not been configured: ' + provider_name) + if provider_name not in auth_settings["identityProviders"]["customOpenIdConnectProviders"]: + raise ArgumentUsageError('Usage Error: The following custom OpenID Connect provider ' + 'has not been configured: ' + provider_name) + + custom_open_id_connect_providers = auth_settings["identityProviders"]["customOpenIdConnectProviders"] + registration = {} + if client_id is not None or client_secret_setting_name is not None or openid_configuration is not None: + if "registration" not in custom_open_id_connect_providers[provider_name]: + custom_open_id_connect_providers[provider_name]["registration"] = {} + registration = custom_open_id_connect_providers[provider_name]["registration"] + + if client_secret_setting_name is not None or client_secret is not None: + if "clientCredential" not in custom_open_id_connect_providers[provider_name]["registration"]: + custom_open_id_connect_providers[provider_name]["registration"]["clientCredential"] = {} + + if openid_configuration is not None: + if "openIdConnectConfiguration" not in custom_open_id_connect_providers[provider_name]["registration"]: + custom_open_id_connect_providers[provider_name]["registration"]["openIdConnectConfiguration"] = {} + + if scopes is not None: + if "login" not in auth_settings["identityProviders"]["customOpenIdConnectProviders"][provider_name]: + custom_open_id_connect_providers[provider_name]["login"] = {} + + if client_id is not None: + registration["clientId"] = client_id + if client_secret_setting_name is not None: + registration["clientCredential"]["clientSecretSettingName"] = client_secret_setting_name + if client_secret is not None: + final_client_secret_setting_name = get_oidc_client_setting_app_setting_name(provider_name) + registration["clientSecretSettingName"] = final_client_secret_setting_name + set_secrets(cmd, name, resource_group_name, secrets=[f"{final_client_secret_setting_name}={client_secret}"], no_wait=True, disable_max_length=True) + if openid_configuration is not None: + registration["openIdConnectConfiguration"]["wellKnownOpenIdConfiguration"] = openid_configuration + if scopes is not None: + custom_open_id_connect_providers[provider_name]["login"]["scopes"] = scopes.split(",") + if client_id is not None or client_secret_setting_name is not None or openid_configuration is not None: + custom_open_id_connect_providers[provider_name]["registration"] = registration + auth_settings["identityProviders"]["customOpenIdConnectProviders"] = custom_open_id_connect_providers + + updated_auth_settings = client.create_or_update(resource_group_name=resource_group_name, container_app_name=name, auth_config_name="current", auth_config_envelope=auth_settings).serialize()["properties"] + return updated_auth_settings["identityProviders"]["customOpenIdConnectProviders"][provider_name] + + +def remove_openid_connect_provider_settings(client, resource_group_name, name, provider_name): + auth_settings = {} + try: + auth_settings = client.get(resource_group_name=resource_group_name, container_app_name=name, auth_config_name="current").serialize()["properties"] + except: + pass + if "identityProviders" not in auth_settings: + raise ArgumentUsageError('Usage Error: The following custom OpenID Connect provider ' + 'has not been configured: ' + provider_name) + if "customOpenIdConnectProviders" not in auth_settings["identityProviders"]: + raise ArgumentUsageError('Usage Error: The following custom OpenID Connect provider ' + 'has not been configured: ' + provider_name) + if provider_name not in auth_settings["identityProviders"]["customOpenIdConnectProviders"]: + raise ArgumentUsageError('Usage Error: The following custom OpenID Connect provider ' + 'has not been configured: ' + provider_name) + auth_settings["identityProviders"]["customOpenIdConnectProviders"].pop(provider_name, None) + client.create_or_update(resource_group_name=resource_group_name, container_app_name=name, auth_config_name="current", auth_config_envelope=auth_settings).serialize() + return {} + + +def update_auth_config(client, resource_group_name, name, set_string=None, enabled=None, + runtime_version=None, config_file_path=None, unauthenticated_client_action=None, + redirect_provider=None, enable_token_store=None, require_https=None, + proxy_convention=None, proxy_custom_host_header=None, + proxy_custom_proto_header=None, excluded_paths=None): + from ._utils import set_field_in_auth_settings, update_http_settings_in_auth_settings + existing_auth = {} + try: + existing_auth = client.get(resource_group_name=resource_group_name, container_app_name=name, auth_config_name="current").serialize()["properties"] + except: + existing_auth["platform"] = {} + existing_auth["platform"]["enabled"] = True + existing_auth["globalValidation"] = {} + existing_auth["login"] = {} + + existing_auth = set_field_in_auth_settings(existing_auth, set_string) + + if enabled is not None: + if "platform" not in existing_auth: + existing_auth["platform"] = {} + existing_auth["platform"]["enabled"] = enabled + + if runtime_version is not None: + if "platform" not in existing_auth: + existing_auth["platform"] = {} + existing_auth["platform"]["runtimeVersion"] = runtime_version + + if config_file_path is not None: + if "platform" not in existing_auth: + existing_auth["platform"] = {} + existing_auth["platform"]["configFilePath"] = config_file_path + + if unauthenticated_client_action is not None: + if "globalValidation" not in existing_auth: + existing_auth["globalValidation"] = {} + existing_auth["globalValidation"]["unauthenticatedClientAction"] = unauthenticated_client_action + + if redirect_provider is not None: + if "globalValidation" not in existing_auth: + existing_auth["globalValidation"] = {} + existing_auth["globalValidation"]["redirectToProvider"] = redirect_provider + + if enable_token_store is not None: + if "login" not in existing_auth: + existing_auth["login"] = {} + if "tokenStore" not in existing_auth["login"]: + existing_auth["login"]["tokenStore"] = {} + existing_auth["login"]["tokenStore"]["enabled"] = enable_token_store + + if excluded_paths is not None: + if "globalValidation" not in existing_auth: + existing_auth["globalValidation"] = {} + excluded_paths_list_string = excluded_paths[1:-1] + existing_auth["globalValidation"]["excludedPaths"] = excluded_paths_list_string.split(",") + + existing_auth = update_http_settings_in_auth_settings(existing_auth, require_https, + proxy_convention, proxy_custom_host_header, + proxy_custom_proto_header) + + return client.create_or_update(resource_group_name=resource_group_name, container_app_name=name, auth_config_name="current", auth_config_envelope=existing_auth).serialize() + + +def show_auth_config(client, resource_group_name, name): + auth_settings = {} + try: + auth_settings = client.get(resource_group_name=resource_group_name, container_app_name=name, auth_config_name="current").serialize()["properties"] + except: + pass + return auth_settings diff --git a/src/containerapp/setup.py b/src/containerapp/setup.py index 1d484ae339c..c5926c9c0ec 100644 --- a/src/containerapp/setup.py +++ b/src/containerapp/setup.py @@ -38,7 +38,8 @@ # TODO: Add any additional SDK dependencies here DEPENDENCIES = [ - 'azure-cli-core' + 'azure-cli-core', + 'azure-mgmt-appcontainers==1.0.0' ] with open('README.rst', 'r', encoding='utf-8') as f: