Skip to content
Draft
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/quantum/HISTORY.rst
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,10 @@
Release History
===============

1.0.0b21
+++++++++++++++
* Added the ``az quantum workspace user list`` command to list the users with access to an Azure Quantum workspace.

1.0.0b20
+++++++++++++++
* Added the ``az quantum workspace user create`` and ``az quantum workspace user delete`` commands to manage user access to an Azure Quantum workspace.
Expand Down
19 changes: 19 additions & 0 deletions src/quantum/azext_quantum/_help.py
Original file line number Diff line number Diff line change
Expand Up @@ -346,6 +346,25 @@
short-summary: Manage users of an Azure Quantum workspace.
"""

helps['quantum workspace user list'] = """
type: command
short-summary: List the users with access to an Azure Quantum workspace.
long-summary: >-
Lists the user principals (excluding groups and service principals) assigned the 'Quantum
Workspace Data Contributor' role at the scope of the given (or current) workspace. Use
'--role' to filter by a different role.
examples:
- name: List all users with access to a workspace.
text: |-
az quantum workspace user list -g MyResourceGroup -w MyWorkspace
- name: List the users granted a specific role.
text: |-
az quantum workspace user list -g MyResourceGroup -w MyWorkspace --role Reader
- name: Include users whose access is inherited from the resource group or subscription.
text: |-
az quantum workspace user list -g MyResourceGroup -w MyWorkspace --include-inherited
"""

helps['quantum workspace user create'] = """
type: command
short-summary: Grant a user, group, or service principal access to an Azure Quantum workspace.
Expand Down
6 changes: 5 additions & 1 deletion src/quantum/azext_quantum/_params.py
Original file line number Diff line number Diff line change
Expand Up @@ -70,8 +70,9 @@ def load_arguments(self, _): # pylint: disable=too-many-locals
order_type = CLIArgumentType(options_list=['--order'], help='How to order the list: `asc` or `desc`')
assignee_type = CLIArgumentType(options_list=['--assignee'], help='Represents a user, group, or service principal. Supported formats: object id, user sign-in name, or service principal name.')
assignee_object_id_type = CLIArgumentType(options_list=['--assignee-object-id'], help="Use this parameter instead of '--assignee' to bypass Graph API invocation in case of insufficient privileges. This parameter only works with object ids for users, groups, service principals, and managed identities. For managed identities use the principal id. For service principals, use the object id and not the app id.")
role_type = CLIArgumentType(options_list=['--role'], help="Role name or id. For 'create', the role granted to the user; for 'delete', the role assignment to remove. Defaults to the 'Quantum Workspace Data Contributor' role.")
role_type = CLIArgumentType(options_list=['--role'], help="Role name or id. For 'create', the role to grant; for 'delete', the assignment to remove; for 'list', the role to filter by. All default to the 'Quantum Workspace Data Contributor' role.")
assignee_principal_type_type = CLIArgumentType(options_list=['--assignee-principal-type'], arg_type=get_enum_type(['User', 'Group', 'ServicePrincipal', 'ForeignGroup']), help="Use with '--assignee-object-id' to avoid errors caused by propagation latency in Microsoft Graph.")
include_inherited_type = CLIArgumentType(options_list=['--include-inherited'], help='If specified, also list role assignments inherited from the parent resource group and subscription scopes.')

with self.argument_context('quantum workspace') as c:
c.argument('workspace_name', workspace_name_type)
Expand All @@ -92,6 +93,9 @@ def load_arguments(self, _): # pylint: disable=too-many-locals
with self.argument_context('quantum workspace user create') as c:
c.argument('assignee_principal_type', assignee_principal_type_type)

with self.argument_context('quantum workspace user list') as c:
c.argument('include_inherited', include_inherited_type)

with self.argument_context('quantum target') as c:
c.argument('workspace_name', workspace_name_type)
c.argument('target_id', target_id_type)
Expand Down
13 changes: 13 additions & 0 deletions src/quantum/azext_quantum/commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,18 @@ def transform_jobs(results):
return [transform_job(job) for job in results]


def transform_users(results):
def one(result):
return OrderedDict([
('Principal Id', result.get('principalId')),
('Principal Name', result.get('principalName')),
('Principal Type', result.get('principalType')),
('Role', result.get('roleDefinitionName')),
('Scope', result.get('scope'))
])
return [one(result) for result in results]


