diff --git a/src/azure-cli/azure/cli/command_modules/rdbms/_helptext_pg.py b/src/azure-cli/azure/cli/command_modules/rdbms/_helptext_pg.py index 5fcb48b66b4..46541a10023 100644 --- a/src/azure-cli/azure/cli/command_modules/rdbms/_helptext_pg.py +++ b/src/azure-cli/azure/cli/command_modules/rdbms/_helptext_pg.py @@ -135,19 +135,20 @@ az postgres flexible-server create -g testGroup -n testServer --location testLocation \\ --key $keyIdentifier --identity testIdentity - name: > - Create a PostgreSQL flexible server with active directory auth as well as password auth. + Create a PostgreSQL flexible server with Microsoft Entra auth as well as password auth. text: > # create flexible server with aad auth and password auth enabled az postgres flexible-server create -g testGroup -n testServer --location testLocation \\ --active-directory-auth Enabled - name: > - Create a PostgreSQL flexible server with active directory auth only. + Create a PostgreSQL flexible server with Microsoft Entra auth only and primary administrator specified. text: > - # create flexible server with aad only auth and password auth disabled + # create flexible server with aad only auth and password auth disabled with primary administrator specified az postgres flexible-server create -g testGroup -n testServer --location testLocation \\ - --active-directory-auth Enabled --password-auth Disabled + --active-directory-auth Enabled --password-auth Disabled \\ + --admin-object-id 00000000-0000-0000-0000-000000000000 --admin-display-name john@contoso.com --admin-type User - name: > Create a PostgreSQL flexible server with public access, geo-redundant backup enabled and add the range of IP address to have access to this server. The --public-access parameter can be 'All', 'None', , or - @@ -256,7 +257,7 @@ text: az postgres flexible-server update --resource-group testGroup --name testserver --tags "k1=v1" "k2=v2" - name: Reset password text: az postgres flexible-server update --resource-group testGroup --name testserver -p password123 - - name: Update a flexible server to enable active directory auth for password auth enabled server + - name: Update a flexible server to enable Microsoft Entra auth for password auth enabled server text: az postgres flexible-server update --resource-group testGroup --name testserver --active-directory-auth Enabled - name: Change key/identity for data encryption. Data encryption cannot be enabled post server creation, this will only update the key/identity. text: > @@ -959,48 +960,48 @@ helps['postgres flexible-server ad-admin'] = """ type: group -short-summary: Manage server Active Directory administrators. +short-summary: Manage server Microsoft Entra administrators. """ helps['postgres flexible-server ad-admin create'] = """ type: command -short-summary: Create an Active Directory administrator. +short-summary: Create a Microsoft Entra administrator. examples: - - name: Create Active Directory administrator with user 'john@contoso.com', administrator ID '00000000-0000-0000-0000-000000000000' and type User. + - name: Create Microsoft Entra administrator with user 'john@contoso.com', administrator ID '00000000-0000-0000-0000-000000000000' and type User. text: az postgres flexible-server ad-admin create -g testgroup -s testsvr -u john@contoso.com -i 00000000-0000-0000-0000-000000000000 -t User """ helps['postgres flexible-server ad-admin delete'] = """ type: command -short-summary: Delete an Active Directory administrator. +short-summary: Delete a Microsoft Entra administrator. examples: - - name: Delete Active Directory administrator with ID '00000000-0000-0000-0000-000000000000'. + - name: Delete Microsoft Entra administrator with ID '00000000-0000-0000-0000-000000000000'. text: az postgres flexible-server ad-admin delete -g testgroup -s testsvr -i 00000000-0000-0000-0000-000000000000 """ helps['postgres flexible-server ad-admin list'] = """ type: command -short-summary: List all Active Directory administrators. +short-summary: List all Microsoft Entra administrators. examples: - - name: List Active Directory administrators. + - name: List Microsoft Entra administrators. text: az postgres flexible-server ad-admin list -g testgroup -s testsvr """ helps['postgres flexible-server ad-admin show'] = """ type: command -short-summary: Get an Active Directory administrator. +short-summary: Get a Microsoft Entra administrator. examples: - - name: Get Active Directory administrator with ID '00000000-0000-0000-0000-000000000000'. + - name: Get Microsoft Entra administrator with ID '00000000-0000-0000-0000-000000000000'. text: az postgres flexible-server ad-admin show -g testgroup -s testsvr -i 00000000-0000-0000-0000-000000000000 """ helps['postgres flexible-server ad-admin wait'] = """ type: command -short-summary: Wait for an Active Directory administrator to satisfy certain conditions. +short-summary: Wait for a Microsoft Entra administrator to satisfy certain conditions. examples: - - name: Wait until an Active Directory administrator exists. + - name: Wait until a Microsoft Entra administrator exists. text: az postgres flexible-server ad-admin wait -g testgroup -s testsvr -i 00000000-0000-0000-0000-000000000000 --exists - - name: Wait for an Active Directory administrator to be deleted. + - name: Wait for a Microsoft Entra administrator to be deleted. text: az postgres flexible-server ad-admin wait -g testgroup -s testsvr -i 00000000-0000-0000-0000-000000000000 --deleted """ diff --git a/src/azure-cli/azure/cli/command_modules/rdbms/_params.py b/src/azure-cli/azure/cli/command_modules/rdbms/_params.py index 4b9bcead324..85bf05e742d 100644 --- a/src/azure-cli/azure/cli/command_modules/rdbms/_params.py +++ b/src/azure-cli/azure/cli/command_modules/rdbms/_params.py @@ -70,7 +70,7 @@ def _complex_params(command_group): # pylint: disable=too-many-statements c.argument('auto_grow', arg_type=get_enum_type(['Enabled', 'Disabled']), options_list=['--auto-grow'], help='Enable or disable autogrow of the storage. Default value is Enabled.') c.argument('auto_scale_iops', arg_type=get_enum_type(['Enabled', 'Disabled']), options_list=['--auto-scale-iops'], help='Enable or disable autoscale of iops. Default value is Disabled.') c.argument('infrastructure_encryption', arg_type=get_enum_type(['Enabled', 'Disabled']), options_list=['--infrastructure-encryption', '-i'], help='Add an optional second layer of encryption for data using new encryption algorithm. Default value is Disabled.') - c.argument('assign_identity', options_list=['--assign-identity'], help='Generate and assign an Azure Active Directory Identity for this server for use with key management services like Azure KeyVault.') + c.argument('assign_identity', options_list=['--assign-identity'], help='Generate and assign an Microsoft Entra Identity for this server for use with key management services like Azure KeyVault.') c.argument('tags', tags_type) if command_group == 'mariadb': @@ -90,7 +90,7 @@ def _complex_params(command_group): # pylint: disable=too-many-statements c.argument('auto_grow', arg_type=get_enum_type(['Enabled', 'Disabled']), options_list=['--auto-grow'], help='Enable or disable autogrow of the storage. Default value is Enabled.') c.argument('auto_scale_iops', arg_type=get_enum_type(['Enabled', 'Disabled']), options_list=['--auto-scale-iops'], help='Enable or disable autogrow of the storage. Default value is Disabled.') c.argument('infrastructure_encryption', arg_type=get_enum_type(['Enabled', 'Disabled']), options_list=['--infrastructure-encryption', '-i'], help='Add an optional second layer of encryption for data using new encryption algorithm. Default value is Disabled.') - c.argument('assign_identity', options_list=['--assign-identity'], help='Generate and assign an Azure Active Directory Identity for this server for use with key management services like Azure KeyVault.') + c.argument('assign_identity', options_list=['--assign-identity'], help='Generate and assign an Microsoft Entra Identity for this server for use with key management services like Azure KeyVault.') c.argument('location', arg_type=get_location_type(self.cli_ctx)) if command_group == 'postgres': @@ -102,7 +102,7 @@ def _complex_params(command_group): # pylint: disable=too-many-statements with self.argument_context('{} server update'.format(command_group)) as c: c.ignore('family', 'capacity', 'tier') c.argument('sku_name', options_list=['--sku-name'], help='The name of the sku. Follows the convention {pricing tier}_{compute generation}_{vCores} in shorthand. Examples: B_Gen5_1, GP_Gen5_4, MO_Gen5_16.') - c.argument('assign_identity', options_list=['--assign-identity'], help='Generate and assign an Azure Active Directory Identity for this server for use with key management services like Azure KeyVault.') + c.argument('assign_identity', options_list=['--assign-identity'], help='Generate and assign an Microsoft Entra Identity for this server for use with key management services like Azure KeyVault.') with self.argument_context('{} server restore'. format(command_group)) as c: c.argument('server_name', options_list=['--name', '-n'], arg_type=overriding_none_arg_type) @@ -211,8 +211,8 @@ def _complex_params(command_group): # pylint: disable=too-many-statements with self.argument_context('{} server ad-admin'.format(command_group)) as c: c.argument('server_name', options_list=['--server-name', '-s']) - c.argument('login', options_list=['--display-name', '-u'], help='Display name of the Azure AD administrator user or group.') - c.argument('sid', options_list=['--object-id', '-i'], help='The unique ID of the Azure AD administrator.') + c.argument('login', options_list=['--display-name', '-u'], help='Display name of the Microsoft Entra administrator user or group.') + c.argument('sid', options_list=['--object-id', '-i'], help='The unique ID of the Microsoft Entra administrator.') if command_group == 'mysql': with self.argument_context('{} server upgrade'.format(command_group)) as c: @@ -529,7 +529,7 @@ def _flexible_server_params(command_group): active_directory_auth_arg_type = CLIArgumentType( options_list=['--active-directory-auth'], arg_type=get_enum_type(['Enabled', 'Disabled']), - help='Whether Azure Active Directory authentication is enabled.' + help='Whether Microsoft Entra authentication is enabled.' ) password_auth_arg_type = CLIArgumentType( @@ -594,6 +594,10 @@ def _flexible_server_params(command_group): c.argument('version', default='16', arg_type=version_arg_type) c.argument('backup_retention', default=7, arg_type=pg_backup_retention_arg_type) c.argument('active_directory_auth', default='Disabled', arg_type=active_directory_auth_arg_type) + c.argument('admin_id', options_list=['--admin-object-id', '-i'], help='The unique ID of the Microsoft Entra administrator.') + c.argument('admin_name', options_list=['--admin-display-name', '-m'], help='Display name of the Microsoft Entra administrator user or group.') + c.argument('admin_type', options_list=['--admin-type', '-t'], + arg_type=get_enum_type(['User', 'Group', 'ServicePrincipal', 'Unknown']), help='Type of the Microsoft Entra administrator.') c.argument('password_auth', default='Enabled', arg_type=password_auth_arg_type) c.argument('auto_grow', default='Disabled', arg_type=auto_grow_arg_type) c.argument('storage_type', default=None, arg_type=storage_type_arg_type) @@ -977,11 +981,11 @@ def _flexible_server_params(command_group): for scope in ['create', 'show', 'delete', 'wait']: with self.argument_context('{} flexible-server ad-admin {}'.format(command_group, scope)) as c: - c.argument('sid', options_list=['--object-id', '-i'], help='The unique ID of the Azure AD administrator.') + c.argument('sid', options_list=['--object-id', '-i'], help='The unique ID of the Microsoft Entra administrator.') with self.argument_context('{} flexible-server ad-admin create'.format(command_group)) as c: - c.argument('login', options_list=['--display-name', '-u'], help='Display name of the Azure AD administrator user or group.') - c.argument('principal_type', options_list=['--type', '-t'], default='User', arg_type=get_enum_type(['User', 'Group', 'ServicePrincipal', 'Unknown']), help='Type of the Azure AD administrator.') + c.argument('login', options_list=['--display-name', '-u'], help='Display name of the Microsoft Entra administrator user or group.') + c.argument('principal_type', options_list=['--type', '-t'], default='User', arg_type=get_enum_type(['User', 'Group', 'ServicePrincipal', 'Unknown']), help='Type of the Microsoft Entra administrator.') c.argument('identity', help='Name or ID of identity used for AAD Authentication.', validator=validate_identity) # server advanced threat protection settings diff --git a/src/azure-cli/azure/cli/command_modules/rdbms/flexible_server_custom_postgres.py b/src/azure-cli/azure/cli/command_modules/rdbms/flexible_server_custom_postgres.py index f71298285a6..5740e6eaaa7 100644 --- a/src/azure-cli/azure/cli/command_modules/rdbms/flexible_server_custom_postgres.py +++ b/src/azure-cli/azure/cli/command_modules/rdbms/flexible_server_custom_postgres.py @@ -9,6 +9,7 @@ import json from importlib import import_module import re +from urllib.parse import quote from urllib.request import urlretrieve from dateutil.tz import tzutc # pylint: disable=import-error import uuid @@ -28,7 +29,8 @@ cf_postgres_check_resource_availability_with_location, \ cf_postgres_flexible_private_dns_zone_suffix_operations, \ cf_postgres_flexible_private_endpoint_connections, \ - cf_postgres_flexible_tuning_options, cf_postgres_flexible_config + cf_postgres_flexible_tuning_options, \ + cf_postgres_flexible_config, cf_postgres_flexible_adadmin from ._flexible_server_util import generate_missing_parameters, resolve_poller, \ generate_password, parse_maintenance_window, get_current_time, build_identity_and_data_encryption, \ _is_resource_name, get_tenant_id, get_case_insensitive_key_value, get_enum_value_true_false @@ -42,6 +44,7 @@ logger = get_logger(__name__) DEFAULT_DB_NAME = 'flexibleserverdb' +POSTGRES_DB_NAME = 'postgres' DELEGATION_SERVICE_NAME = "Microsoft.DBforPostgreSQL/flexibleServers" RESOURCE_PROVIDER = 'Microsoft.DBforPostgreSQL' @@ -54,14 +57,15 @@ def flexible_server_create(cmd, client, resource_group_name=None, server_name=None, location=None, backup_retention=None, sku_name=None, tier=None, - storage_gb=None, administrator_login=None, - administrator_login_password=None, version=None, + storage_gb=None, version=None, active_directory_auth=None, + admin_name=None, admin_id=None, admin_type=None, + password_auth=None, administrator_login=None, administrator_login_password=None, tags=None, database_name=None, subnet=None, subnet_address_prefix=None, vnet=None, vnet_address_prefix=None, private_dns_zone_arguments=None, public_access=None, high_availability=None, zone=None, standby_availability_zone=None, geo_redundant_backup=None, byok_identity=None, byok_key=None, backup_byok_identity=None, backup_byok_key=None, - active_directory_auth=None, password_auth=None, auto_grow=None, performance_tier=None, + auto_grow=None, performance_tier=None, storage_type=None, iops=None, throughput=None, create_default_db='Enabled', create_cluster=None, cluster_size=None, yes=False): if not check_resource_group(resource_group_name): @@ -100,7 +104,9 @@ def flexible_server_create(cmd, client, backup_byok_identity=backup_byok_identity, backup_byok_key=backup_byok_key, performance_tier=performance_tier, - create_cluster=create_cluster) + create_cluster=create_cluster, + password_auth=password_auth, active_directory_auth=active_directory_auth, + admin_name=admin_name, admin_id=admin_id, admin_type=admin_type,) cluster = None if create_cluster == 'ElasticCluster': @@ -132,7 +138,10 @@ def flexible_server_create(cmd, client, high_availability = postgresql_flexibleservers.models.HighAvailability(mode=high_availability, standby_availability_zone=standby_availability_zone) - administrator_login_password = generate_password(administrator_login_password) + is_password_auth_enabled = bool(password_auth is not None and password_auth.lower() == 'enabled') + is_microsoft_entra_auth_enabled = bool(active_directory_auth is not None and active_directory_auth.lower() == 'enabled') + if is_password_auth_enabled: + administrator_login_password = generate_password(administrator_login_password) identity, data_encryption = build_identity_and_data_encryption(db_engine='postgres', byok_identity=byok_identity, @@ -162,16 +171,26 @@ def flexible_server_create(cmd, client, auth_config=auth_config, cluster=cluster) + # Add Microsoft Entra Admin + if is_microsoft_entra_auth_enabled and admin_name is not None or admin_id is not None: + server_admin_client = cf_postgres_flexible_adadmin(cmd.cli_ctx, '_') + logger.warning("Add Microsoft Entra Admin '%s'.", admin_name) + _create_admin(server_admin_client, resource_group_name, server_name, admin_name, admin_id, admin_type) + # Adding firewall rule if start_ip != -1 and end_ip != -1: firewall_id = create_firewall_rule(db_context, cmd, resource_group_name, server_name, start_ip, end_ip) - # Create mysql database if it does not exist + # Create database if it does not exist if (database_name is not None or (create_default_db and create_default_db.lower() == 'enabled') and create_cluster != 'ElasticCluster'): db_name = database_name if database_name else DEFAULT_DB_NAME _create_database(db_context, cmd, resource_group_name, server_name, db_name) + else: + db_name = POSTGRES_DB_NAME - user = server_result.administrator_login + user = server_result.administrator_login if is_password_auth_enabled else '' + password = administrator_login_password if is_password_auth_enabled else '' + admin = admin_name if admin_name else '' server_id = server_result.id loc = server_result.location version = server_result.version @@ -179,17 +198,18 @@ def flexible_server_create(cmd, client, host = server_result.fully_qualified_domain_name subnet_id = None if network is None else network.delegated_subnet_resource_id - logger.warning('Make a note of your password. If you forget, you would have to ' - 'reset your password with "az postgres flexible-server update -n %s -g %s -p ".', - server_name, resource_group_name) + if is_password_auth_enabled: + logger.warning('Make a note of your password. If you forget, you would have to ' + 'reset your password with "az postgres flexible-server update -n %s -g %s -p ".', + server_name, resource_group_name) logger.warning('Try using \'az postgres flexible-server connect\' command to test out connection.') - _update_local_contexts(cmd, server_name, resource_group_name, database_name, location, user) + _update_local_contexts(cmd, server_name, resource_group_name, db_name, location, user) - return _form_response(user, sku, loc, server_id, host, version, - administrator_login_password if administrator_login_password is not None else '*****', - _create_postgresql_connection_string(host, user, administrator_login_password, database_name), database_name, firewall_id, - subnet_id) + return _form_response(user, sku, loc, server_id, host, version, password, + _create_postgresql_connection_string(host, user, password, db_name), + db_name, firewall_id, subnet_id, is_password_auth_enabled, is_microsoft_entra_auth_enabled, admin_name, + _create_microsoft_entra_connection_string(host, db_name, admin)) # endregion create without args @@ -1088,10 +1108,15 @@ def flexible_server_ad_admin_set(cmd, client, resource_group_name, server_name, instance = server_operations_client.get(resource_group_name, server_name) if 'replica' in instance.replication_role.lower(): - raise CLIError("Cannot create an AD admin on a server with replication role. Use the primary server instead.") + raise CLIError("Cannot create a Microsoft Entra admin on a server with replication role. Use the primary server instead.") + + return _create_admin(client, resource_group_name, server_name, login, sid, principal_type, no_wait) + +# Create Microsoft Entra admin +def _create_admin(client, resource_group_name, server_name, principal_name, sid, principal_type=None, no_wait=False): parameters = { - 'principal_name': login, + 'principal_name': principal_name, 'tenant_id': get_tenant_id(), 'principal_type': principal_type } @@ -1107,7 +1132,7 @@ def flexible_server_ad_admin_delete(cmd, client, resource_group_name, server_nam instance = server_operations_client.get(resource_group_name, server_name) if 'replica' in instance.replication_role.lower(): - raise CLIError("Cannot delete an AD admin on a server with replication role. Use the primary server instead.") + raise CLIError("Cannot delete an Microsoft Entra admin on a server with replication role. Use the primary server instead.") return sdk_no_wait(no_wait, client.begin_delete, resource_group_name, server_name, sid) @@ -1755,7 +1780,7 @@ def _create_postgresql_connection_strings(host, user, password, database, port): def _create_postgresql_connection_string(host, user, password, database): connection_kwargs = { - 'user': user, + 'user': user if user is not None else '{user}', 'host': host, 'password': password if password is not None else '{password}', 'database': database, @@ -1763,13 +1788,22 @@ def _create_postgresql_connection_string(host, user, password, database): return 'postgresql://{user}:{password}@{host}/{database}?sslmode=require'.format(**connection_kwargs) +def _create_microsoft_entra_connection_string(host, database, admin=''): + connection_kwargs = { + 'user': quote(admin), + 'host': host, + 'database': database, + } + return 'postgresql://{user}:@{host}/{database}?sslmode=require'.format(**connection_kwargs) + + def _form_response(username, sku, location, server_id, host, version, password, connection_string, database_name, firewall_id=None, - subnet_id=None): + subnet_id=None, is_password_auth=True, is_microsoft_entra_auth_enabled=False, microsoft_admin=None, connection_string_microsoft_entra=None): output = { 'host': host, - 'username': username, - 'password': password, + 'username': username if is_password_auth else None, + 'password': password if is_password_auth else None, 'skuname': sku, 'location': location, 'id': server_id, @@ -1777,6 +1811,9 @@ def _form_response(username, sku, location, server_id, host, version, password, 'databaseName': database_name, 'connectionString': connection_string } + if is_microsoft_entra_auth_enabled: + output['admin'] = microsoft_admin + output['connectionStringMicrosoftEntra'] = connection_string_microsoft_entra if firewall_id is not None: output['firewallName'] = firewall_id if subnet_id is not None: diff --git a/src/azure-cli/azure/cli/command_modules/rdbms/validators.py b/src/azure-cli/azure/cli/command_modules/rdbms/validators.py index abe4e645e12..b2526b2473b 100644 --- a/src/azure-cli/azure/cli/command_modules/rdbms/validators.py +++ b/src/azure-cli/azure/cli/command_modules/rdbms/validators.py @@ -309,7 +309,9 @@ def pg_arguments_validator(db_context, location, tier, sku_name, storage_gb, ser version=None, instance=None, geo_redundant_backup=None, byok_identity=None, byok_key=None, backup_byok_identity=None, backup_byok_key=None, auto_grow=None, performance_tier=None, - storage_type=None, iops=None, throughput=None, create_cluster=None, cluster_size=None): + storage_type=None, iops=None, throughput=None, create_cluster=None, cluster_size=None, + password_auth=None, active_directory_auth=None, admin_name=None, admin_id=None, + admin_type=None): validate_server_name(db_context, server_name, 'Microsoft.DBforPostgreSQL/flexibleServers') is_create = not instance if is_create: @@ -349,6 +351,7 @@ def pg_arguments_validator(db_context, location, tier, sku_name, storage_gb, ser _pg_high_availability_validator(high_availability, standby_availability_zone, zone, tier, single_az, instance) _pg_version_validator(version, list_location_capability_info['server_versions'], is_create) pg_byok_validator(byok_identity, byok_key, backup_byok_identity, backup_byok_key, geo_redundant_backup, instance) + _pg_authentication_validator(password_auth, active_directory_auth, admin_name, admin_id, admin_type, instance) def _cluster_validator(create_cluster, cluster_size, auto_grow, geo_redundant_backup, version, tier, @@ -922,6 +925,23 @@ def _pg_storage_type_validator(storage_type, auto_grow, high_availability, geo_r raise CLIError('Updating storage iops is only capable for server created with Premium SSD v2.') +def _pg_authentication_validator(password_auth, active_directory_auth, admin_name, admin_id, admin_type, instance): + if instance is None: + if (password_auth is not None and password_auth.lower() == 'disabled') and \ + (active_directory_auth is not None and active_directory_auth.lower() == 'disabled'): + raise CLIError('Need to have an authentication method enabled, please set --active-directory-auth ' + 'to "Enabled" or --password-auth to "Enabled".') + + is_microsoft_entra = active_directory_auth is not None and active_directory_auth.lower() == 'enabled' + if not is_microsoft_entra and (admin_name or admin_id or admin_type): + raise CLIError('To provide values for --admin-object-id, --admin-display-name, and --admin-type ' + 'please set --active-directory-auth to "Enabled".') + if (admin_name is not None or admin_id is not None or admin_type is not None) and \ + not (admin_name is not None and admin_id is not None and admin_type is not None): + raise CLIError('To add Microsoft Entra admin, please provide values for --admin-object-id, ' + '--admin-display-name, and --admin-type.') + + def check_resource_group(resource_group_name): # check if rg is already null originally if not resource_group_name: