Skip to content

Commit 382d47f

Browse files
authored
[Storage] az storage blob/container/fs/share/file/queue generate-sas: Support user delegated sas with OAuth (#32508)
1 parent ac2828a commit 382d47f

File tree

62 files changed

+5601
-1406
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

62 files changed

+5601
-1406
lines changed

src/azure-cli-core/azure/cli/core/profiles/_shared.py

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -115,10 +115,10 @@ class ResourceType(Enum): # pylint: disable=too-few-public-methods
115115
MGMT_SQLVM = ('azure.mgmt.sqlvirtualmachine', None)
116116
MGMT_MANAGEDSERVICES = ('azure.mgmt.managedservices', None)
117117
MGMT_NETAPPFILES = ('azure.mgmt.netappfiles', None)
118-
DATA_STORAGE_BLOB = ('azure.multiapi.storagev2.blob', None)
119-
DATA_STORAGE_FILEDATALAKE = ('azure.multiapi.storagev2.filedatalake', None)
120-
DATA_STORAGE_FILESHARE = ('azure.multiapi.storagev2.fileshare', None)
121-
DATA_STORAGE_QUEUE = ('azure.multiapi.storagev2.queue', None)
118+
DATA_STORAGE_BLOB = ('azure.storage.blob', None)
119+
DATA_STORAGE_FILEDATALAKE = ('azure.storage.filedatalake', None)
120+
DATA_STORAGE_FILESHARE = ('azure.storage.fileshare', None)
121+
DATA_STORAGE_QUEUE = ('azure.storage.queue', None)
122122
DATA_STORAGE_TABLE = ('azure.data.tables', None)
123123
DATA_BATCH = ('azure.batch', None)
124124

@@ -204,10 +204,10 @@ def default_api_version(self):
204204
ResourceType.DATA_KEYVAULT_ADMINISTRATION_SETTING: None,
205205
ResourceType.DATA_KEYVAULT_ADMINISTRATION_BACKUP: '7.5-preview.1',
206206
ResourceType.DATA_KEYVAULT_ADMINISTRATION_ACCESS_CONTROL: '7.4',
207-
ResourceType.DATA_STORAGE_BLOB: '2022-11-02',
208-
ResourceType.DATA_STORAGE_FILEDATALAKE: '2021-08-06',
209-
ResourceType.DATA_STORAGE_FILESHARE: '2025-07-05',
210-
ResourceType.DATA_STORAGE_QUEUE: '2018-03-28',
207+
ResourceType.DATA_STORAGE_BLOB: None,
208+
ResourceType.DATA_STORAGE_FILEDATALAKE: None,
209+
ResourceType.DATA_STORAGE_FILESHARE: None,
210+
ResourceType.DATA_STORAGE_QUEUE: None,
211211
ResourceType.DATA_STORAGE_TABLE: None,
212212
ResourceType.MGMT_SERVICEBUS: None,
213213
ResourceType.MGMT_EVENTHUB: None,

src/azure-cli/azure/cli/command_modules/storage/_client_factory.py

Lines changed: 23 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -141,7 +141,12 @@ def cf_blob_service(cli_ctx, kwargs):
141141
'to get a valid connection string')
142142
if not account_url:
143143
account_url = get_account_url(cli_ctx, account_name=account_name, service='blob')
144+
144145
credential = account_key or sas_token or token_credential
146+
if sas_token and 'sduoid=' in sas_token and token_credential:
147+
credential = token_credential
148+
account_url = account_url + '?' + sas_token
149+
145150
if account_name and account_key:
146151
# For non-standard account URL such as Edge Zone, account_name can't be parsed from account_url. Use credential
147152
# dict instead.
@@ -151,26 +156,29 @@ def cf_blob_service(cli_ctx, kwargs):
151156
connection_timeout=kwargs.pop('connection_timeout', None), **client_kwargs)
152157

153158

