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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions src/serviceconnector-passwordless/HISTORY.rst
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@

Release History
===============
3.2.0
++++++
* Introduce support for Fabric SQL as a target service. Introduce new `connstr_props` argument to configure Fabric SQL.

3.1.3
++++++
* Fix argument missing
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ def cf_connection_cl(cli_ctx, *_):
os.environ['AZURE_HTTP_USER_AGENT'] = (os.environ.get('AZURE_HTTP_USER_AGENT')
or '') + " CliExtension/{}({})".format(NAME, VERSION)
return get_mgmt_service_client(cli_ctx, ServiceLinkerManagementClient,
subscription_bound=False, api_version="2023-04-01-preview")
subscription_bound=False, api_version="2024-07-01-preview")


def cf_linker(cli_ctx, *_):
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import struct
import sys
import re
import requests
from knack.log import get_logger
from azure.mgmt.core.tools import parse_resource_id
from azure.cli.core import telemetry
Expand Down Expand Up @@ -46,7 +47,7 @@
# For db(mysqlFlex/psql/psqlFlex/sql) linker with auth type=systemAssignedIdentity, enable Microsoft Entra auth and create db user on data plane
# For other linker, ignore the steps
def get_enable_mi_for_db_linker_func(yes=False, new=False):
def enable_mi_for_db_linker(cmd, source_id, target_id, auth_info, client_type, connection_name, *args, **kwargs):
def enable_mi_for_db_linker(cmd, source_id, target_id, auth_info, client_type, connection_name, connstr_props, *args, **kwargs):
# return if connection is not for db mi
if auth_info['auth_type'] not in [AUTHTYPES[AUTH_TYPE.SystemIdentity],
AUTHTYPES[AUTH_TYPE.UserIdentity],
Expand All @@ -61,7 +62,7 @@ def enable_mi_for_db_linker(cmd, source_id, target_id, auth_info, client_type, c
if source_handler is None:
return None
target_handler = getTargetHandler(
cmd, target_id, target_type, auth_info, client_type, connection_name, skip_prompt=yes, new_user=new)
cmd, target_id, target_type, auth_info, client_type, connection_name, connstr_props, skip_prompt=yes, new_user=new)
if target_handler is None:
return None
target_handler.check_db_existence()
Expand All @@ -88,7 +89,7 @@ def enable_mi_for_db_linker(cmd, source_id, target_id, auth_info, client_type, c
source_object_id = source_handler.get_identity_pid()
target_handler.identity_object_id = source_object_id
try:
if target_type in [RESOURCE.Sql]:
if target_type in [RESOURCE.Sql, RESOURCE.FabricSql]:
target_handler.identity_name = source_handler.get_identity_name()
elif target_type in [RESOURCE.Postgres, RESOURCE.MysqlFlexible]:
identity_info = run_cli_cmd(
Expand Down Expand Up @@ -149,7 +150,7 @@ def enable_mi_for_db_linker(cmd, source_id, target_id, auth_info, client_type, c


# pylint: disable=unused-argument, too-many-instance-attributes
def getTargetHandler(cmd, target_id, target_type, auth_info, client_type, connection_name, skip_prompt, new_user):
def getTargetHandler(cmd, target_id, target_type, auth_info, client_type, connection_name, connstr_props, skip_prompt, new_user):
if target_type in {RESOURCE.Sql}:
return SqlHandler(cmd, target_id, target_type, auth_info, connection_name, skip_prompt, new_user)
if target_type in {RESOURCE.Postgres}:
Expand All @@ -158,6 +159,8 @@ def getTargetHandler(cmd, target_id, target_type, auth_info, client_type, connec
return PostgresFlexHandler(cmd, target_id, target_type, auth_info, connection_name, skip_prompt, new_user)
if target_type in {RESOURCE.MysqlFlexible}:
return MysqlFlexibleHandler(cmd, target_id, target_type, auth_info, connection_name, skip_prompt, new_user)
if target_type in {RESOURCE.FabricSql}:
return FabricSqlHandler(cmd, target_id, target_type, auth_info, connection_name, connstr_props, skip_prompt, new_user)
return None


Expand Down Expand Up @@ -960,6 +963,89 @@ def get_create_query(self):
]


class FabricSqlHandler(SqlHandler):
def __init__(self, cmd, target_id, target_type, auth_info, connection_name, connstr_props, skip_prompt, new_user):
super().__init__(cmd, target_id, target_type,
auth_info, connection_name, skip_prompt, new_user)

self.target_id = target_id

if not connstr_props:
raise CLIInternalError("Missing additional connection string properties for Fabric SQL target.")

Server = connstr_props.get('Server') or connstr_props.get('Data Source')
Database = connstr_props.get('Database') or connstr_props.get('Initial Catalog')
if not Server or not Database:
raise CLIInternalError("Missing 'Server' or 'Database' in additonal connection string properties keys."
"Use --connstr_props 'Server=xxx' 'Database=xxx' to provide the values.")

# Construct the ODBC connection string
self.ODBCConnectionString = self.construct_odbc_connection_string(Server, Database)
logger.warning("ODBC connection string: %s", self.ODBCConnectionString)

def check_db_existence(self):
fabric_token = self.get_fabric_access_token()
headers = {"Authorization": "Bearer {}".format(fabric_token)}
response = requests.get(self.target_id, headers=headers)

if response:
response_json = response.json()
if response_json["id"]:
return

e = ResourceNotFoundError("No database found with name {}".format(self.dbname))
telemetry.set_exception(e, "No-Db")
raise e

def construct_odbc_connection_string(self, server, database):
# Map fields to ODBC fields
odbc_dict = {
'Driver': '{driver}',
'Server': server,
'Database': database,
}

odbc_connection_string = ';'.join([f'{key}={value}' for key, value in odbc_dict.items()])
return odbc_connection_string

def create_aad_user(self):
query_list = self.get_create_query()
connection_args = self.get_connection_string()

logger.warning("Connecting to database...")
self.create_aad_user_in_sql(connection_args, query_list)

def get_fabric_access_token(self):
return run_cli_cmd('az account get-access-token --output json --resource https://api.fabric.microsoft.com/').get('accessToken')

def set_user_admin(self, user_object_id, **kwargs):
return

def get_connection_string(self, dbname=""):
token_bytes = self.get_fabric_access_token().encode('utf-16-le')

token_struct = struct.pack(
f'<I{len(token_bytes)}s', len(token_bytes), token_bytes)
# This connection option is defined by microsoft in msodbcsql.h
SQL_COPT_SS_ACCESS_TOKEN = 1256
conn_string = self.ODBCConnectionString
return {'connection_string': conn_string, 'attrs_before': {SQL_COPT_SS_ACCESS_TOKEN: token_struct}}

def get_create_query(self):
if self.auth_type in [AUTHTYPES[AUTH_TYPE.SystemIdentity], AUTHTYPES[AUTH_TYPE.UserIdentity]]:
self.aad_username = self.identity_name
else:
raise CLIInternalError("Unsupported auth type: " + self.auth_type)

delete_q = "DROP USER IF EXISTS \"{}\";".format(self.aad_username)
role_q = "CREATE USER \"{}\" FROM EXTERNAL PROVIDER;".format(self.aad_username)
grant_q1 = "ALTER ROLE db_datareader ADD MEMBER \"{}\"".format(self.aad_username)
grant_q2 = "ALTER ROLE db_datawriter ADD MEMBER \"{}\"".format(self.aad_username)
grant_q3 = "ALTER ROLE db_ddladmin ADD MEMBER \"{}\"".format(self.aad_username)

return [delete_q, role_q, grant_q1, grant_q2, grant_q3]


def getSourceHandler(source_id, source_type):
if source_type in {RESOURCE.WebApp, RESOURCE.FunctionApp}:
return WebappHandler(source_id, source_type)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
add_secret_store_argument,
add_local_connection_block,
add_customized_keys_argument,
add_connstr_props_argument,
add_configuration_store_argument,
add_opt_out_argument
)
Expand Down Expand Up @@ -89,6 +90,7 @@ def load_arguments(self, _):
add_vnet_block(c, target)
add_connection_string_argument(c, source, target)
add_customized_keys_argument(c)
add_connstr_props_argument(c)
add_opt_out_argument(c)
c.argument('yes', arg_type=yes_arg_type)
c.argument('new', arg_type=new_arg_type)
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,8 @@
# RESOURCE.Postgres,
RESOURCE.PostgresFlexible,
RESOURCE.MysqlFlexible,
RESOURCE.Sql
RESOURCE.Sql,
RESOURCE.FabricSql
]

# pylint: disable=line-too-long
Expand All @@ -58,6 +59,7 @@
RESOURCE.PostgresFlexible: [AUTH_TYPE.Secret, AUTH_TYPE.SystemIdentity, AUTH_TYPE.UserIdentity, AUTH_TYPE.ServicePrincipalSecret],
RESOURCE.MysqlFlexible: [AUTH_TYPE.Secret, AUTH_TYPE.SystemIdentity, AUTH_TYPE.UserIdentity, AUTH_TYPE.ServicePrincipalSecret],
RESOURCE.Sql: [AUTH_TYPE.Secret, AUTH_TYPE.SystemIdentity, AUTH_TYPE.UserIdentity, AUTH_TYPE.ServicePrincipalSecret],
RESOURCE.FabricSql: [AUTH_TYPE.SystemIdentity, AUTH_TYPE.UserIdentity],
}

TARGET_RESOURCES_PARAMS = {
Expand Down Expand Up @@ -130,6 +132,13 @@
'placeholder': 'MyDB'
}
},
RESOURCE.FabricSql: {
'connstr_props': {
'options': ['--connstr-props'],
'help': 'Connection string properties of the Fabric SQL server. Format like: --connstr-props "Server=<Server_Host>,<Port>" "Database=<Database_Name>".',
'placeholder': 'Server=MyServer,1433 Database=MyDB'
}
}
}

