1# -*- coding: utf-8 -*- #
2# Copyright 2020 Google LLC. All Rights Reserved.
3#
4# Licensed under the Apache License, Version 2.0 (the "License");
5# you may not use this file except in compliance with the License.
6# You may obtain a copy of the License at
7#
8#    http://www.apache.org/licenses/LICENSE-2.0
9#
10# Unless required by applicable law or agreed to in writing, software
11# distributed under the License is distributed on an "AS IS" BASIS,
12# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13# See the License for the specific language governing permissions and
14# limitations under the License.
15"""Declarative hooks for Cloud Identity Groups CLI."""
16from __future__ import absolute_import
17from __future__ import division
18from __future__ import unicode_literals
19
20import collections
21
22from apitools.base.py import encoding
23from apitools.base.py import exceptions as apitools_exceptions
24
25from googlecloudsdk.api_lib.identity import cloudidentity_client as ci_client
26from googlecloudsdk.calliope import base
27from googlecloudsdk.calliope import exceptions
28from googlecloudsdk.command_lib.organizations import org_utils
29import six
30
31
32# request hooks
33def SetParent(unused_ref, args, request):
34  """Set obfuscated customer id to request.group.parent or request.parent.
35
36  Args:
37    unused_ref: A string representing the operation reference. Unused and may be
38      None.
39    args: The argparse namespace.
40    request: The request to modify.
41
42  Returns:
43    The updated request.
44  """
45
46  version = GetApiVersion(args)
47  messages = ci_client.GetMessages(version)
48
49  group = getattr(request, 'group', None)
50  if group is None:
51    request.group = messages.Group()
52
53  request.group.parent = GetCustomerId(args)
54
55  return request
56
57
58def SetEntityKey(unused_ref, args, request):
59  """Set EntityKey to request.group.groupKey.
60
61  Args:
62    unused_ref: unused.
63    args: The argparse namespace.
64    request: The request to modify.
65
66  Returns:
67    The updated request.
68  """
69
70  if hasattr(args, 'email'):
71    version = GetApiVersion(args)
72    messages = ci_client.GetMessages(version)
73    request.group.groupKey = messages.EntityKey(id=args.email)
74
75  return request
76
77
78def SetLabels(unused_ref, args, request):
79  """Set Labels to request.group.labels.
80
81  Args:
82    unused_ref: unused.
83    args: The argparse namespace.
84    request: The request to modify.
85
86  Returns:
87    The updated request.
88  """
89
90  if args.IsSpecified('labels'):
91    if hasattr(request.group, 'labels'):
92      request.group.labels = ReformatLabels(args, args.labels)
93    else:
94      version = GetApiVersion(args)
95      messages = ci_client.GetMessages(version)
96      request.group = messages.Group(labels=ReformatLabels(args, args.labels))
97
98  return request
99
100
101def SetResourceName(unused_ref, args, request):
102  """Set resource name to request.name.
103
104  Args:
105    unused_ref: unused.
106    args: The argparse namespace.
107    request: The request to modify.
108
109  Returns:
110    The updated request.
111  """
112
113  if args.IsSpecified('email'):
114    version = GetApiVersion(args)
115    request.name = ConvertEmailToResourceName(version, args.email, '--email')
116
117  return request
118
119
120def SetPageSize(unused_ref, args, request):
121  """Set page size to request.pageSize.
122
123  Args:
124    unused_ref: unused.
125    args: The argparse namespace.
126    request: The request to modify.
127
128  Returns:
129    The updated request.
130  """
131
132  if args.IsSpecified('page_size'):
133    request.pageSize = int(args.page_size)
134
135  return request
136
137
138def SetGroupUpdateMask(unused_ref, args, request):
139  """Set the update mask on the request based on the args.
140
141  Args:
142    unused_ref: unused.
143    args: The argparse namespace.
144    request: The request to modify.
145
146  Returns:
147    The updated request.
148  Raises:
149    InvalidArgumentException: If no fields are specified to update.
150  """
151  update_mask = []
152
153  if (args.IsSpecified('display_name') or
154      args.IsSpecified('clear_display_name')):
155    update_mask.append('display_name')
156
157  if (args.IsSpecified('description') or args.IsSpecified('clear_description')):
158    update_mask.append('description')
159
160  if hasattr(args, 'labels'):
161    if args.IsSpecified('labels'):
162      update_mask.append('labels')
163
164  if hasattr(args, 'add_posix_group'):
165    if (args.IsSpecified('add_posix_group') or
166        args.IsSpecified('remove_posix_groups') or
167        args.IsSpecified('clear_posix_groups')):
168      update_mask.append('posix_groups')
169
170  if args.IsSpecified('dynamic_user_query'):
171    update_mask.append('dynamic_group_metadata')
172
173  if not update_mask:
174    raise exceptions.InvalidArgumentException(
175        'Must specify at least one field mask.')
176
177  request.updateMask = ','.join(update_mask)
178
179  return request
180
181
182def GenerateQuery(unused_ref, args, request):
183  """Generate and set the query on the request based on the args.
184
185  Args:
186    unused_ref: unused.
187    args: The argparse namespace.
188    request: The request to modify.
189
190  Returns:
191    The updated request.
192  """
193  customer_id = GetCustomerId(args)
194  labels = FilterLabels(args.labels)
195  labels_str = ','.join(labels)
196  request.query = 'parent==\"{0}\" && \"{1}\" in labels'.format(
197      customer_id, labels_str)
198
199  return request
200
201
202def UpdateDisplayName(unused_ref, args, request):
203  """Update displayName.
204
205  Args:
206    unused_ref: unused.
207    args: The argparse namespace.
208    request: The request to modify.
209
210  Returns:
211    The updated request.
212  """
213
214  if args.IsSpecified('clear_display_name'):
215    request.group.displayName = ''
216  elif args.IsSpecified('display_name'):
217    request.group.displayName = args.display_name
218
219  return request
220
221
222def UpdateDescription(unused_ref, args, request):
223  """Update description.
224
225  Args:
226    unused_ref: unused.
227    args: The argparse namespace.
228    request: The request to modify.
229
230  Returns:
231    The updated request.
232  """
233
234  if args.IsSpecified('clear_description'):
235    request.group.description = ''
236  elif args.IsSpecified('description'):
237    request.group.description = args.description
238
239  return request
240
241
242def UpdatePosixGroups(unused_ref, args, request):
243  """Update posix groups.
244
245  When adding posix groups, the posix groups in the request will be combined
246  with the current posix groups. When removing groups, the current list of
247  posix groups is retrieved and if any value in args.remove_posix_groups
248  matches either a name or gid in a current posix group, it will be removed
249  from the list and the remaining posix groups will be added to the update
250  request.
251
252  Args:
253    unused_ref: unused.
254    args: The argparse namespace.
255    request: The request to modify.
256
257  Returns:
258    The updated request.
259  """
260  version = GetApiVersion(args)
261  group = ci_client.GetGroup(version, request.name)
262  if args.IsSpecified('add_posix_group'):
263    request.group.posixGroups = request.group.posixGroups + group.posixGroups
264  elif args.IsSpecified('remove_posix_groups'):
265    if request.group is None:
266      request.group = group
267    for pg in list(group.posixGroups):
268      if (six.text_type(pg.gid) in args.remove_posix_groups or
269          pg.name in args.remove_posix_groups):
270        group.posixGroups.remove(pg)
271    request.group.posixGroups = group.posixGroups
272
273  return request
274
275
276# processor hooks
277def SetDynamicUserQuery(unused_ref, args, request):
278  """Add DynamicGroupUserQuery to DynamicGroupQueries object list.
279
280  Args:
281    unused_ref: unused.
282    args: The argparse namespace.
283    request: The request to modify.
284
285  Returns:
286    The updated dynamic group queries.
287  """
288
289  queries = []
290
291  if args.IsSpecified('dynamic_user_query'):
292    dg_user_query = args.dynamic_user_query
293    version = GetApiVersion(args)
294    messages = ci_client.GetMessages(version)
295    resource_type = messages.DynamicGroupQuery.ResourceTypeValueValuesEnum
296    new_dynamic_group_query = messages.DynamicGroupQuery(
297        resourceType=resource_type.USER, query=dg_user_query)
298    queries.append(new_dynamic_group_query)
299    dynamic_group_metadata = messages.DynamicGroupMetadata(queries=queries)
300
301    if hasattr(request.group, 'dynamicGroupMetadata'):
302      request.group.dynamicGroupMetadata = dynamic_group_metadata
303    else:
304      request.group = messages.Group(
305          dynamicGroupMetadata=dynamic_group_metadata)
306
307  return request
308
309
310def ReformatLabels(args, labels):
311  """Reformat label list to encoded labels message.
312
313  Reformatting labels will be done within following two steps,
314  1. Filter label strings in a label list.
315  2. Convert the filtered label list to OrderedDict.
316  3. Encode the OrderedDict format of labels to group.labels message.
317
318  Args:
319    args: The argparse namespace.
320    labels: list of label strings. e.g.
321      ["cloudidentity.googleapis.com/security=",
322      "cloudidentity.googleapis.com/groups.discussion_forum"]
323
324  Returns:
325    Encoded labels message.
326
327  Raises:
328    InvalidArgumentException: If invalid labels string is input.
329  """
330
331  # Filter label strings in a label list.
332  filtered_labels = FilterLabels(labels)
333
334  # Convert the filtered label list to OrderedDict.
335  labels_dict = collections.OrderedDict()
336  for label in filtered_labels:
337    if '=' in label:
338      split_label = label.split('=')
339      labels_dict[split_label[0]] = split_label[1]
340    else:
341      labels_dict[label] = ''
342
343  # Encode the OrderedDict format of labels to group.labels message.
344  version = GetApiVersion(args)
345  messages = ci_client.GetMessages(version)
346  return encoding.DictToMessage(labels_dict, messages.Group.LabelsValue)
347
348
349# private methods
350def ConvertOrgArgToObfuscatedCustomerId(org_arg):
351  """Convert organization argument to obfuscated customer id.
352
353  Args:
354    org_arg: organization argument
355
356  Returns:
357    Obfuscated customer id
358
359  Example:
360    org_id: 12345
361    organization_obj:
362    {
363      owner: {
364        directoryCustomerId: A08w1n5gg
365      }
366    }
367  """
368  organization_obj = org_utils.GetOrganization(org_arg)
369  if organization_obj:
370    return organization_obj.owner.directoryCustomerId
371  else:
372    raise org_utils.UnknownOrganizationError(org_arg, metavar='ORGANIZATION')
373
374
375def ConvertEmailToResourceName(version, email, arg_name):
376  """Convert email to resource name.
377
378  Args:
379    version: Release track information
380    email: group email
381    arg_name: argument/parameter name
382
383  Returns:
384    Group Id (e.g. groups/11zu0gzc3tkdgn2)
385
386  """
387  try:
388    return ci_client.LookupGroupName(version, email).name
389  except (apitools_exceptions.HttpForbiddenError,
390          apitools_exceptions.HttpNotFoundError):
391    # If there is no group exists (or deleted) for the given group email,
392    # print out an error message.
393    error_msg = ('There is no such a group associated with the specified '
394                 'argument:' + email)
395    raise exceptions.InvalidArgumentException(arg_name, error_msg)
396
397
398def FilterLabels(labels):
399  """Filter label strings in label list.
400
401  Filter labels (list of strings) with the following conditions,
402  1. If 'label' has 'key' and 'value' OR 'key' only, then add the label to
403  filtered label list. (e.g. 'label_key=label_value', 'label_key')
404  2. If 'label' has an equal sign but no 'value', then add the 'key' to filtered
405  label list. (e.g. 'label_key=' ==> 'label_key')
406  3. If 'label' has invalid format of string, throw an InvalidArgumentException.
407  (e.g. 'label_key=value1=value2')
408
409  Args:
410    labels: list of label strings.
411
412  Returns:
413    Filtered label list.
414
415  Raises:
416    InvalidArgumentException: If invalid labels string is input.
417  """
418
419  if not labels:
420    raise exceptions.InvalidArgumentException(
421        'labels', 'labels can not be an empty string')
422
423  # Convert a comma separated string to a list of strings.
424  label_list = labels.split(',')
425
426  filtered_labels = []
427  for label in label_list:
428    if '=' in label:
429      split_label = label.split('=')
430
431      # Catch invalid format like 'key=value1=value2'
432      if len(split_label) > 2:
433        raise exceptions.InvalidArgumentException(
434            'labels',
435            'Invalid format of label string has been input. Label: ' + label)
436
437      if split_label[1]:
438        filtered_labels.append(label)  # Valid format #1: 'key=value'
439      else:
440        filtered_labels.append(split_label[0])  # Valid format #2: 'key'
441
442    else:
443      filtered_labels.append(label)
444
445  return filtered_labels
446
447
448def GetApiVersion(args):
449  """Return release track information.
450
451  Args:
452    args: The argparse namespace.
453
454  Returns:
455    Release track.
456
457  Raises:
458    UnsupportedReleaseTrackError: If invalid release track is input.
459  """
460
461  release_track = args.calliope_command.ReleaseTrack()
462
463  if release_track == base.ReleaseTrack.ALPHA:
464    return 'v1alpha1'
465  elif release_track == base.ReleaseTrack.BETA:
466    return 'v1beta1'
467  elif release_track == base.ReleaseTrack.GA:
468    return 'v1'
469  else:
470    raise UnsupportedReleaseTrackError(release_track)
471
472
473def GetCustomerId(args):
474  """Return customer_id.
475
476  Args:
477    args: The argparse namespace.
478
479  Returns:
480    customer_id.
481
482  """
483
484  if hasattr(args, 'customer') and args.IsSpecified('customer'):
485    customer_id = args.customer
486  elif hasattr(args, 'organization') and args.IsSpecified('organization'):
487    customer_id = ConvertOrgArgToObfuscatedCustomerId(args.organization)
488  return 'customerId/' + customer_id
489
490
491class UnsupportedReleaseTrackError(Exception):
492  """Raised when requesting an api for an unsupported release track."""
493