154-
def get_credential(kwargs):
159+
def get_credential(client_url, kwargs):
155160
account_key = kwargs.pop('account_key', None)
156161
token_credential = kwargs.pop('token_credential', None)
157162
sas_token = kwargs.pop('sas_token', None)
158163
credential = account_key or sas_token or token_credential
159-
return credential
164+
if sas_token and 'sduoid=' in sas_token and token_credential:
165+
credential = token_credential
166+
client_url = client_url + '?' + sas_token
167+
return client_url, credential
160168

161169

162170
def cf_blob_client(cli_ctx, kwargs):
163171
# track2 partial migration
164172
if kwargs.get('blob_url'):
165173
t_blob_client = get_sdk(cli_ctx, ResourceType.DATA_STORAGE_BLOB, '_blob_client#BlobClient')
166-
credential = get_credential(kwargs)
174+
blob_url, credential = get_credential(kwargs.pop('blob_url'), kwargs)
167175
# del unused kwargs
168176
kwargs.pop('connection_string')
169177
kwargs.pop('account_name')
170178
kwargs.pop('account_url')
171179
kwargs.pop('container_name')
172180
kwargs.pop('blob_name')
173-
return t_blob_client.from_blob_url(blob_url=kwargs.pop('blob_url'),
181+
return t_blob_client.from_blob_url(blob_url=blob_url,
174182
credential=credential,
175183
snapshot=kwargs.pop('snapshot', None),
176184
connection_timeout=kwargs.pop('connection_timeout', None))
@@ -220,6 +228,9 @@ def cf_adls_service(cli_ctx, kwargs):
220228
if not account_url:
221229
account_url = get_account_url(cli_ctx, account_name=account_name, service='dfs')
222230
credential = account_key or sas_token or token_credential
231+
if sas_token and 'sduoid=' in sas_token and token_credential:
232+
credential = token_credential
233+
account_url = account_url + '?' + sas_token
223234

224235
return t_adls_service(account_url=account_url, credential=credential, **client_kwargs)
225236

@@ -257,6 +268,9 @@ def cf_queue_service(cli_ctx, kwargs):
257268
if not account_url:
258269
account_url = get_account_url(cli_ctx, account_name=account_name, service='queue')
259270
credential = account_key or sas_token or token_credential
271+
if sas_token and 'sduoid=' in sas_token and token_credential:
272+
credential = token_credential
273+
account_url = account_url + '?' + sas_token
260274

261275
return t_queue_service(account_url=account_url, credential=credential, **client_kwargs)
262276

@@ -329,6 +343,9 @@ def cf_share_service(cli_ctx, kwargs):
329343
if not account_url:
330344
account_url = get_account_url(cli_ctx, account_name=account_name, service='file')
331345
credential = account_key or sas_token or token_credential
346+
if sas_token and 'sduoid=' in sas_token and token_credential:
347+
credential = token_credential
348+
account_url = account_url + '?' + sas_token
332349

333350
return t_share_service(account_url=account_url, credential=credential, **client_kwargs)
334351

@@ -351,7 +368,7 @@ def cf_share_file_client(cli_ctx, kwargs):
351368
token_intent = 'backup' if enable_file_backup_request_intent else None
352369
if token_credential is not None and not enable_file_backup_request_intent:
353370
raise RequiredArgumentMissingError("--enable-file-backup-request-intent is required for file share OAuth")
354-
credential = get_credential(kwargs)
371+
file_url, credential = get_credential(kwargs.pop('file_url'), kwargs)
355372
# del unused kwargs
356373
kwargs.pop('connection_string')
357374
kwargs.pop('account_name')
@@ -372,7 +389,7 @@ def cf_share_file_client(cli_ctx, kwargs):
372389
if kwargs.get("source_share") or (copy_url and re.match(r"https?:\/\/.*?\.file\..*", copy_url)):
373390
client_kwargs["allow_source_trailing_dot"] = not disallow_source_trailing_dot
374391

375-
return t_file_client.from_file_url(file_url=kwargs.pop('file_url'),
392+
return t_file_client.from_file_url(file_url=file_url,
376393
credential=credential, token_intent=token_intent, **client_kwargs)
377394
if 'file_url' in kwargs:
378395
kwargs.pop('file_url')

src/azure-cli/azure/cli/command_modules/storage/_help.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2681,6 +2681,16 @@
26812681
az storage fs file upload --source a.txt -p dir/a.txt -f fsname --account-name myadlsaccount --account-key 0000-0000
26822682
"""
26832683

2684+
helps['storage fs file generate-sas'] = """
2685+
type: command
2686+
short-summary: Generate a SAS token for file in ADLS Gen2 account.
2687+
examples:
2688+
- name: Generate a sas token for file.
2689+
text: |
2690+
end=`date -u -d "30 minutes" '+%Y-%m-%dT%H:%MZ'`
2691+
az storage fs file generate-sas -p dir/a.txt --file-system myfilesystem --https-only --permissions dlrw --expiry $end -o tsv
2692+
"""
2693+
26842694
helps['storage fs metadata'] = """
26852695
type: group
26862696
short-summary: Manage the metadata for file system.

src/azure-cli/azure/cli/command_modules/storage/_params.py

Lines changed: 74 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@
2424
get_api_version_type, blob_download_file_path_validator, blob_tier_validator, validate_subnet,
2525
validate_immutability_arguments, validate_blob_name_for_upload, validate_share_close_handle,
2626
blob_tier_validator_track2, services_type_v2, resource_type_type_v2, PermissionScopeAddAction,
27-
SshPublicKeyAddAction)
27+
SshPublicKeyAddAction, user_delegation_oid_validator)
2828