def transform_offerings(offerings):
def one(offering):
return OrderedDict([
Expand Down Expand Up @@ -139,6 +151,7 @@ def load_command_table(self, _):
with self.command_group('quantum workspace user', workspace_ops) as u:
u.command('create', 'add_user', validator=validate_workspace_info)
u.command('delete', 'remove_user', validator=validate_workspace_info, confirmation=True)
u.command('list', 'list_users', validator=validate_workspace_info, table_transformer=transform_users)

with self.command_group('quantum target', target_ops) as t:
t.command('list', 'list', validator=validate_workspace_info, table_transformer=transform_targets)
Expand Down
14 changes: 14 additions & 0 deletions src/quantum/azext_quantum/operations/workspace.py
Original file line number Diff line number Diff line change
Expand Up @@ -497,3 +497,17 @@ def remove_user(cmd, resource_group_name=None, workspace_name=None, assignee=Non
scope = _get_workspace_resource_id(info)
role = role or QUANTUM_WORKSPACE_DATA_CONTRIBUTOR_ROLE_ID
return delete_role_assignments(cmd, role=role, scope=scope, assignee=assignee, assignee_object_id=assignee_object_id)


def list_users(cmd, resource_group_name=None, workspace_name=None, assignee=None, assignee_object_id=None, role=None, include_inherited=False):
"""
List the users with access to an Azure Quantum workspace.
"""
from azure.cli.command_modules.role.custom import list_role_assignments

info = WorkspaceInfo(cmd, resource_group_name, workspace_name)
scope = _get_workspace_resource_id(info)
role = role or QUANTUM_WORKSPACE_DATA_CONTRIBUTOR_ROLE_ID
assignments = list_role_assignments(cmd, assignee=assignee, assignee_object_id=assignee_object_id, role=role, scope=scope, include_inherited=include_inherited)
# Match the Quantum portal, which lists only user principals (not groups or service principals).
return [assignment for assignment in assignments if assignment.get("principalType") == "User"]
68 changes: 67 additions & 1 deletion src/quantum/azext_quantum/tests/latest/test_quantum_workspace.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@
import pytest
import unittest
import time
from types import SimpleNamespace
from unittest.mock import patch

from azure.cli.testsdk.scenario_tests import AllowLargeResponse, live_only
from azure.cli.testsdk import (ScenarioTest, ResourceGroupPreparer)
Expand All @@ -15,7 +17,7 @@
from ..._version_check_helper import check_version
from datetime import datetime
from ...__init__ import CLI_REPORTED_VERSION
from ...operations.workspace import _validate_storage_account, _autoadd_providers, SUPPORTED_STORAGE_SKU_TIERS, SUPPORTED_STORAGE_KINDS, DEPLOYMENT_NAME_PREFIX
from ...operations.workspace import _validate_storage_account, _autoadd_providers, list_users, QUANTUM_WORKSPACE_DATA_CONTRIBUTOR_ROLE_ID, SUPPORTED_STORAGE_SKU_TIERS, SUPPORTED_STORAGE_KINDS, DEPLOYMENT_NAME_PREFIX

TEST_DIR = os.path.abspath(os.path.join(os.path.abspath(__file__), '..'))

Expand Down Expand Up @@ -312,6 +314,11 @@ def test_workspace_user(self):
self.check("ends_with(roleDefinitionId, 'c1410b24-3e69-4857-8f86-4d0a2e603250')", True)
])

# list users and verify the new assignment appears
self.cmd(f'az quantum workspace user list -g {test_resource_group} --workspace-name {test_workspace_temp} -o json', checks=[
self.check(f"length([?principalId=='{test_object_id}'])", 1)
])

# remove access using the object id and an explicit role
self.cmd(f'az quantum workspace user delete -g {test_resource_group} --workspace-name {test_workspace_temp} --assignee-object-id {test_object_id} --role c1410b24-3e69-4857-8f86-4d0a2e603250 --yes')

Expand All @@ -324,6 +331,65 @@ def test_workspace_user(self):
self.check("properties.provisioningState", "Deleting")
])

