Skip to content

Commit 07c78a6

Browse files
Tonychen0227Tony Chen (DevDiv)
andauthored
{serviceconnector-passwordless}: Support Fabric As Target Service (#8482)
* First changes * Fix import * Add inheritance * Set up some proper inheritance * Fix style guide * Fix args order * Remove tcp * error message improve * Versioning * Undo sql handlers refactor * Undo sql handlers refactor * Undo sql handlers refactor * Maintain arg and kwargs * Fix versioning --------- Co-authored-by: Tony Chen (DevDiv) <[email protected]>
1 parent f15cd07 commit 07c78a6

File tree

10 files changed

+152
-8
lines changed

10 files changed

+152
-8
lines changed

src/serviceconnector-passwordless/HISTORY.rst

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,10 @@
22
33
Release History
44
===============
5+
3.2.0
6+
++++++
7+
* Introduce support for Fabric SQL as a target service. Introduce new `connstr_props` argument to configure Fabric SQL.
8+
59
3.1.3
610
++++++
711
* Fix argument missing

src/serviceconnector-passwordless/azext_serviceconnector_passwordless/_client_factory.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ def cf_connection_cl(cli_ctx, *_):
1313
os.environ['AZURE_HTTP_USER_AGENT'] = (os.environ.get('AZURE_HTTP_USER_AGENT')
1414
or '') + " CliExtension/{}({})".format(NAME, VERSION)
1515
return get_mgmt_service_client(cli_ctx, ServiceLinkerManagementClient,
16-
subscription_bound=False, api_version="2023-04-01-preview")
16+
subscription_bound=False, api_version="2024-07-01-preview")
1717

1818

1919
def cf_linker(cli_ctx, *_):

src/serviceconnector-passwordless/azext_serviceconnector_passwordless/_credential_free.py

