Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add auth subgroups #108

Merged
merged 11 commits into from
May 19, 2022
9 changes: 9 additions & 0 deletions src/containerapp/azext_containerapp/_constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,4 +14,13 @@

MAX_ENV_PER_LOCATION = 2

MICROSOFT_SECRET_SETTING_NAME = "MICROSOFT_PROVIDER_AUTHENTICATION_SECRET".lower().replace("_", "-")
FACEBOOK_SECRET_SETTING_NAME = "FACEBOOK_PROVIDER_AUTHENTICATION_SECRET".lower().replace("_", "-")
GITHUB_SECRET_SETTING_NAME = "GITHUB_PROVIDER_AUTHENTICATION_SECRET".lower().replace("_", "-")
GOOGLE_SECRET_SETTING_NAME = "GOOGLE_PROVIDER_AUTHENTICATION_SECRET".lower().replace("_", "-")
MSA_SECRET_SETTING_NAME = "MSA_PROVIDER_AUTHENTICATION_SECRET".lower().replace("_", "-")
TWITTER_SECRET_SETTING_NAME = "TWITTER_PROVIDER_AUTHENTICATION_SECRET".lower().replace("_", "-")
APPLE_SECRET_SETTING_NAME = "APPLE_PROVIDER_AUTHENTICATION_SECRET".lower().replace("_", "-")
UNAUTHENTICATED_CLIENT_ACTION = ['RedirectToLoginPage', 'AllowAnonymous', 'RejectWith401', 'RejectWith404']
FORWARD_PROXY_CONVENTION = ['NoProxy', 'Standard', 'Custom']
CHECK_CERTIFICATE_NAME_AVAILABILITY_TYPE = "Microsoft.App/managedEnvironments/certificates"
32 changes: 32 additions & 0 deletions src/containerapp/azext_containerapp/_params.py
Original file line number Diff line number Diff line change
Expand Up @@ -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, _):
Expand Down Expand Up @@ -272,6 +273,37 @@ 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:
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 setting 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-setting-name', '--secret-setting'], help='The app setting 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('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.')
c.argument('set_string', options_list=['--set'], help='Value of a specific field within the configuration settings for the Azure App Service Authentication / Authorization V2 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.')
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.')
Expand Down
68 changes: 68 additions & 0 deletions src/containerapp/azext_containerapp/_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -1193,6 +1193,74 @@ 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.upper()

# an appsetting name can be up to 64 characters, and the suffix _PROVIDER_AUTHENTICATION_SECRET is 31 characters so limitting this to 32
if len(provider_name_prefix) > 32:
provider_name_prefix = provider_name_prefix[0:31]
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
Expand Down
42 changes: 42 additions & 0 deletions src/containerapp/azext_containerapp/commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,15 @@ 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='2022-03-01').container_apps_auth_configs


def load_command_table(self, _):

with self.command_group('containerapp', is_preview=True) 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)
Expand Down Expand Up @@ -132,6 +140,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_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_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())
Copy link
Collaborator

Choose a reason for hiding this comment

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

Curious any reason this sub-group alone has add & remove?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

All the other providers are unique (one per CA) except for providers, so there is no way of updating them if the provider name changes, since that will be considered a different identity provider. That's my understanding of it anyways, I tried to follow the webapp spec as much as possible.

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())

Expand Down
Loading