def test_list_users_scopes_to_workspace(self):
info = SimpleNamespace(subscription="sub", resource_group="rg", name="ws", endpoint=None)
assignments = [{"principalId": "oid", "principalType": "User", "roleDefinitionName": "Quantum Workspace Data Contributor"}]
with patch("azext_quantum.operations.workspace.WorkspaceInfo", return_value=info), \
patch("azure.cli.command_modules.role.custom.list_role_assignments", return_value=assignments) as list_role_assignments:
cmd = SimpleNamespace(cli_ctx=object())
result = list_users(cmd, "rg", "ws")

expected_scope = "/subscriptions/sub/resourceGroups/rg/providers/Microsoft.Quantum/Workspaces/ws"
list_role_assignments.assert_called_once_with(cmd, assignee=None, assignee_object_id=None, role=QUANTUM_WORKSPACE_DATA_CONTRIBUTOR_ROLE_ID, scope=expected_scope, include_inherited=False)
assert result == assignments

def test_list_users_include_inherited(self):
info = SimpleNamespace(subscription="sub", resource_group="rg", name="ws", endpoint=None)
with patch("azext_quantum.operations.workspace.WorkspaceInfo", return_value=info), \
patch("azure.cli.command_modules.role.custom.list_role_assignments", return_value=[]) as list_role_assignments:
cmd = SimpleNamespace(cli_ctx=object())
list_users(cmd, "rg", "ws", include_inherited=True)

expected_scope = "/subscriptions/sub/resourceGroups/rg/providers/Microsoft.Quantum/Workspaces/ws"
list_role_assignments.assert_called_once_with(cmd, assignee=None, assignee_object_id=None, role=QUANTUM_WORKSPACE_DATA_CONTRIBUTOR_ROLE_ID, scope=expected_scope, include_inherited=True)

def test_list_users_role_override(self):
info = SimpleNamespace(subscription="sub", resource_group="rg", name="ws", endpoint=None)
with patch("azext_quantum.operations.workspace.WorkspaceInfo", return_value=info), \
patch("azure.cli.command_modules.role.custom.list_role_assignments", return_value=[]) as list_role_assignments:
cmd = SimpleNamespace(cli_ctx=object())
list_users(cmd, "rg", "ws", role="Reader")

expected_scope = "/subscriptions/sub/resourceGroups/rg/providers/Microsoft.Quantum/Workspaces/ws"
list_role_assignments.assert_called_once_with(cmd, assignee=None, assignee_object_id=None, role="Reader", scope=expected_scope, include_inherited=False)

def test_list_users_excludes_groups_and_service_principals(self):
info = SimpleNamespace(subscription="sub", resource_group="rg", name="ws", endpoint=None)
assignments = [
{"principalId": "u", "principalType": "User"},
{"principalId": "g", "principalType": "Group"},
{"principalId": "sp", "principalType": "ServicePrincipal"},
]
with patch("azext_quantum.operations.workspace.WorkspaceInfo", return_value=info), \
patch("azure.cli.command_modules.role.custom.list_role_assignments", return_value=assignments):
cmd = SimpleNamespace(cli_ctx=object())
result = list_users(cmd, "rg", "ws")

assert result == [{"principalId": "u", "principalType": "User"}]

def test_transform_users(self):
from ...commands import transform_users
rows = transform_users([{
"principalId": "oid",
"principalName": "user@contoso.com",
"principalType": "User",
"roleDefinitionName": "Quantum Workspace Data Contributor",
"scope": "/subscriptions/sub/resourceGroups/rg/providers/Microsoft.Quantum/Workspaces/ws"
}])
assert rows[0]["Principal Name"] == "user@contoso.com"
assert rows[0]["Principal Type"] == "User"
assert rows[0]["Role"] == "Quantum Workspace Data Contributor"

# @pytest.fixture(autouse=True)
# def _pass_fixtures(self, capsys):
# self.capsys = capsys
Expand Down
2 changes: 1 addition & 1 deletion src/quantum/setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
# This version should match the latest entry in HISTORY.rst
# Also, when updating this, please review the version used by the extension to
# submit requests, which can be found at './azext_quantum/__init__.py'
VERSION = '1.0.0b20'
VERSION = '1.0.0b21'

# The full list of classifiers is available at
# https://pypi.python.org/pypi?%3Aaction=list_classifiers
Expand Down
Loading