1# -*- coding: utf-8 -*- #
2# Copyright 2019 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
16"""Declarative hooks for Cloud Identity Groups Memberships CLI."""
17from __future__ import absolute_import
18from __future__ import division
19from __future__ import unicode_literals
20
21from apitools.base.py import exceptions as apitools_exceptions
22
23from googlecloudsdk.api_lib.identity import cloudidentity_client as ci_client
24from googlecloudsdk.calliope import exceptions
25from googlecloudsdk.command_lib.identity.groups import hooks as groups_hooks
26from googlecloudsdk.core.util import times
27
28
29# request hooks
30def SetMembership(unused_ref, args, request):
31  """Set Membership in request.
32
33  Args:
34    unused_ref: unused.
35    args: The argparse namespace.
36    request: The request to modify.
37
38  Returns:
39    The updated request.
40
41  """
42
43  version = groups_hooks.GetApiVersion(args)
44  messages = ci_client.GetMessages(version)
45  request.membership = messages.Membership()
46
47  return request
48
49
50def SetEntityKey(unused_ref, args, request):
51  """Set EntityKey in group resource.
52
53  Args:
54    unused_ref: unused.
55    args: The argparse namespace.
56    request: The request to modify.
57
58  Returns:
59    The updated request.
60
61  """
62
63  version = groups_hooks.GetApiVersion(args)
64  messages = ci_client.GetMessages(version)
65  if hasattr(args, 'member_email') and args.IsSpecified('member_email'):
66    entity_key = messages.EntityKey(id=args.member_email)
67    request.membership.preferredMemberKey = entity_key
68
69  return request
70
71
72def SetPageSize(unused_ref, args, request):
73  """Set page size to request.pageSize.
74
75  Args:
76    unused_ref: unused.
77    args: The argparse namespace.
78    request: The request to modify.
79
80  Returns:
81    The updated request.
82
83  """
84
85  if hasattr(args, 'page_size') and args.IsSpecified('page_size'):
86    request.pageSize = int(args.page_size)
87
88  return request
89
90
91def SetMembershipParent(unused_ref, args, request):
92  """Set resource name to request.parent.
93
94  Args:
95    unused_ref: unused.
96    args: The argparse namespace.
97    request: The request to modify.
98
99  Returns:
100    The updated request.
101
102  """
103
104  version = groups_hooks.GetApiVersion(args)
105  if args.IsSpecified('group_email'):
106    # Resource name example: groups/03qco8b4452k99t
107    request.parent = groups_hooks.ConvertEmailToResourceName(
108        version, args.group_email, '--group-email')
109
110  return request
111
112
113def SetTransitiveMembershipParent(unused_ref, args, request):
114  """Set resource name to request.parent.
115
116  Args:
117    unused_ref: unused.
118    args: The argparse namespace.
119    request: The request to modify.
120
121  Returns:
122    The updated request.
123
124  """
125
126  version = groups_hooks.GetApiVersion(args)
127  if hasattr(args, 'group_email') and args.IsSpecified('group_email'):
128    # Resource name example: groups/03qco8b4452k99t
129    request.parent = groups_hooks.ConvertEmailToResourceName(
130        version, args.group_email, '--group-email')
131  else:
132    # If this hook is used and no group_emails provided then set the parent to
133    # be the groups wildcard.
134    request.parent = 'groups/-'
135  return request
136
137
138def SetMembershipResourceName(unused_ref, args, request):
139  """Set membership resource name to request.name.
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
149  """
150
151  version = groups_hooks.GetApiVersion(args)
152  name = ''
153  if args.IsSpecified('group_email') and args.IsSpecified('member_email'):
154    name = ConvertEmailToMembershipResourceName(
155        version, args, '--group-email', '--member-email')
156  else:
157    raise exceptions.InvalidArgumentException(
158        'Must specify `--group-email` and `--member-email` argument.')
159
160  request.name = name
161
162  if hasattr(request, 'membership'):
163    request.membership.name = name
164
165  return request
166
167
168def SetMembershipRoles(unused_ref, args, request):
169  """Set MembershipRoles to request.membership.roles.
170
171  Args:
172    unused_ref: unused.
173    args: The argparse namespace.
174    request: The request to modify.
175
176  Returns:
177    The updated request.
178
179  """
180
181  version = groups_hooks.GetApiVersion(args)
182  if not hasattr(args, 'roles') or not args.IsSpecified('roles'):
183    empty_list = []
184    request.membership.roles = ReformatMembershipRoles(version, empty_list)
185  else:
186    request.membership.roles = ReformatMembershipRoles(version, args.roles)
187
188  return request
189
190
191def SetExpiryDetail(unused_ref, args, request):
192  """Set expiration to request.membership.expiryDetail (v1alpha1) or in request.membership.roles (v1beta1).
193
194  Args:
195    unused_ref: unused.
196    args: The argparse namespace.
197    request: The request to modify.
198
199  Returns:
200    The updated request.
201
202  Raises:
203    InvalidArgumentException: If 'expiration' is specified upon following cases:
204    1. 'request.membership' doesn't have 'roles' attribute, or
205    2. multiple roles are provided.
206
207  """
208
209  ### Pre-validations ###
210
211  # #1. In order to set Expiry Detail, there should be a role in
212  # 'request.membership'
213  if not hasattr(request.membership, 'roles'):
214    raise exceptions.InvalidArgumentException(
215        'expiration', 'roles must be specified.')
216
217  # #2. When setting 'expiration', a single role should be input.
218  if len(request.membership.roles) != 1:
219    raise exceptions.InvalidArgumentException(
220        'roles',
221        'When setting "expiration", a single role should be input.')
222
223  version = groups_hooks.GetApiVersion(args)
224  if hasattr(args, 'expiration') and args.IsSpecified('expiration'):
225    if version == 'v1alpha1':
226      request.membership.expiryDetail = ReformatExpiryDetail(
227          version, args.expiration, 'add')
228    else:
229      request.membership.roles = AddExpiryDetailInMembershipRoles(
230          version, request, args.expiration)
231
232  return request
233
234
235def SetTransitiveQuery(unused_ref, args, request):
236  """Sets query paremeters to request.query.
237
238  Args:
239    unused_ref: unused.
240    args: The argparse namespace.
241    request: The request to modify.
242
243  Returns:
244    The updated request.
245  """
246  params = []
247
248  if hasattr(args, 'member_email') and args.IsSpecified('member_email'):
249    params.append("member_key_id=='{}'".format(args.member_email))
250
251  if hasattr(args, 'labels') and args.IsSpecified('labels'):
252    params.append("'{}' in labels".format(args.labels))
253
254  request.query = '&&'.join(params)
255
256  return request
257
258
259def UpdateMembershipRoles(unused_ref, args, request):
260  """Update MembershipRoles to request.membership.roles.
261
262  Args:
263    unused_ref: unused.
264    args: The argparse namespace.
265    request: The request to modify.
266
267  Returns:
268    The updated request.
269
270  """
271
272  version = groups_hooks.GetApiVersion(args)
273  if hasattr(args, 'roles') and args.IsSpecified('roles'):
274    request.membership.roles = ReformatMembershipRoles(version, args.roles)
275
276  return request
277
278
279def UpdateRoles(unused_ref, args, request):
280  """Update 'MembershipRoles' to request.modifyMembershipRolesRequest.
281
282  Args:
283    unused_ref: unused.
284    args: The argparse namespace.
285    request: The request to modify.
286
287  Returns:
288    The updated request.
289
290  """
291
292  # Following logic is used only when 'add-roles' parameter is used.
293  if hasattr(args, 'add_roles') and args.IsSpecified('add_roles'):
294    # Convert a comma separated string to a list of strings.
295    role_list = args.add_roles.split(',')
296
297    # Convert a list of strings to a list of MembershipRole objects.
298    version = groups_hooks.GetApiVersion(args)
299    roles = []
300    messages = ci_client.GetMessages(version)
301    for role in role_list:
302      membership_role = messages.MembershipRole(name=role)
303      roles.append(membership_role)
304
305    request.modifyMembershipRolesRequest = messages.ModifyMembershipRolesRequest(
306        addRoles=roles)
307
308  return request
309
310
311def SetUpdateRolesParams(unused_ref, args, request):
312  """Update 'MembershipRoles' to request.modifyMembershipRolesRequest.
313
314  Args:
315    unused_ref: A string representing the operation reference. Unused and may
316      be None.
317    args: The argparse namespace.
318    request: The request to modify.
319
320  Returns:
321    The updated request.
322
323  """
324
325  if hasattr(args,
326             'update_roles_params') and args.IsSpecified('update_roles_params'):
327    version = groups_hooks.GetApiVersion(args)
328    messages = ci_client.GetMessages(version)
329    request.modifyMembershipRolesRequest = messages.ModifyMembershipRolesRequest(
330        updateRolesParams=ReformatUpdateRolesParams(
331            args, args.update_roles_params))
332
333  return request
334
335
336# private methods
337def ConvertEmailToMembershipResourceName(
338    version, args, group_arg_name, member_arg_name):
339  """Convert email to membership resource name.
340
341  Args:
342    version: Release track information
343    args: The argparse namespace
344    group_arg_name: argument/parameter name related to group info
345    member_arg_name: argument/parameter name related to member info
346
347  Returns:
348    Membership Id (e.g. groups/11zu0gzc3tkdgn2/memberships/1044279104595057141)
349
350  """
351
352  # Resource name example: groups/03qco8b4452k99t
353  group_id = groups_hooks.ConvertEmailToResourceName(
354      version, args.group_email, group_arg_name)
355
356  try:
357    return ci_client.LookupMembershipName(
358        version, group_id, args.member_email).name
359  except (apitools_exceptions.HttpForbiddenError,
360          apitools_exceptions.HttpNotFoundError):
361    # If there is no group exists (or deleted) for the given group email,
362    # print out an error message.
363    parameter_name = group_arg_name + ', ' + member_arg_name
364    error_msg = ('There is no such membership associated with the specified '
365                 'arguments: {}, {}').format(args.group_email,
366                                             args.member_email)
367
368    raise exceptions.InvalidArgumentException(parameter_name, error_msg)
369
370
371def ReformatExpiryDetail(version, expiration, command):
372  """Reformat expiration string to ExpiryDetail object.
373
374  Args:
375    version: Release track information
376    expiration: expiration string.
377    command: gcloud command name.
378
379  Returns:
380    ExpiryDetail object that contains the expiration data.
381
382  """
383
384  messages = ci_client.GetMessages(version)
385  duration = 'P' + expiration
386  expiration_ts = FormatDateTime(duration)
387
388  if version == 'v1alpha1' and command == 'modify-membership-roles':
389    return messages.MembershipRoleExpiryDetail(expireTime=expiration_ts)
390
391  return messages.ExpiryDetail(expireTime=expiration_ts)
392
393
394def ReformatMembershipRoles(version, roles_list):
395  """Reformat roles string to MembershipRoles object list.
396
397  Args:
398    version: Release track information
399    roles_list: list of roles in a string format.
400
401  Returns:
402    List of MembershipRoles object.
403
404  """
405
406  messages = ci_client.GetMessages(version)
407  roles = []
408  if not roles_list:
409    # If no MembershipRole is provided, 'MEMBER' is used as a default value.
410    roles.append(messages.MembershipRole(name='MEMBER'))
411    return roles
412
413  for role in roles_list:
414    new_membership_role = messages.MembershipRole(name=role)
415    roles.append(new_membership_role)
416
417  return roles
418
419
420def GetUpdateMask(role_param, arg_name):
421  """Set the update mask on the request based on the role param.
422
423  Args:
424    role_param: The param that needs to be updated for a specified role.
425    arg_name: The argument name
426
427  Returns:
428    Update mask
429
430  Raises:
431    InvalidArgumentException: If no fields are specified to update.
432
433  """
434  update_mask = []
435
436  if role_param == 'expiration':
437    update_mask.append('expiry_detail.expire_time')
438
439  if not update_mask:
440    raise exceptions.InvalidArgumentException(
441        arg_name, 'Must specify at least one field mask.')
442
443  return ','.join(update_mask)
444
445
446def FormatDateTime(duration):
447  """Return RFC3339 string for datetime that is now + given duration.
448
449  Args:
450    duration: string ISO 8601 duration, e.g. 'P5D' for period 5 days.
451
452  Returns:
453    string timestamp
454
455  """
456
457  # We use a format that preserves +00:00 for UTC to match timestamp format
458  # returned by container API.
459  fmt = '%Y-%m-%dT%H:%M:%S.%3f%Oz'
460
461  return times.FormatDateTime(
462      times.ParseDateTime(duration, tzinfo=times.UTC), fmt=fmt)
463
464
465def AddExpiryDetailInMembershipRoles(version, request, expiration):
466  """Add an expiration in request.membership.roles.
467
468  Args:
469    version: version
470    request: The request to modify
471    expiration: expiration date to set
472
473  Returns:
474    The updated roles.
475
476  Raises:
477    InvalidArgumentException: If 'expiration' is specified without MEMBER role.
478
479  """
480
481  messages = ci_client.GetMessages(version)
482  roles = []
483  has_member_role = False
484  for role in request.membership.roles:
485    if hasattr(role, 'name') and role.name == 'MEMBER':
486      has_member_role = True
487      roles.append(messages.MembershipRole(
488          name='MEMBER',
489          expiryDetail=ReformatExpiryDetail(version, expiration, 'add')))
490    else:
491      roles.append(role)
492
493  # Checking whether the 'expiration' is specified with a MEMBER role.
494  if not has_member_role:
495    raise exceptions.InvalidArgumentException(
496        'expiration', 'Expiration date can be set with a MEMBER role only.')
497
498  return roles
499
500
501def ReformatUpdateRolesParams(args, update_roles_params):
502  """Reformat update_roles_params string.
503
504  Reformatting update_roles_params will be done by following steps,
505  1. Split the comma separated string to a list of strings.
506  2. Convert the splitted string to UpdateMembershipRolesParams message.
507
508  Args:
509    args: The argparse namespace.
510    update_roles_params: A comma separated string.
511
512  Returns:
513    A list of reformatted 'UpdateMembershipRolesParams'.
514
515  Raises:
516    InvalidArgumentException: If invalid update_roles_params string is input.
517  """
518
519  # Split a comma separated 'update_roles_params' string.
520  update_roles_params_list = update_roles_params.split(',')
521
522  version = groups_hooks.GetApiVersion(args)
523  messages = ci_client.GetMessages(version)
524  roles_params = []
525  arg_name = '--update-roles-params'
526  for update_roles_param in update_roles_params_list:
527    role, param_key, param_value = TokenizeUpdateRolesParams(
528        update_roles_param, arg_name)
529
530    # Membership expiry is supported only on a MEMBER role.
531    if param_key == 'expiration' and role != 'MEMBER':
532      error_msg = ('Membership Expiry is not supported on a specified role: {}.'
533                   ).format(role)
534      raise exceptions.InvalidArgumentException(arg_name, error_msg)
535
536    # Instantiate MembershipRole object.
537    expiry_detail = ReformatExpiryDetail(
538        version, param_value, 'modify-membership-roles')
539    membership_role = messages.MembershipRole(
540        name=role, expiryDetail=expiry_detail)
541
542    update_mask = GetUpdateMask(param_key, arg_name)
543
544    update_membership_roles_params = messages.UpdateMembershipRolesParams(
545        fieldMask=update_mask, membershipRole=membership_role)
546
547    roles_params.append(update_membership_roles_params)
548
549  return roles_params
550
551
552def TokenizeUpdateRolesParams(update_roles_param, arg_name):
553  """Tokenize update_roles_params string.
554
555  Args:
556    update_roles_param: 'update_roles_param' string (e.g. MEMBER=expiration=3d)
557    arg_name: The argument name
558
559  Returns:
560    Tokenized strings: role (e.g. MEMBER), param_key (e.g. expiration), and
561    param_value (e.g. 3d)
562
563  Raises:
564    InvalidArgumentException: If invalid update_roles_param string is input.
565  """
566
567  token_list = update_roles_param.split('=')
568  if len(token_list) == 3:
569    return token_list[0], token_list[1], token_list[2]
570
571  raise exceptions.InvalidArgumentException(
572      arg_name, 'Invalid format: ' + update_roles_param)
573