Lines changed: 90 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
import struct
88
import sys
99
import re
10+
import requests
1011
from knack.log import get_logger
1112
from azure.mgmt.core.tools import parse_resource_id
1213
from azure.cli.core import telemetry
@@ -46,7 +47,7 @@
4647
# For db(mysqlFlex/psql/psqlFlex/sql) linker with auth type=systemAssignedIdentity, enable Microsoft Entra auth and create db user on data plane
4748
# For other linker, ignore the steps
4849
def get_enable_mi_for_db_linker_func(yes=False, new=False):
49-
def enable_mi_for_db_linker(cmd, source_id, target_id, auth_info, client_type, connection_name, *args, **kwargs):
50+
def enable_mi_for_db_linker(cmd, source_id, target_id, auth_info, client_type, connection_name, connstr_props, *args, **kwargs):
5051
# return if connection is not for db mi
5152
if auth_info['auth_type'] not in [AUTHTYPES[AUTH_TYPE.SystemIdentity],
5253
AUTHTYPES[AUTH_TYPE.UserIdentity],
@@ -61,7 +62,7 @@ def enable_mi_for_db_linker(cmd, source_id, target_id, auth_info, client_type, c
6162
if source_handler is None:
6263
return None
6364
target_handler = getTargetHandler(
64-
cmd, target_id, target_type, auth_info, client_type, connection_name, skip_prompt=yes, new_user=new)
65+
cmd, target_id, target_type, auth_info, client_type, connection_name, connstr_props, skip_prompt=yes, new_user=new)
6566
if target_handler is None:
6667
return None
6768
target_handler.check_db_existence()
@@ -88,7 +89,7 @@ def enable_mi_for_db_linker(cmd, source_id, target_id, auth_info, client_type, c
8889
source_object_id = source_handler.get_identity_pid()
8990
target_handler.identity_object_id = source_object_id
9091
try:
91-
if target_type in [RESOURCE.Sql]:
92+
if target_type in [RESOURCE.Sql, RESOURCE.FabricSql]:
9293
target_handler.identity_name = source_handler.get_identity_name()
9394
elif target_type in [RESOURCE.Postgres, RESOURCE.MysqlFlexible]:
9495
identity_info = run_cli_cmd(
@@ -149,7 +150,7 @@ def enable_mi_for_db_linker(cmd, source_id, target_id, auth_info, client_type, c
149150

150151

151152
# pylint: disable=unused-argument, too-many-instance-attributes
152-
def getTargetHandler(cmd, target_id, target_type, auth_info, client_type, connection_name, skip_prompt, new_user):
153+
def getTargetHandler(cmd, target_id, target_type, auth_info, client_type, connection_name, connstr_props, skip_prompt, new_user):
153154
if target_type in {RESOURCE.Sql}:
154155
return SqlHandler(cmd, target_id, target_type, auth_info, connection_name, skip_prompt, new_user)
155156
if target_type in {RESOURCE.Postgres}:
@@ -158,6 +159,8 @@ def getTargetHandler(cmd, target_id, target_type, auth_info, client_type, connec
158159
return PostgresFlexHandler(cmd, target_id, target_type, auth_info, connection_name, skip_prompt, new_user)
159160
if target_type in {RESOURCE.MysqlFlexible}:
160161
return MysqlFlexibleHandler(cmd, target_id, target_type, auth_info, connection_name, skip_prompt, new_user)
162+
if target_type in {RESOURCE.FabricSql}:
163+
return FabricSqlHandler(cmd, target_id, target_type, auth_info, connection_name, connstr_props, skip_prompt, new_user)
161164
return None
162165

163166

@@ -960,6 +963,89 @@ def get_create_query(self):
960963
]
961964

962965

966+
class FabricSqlHandler(SqlHandler):
967+
def __init__(self, cmd, target_id, target_type, auth_info, connection_name, connstr_props, skip_prompt, new_user):
968+
super().__init__(cmd, target_id, target_type,
969+
auth_info, connection_name, skip_prompt, new_user)
970+
971+
self.target_id = target_id
972+
973+
if not connstr_props:
974+
raise CLIInternalError("Missing additional connection string properties for Fabric SQL target.")
975+
976+
Server = connstr_props.get('Server') or connstr_props.get('Data Source')
977+
Database = connstr_props.get('Database') or connstr_props.get('Initial Catalog')
978+
if not Server or not Database:
979+
raise CLIInternalError("Missing 'Server' or 'Database' in additonal connection string properties keys."
980+
"Use --connstr_props 'Server=xxx' 'Database=xxx' to provide the values.")
981+
982+
# Construct the ODBC connection string
983+
self.ODBCConnectionString = self.construct_odbc_connection_string(Server, Database)
984+
logger.warning("ODBC connection string: %s", self.ODBCConnectionString)
985+
986+
def check_db_existence(self):
987+
fabric_token = self.get_fabric_access_token()
988+
headers = {"Authorization": "Bearer {}".format(fabric_token)}
989+
response = requests.get(self.target_id, headers=headers)
990+
991+
if response:
992+
response_json = response.json()
993+
if response_json["id"]:
994+
return
995+
996+
e = ResourceNotFoundError("No database found with name {}".format(self.dbname))
997+
telemetry.set_exception(e, "No-Db")
998+
raise e
999+
1000+
def construct_odbc_connection_string(self, server, database):
1001+
# Map fields to ODBC fields
1002+
odbc_dict = {
1003+
'Driver': '{driver}',
1004+
'Server': server,
1005+
'Database': database,
1006+
}
1007+
1008+
odbc_connection_string = ';'.join([f'{key}={value}' for key, value in odbc_dict.items()])
1009+
return odbc_connection_string
1010+
1011+
def create_aad_user(self):
1012+
query_list = self.get_create_query()
1013+
connection_args = self.get_connection_string()
1014+
1015+
logger.warning("Connecting to database...")
1016+
self.create_aad_user_in_sql(connection_args, query_list)
1017+
1018+
def get_fabric_access_token(self):
1019+
return run_cli_cmd('az account get-access-token --output json --resource https://api.fabric.microsoft.com/').get('accessToken')
1020+
1021+
def set_user_admin(self, user_object_id, **kwargs):
1022+
return
1023+
1024+
def get_connection_string(self, dbname=""):
1025+
token_bytes = self.get_fabric_access_token().encode('utf-16-le')
1026+
1027+
token_struct = struct.pack(
1028+
f'<I{len(token_bytes)}s', len(token_bytes), token_bytes)
1029+
# This connection option is defined by microsoft in msodbcsql.h
1030+
SQL_COPT_SS_ACCESS_TOKEN = 1256
1031+
conn_string = self.ODBCConnectionString
1032+
return {'connection_string': conn_string, 'attrs_before': {SQL_COPT_SS_ACCESS_TOKEN: token_struct}}
1033+
1034+
def get_create_query(self):
1035+
if self.auth_type in [AUTHTYPES[AUTH_TYPE.SystemIdentity], AUTHTYPES[AUTH_TYPE.UserIdentity]]:
1036+
self.aad_username = self.identity_name
1037+
else:
1038+
raise CLIInternalError("Unsupported auth type: " + self.auth_type)
1039+
1040+
delete_q = "DROP USER IF EXISTS \"{}\";".format(self.aad_username)
1041+
role_q = "CREATE USER \"{}\" FROM EXTERNAL PROVIDER;".format(self.aad_username)
1042+
grant_q1 = "ALTER ROLE db_datareader ADD MEMBER \"{}\"".format(self.aad_username)
1043+
grant_q2 = "ALTER ROLE db_datawriter ADD MEMBER \"{}\"".format(self.aad_username)
1044+
grant_q3 = "ALTER ROLE db_ddladmin ADD MEMBER \"{}\"".format(self.aad_username)
1045+
1046+
return [delete_q, role_q, grant_q1, grant_q2, grant_q3]
1047+
1048+
9631049
def getSourceHandler(source_id, source_type):
9641050
if source_type in {RESOURCE.WebApp, RESOURCE.FunctionApp}:
9651051
return WebappHandler(source_id, source_type)

src/serviceconnector-passwordless/azext_serviceconnector_passwordless/_params.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
add_secret_store_argument,
1717
add_local_connection_block,
1818
add_customized_keys_argument,
19+
add_connstr_props_argument,
1920
add_configuration_store_argument,
2021
add_opt_out_argument
2122
)
@@ -89,6 +90,7 @@ def load_arguments(self, _):
8990
add_vnet_block(c, target)
9091
add_connection_string_argument(c, source, target)
9192
add_customized_keys_argument(c)
93+
add_connstr_props_argument(c)
9294
add_opt_out_argument(c)
9395
c.argument('yes', arg_type=yes_arg_type)
9496
c.argument('new', arg_type=new_arg_type)

src/serviceconnector-passwordless/azext_serviceconnector_passwordless/_resource_config.py

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,8 @@
3434
# RESOURCE.Postgres,
3535
RESOURCE.PostgresFlexible,
3636
RESOURCE.MysqlFlexible,
37-
RESOURCE.Sql
37+
RESOURCE.Sql,
38+
RESOURCE.FabricSql
3839
]
3940

4041
# pylint: disable=line-too-long
@@ -58,6 +59,7 @@
5859
RESOURCE.PostgresFlexible: [AUTH_TYPE.Secret, AUTH_TYPE.SystemIdentity, AUTH_TYPE.UserIdentity, AUTH_TYPE.ServicePrincipalSecret],
5960
RESOURCE.MysqlFlexible: [AUTH_TYPE.Secret, AUTH_TYPE.SystemIdentity, AUTH_TYPE.UserIdentity, AUTH_TYPE.ServicePrincipalSecret],
6061
RESOURCE.Sql: [AUTH_TYPE.Secret, AUTH_TYPE.SystemIdentity, AUTH_TYPE.UserIdentity, AUTH_TYPE.ServicePrincipalSecret],
62+
RESOURCE.FabricSql: [AUTH_TYPE.SystemIdentity, AUTH_TYPE.UserIdentity],
6163
}
6264

6365
TARGET_RESOURCES_PARAMS = {
@@ -130,6 +132,13 @@
130132
'placeholder': 'MyDB'
131133
}
132134
},
135+
RESOURCE.FabricSql: {
136+
'connstr_props': {
137+
'options': ['--connstr-props'],
138+
'help': 'Connection string properties of the Fabric SQL server. Format like: --connstr-props "Server=<Server_Host>,<Port>" "Database=<Database_Name>".',
139+
'placeholder': 'Server=MyServer,1433 Database=MyDB'
140+
}
141+
}
133142
}
134143

