1# -*- coding: utf-8 -*- 2# Copyright 2016 Google Inc. 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"""Helper module for the IAM command.""" 16 17from __future__ import absolute_import 18from __future__ import print_function 19from __future__ import division 20from __future__ import unicode_literals 21 22from collections import defaultdict 23from collections import namedtuple 24 25import six 26from apitools.base.protorpclite import protojson 27from gslib.exception import CommandException 28from gslib.third_party.storage_apitools import storage_v1_messages as apitools_messages 29 30TYPES = set([ 31 'user', 'deleted:user', 'serviceAccount', 'deleted:serviceAccount', 'group', 32 'deleted:group', 'domain', 'principal', 'principalSet', 'principalHierarchy' 33]) 34 35DISCOURAGED_TYPES = set([ 36 'projectOwner', 37 'projectEditor', 38 'projectViewer', 39]) 40 41DISCOURAGED_TYPES_MSG = ( 42 'Assigning roles (e.g. objectCreator, legacyBucketOwner) for project ' 43 'convenience groups is not supported by gsutil, as it goes against the ' 44 'principle of least privilege. Consider creating and using more granular ' 45 'groups with which to assign permissions. See ' 46 'https://cloud.google.com/iam/docs/using-iam-securely for more ' 47 'information. Assigning a role to a project group can be achieved by ' 48 'setting the IAM policy directly (see gsutil help iam for specifics).') 49 50PUBLIC_MEMBERS = set([ 51 'allUsers', 52 'allAuthenticatedUsers', 53]) 54 55# This is a convenience class to handle returned results from 56# BindingStringToTuple. is_grant is a boolean specifying if the 57# bindings are to be granted or removed from a bucket / object, 58# and bindings is a list of BindingsValueListEntry instances. 59BindingsTuple = namedtuple('BindingsTuple', ['is_grant', 'bindings']) 60 61# This is a special role value assigned to a specific member when all roles 62# assigned to the member should be dropped in the policy. A member:DROP_ALL 63# binding will be passed from BindingStringToTuple into PatchBindings. 64# This role will only ever appear on client-side (i.e. user-generated). It 65# will never be returned as a real role from an IAM get request. All roles 66# returned by PatchBindings are guaranteed to be "real" roles, i.e. not a 67# DROP_ALL role. 68DROP_ALL = '' 69 70 71def SerializeBindingsTuple(bindings_tuple): 72 """Serializes the BindingsValueListEntry instances in a BindingsTuple. 73 74 This is necessary when passing instances of BindingsTuple through 75 Command.Apply, as apitools_messages classes are not by default pickleable. 76 77 Args: 78 bindings_tuple: A BindingsTuple instance to be serialized. 79 80 Returns: 81 A serialized BindingsTuple object. 82 """ 83 return (bindings_tuple.is_grant, 84 [protojson.encode_message(t) for t in bindings_tuple.bindings]) 85 86 87def DeserializeBindingsTuple(serialized_bindings_tuple): 88 (is_grant, bindings) = serialized_bindings_tuple 89 return BindingsTuple(is_grant=is_grant, 90 bindings=[ 91 protojson.decode_message( 92 apitools_messages.Policy.BindingsValueListEntry, 93 t) for t in bindings 94 ]) 95 96 97def BindingsToDict(bindings): 98 """Converts a list of BindingsValueListEntry to a dictionary. 99 100 Args: 101 bindings: A list of BindingsValueListEntry instances. 102 103 Returns: 104 A {role: set(members)} dictionary. 105 """ 106 107 tmp_bindings = defaultdict(set) 108 for binding in bindings: 109 tmp_bindings[binding.role].update(binding.members) 110 return tmp_bindings 111 112 113def IsEqualBindings(a, b): 114 (granted, removed) = DiffBindings(a, b) 115 return not granted.bindings and not removed.bindings 116 117 118def DiffBindings(old, new): 119 """Computes the difference between two BindingsValueListEntry lists. 120 121 Args: 122 old: The original list of BindingValuesListEntry instances 123 new: The updated list of BindingValuesListEntry instances 124 125 Returns: 126 A pair of BindingsTuple instances, one for roles granted between old and 127 new, and one for roles removed between old and new. 128 """ 129 tmp_old = BindingsToDict(old) 130 tmp_new = BindingsToDict(new) 131 132 granted = BindingsToDict([]) 133 removed = BindingsToDict([]) 134 135 for (role, members) in six.iteritems(tmp_old): 136 removed[role].update(members.difference(tmp_new[role])) 137 for (role, members) in six.iteritems(tmp_new): 138 granted[role].update(members.difference(tmp_old[role])) 139 140 granted = [ 141 apitools_messages.Policy.BindingsValueListEntry(role=r, members=list(m)) 142 for (r, m) in six.iteritems(granted) 143 if m 144 ] 145 removed = [ 146 apitools_messages.Policy.BindingsValueListEntry(role=r, members=list(m)) 147 for (r, m) in six.iteritems(removed) 148 if m 149 ] 150 151 return (BindingsTuple(True, granted), BindingsTuple(False, removed)) 152 153 154def PatchBindings(base, diff): 155 """Patches a diff list of BindingsValueListEntry to the base. 156 157 Will remove duplicate members for any given role on a grant operation. 158 159 Args: 160 base: A list of BindingsValueListEntry instances. 161 diff: A BindingsTuple instance of diff to be applied. 162 163 Returns: 164 The computed difference, as a list of 165 apitools_messages.Policy.BindingsValueListEntry instances. 166 """ 167 168 # Convert the list of bindings into an {r: [m]} dictionary object. 169 tmp_base = BindingsToDict(base) 170 tmp_diff = BindingsToDict(diff.bindings) 171 172 # Patch the diff into base 173 if diff.is_grant: 174 for (role, members) in six.iteritems(tmp_diff): 175 if not role: 176 raise CommandException('Role must be specified for a grant request.') 177 tmp_base[role].update(members) 178 else: 179 for role in tmp_base: 180 tmp_base[role].difference_update(tmp_diff[role]) 181 # Drop all members with the DROP_ALL role specifed from input. 182 tmp_base[role].difference_update(tmp_diff[DROP_ALL]) 183 184 # Construct the BindingsValueListEntry list 185 bindings = [ 186 apitools_messages.Policy.BindingsValueListEntry(role=r, members=list(m)) 187 for (r, m) in six.iteritems(tmp_base) 188 if m 189 ] 190 191 return bindings 192 193 194def BindingStringToTuple(is_grant, input_str): 195 """Parses an iam ch bind string to a list of binding tuples. 196 197 Args: 198 is_grant: If true, binding is to be appended to IAM policy; else, delete 199 this binding from the policy. 200 input_str: A string representing a member-role binding. 201 e.g. user:foo@bar.com:objectAdmin 202 user:foo@bar.com:objectAdmin,objectViewer 203 user:foo@bar.com 204 allUsers 205 deleted:user:foo@bar.com?uid=123:objectAdmin,objectViewer 206 deleted:serviceAccount:foo@bar.com?uid=123 207 208 Raises: 209 CommandException in the case of invalid input. 210 211 Returns: 212 A BindingsTuple instance. 213 """ 214 215 if not input_str.count(':'): 216 input_str += ':' 217 if input_str.count(':') == 1: 218 tokens = input_str.split(':') 219 if '%s:%s' % (tokens[0], tokens[1]) in TYPES: 220 raise CommandException('Incorrect public member type for binding %s' % 221 input_str) 222 if tokens[0] in PUBLIC_MEMBERS: 223 (member, roles) = tokens 224 elif tokens[0] in TYPES: 225 member = ':'.join(tokens) 226 roles = DROP_ALL 227 else: 228 raise CommandException('Incorrect public member type for binding %s' % 229 input_str) 230 elif input_str.count(':') == 2: 231 tokens = input_str.split(':') 232 if '%s:%s' % (tokens[0], tokens[1]) in TYPES: 233 # case "deleted:user:foo@bar.com?uid=1234" 234 member = ':'.join(tokens) 235 roles = DROP_ALL 236 else: 237 (member_type, member_id, roles) = tokens 238 _check_member_type(member_type, input_str) 239 member = '%s:%s' % (member_type, member_id) 240 elif input_str.count(':') == 3: 241 # case "deleted:user:foo@bar.com?uid=1234:objectAdmin,objectViewer" 242 (member_type_p1, member_type_p2, member_id, roles) = input_str.split(':') 243 member_type = '%s:%s' % (member_type_p1, member_type_p2) 244 _check_member_type(member_type, input_str) 245 member = '%s:%s' % (member_type, member_id) 246 else: 247 raise CommandException('Invalid ch format %s' % input_str) 248 249 if is_grant and not roles: 250 raise CommandException('Must specify a role to grant.') 251 252 roles = [ResolveRole(r) for r in roles.split(',')] 253 254 bindings = [ 255 apitools_messages.Policy.BindingsValueListEntry(members=[member], role=r) 256 for r in set(roles) 257 ] 258 return BindingsTuple(is_grant=is_grant, bindings=bindings) 259 260 261def _check_member_type(member_type, input_str): 262 if member_type in DISCOURAGED_TYPES: 263 raise CommandException(DISCOURAGED_TYPES_MSG) 264 elif member_type not in TYPES: 265 raise CommandException('Incorrect member type for binding %s' % input_str) 266 267 268def ResolveRole(role): 269 if not role: 270 return DROP_ALL 271 if 'roles/' in role: 272 return role 273 return 'roles/storage.%s' % role 274