2929

3030
def load_arguments(self, _): # pylint: disable=too-many-locals, too-many-statements, too-many-lines, too-many-branches, line-too-long
@@ -961,6 +961,10 @@ def load_arguments(self, _): # pylint: disable=too-many-locals, too-many-statem
961961
'specifies the blob snapshot to grant permission.')
962962
c.extra('encryption_scope', help='A predefined encryption scope used to encrypt the data on the service.')
963963
c.ignore('sas_token')
964+
c.argument('user_delegation_oid', validator=user_delegation_oid_validator, is_preview=True,
965+
help='Specifies the Entra ID of the user that is authorized to use the resulting SAS URL. '
966+
'The resulting SAS URL must be used in conjunction with an Entra ID token that has been issued '
967+
'to the user specified in this value.')
964968

965969
with self.argument_context('storage blob restore', resource_type=ResourceType.MGMT_STORAGE) as c:
966970
from ._validators import BlobRangeAddAction
@@ -1680,6 +1684,10 @@ def load_arguments(self, _): # pylint: disable=too-many-locals, too-many-statem
16801684
"The expiry parameter and '--auth-mode login' are required if this argument is specified. ")
16811685
c.extra('encryption_scope', help='A predefined encryption scope used to encrypt the data on the service.')
16821686
c.ignore('sas_token')
1687+
c.argument('user_delegation_oid', validator=user_delegation_oid_validator, is_preview=True,
1688+
help='Specifies the Entra ID of the user that is authorized to use the resulting SAS URL. '
1689+
'The resulting SAS URL must be used in conjunction with an Entra ID token that has been issued '
1690+
'to the user specified in this value.')
16831691

16841692
for cmd in ['acquire', 'renew', 'break', 'change', 'release']:
16851693
with self.argument_context(f'storage container lease {cmd}') as c:
@@ -1941,6 +1949,13 @@ def load_arguments(self, _): # pylint: disable=too-many-locals, too-many-statem
19411949
help=sas_help.format(get_permission_help_string(t_share_permissions)),
19421950
validator=get_permission_validator(t_share_permissions))
19431951
c.ignore('sas_token')
1952+
c.argument('as_user', action='store_true', validator=as_user_validator, is_preview=True,
1953+
help="Indicates that this command return the SAS signed with the user delegation key. "
1954+
"The expiry parameter and '--auth-mode login' are required if this argument is specified. ")
1955+
c.argument('user_delegation_oid', validator=user_delegation_oid_validator, is_preview=True,
1956+
help='Specifies the Entra ID of the user that is authorized to use the resulting SAS URL. '
1957+
'The resulting SAS URL must be used in conjunction with an Entra ID token that has been issued '
1958+
'to the user specified in this value.')
19441959

19451960
with self.argument_context('storage share update') as c:
19461961
c.extra('share_name', share_name_type, options_list=('--name', '-n'), required=True)
@@ -2149,6 +2164,13 @@ def load_arguments(self, _): # pylint: disable=too-many-locals, too-many-statem
21492164
c.extra('content_type', help='Response header value for Content-Type when resource is accessed '
21502165
'using this shared access signature.')
21512166
c.ignore('sas_token')
2167+
c.extra('as_user', action='store_true', validator=as_user_validator, is_preview=True,
2168+
help="Indicates that this command return the SAS signed with the user delegation key. "
2169+
"The expiry parameter and '--auth-mode login' are required if this argument is specified. ")
2170+
c.extra('user_delegation_oid', validator=user_delegation_oid_validator, is_preview=True,
2171+
help='Specifies the Entra ID of the user that is authorized to use the resulting SAS URL. '
2172+
'The resulting SAS URL must be used in conjunction with an Entra ID token that has been issued '
2173+
'to the user specified in this value.')
21522174

21532175
with self.argument_context('storage file list') as c:
21542176
c.extra('share_name', share_name_type, required=True)
@@ -2295,6 +2317,13 @@ def load_arguments(self, _): # pylint: disable=too-many-locals, too-many-statem
22952317
help=sas_help.format(get_permission_help_string(t_queue_permissions)),
22962318
validator=get_permission_validator(t_queue_permissions))
22972319
c.ignore('sas_token')
2320+
c.argument('as_user', action='store_true', validator=as_user_validator, is_preview=True,
2321+
help="Indicates that this command return the SAS signed with the user delegation key. "
2322+
"The expiry parameter and '--auth-mode login' are required if this argument is specified. ")
2323+
c.argument('user_delegation_oid', validator=user_delegation_oid_validator, is_preview=True,
2324+
help='Specifies the Entra ID of the user that is authorized to use the resulting SAS URL. '
2325+
'The resulting SAS URL must be used in conjunction with an Entra ID token that has been issued '
2326+
'to the user specified in this value.')
22982327

22992328
with self.argument_context('storage queue list') as c:
23002329
c.argument('include_metadata', help='Specify that queue metadata be returned in the response.')
@@ -2540,6 +2569,10 @@ def load_arguments(self, _): # pylint: disable=too-many-locals, too-many-statem
25402569
help='Indicate that this command return the full blob URI and the shared access signature token.')
25412570
c.argument('encryption_scope', help='Specify the encryption scope for a request made so that all '
25422571
'write operations will be service encrypted.')
2572+
c.argument('user_delegation_oid', validator=user_delegation_oid_validator, is_preview=True,
2573+
help='Specifies the Entra ID of the user that is authorized to use the resulting SAS URL. '
2574+
'The resulting SAS URL must be used in conjunction with an Entra ID token that has been issued '
2575+
'to the user specified in this value.')
25432576