135144
AUTH_TYPE_PARAMS = {

src/serviceconnector-passwordless/azext_serviceconnector_passwordless/commands.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
from azure.cli.core.commands import CliCommandType
88

99
from azure.cli.command_modules.serviceconnector._resource_config import (
10+
RESOURCE,
1011
SOURCE_RESOURCES,
1112
TARGET_RESOURCES_DEPRECATED
1213
)
@@ -31,6 +32,9 @@ def load_command_table(self, _):
3132
client_factory=cf_connector)
3233

3334
for target in PASSWORDLESS_TARGET_RESOURCES:
35+
# FabricSql is not supported for Local Connector
36+
if target == RESOURCE.FabricSql:
37+
continue
3438
with self.command_group('connection create',
3539
local_connection_type, client_factory=cf_connector) as ig:
3640
if target in TARGET_RESOURCES_DEPRECATED:
@@ -45,6 +49,8 @@ def load_command_table(self, _):
4549
# only when the extension is installed
4650
if should_load_source(source):
4751
for target in PASSWORDLESS_TARGET_RESOURCES:
52+
if source == RESOURCE.KubernetesCluster and target == RESOURCE.FabricSql:
53+
continue
4854
with self.command_group(f'{source.value} connection create',
4955
connection_type, client_factory=cf_linker) as ig:
5056
if target in TARGET_RESOURCES_DEPRECATED:

src/serviceconnector-passwordless/azext_serviceconnector_passwordless/config.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,5 +4,5 @@
44
# --------------------------------------------------------------------------------------------
55

66

7-
VERSION = '3.1.3'
7+
VERSION = '3.2.0'
88
NAME = 'serviceconnector-passwordless'

src/serviceconnector-passwordless/azext_serviceconnector_passwordless/custom.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ def connection_create_ext(cmd, client,
2828
spring=None, app=None, deployment='default', # Resource.SpringCloud
2929
# Resource.*Postgres, Resource.*Sql*
3030
server=None, database=None,
31+
connstr_props=None,
3132
**kwargs,
3233
):
3334
from azure.cli.command_modules.serviceconnector.custom import connection_create_func
@@ -52,6 +53,7 @@ def connection_create_ext(cmd, client,
5253
customized_keys=customized_keys,
5354
opt_out_list=opt_out_list,
5455
app_config_id=app_config_id,
56+
connstr_props=connstr_props,
5557
**kwargs)
5658

5759

src/serviceconnector-passwordless/azext_serviceconnector_passwordless/tests/latest/test_serviceconnector-passwordless_scenario.py

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -355,3 +355,38 @@ def test_local_sql_passwordless(self):
355355

356356
# delete connection
357357
self.cmd('connection delete --id {} --yes'.format(connection_id))
358+
359+
def test_aad_webapp_fabric_sql(self):
360+
self.kwargs.update({
361+
'subscription': get_subscription_id(self.cli_ctx),
362+
'source_resource_group': 'azure-service-connector',
363+
'site': 'DotNetAppSqlDb20240704',
364+
'database': 'clitest'
365+
})
366+
name = 'testfabricconn'
367+
source_id = SOURCE_RESOURCES.get(RESOURCE.WebApp).format(**self.kwargs)
368+
connection_id = source_id + "/providers/Microsoft.ServiceLinker/linkers/" + name
369+
target_id = 'https://api.fabric.microsoft.com/v1/workspaces/13c65326-ecab-43f6-8a05-60927aaa4cec/SqlDatabases/4fdf6efe-23a9-4d74-8c4a-4ecc70c4d323'
370+
server = 'renzo-srv-6ae35870-c362-44b9-8389-ada214a46bb5-51240650dd56.database.windows.net,1433'
371+
database = 'AzureServiceConnectorTestSqlDb-4fdf6efe-23a9-4d74-8c4a-4ecc70c4d323'
372+
373+
# prepare
374+
self.cmd('webapp identity remove --ids {}'.format(source_id))
375+
376+
# create
377+
self.cmd('webapp connection create fabric-sql --connection {} --source-id {} --target-id {} \
378+
--system-identity --client-type dotnet --opt-out publicnetwork \
379+
--connstr-props "Server={}" "Database={}" '.format(name, source_id, target_id, server, database)
380+
)
381+
# clean
382+
self.cmd('webapp connection delete --id {} --yes'.format(connection_id))
383+
384+
# recreate and test
385+
self.cmd('webapp connection create fabric-sql --connection {} --source-id {} --target-id {} \
386+
--system-identity --client-type dotnet --opt-out publicnetwork \
387+
--connstr-props "Server={}" \
388+
"Database={}" '.format(name, source_id, target_id, server, database)
389+
)
390+
391+
# clean
392+
self.cmd('webapp connection delete --id {} --yes'.format(connection_id))

src/serviceconnector-passwordless/setup.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
logger.warn("Wheel is not available, disabling bdist_wheel hook")
1616

1717

18-
VERSION = '3.1.3'
18+
VERSION = '3.2.0'
1919
try:
2020
from azext_serviceconnector_passwordless.config import VERSION
2121
except ImportError:

0 commit comments

Comments
 (0)