Skip to content

Commit 8ec5eeb

Browse files
[Storage] [STG 100] Datalake Security Principal Bound Identity SAS (#42647)
1 parent 5ff401f commit 8ec5eeb

File tree

5 files changed

+261
-1
lines changed

5 files changed

+261
-1
lines changed

sdk/storage/azure-storage-file-datalake/assets.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,5 +2,5 @@
22
"AssetsRepo": "Azure/azure-sdk-assets",
33
"AssetsRepoPrefixPath": "python",
44
"TagPrefix": "python/storage/azure-storage-file-datalake",
5-
"Tag": "python/storage/azure-storage-file-datalake_a7ee812abc"
5+
"Tag": "python/storage/azure-storage-file-datalake_75e92b68c3"
66
}

sdk/storage/azure-storage-file-datalake/azure/storage/filedatalake/_shared/shared_access_signature.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -209,6 +209,9 @@ def add_resource(self, resource):
209209
def add_id(self, policy_id):
210210
self._add_query(QueryStringConstants.SIGNED_IDENTIFIER, policy_id)
211211

212+
def add_user_delegation_oid(self, user_delegation_oid):
213+
self._add_query(QueryStringConstants.SIGNED_DELEGATED_USER_OID, user_delegation_oid)
214+
212215
def add_account(self, services, resource_types):
213216
self._add_query(QueryStringConstants.SIGNED_SERVICES, services)
214217
self._add_query(QueryStringConstants.SIGNED_RESOURCE_TYPES, resource_types)

sdk/storage/azure-storage-file-datalake/azure/storage/filedatalake/_shared_access_signature.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,7 @@ def generate_file_system_sas(
108108
permission: Optional[Union["FileSystemSasPermissions", str]] = None,
109109
expiry: Optional[Union["datetime", str]] = None,
110110
*,
111+
user_delegation_oid: Optional[str] = None,
111112
sts_hook: Optional[Callable[[str], None]] = None,
112113
**kwargs: Any
113114
) -> str:
@@ -193,6 +194,10 @@ def generate_file_system_sas(
193194
generating and distributing the SAS.
194195
:keyword str encryption_scope:
195196
Specifies the encryption scope for a request made so that all write operations will be service encrypted.
197+
:keyword str user_delegation_oid:
198+
Specifies the Entra ID of the user that is authorized to use the resulting SAS URL.
199+
The resulting SAS URL must be used in conjunction with an Entra ID token that has been
200+
issued to the user specified in this value.
196201
:keyword sts_hook:
197202
For debugging purposes only. If provided, the hook is called with the string to sign
198203
that was used to generate the SAS.
@@ -207,6 +212,7 @@ def generate_file_system_sas(
207212
user_delegation_key=credential if not isinstance(credential, str) else None,
208213
permission=cast(Optional[Union["ContainerSasPermissions", str]], permission),
209214
expiry=expiry,
215+
user_delegation_oid=user_delegation_oid,
210216
sts_hook=sts_hook,
211217
**kwargs
212218
)
@@ -220,6 +226,7 @@ def generate_directory_sas(
220226
permission: Optional[Union["DirectorySasPermissions", str]] = None,
221227
expiry: Optional[Union["datetime", str]] = None,
222228
*,
229+
user_delegation_oid: Optional[str] = None,
223230
sts_hook: Optional[Callable[[str], None]] = None,
224231
**kwargs: Any
225232
) -> str:
@@ -307,6 +314,10 @@ def generate_directory_sas(
307314
generating and distributing the SAS.
308315
:keyword str encryption_scope:
309316
Specifies the encryption scope for a request made so that all write operations will be service encrypted.
317+
:keyword str user_delegation_oid:
318+
Specifies the Entra ID of the user that is authorized to use the resulting SAS URL.
319+
The resulting SAS URL must be used in conjunction with an Entra ID token that has been
320+
issued to the user specified in this value.
310321
:keyword sts_hook:
311322
For debugging purposes only. If provided, the hook is called with the string to sign
312323
that was used to generate the SAS.
@@ -325,6 +336,7 @@ def generate_directory_sas(
325336
expiry=expiry,
326337
sdd=depth,
327338
is_directory=True,
339+
user_delegation_oid=user_delegation_oid,
328340
sts_hook=sts_hook,
329341
**kwargs
330342
)
@@ -339,6 +351,7 @@ def generate_file_sas(
339351
permission: Optional[Union["FileSasPermissions", str]] = None,
340352
expiry: Optional[Union["datetime", str]] = None,
341353
*,
354+
user_delegation_oid: Optional[str] = None,
342355
sts_hook: Optional[Callable[[str], None]] = None,
343356
**kwargs: Any
344357
) -> str:
@@ -428,6 +441,10 @@ def generate_file_sas(
428441
generating and distributing the SAS. This can only be used when generating a SAS with delegation key.
429442
:keyword str encryption_scope:
430443
Specifies the encryption scope for a request made so that all write operations will be service encrypted.
444+
:keyword str user_delegation_oid:
445+
Specifies the Entra ID of the user that is authorized to use the resulting SAS URL.
446+
The resulting SAS URL must be used in conjunction with an Entra ID token that has been
447+
issued to the user specified in this value.
431448
:keyword sts_hook:
432449
For debugging purposes only. If provided, the hook is called with the string to sign
433450
that was used to generate the SAS.
@@ -448,6 +465,7 @@ def generate_file_sas(
448465
permission=cast(Optional[Union["BlobSasPermissions", str]], permission),
449466
expiry=expiry,
450467
sts_hook=sts_hook,
468+
user_delegation_oid=user_delegation_oid,
451469
**kwargs
452470
)
453471

sdk/storage/azure-storage-file-datalake/tests/test_file_system.py

Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
# Licensed under the MIT License. See License.txt in the project root for
44
# license information.
55
# --------------------------------------------------------------------------
6+
import jwt
67
import unittest
78
from datetime import datetime, timedelta
89
from time import sleep
@@ -1149,6 +1150,123 @@ def test_get_and_set_access_control_oauth(self, **kwargs):
11491150
# Assert
11501151
assert acl == access_control['acl']
11511152

1153+
@DataLakePreparer()
1154+
@recorded_by_proxy
1155+
def test_get_user_delegation_sas(self, **kwargs):
1156+
datalake_storage_account_name = kwargs.pop("datalake_storage_account_name")
1157+
variables = kwargs.pop("variables", {})
1158+
1159+
token_credential = self.get_credential(DataLakeServiceClient)
1160+
service = DataLakeServiceClient(
1161+
self.account_url(datalake_storage_account_name, "dfs"),
1162+
credential=token_credential,
1163+
)
1164+
start = self.get_datetime_variable(variables, 'start_time', datetime.utcnow())
1165+
expiry = self.get_datetime_variable(variables, 'expiry_time', datetime.utcnow() + timedelta(hours=1))
1166+
user_delegation_key_1 = service.get_user_delegation_key(key_start_time=start, key_expiry_time=expiry)
1167+
user_delegation_key_2 = service.get_user_delegation_key(key_start_time=start, key_expiry_time=expiry)
1168+
1169+
# Assert key1 is valid
1170+
assert user_delegation_key_1.signed_oid is not None
1171+
assert user_delegation_key_1.signed_tid is not None
1172+
assert user_delegation_key_1.signed_start is not None
1173+
assert user_delegation_key_1.signed_expiry is not None
1174+
assert user_delegation_key_1.signed_version is not None
1175+
assert user_delegation_key_1.signed_service is not None
1176+
assert user_delegation_key_1.value is not None
1177+
1178+
# Assert key1 and key2 are equal, since they have the exact same start and end times
1179+
assert user_delegation_key_1.signed_oid == user_delegation_key_2.signed_oid
1180+
assert user_delegation_key_1.signed_tid == user_delegation_key_2.signed_tid
1181+
assert user_delegation_key_1.signed_start == user_delegation_key_2.signed_start
1182+
assert user_delegation_key_1.signed_expiry == user_delegation_key_2.signed_expiry
1183+
assert user_delegation_key_1.signed_version == user_delegation_key_2.signed_version
1184+
assert user_delegation_key_1.signed_service == user_delegation_key_2.signed_service
1185+
assert user_delegation_key_1.value == user_delegation_key_2.value
1186+
1187+
return variables
1188+
1189+
@pytest.mark.live_test_only
1190+
@DataLakePreparer()
1191+
def test_datalake_user_delegation_oid(self, **kwargs):
1192+
datalake_storage_account_name = kwargs.pop("datalake_storage_account_name")
1193+
data = b"abc123"
1194+
1195+
token_credential = self.get_credential(DataLakeServiceClient)
1196+
account_url = self.account_url(datalake_storage_account_name, "dfs")
1197+
dsc = DataLakeServiceClient(account_url, credential=token_credential)
1198+
file_system_name = self.get_resource_name(TEST_FILE_SYSTEM_PREFIX)
1199+
file_system = dsc.create_file_system(file_system_name)
1200+
directory_name = "dir"
1201+
directory = file_system.create_directory(directory_name)
1202+
file_name = "file"
1203+
file = directory.create_file(file_name)
1204+
file.upload_data(data, length=len(data), overwrite=True)
1205+
1206+
start = datetime.utcnow()
1207+
expiry = datetime.utcnow() + timedelta(hours=1)
1208+
user_delegation_key = dsc.get_user_delegation_key(key_start_time=start, key_expiry_time=expiry)
1209+
token = token_credential.get_token("https://storage.azure.com/.default")
1210+
user_delegation_oid = jwt.decode(token.token, options={"verify_signature": False}).get("oid")
1211+
1212+
file_system_token = self.generate_sas(
1213+
generate_file_system_sas,
1214+
dsc.account_name,
1215+
file_system_name,
1216+
user_delegation_key,
1217+
permission=FileSystemSasPermissions(write=True, read=True, delete=True, list=True),
1218+
expiry=expiry,
1219+
user_delegation_oid=user_delegation_oid,
1220+
)
1221+
file_system_client = FileSystemClient(
1222+
f"{account_url}?{file_system_token}",
1223+
file_system_name=file_system_name,
1224+
credential=token_credential
1225+
)
1226+
paths = list(file_system_client.get_paths())
1227+
assert len(paths) == 2
1228+
assert paths[0]["name"] == directory.path_name
1229+
assert paths[1]["name"] == file.path_name
1230+
1231+
directory_token = self.generate_sas(
1232+
generate_directory_sas,
1233+
dsc.account_name,
1234+
file_system_name,
1235+
directory_name,
1236+
user_delegation_key,
1237+
permission=FileSasPermissions(write=True, read=True, delete=True),
1238+
expiry=expiry,
1239+
user_delegation_oid=user_delegation_oid
1240+
)
1241+
directory_client = DataLakeDirectoryClient(
1242+
f"{account_url}?{directory_token}",
1243+
file_system_name=file_system_name,
1244+
directory_name=directory_name,
1245+
credential=token_credential
1246+
)
1247+
props = directory_client.get_directory_properties()
1248+
assert props is not None
1249+
1250+
file_token = self.generate_sas(
1251+
generate_file_sas,
1252+
datalake_storage_account_name,
1253+
file_system_name,
1254+
directory_name,
1255+
file_name,
1256+
credential=user_delegation_key,
1257+
permission=FileSasPermissions(write=True, read=True, delete=True),
1258+
expiry=expiry,
1259+
user_delegation_oid=user_delegation_oid
1260+
)
1261+
file_client = DataLakeFileClient(
1262+
f"{account_url}?{file_token}",
1263+
file_system_name=file_system_name,
1264+
file_path=f"{directory_name}/{file_name}",
1265+
credential=token_credential
1266+
)
1267+
content = file_client.download_file().readall()
1268+
assert content == data
1269+
11521270
# ------------------------------------------------------------------------------
11531271
if __name__ == '__main__':
11541272
unittest.main()

sdk/storage/azure-storage-file-datalake/tests/test_file_system_async.py

Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
# license information.
55
# --------------------------------------------------------------------------
66
import asyncio
7+
import jwt
78
import unittest
89
import uuid
910
from datetime import datetime, timedelta
@@ -1279,6 +1280,126 @@ async def test_get_and_set_access_control_oauth(self, **kwargs):
12791280
# Assert
12801281
assert acl == access_control['acl']
12811282

1283+
@DataLakePreparer()
1284+
@recorded_by_proxy_async
1285+
async def test_get_user_delegation_sas(self, **kwargs):
1286+
datalake_storage_account_name = kwargs.pop("datalake_storage_account_name")
1287+
variables = kwargs.pop("variables", {})
1288+
1289+
token_credential = self.get_credential(DataLakeServiceClient, is_async=True)
1290+
service = DataLakeServiceClient(
1291+
self.account_url(datalake_storage_account_name, "dfs"),
1292+
credential=token_credential,
1293+
)
1294+
start = self.get_datetime_variable(variables, 'start_time', datetime.utcnow())
1295+
expiry = self.get_datetime_variable(variables, 'expiry_time', datetime.utcnow() + timedelta(hours=1))
1296+
user_delegation_key_1 = await service.get_user_delegation_key(key_start_time=start, key_expiry_time=expiry)
1297+
user_delegation_key_2 = await service.get_user_delegation_key(key_start_time=start, key_expiry_time=expiry)
1298+
1299+
# Assert key1 is valid
1300+
assert user_delegation_key_1.signed_oid is not None
1301+
assert user_delegation_key_1.signed_tid is not None
1302+
assert user_delegation_key_1.signed_start is not None
1303+
assert user_delegation_key_1.signed_expiry is not None
1304+
assert user_delegation_key_1.signed_version is not None
1305+
assert user_delegation_key_1.signed_service is not None
1306+
assert user_delegation_key_1.value is not None
1307+
1308+
# Assert key1 and key2 are equal, since they have the exact same start and end times
1309+
assert user_delegation_key_1.signed_oid == user_delegation_key_2.signed_oid
1310+
assert user_delegation_key_1.signed_tid == user_delegation_key_2.signed_tid
1311+
assert user_delegation_key_1.signed_start == user_delegation_key_2.signed_start
1312+
assert user_delegation_key_1.signed_expiry == user_delegation_key_2.signed_expiry
1313+
assert user_delegation_key_1.signed_version == user_delegation_key_2.signed_version
1314+
assert user_delegation_key_1.signed_service == user_delegation_key_2.signed_service
1315+
assert user_delegation_key_1.value == user_delegation_key_2.value
1316+
1317+
return variables
1318+
1319+
@pytest.mark.live_test_only
1320+
@DataLakePreparer()
1321+
async def test_datalake_user_delegation_oid(self, **kwargs):
1322+
datalake_storage_account_name = kwargs.pop("datalake_storage_account_name")
1323+
data = b"abc123"
1324+
1325+
token_credential = self.get_credential(DataLakeServiceClient, is_async=True)
1326+
account_url = self.account_url(datalake_storage_account_name, "dfs")
1327+
dsc = DataLakeServiceClient(account_url, credential=token_credential)
1328+
file_system_name = self.get_resource_name(TEST_FILE_SYSTEM_PREFIX)
1329+
file_system = await dsc.create_file_system(file_system_name)
1330+
directory_name = "dir"
1331+
directory = await file_system.create_directory(directory_name)
1332+
file_name = "file"
1333+
file = await directory.create_file(file_name)
1334+
await file.upload_data(data, length=len(data), overwrite=True)
1335+
1336+
start = datetime.utcnow()
1337+
expiry = datetime.utcnow() + timedelta(hours=1)
1338+
user_delegation_key = await dsc.get_user_delegation_key(key_start_time=start, key_expiry_time=expiry)
1339+
token = await token_credential.get_token("https://storage.azure.com/.default")
1340+
user_delegation_oid = jwt.decode(token.token, options={"verify_signature": False}).get("oid")
1341+
1342+
file_system_token = self.generate_sas(
1343+
generate_file_system_sas,
1344+
dsc.account_name,
1345+
file_system_name,
1346+
user_delegation_key,
1347+
permission=FileSystemSasPermissions(write=True, read=True, delete=True, list=True),
1348+
expiry=expiry,
1349+
user_delegation_oid=user_delegation_oid,
1350+
)
1351+
file_system_client = FileSystemClient(
1352+
f"{account_url}?{file_system_token}",
1353+
file_system_name=file_system_name,
1354+
credential=token_credential
1355+
)
1356+
paths = []
1357+
async for path in file_system_client.get_paths() :
1358+
paths.append(path)
1359+
1360+
assert len(paths) == 2
1361+
assert paths[0]["name"] == directory.path_name
1362+
assert paths[1]["name"] == file.path_name
1363+
1364+
directory_token = self.generate_sas(
1365+
generate_directory_sas,
1366+
dsc.account_name,
1367+
file_system_name,
1368+
directory_name,
1369+
user_delegation_key,
1370+
permission=FileSasPermissions(write=True, read=True, delete=True),
1371+
expiry=expiry,
1372+
user_delegation_oid=user_delegation_oid
1373+
)
1374+
directory_client = DataLakeDirectoryClient(
1375+
f"{account_url}?{directory_token}",
1376+
file_system_name=file_system_name,
1377+
directory_name=directory_name,
1378+
credential=token_credential
1379+
)
1380+
props = await directory_client.get_directory_properties()
1381+
assert props is not None
1382+
1383+
file_token = self.generate_sas(
1384+
generate_file_sas,
1385+
datalake_storage_account_name,
1386+
file_system_name,
1387+
directory_name,
1388+
file_name,
1389+
credential=user_delegation_key,
1390+
permission=FileSasPermissions(write=True, read=True, delete=True),
1391+
expiry=expiry,
1392+
user_delegation_oid=user_delegation_oid
1393+
)
1394+
file_client = DataLakeFileClient(
1395+
f"{account_url}?{file_token}",
1396+
file_system_name=file_system_name,
1397+
file_path=f"{directory_name}/{file_name}",
1398+
credential=token_credential
1399+
)
1400+
content = await (await file_client.download_file()).readall()
1401+
assert content == data
1402+
12821403
# ------------------------------------------------------------------------------
12831404
if __name__ == '__main__':
12841405
unittest.main()

0 commit comments

Comments
 (0)