25442577
with self.argument_context('storage fs list') as c:
25452578
c.argument('include_metadata', arg_type=get_three_state_flag(),
@@ -2666,6 +2699,46 @@ def load_arguments(self, _): # pylint: disable=too-many-locals, too-many-statem
26662699
help='Indicate that this command return the full blob URI and the shared access signature token.')
26672700
c.argument('encryption_scope', help='Specify the encryption scope for a request made so that all '
26682701
'write operations will be service encrypted.')
2702+
c.argument('user_delegation_oid', validator=user_delegation_oid_validator, is_preview=True,
2703+
help='Specifies the Entra ID of the user that is authorized to use the resulting SAS URL. '
2704+
'The resulting SAS URL must be used in conjunction with an Entra ID token that has been issued '
2705+
'to the user specified in this value.')
2706+
2707+
with self.argument_context('storage fs file generate-sas') as c:
2708+
t_file_system_permissions = self.get_sdk('_models#FileSystemSasPermissions',
2709+
resource_type=ResourceType.DATA_STORAGE_FILEDATALAKE)
2710+
c.register_sas_arguments()
2711+
c.argument('file_system_name', options_list=['-f', '--file-system'],
2712+
help='File system name (i.e. container name).', required=True)
2713+
c.argument('path', options_list=['-p', '--path'], help="The file path in a file system.", required=True)
2714+
c.argument('id', options_list='--policy-name',
2715+
help='The name of a stored access policy.')
2716+
c.argument('permission', options_list='--permissions',
2717+
help=sas_help.format(get_permission_help_string(t_file_system_permissions)),
2718+
validator=get_permission_validator(t_file_system_permissions))
2719+
c.argument('cache_control', help='Response header value for Cache-Control when resource is accessed'
2720+
'using this shared access signature.')
2721+
c.argument('content_disposition', help='Response header value for Content-Disposition when resource is accessed'
2722+
'using this shared access signature.')
2723+
c.argument('content_encoding', help='Response header value for Content-Encoding when resource is accessed'
2724+
'using this shared access signature.')
2725+
c.argument('content_language', help='Response header value for Content-Language when resource is accessed'
2726+
'using this shared access signature.')
2727+
c.argument('content_type', help='Response header value for Content-Type when resource is accessed'
2728+
'using this shared access signature.')
2729+
c.argument('as_user', action='store_true',
2730+
validator=as_user_validator,
2731+
help="Indicates that this command return the SAS signed with the user delegation key. "
2732+
"The expiry parameter and '--auth-mode login' are required if this argument is specified. ")
2733+
c.ignore('sas_token')
2734+
c.argument('full_uri', action='store_true',
2735+
help='Indicate that this command return the full blob URI and the shared access signature token.')
2736+
c.argument('encryption_scope', help='Specify the encryption scope for a request made so that all '
2737+
'write operations will be service encrypted.')
2738+
c.argument('user_delegation_oid', validator=user_delegation_oid_validator, is_preview=True,
2739+
help='Specifies the Entra ID of the user that is authorized to use the resulting SAS URL. '
2740+
'The resulting SAS URL must be used in conjunction with an Entra ID token that has been issued '
2741+
'to the user specified in this value.')
26692742

26702743
with self.argument_context('storage fs file list') as c:
26712744
c.extra('file_system_name', options_list=['-f', '--file-system'],

src/azure-cli/azure/cli/command_modules/storage/_validators.py

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -142,8 +142,10 @@ def validate_client_parameters(cmd, namespace):
142142
account_key_args = [arg for arg in account_key_args if arg]
143143

144144
if account_key_args:
145-
logger.warning('In "login" auth mode, the following arguments are ignored: %s',
146-
' ,'.join(account_key_args))
145+
# user delegation oid with oauth
146+
if not (n.sas_token and 'sduoid=' in n.sas_token):
147+
logger.warning('In "login" auth mode, the following arguments are ignored: %s',
148+
' ,'.join(account_key_args))
147149
return
148150

149151
# When there is no input for credential, we will read environment variable
@@ -1557,6 +1559,13 @@ def as_user_validator(namespace):
15571559
None, "incorrect usage: specify '--auth-mode login' when as-user is enabled")
15581560

15591561

1562+
def user_delegation_oid_validator(namespace):
1563+
if namespace.user_delegation_oid and not namespace.as_user:
1564+
raise argparse.ArgumentError(
1565+
None, "incorrect usage: need to specify '--as-user' when '--user-delegation-oid' is "
1566+
"provided")
1567+
1568+
15601569
def validator_change_feed_retention_days(namespace):
15611570
enable = namespace.enable_change_feed
15621571
days = namespace.change_feed_retention_days

0 commit comments

Comments
 (0)