AUTH_TYPE_PARAMS = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
from azure.cli.core.commands import CliCommandType

from azure.cli.command_modules.serviceconnector._resource_config import (
RESOURCE,
SOURCE_RESOURCES,
TARGET_RESOURCES_DEPRECATED
)
Expand All @@ -31,6 +32,9 @@ def load_command_table(self, _):
client_factory=cf_connector)

for target in PASSWORDLESS_TARGET_RESOURCES:
# FabricSql is not supported for Local Connector
if target == RESOURCE.FabricSql:
continue
with self.command_group('connection create',
local_connection_type, client_factory=cf_connector) as ig:
if target in TARGET_RESOURCES_DEPRECATED:
Expand All @@ -45,6 +49,8 @@ def load_command_table(self, _):
# only when the extension is installed
if should_load_source(source):
for target in PASSWORDLESS_TARGET_RESOURCES:
if source == RESOURCE.KubernetesCluster and target == RESOURCE.FabricSql:
continue
with self.command_group(f'{source.value} connection create',
connection_type, client_factory=cf_linker) as ig:
if target in TARGET_RESOURCES_DEPRECATED:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,5 @@
# --------------------------------------------------------------------------------------------


VERSION = '3.1.3'
VERSION = '3.2.0'
NAME = 'serviceconnector-passwordless'
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ def connection_create_ext(cmd, client,
spring=None, app=None, deployment='default', # Resource.SpringCloud
# Resource.*Postgres, Resource.*Sql*
server=None, database=None,
connstr_props=None,
**kwargs,
):
from azure.cli.command_modules.serviceconnector.custom import connection_create_func
Expand All @@ -52,6 +53,7 @@ def connection_create_ext(cmd, client,
customized_keys=customized_keys,
opt_out_list=opt_out_list,
app_config_id=app_config_id,
connstr_props=connstr_props,
**kwargs)


Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -355,3 +355,38 @@ def test_local_sql_passwordless(self):

