Skip to content

Commit 059dfa7

Browse files
Merge pull request #753 from microsoft/psl-fdpobugfix
fix: FDPO Windows SQL Server interactive auth issue
2 parents 331bd04 + eabb739 commit 059dfa7

File tree

4 files changed

+146
-14
lines changed

4 files changed

+146
-14
lines changed

docs/DeploymentGuide.md

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -116,7 +116,6 @@ If you're not using one of the above options for opening the project, then you'l
116116
- [Docker Desktop](https://www.docker.com/products/docker-desktop/)
117117
- [Git](https://git-scm.com/downloads)
118118
- [Microsoft ODBC Driver 18 for SQL Server](https://learn.microsoft.com/en-us/sql/connect/odbc/download-odbc-driver-for-sql-server?view=sql-server-ver16)
119-
- [sqlcmd(ODBC-Windows)](https://learn.microsoft.com/en-us/sql/tools/sqlcmd/sqlcmd-utility?view=sql-server-ver16&tabs=odbc%2Cwindows%2Cwindows-support&pivots=cs1-bash#download-and-install-sqlcmd) / [sqlcmd(Linux/Mac)](https://learn.microsoft.com/en-us/sql/linux/sql-server-linux-setup-tools?view=sql-server-ver16&tabs=redhat-install)
120119

121120
2. Clone the repository or download the project code via command-line:
122121

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
#!/usr/bin/env python3
2+
"""Assign SQL roles for Azure AD principals (managed identities/service principals) using Azure AD token auth.
3+
4+
Simplified: requires --server and --database provided explicitly (no Key Vault lookup).
5+
Roles JSON format (single arg):
6+
[
7+
{"clientId":"<guid>", "displayName":"Name", "role":"db_datareader"},
8+
{"clientId":"<guid>", "displayName":"Name", "role":"db_datawriter"}
9+
]
10+
11+
Uses pyodbc + azure-identity (AzureCliCredential)."""
12+
import argparse
13+
import json
14+
import struct
15+
import sys
16+
from typing import List, Dict
17+
18+
import pyodbc
19+
from azure.identity import AzureCliCredential
20+
21+
SQL_COPT_SS_ACCESS_TOKEN = 1256 # msodbcsql.h constant
22+
23+
24+
def build_sql(role_items: List[Dict]) -> str:
25+
statements = []
26+
for idx, item in enumerate(role_items, start=1):
27+
client_id = item["clientId"].strip()
28+
display_name = item["displayName"].replace("'", "''")
29+
role = item["role"].strip()
30+
# Construct dynamic SQL similar to prior bash script
31+
stmt = f"""
32+
DECLARE @username{idx} nvarchar(max) = N'{display_name}';
33+
DECLARE @clientId{idx} uniqueidentifier = '{client_id}';
34+
DECLARE @sid{idx} NVARCHAR(max) = CONVERT(VARCHAR(max), CONVERT(VARBINARY(16), @clientId{idx}), 1);
35+
DECLARE @cmd{idx} NVARCHAR(max) = N'CREATE USER [' + @username{idx} + '] WITH SID = ' + @sid{idx} + ', TYPE = E;';
36+
IF NOT EXISTS (SELECT * FROM sys.database_principals WHERE name = @username{idx})
37+
BEGIN
38+
EXEC(@cmd{idx})
39+
END
40+
EXEC sp_addrolemember '{role}', @username{idx};
41+
""".strip()
42+
statements.append(stmt)
43+
return "\n".join(statements)
44+
45+
46+
def connect_with_token(server: str, database: str, credential: AzureCliCredential):
47+
token_bytes = credential.get_token("https://database.windows.net/.default").token.encode("utf-16-le")
48+
token_struct = struct.pack(f"<I{len(token_bytes)}s", len(token_bytes), token_bytes)
49+
for driver in ["{ODBC Driver 18 for SQL Server}", "{ODBC Driver 17 for SQL Server}"]:
50+
try:
51+
conn_str = f"DRIVER={driver};SERVER={server};DATABASE={database};"
52+
return pyodbc.connect(conn_str, attrs_before={SQL_COPT_SS_ACCESS_TOKEN: token_struct})
53+
except pyodbc.Error:
54+
continue
55+
raise RuntimeError("Unable to connect using ODBC Driver 18 or 17. Install driver msodbcsql17/18.")
56+
57+
58+
def execute_sql(conn, sql: str):
59+
cursor = conn.cursor()
60+
# Split on GO batches if present (simple handling)
61+
batches = []
62+
current = []
63+
for line in sql.splitlines():
64+
if line.strip().upper() == "GO":
65+
if current:
66+
batches.append("\n".join(current))
67+
current = []
68+
else:
69+
current.append(line)
70+
if current:
71+
batches.append("\n".join(current))
72+
73+
for batch in batches:
74+
if batch.strip():
75+
cursor.execute(batch)
76+
conn.commit()
77+
cursor.close()
78+
79+
80+
def main():
81+
parser = argparse.ArgumentParser(description="Assign SQL roles for Azure AD principals.")
82+
parser.add_argument("--server", required=True, help="SQL server FQDN (e.g. myserver.database.windows.net)")
83+
parser.add_argument("--database", required=True, help="Database name")
84+
parser.add_argument("--roles-json", required=True, help="JSON array of role assignment objects")
85+
args = parser.parse_args()
86+
87+
try:
88+
role_items = json.loads(args.roles_json)
89+
if not isinstance(role_items, list):
90+
raise ValueError("roles-json must be a JSON array")
91+
except (json.JSONDecodeError, ValueError, KeyError) as e:
92+
print(f"Error parsing roles-json: {e}", file=sys.stderr)
93+
return 1
94+
95+
credential = AzureCliCredential()
96+
97+
try:
98+
server, database = args.server, args.database
99+
print(f"Target SQL: {server} / {database}")
100+
sql = build_sql(role_items)
101+
conn = connect_with_token(server, database, credential)
102+
print("Connected. Assigning roles...")
103+
execute_sql(conn, sql)
104+
conn.close()
105+
print("Role assignment completed successfully.")
106+
return 0
107+
except pyodbc.Error as e:
108+
print(f"Database error during role assignment: {e}", file=sys.stderr)
109+
return 1
110+
except (RuntimeError, OSError) as e:
111+
print(f"Environment error during role assignment: {e}", file=sys.stderr)
112+
return 1
113+
114+
115+
if __name__ == "__main__":
116+
sys.exit(main())

infra/scripts/process_sample_data.sh

Lines changed: 3 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -617,24 +617,15 @@ echo "copy_kb_files.sh completed successfully."
617617

618618
# Call run_create_index_scripts.sh
619619
echo "Running run_create_index_scripts.sh"
620-
bash infra/scripts/run_create_index_scripts.sh "$keyvaultName" "" "" "$resourceGroupName" "$sqlServerName" "$aiSearchName" "$aif_resource_id"
620+
# Pass SQL managed identity client id and display name so index script can perform role assignment centrally
621+
bash infra/scripts/run_create_index_scripts.sh "$keyvaultName" "" "" "$resourceGroupName" "$sqlServerName" "$aiSearchName" "$aif_resource_id" "$SqlDatabaseName" "$sqlManagedIdentityDisplayName" "$sqlManagedIdentityClientId"
621622
if [ $? -ne 0 ]; then
622623
echo "Error: run_create_index_scripts.sh failed."
623624
exit 1
624625
fi
625626
echo "run_create_index_scripts.sh completed successfully."
626627

627-
# Call create_sql_user_and_role.sh
628-
echo "Running create_sql_user_and_role.sh"
629-
bash infra/scripts/add_user_scripts/create_sql_user_and_role.sh "$sqlServerName.database.windows.net" "$SqlDatabaseName" '[
630-
{"clientId":"'"$sqlManagedIdentityClientId"'", "displayName":"'"$sqlManagedIdentityDisplayName"'", "role":"db_datareader"},
631-
{"clientId":"'"$sqlManagedIdentityClientId"'", "displayName":"'"$sqlManagedIdentityDisplayName"'", "role":"db_datawriter"}
632-
]'
633-
if [ $? -ne 0 ]; then
634-
echo "Error: create_sql_user_and_role.sh failed."
635-
exit 1
636-
fi
637-
echo "create_sql_user_and_role.sh completed successfully."
628+
## SQL role assignment now centralized in run_create_index_scripts.sh; removed local duplicate block.
638629

639630
echo "All scripts executed successfully."
640631
echo "Network access will be restored to original settings..."

infra/scripts/run_create_index_scripts.sh

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,9 @@ resourceGroupName="$4"
88
sqlServerName="$5"
99
aiSearchName="$6"
1010
aif_resource_id="$7"
11+
sqlDatabaseName="$8"
12+
sqlManagedIdentityDisplayName="${9}"
13+
sqlManagedIdentityClientId="${10}"
1114

1215
echo "Script Started"
1316

@@ -147,9 +150,10 @@ if [ -n "$baseUrl" ] && [ -n "$managedIdentityClientId" ]; then
147150
requirementFile="requirements.txt"
148151
requirementFileUrl=${baseUrl}${pythonScriptPath}"requirements.txt"
149152

150-
# Download the create_index and create table python files
153+
# Download the create_index, create table, assign_sql_roles python files
151154
curl --output "create_search_index.py" ${baseUrl}${pythonScriptPath}"create_search_index.py"
152155
curl --output "create_sql_tables.py" ${baseUrl}${pythonScriptPath}"create_sql_tables.py"
156+
curl --output "assign_sql_roles.py" ${baseUrl}${pythonScriptPath}"assign_sql_roles.py"
153157

154158
# Download the requirement file
155159
curl --output "$requirementFile" "$requirementFileUrl"
@@ -220,6 +224,28 @@ if [ $? -ne 0 ]; then
220224
error_flag=true
221225
fi
222226

227+
# Assign SQL roles to managed identity using Python (pyodbc + azure-identity)
228+
if [ -n "$sqlManagedIdentityClientId" ] && [ -n "$sqlManagedIdentityDisplayName" ] && [ -n "$sqlDatabaseName" ]; then
229+
mi_display_name="$sqlManagedIdentityDisplayName"
230+
server_fqdn="$sqlServerName.database.windows.net"
231+
roles_json="[{\"clientId\":\"$sqlManagedIdentityClientId\",\"displayName\":\"$mi_display_name\",\"role\":\"db_datareader\"},{\"clientId\":\"$sqlManagedIdentityClientId\",\"displayName\":\"$mi_display_name\",\"role\":\"db_datawriter\"}]"
232+
echo "[RoleAssign] Invoking assign_sql_roles.py for roles: db_datareader, db_datawriter"
233+
234+
role_script_path="${pythonScriptPath}assign_sql_roles.py"
235+
if [ -z "$pythonScriptPath" ]; then
236+
role_script_path="./assign_sql_roles.py"
237+
fi
238+
python "$role_script_path" --server "$server_fqdn" --database "$sqlDatabaseName" --roles-json "$roles_json"
239+
if [ $? -ne 0 ]; then
240+
echo "[RoleAssign] Warning: SQL role assignment failed."
241+
error_flag=true
242+
else
243+
echo "[RoleAssign] SQL roles assignment completed successfully."
244+
fi
245+
else
246+
echo "[RoleAssign] Skipped SQL role assignment due to missing required values (sqlManagedIdentityClientId, sqlManagedIdentityDisplayName, sqlDatabaseName)."
247+
fi
248+
223249
# revert the key vault name and managed identity client id in the python files
224250
sed -i "s/${keyvaultName}/kv_to-be-replaced/g" ${pythonScriptPath}"create_search_index.py"
225251
sed -i "s/${keyvaultName}/kv_to-be-replaced/g" ${pythonScriptPath}"create_sql_tables.py"

0 commit comments

Comments
 (0)