1# --------------------------------------------------------------------------------------------
2# Copyright (c) Microsoft Corporation. All rights reserved.
3# Licensed under the MIT License. See License.txt in the project root for license information.
4# --------------------------------------------------------------------------------------------
5
6from knack.util import CLIError
7from azure.cli.core.azclierror import InvalidArgumentValueError, ArgumentUsageError
8from azure.cli.core.util import is_guid
9from azure.graphrbac.models import GraphErrorException
10from msrestazure.azure_exceptions import CloudError
11from .._client_factory import cf_synapse_role_assignments, cf_synapse_role_definitions, cf_graph_client_factory
12from ..constant import ITEM_NAME_MAPPING
13
14
15# List Synapse Role Assignment
16def list_role_assignments(cmd, workspace_name, role=None, assignee=None, assignee_object_id=None,
17                          scope=None, item=None, item_type=None):
18    if bool(assignee) and bool(assignee_object_id):
19        raise ArgumentUsageError('usage error: --assignee STRING | --assignee-object-id GUID')
20
21    if bool(item) != bool(item_type):
22        raise ArgumentUsageError('usage error: --item-type STRING --item STRING')
23
24    return _list_role_assignments(cmd, workspace_name, role, assignee or assignee_object_id,
25                                  scope, resolve_assignee=(not assignee_object_id), item=item, item_type=item_type)
26
27
28def _list_role_assignments(cmd, workspace_name, role=None, assignee=None, scope=None,
29                           resolve_assignee=True, item=None, item_type=None):
30    """Prepare scope, role ID and resolve object ID from Graph API."""
31    if any([scope, item, item_type]):
32        scope = _build_role_scope(workspace_name, scope, item, item_type)
33    role_id = _resolve_role_id(cmd, role, workspace_name)
34    object_id = _resolve_object_id(cmd, assignee, fallback_to_object_id=True) if resolve_assignee else assignee
35    client = cf_synapse_role_assignments(cmd.cli_ctx, workspace_name)
36    role_assignments = client.list_role_assignments(role_id, object_id, scope).value
37    return role_assignments
38
39
40# Show Synapse Role Assignment By Id
41def get_role_assignment_by_id(cmd, workspace_name, role_assignment_id):
42    client = cf_synapse_role_assignments(cmd.cli_ctx, workspace_name)
43    return client.get_role_assignment_by_id(role_assignment_id)
44
45
46# Delete Synapse Role Assignment
47def delete_role_assignment(cmd, workspace_name, ids=None, assignee=None, assignee_object_id=None, role=None,
48                           scope=None, item=None, item_type=None):
49    client = cf_synapse_role_assignments(cmd.cli_ctx, workspace_name)
50    if not any([ids, assignee, assignee_object_id, role, scope, item, item_type]):
51        raise ArgumentUsageError('usage error: No argument are provided. --assignee STRING | --ids GUID')
52
53    if ids:
54        if any([assignee, assignee_object_id, role, scope, item, item_type]):
55            raise ArgumentUsageError('You should not provide --role or --assignee or --assignee_object_id '
56                                     'or --scope or --principal-type when --ids is provided.')
57        role_assignments = list_role_assignments(cmd, workspace_name, None, None, None, None, None, None)
58        assignment_id_list = [x.id for x in role_assignments]
59        # check role assignment id
60        for assignment_id in ids:
61            if assignment_id not in assignment_id_list:
62                raise ArgumentUsageError("role assignment id:'{}' doesn't exist.".format(assignment_id))
63        # delete when all ids check pass
64        for assignment_id in ids:
65            client.delete_role_assignment_by_id(assignment_id)
66        return
67
68    role_assignments = list_role_assignments(cmd, workspace_name, role, assignee, assignee_object_id,
69                                             scope, item, item_type)
70    if any([scope, item, item_type]):
71        scope = _build_role_scope(workspace_name, scope, item, item_type)
72        role_assignments = [x for x in role_assignments if x.scope == scope]
73
74    if role_assignments:
75        for assignment in role_assignments:
76            client.delete_role_assignment_by_id(assignment.id)
77    else:
78        raise CLIError('No matched assignments were found to delete, please provide correct --role or --assignee.'
79                       'Use `az synapse role assignment list` to get role assignments.')
80
81
82def create_role_assignment(cmd, workspace_name, role, assignee=None, assignee_object_id=None,
83                           scope=None, assignee_principal_type=None, item_type=None, item=None, assignment_id=None):
84    """Check parameters are provided correctly, then call _create_role_assignment."""
85    if assignment_id and not is_guid(assignment_id):
86        raise InvalidArgumentValueError('usage error: --id GUID')
87
88    if bool(assignee) == bool(assignee_object_id):
89        raise ArgumentUsageError('usage error: --assignee STRING | --assignee-object-id GUID')
90
91    if assignee_principal_type and not assignee_object_id:
92        raise ArgumentUsageError('usage error: --assignee-object-id GUID [--assignee-principal-type]')
93
94    if bool(item) != bool(item_type):
95        raise ArgumentUsageError('usage error: --item-type STRING --item STRING')
96
97    try:
98        return _create_role_assignment(cmd, workspace_name, role, assignee or assignee_object_id, scope, item,
99                                       item_type, resolve_assignee=(not assignee_object_id),
100                                       assignee_principal_type=assignee_principal_type, assignment_id=assignment_id)
101    except Exception as ex:  # pylint: disable=broad-except
102        if _error_caused_by_role_assignment_exists(ex):  # for idempotent
103            return list_role_assignments(cmd, workspace_name, role=role,
104                                         assignee=assignee, assignee_object_id=assignee_object_id,
105                                         scope=scope, item=item, item_type=item_type)
106        raise
107
108
109def _resolve_object_id(cmd, assignee, fallback_to_object_id=False):
110    if assignee is None:
111        return None
112    client = cf_graph_client_factory(cmd.cli_ctx)
113    result = None
114    try:
115        result = list(client.users.list(filter="userPrincipalName eq '{0}' or mail eq '{0}' or displayName eq '{0}'"
116                                        .format(assignee)))
117        if not result:
118            result = list(client.service_principals.list(filter="displayName eq '{}'".format(assignee)))
119        if not result:
120            result = list(client.groups.list(filter="mail eq '{}'".format(assignee)))
121        if not result and is_guid(assignee):  # assume an object id, let us verify it
122            result = _get_object_stubs(client, [assignee])
123
124        # 2+ matches should never happen, so we only check 'no match' here
125        if not result:
126            raise CLIError("Cannot find user or group or service principal in graph database for '{assignee}'. "
127                           "If the assignee is a principal id, make sure the corresponding principal is created "
128                           "with 'az ad sp create --id {assignee}'.".format(assignee=assignee))
129
130        if len(result) > 1:
131            raise CLIError("Find more than one user or group or service principal in graph database for '{assignee}'. "
132                           "Please using --assignee-object-id GUID to specify assignee accurately"
133                           .format(assignee=assignee))
134
135        return result[0].object_id
136    except (CloudError, GraphErrorException):
137        if fallback_to_object_id and is_guid(assignee):
138            return assignee
139        raise
140
141
142def _get_object_stubs(graph_client, assignees):
143    from azure.graphrbac.models import GetObjectsParameters
144    result = []
145    assignees = list(assignees)  # callers could pass in a set
146    for i in range(0, len(assignees), 1000):
147        params = GetObjectsParameters(include_directory_object_references=True, object_ids=assignees[i:i + 1000])
148        result += list(graph_client.objects.get_objects_by_object_ids(params))
149    return result
150
151
152def _error_caused_by_role_assignment_exists(ex):
153    return getattr(ex, 'status_code', None) == 409 and 'role assignment already exists' in ex.message
154
155
156def _create_role_assignment(cmd, workspace_name, role, assignee, scope=None, item=None, item_type=None,
157                            resolve_assignee=True, assignee_principal_type=None, assignment_id=None):
158    """Prepare scope, role ID and resolve object ID from Graph API."""
159    scope = _build_role_scope(workspace_name, scope, item, item_type)
160    role_id = _resolve_role_id(cmd, role, workspace_name)
161    object_id = _resolve_object_id(cmd, assignee, fallback_to_object_id=True) if resolve_assignee else assignee
162
163    assignment_client = cf_synapse_role_assignments(cmd.cli_ctx, workspace_name)
164    return assignment_client.create_role_assignment(assignment_id if assignment_id is not None else _gen_guid(),
165                                                    role_id, object_id, scope, assignee_principal_type)
166
167
168def _build_role_scope(workspace_name, scope, item, item_type):
169    if scope:
170        return scope
171
172    if item and item_type:
173        # workspaces/{workspaceName}/bigDataPools/{bigDataPoolName}
174        scope = "workspaces/" + workspace_name + "/" + item_type + "/" + item
175    else:
176        scope = "workspaces/" + workspace_name
177
178    return scope
179
180
181def _resolve_role_id(cmd, role, workspace_name):
182    role_id = None
183    if not role:
184        return role_id
185    if is_guid(role):
186        role_id = role
187    else:
188        role_definition_client = cf_synapse_role_definitions(cmd.cli_ctx, workspace_name)
189        role_definition = role_definition_client.list_role_definitions()
190        role_dict = {x.name.lower(): x.id for x in role_definition if x.name}
191        if role.lower() not in role_dict:
192            raise CLIError("Role '{}' doesn't exist.".format(role))
193        role_id = role_dict[role.lower()]
194    return role_id
195
196
197def _gen_guid():
198    import uuid
199    return uuid.uuid4()
200
201
202# List Synapse Role Definitions Scope
203def list_scopes(cmd, workspace_name):
204    client = cf_synapse_role_definitions(cmd.cli_ctx, workspace_name)
205    return client.list_scopes()
206
207
208# List Synapse Role Definitions
209def list_role_definitions(cmd, workspace_name, is_built_in=None):
210    client = cf_synapse_role_definitions(cmd.cli_ctx, workspace_name)
211    role_definitions = client.list_role_definitions(is_built_in)
212    return role_definitions
213
214
215def _build_role_scope_format(scope, item_type):
216    if scope:
217        return scope
218
219    if item_type:
220        scope = "workspaces/{workspaceName}/" + item_type + "/" + ITEM_NAME_MAPPING[item_type]
221    else:
222        scope = "workspaces/{workspaceName}"
223
224    return scope
225
226
227# Get Synapse Role Definition
228def get_role_definition(cmd, workspace_name, role):
229    role_id = _resolve_role_id(cmd, role, workspace_name)
230    client = cf_synapse_role_definitions(cmd.cli_ctx, workspace_name)
231    return client.get_role_definition_by_id(role_id)
232