# delete connection
self.cmd('connection delete --id {} --yes'.format(connection_id))

def test_aad_webapp_fabric_sql(self):
self.kwargs.update({
'subscription': get_subscription_id(self.cli_ctx),
'source_resource_group': 'azure-service-connector',
'site': 'DotNetAppSqlDb20240704',
'database': 'clitest'
})
name = 'testfabricconn'
source_id = SOURCE_RESOURCES.get(RESOURCE.WebApp).format(**self.kwargs)
connection_id = source_id + "/providers/Microsoft.ServiceLinker/linkers/" + name
target_id = 'https://api.fabric.microsoft.com/v1/workspaces/13c65326-ecab-43f6-8a05-60927aaa4cec/SqlDatabases/4fdf6efe-23a9-4d74-8c4a-4ecc70c4d323'
server = 'renzo-srv-6ae35870-c362-44b9-8389-ada214a46bb5-51240650dd56.database.windows.net,1433'
database = 'AzureServiceConnectorTestSqlDb-4fdf6efe-23a9-4d74-8c4a-4ecc70c4d323'

# prepare
self.cmd('webapp identity remove --ids {}'.format(source_id))

# create
self.cmd('webapp connection create fabric-sql --connection {} --source-id {} --target-id {} \
--system-identity --client-type dotnet --opt-out publicnetwork \
--connstr-props "Server={}" "Database={}" '.format(name, source_id, target_id, server, database)
)
# clean
self.cmd('webapp connection delete --id {} --yes'.format(connection_id))

# recreate and test
self.cmd('webapp connection create fabric-sql --connection {} --source-id {} --target-id {} \
--system-identity --client-type dotnet --opt-out publicnetwork \
--connstr-props "Server={}" \
"Database={}" '.format(name, source_id, target_id, server, database)
)

# clean
self.cmd('webapp connection delete --id {} --yes'.format(connection_id))
2 changes: 1 addition & 1 deletion src/serviceconnector-passwordless/setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
logger.warn("Wheel is not available, disabling bdist_wheel hook")


VERSION = '3.1.3'
VERSION = '3.2.0'
try:
from azext_serviceconnector_passwordless.config import VERSION
except ImportError:
Expand Down
Loading