class MU::Cloud::Google::Role

A role as configured in {MU::Config::BasketofKittens::roles}

Public Class Methods

bindFromConfig(entity_type, entity_id, cfg, credentials: nil, deploy: nil, debug: false) click to toggle source

Add role bindings for a given entity from its BoK config @param entity_type [String]: The kind of entity to bind; typically user, group, or domain @param entity_id [String]: The cloud identifier of the entity @param cfg [Hash]: A configuration block confirming to our own {MU::Cloud::Google::Role.ref_schema} @param credentials [String]:

# File modules/mu/providers/google/role.rb, line 342
        def self.bindFromConfig(entity_type, entity_id, cfg, credentials: nil, deploy: nil, debug: false)
          loglevel = debug ? MU::NOTICE : MU::DEBUG

          return if !cfg
          MU.log "Google::Role::bindFromConfig binding called for #{entity_type} #{entity_id}", loglevel, details: cfg

          cfg.each { |binding|
            if deploy and binding["role"]["name"] and !binding["role"]["id"]
              role_obj = deploy.findLitterMate(name: binding["role"]["name"], type: "roles")
              binding["role"]["id"] = role_obj.cloud_id if role_obj
            end
            ["organizations", "projects", "folders"].each { |scopetype|
              next if !binding[scopetype]

              binding[scopetype].each { |scope|
# XXX resolution of Ref bits (roles, projects, and folders anyway; organizations and domains are direct)
                MU::Cloud::Google::Role.bindToIAM(
                  binding["role"]["id"],
                  entity_type,
                  entity_id,
                  scopetype,
                  scope,
                  credentials: credentials
                )
              }
            }
            if binding["directories"]
              binding["directories"].each { |dir|
                # this is either an organization cloud_id, or the name of one
                # of our credential sets, which we must map to an organization
                # cloud id
                creds = MU::Cloud::Google.credConfig(dir)

                customer = if creds
                  my_org = MU::Cloud::Google.getOrg(dir)
                  if !my_org
                    raise MuError, "Google directory role binding specified directory #{dir}, which looks like one of our credential sets, but does not appear to map to an organization!"
                  end
                  my_org.owner.directory_customer_id
                elsif dir.match(/^organizations\//)
                  # Not sure if there's ever a case where we can do this with
                  # an org that's different from the one our credentials go with
                  my_org = MU::Cloud::Google.getOrg(credentials, with_id: dir)
                  if !my_org
                    raise MuError, "Failed to retrieve #{dir} with credentials #{credentials} in Google directory role binding for role #{binding["role"].to_s}"
                  end
                  my_org.owner.directory_customer_id
                else
                  # assume it's a raw customer id and hope for the best
                  dir
                end

                if !binding["role"]["id"].match(/^\d+$/)
                  resp = MU::Cloud::Google.admin_directory(credentials: credentials).list_roles(customer)
                  if resp and resp.items
                    resp.items.each { |role|
                      if role.role_name == binding["role"]["id"]
                        binding["role"]["id"] = role.role_id
                        break
                      end
                    }
                  end
                end

                # Ensure we're using the stupid internal id, instead of the
                # email field (which is the "real" id most of the time)
                real_id = nil
                if entity_type == "group"
                  found = MU::Cloud.resourceClass("Google", "Group").find(cloud_id: entity_id, credentials: credentials)
                  if found[entity_id]
                    real_id = found[entity_id].id
                  end
                elsif entity_type == "user"
                  found = MU::Cloud.resourceClass("Google", "User").find(cloud_id: entity_id, credentials: credentials)
                  if found[entity_id]
                    real_id = found[entity_id].id
                  end
                else
                  raise MuError, "I don't know how to identify entity type #{entity_type} with id #{entity_id} in directory role binding"
                end
                real_id ||= entity_id # fingers crossed

                assign_obj = MU::Cloud::Google.admin_directory(:RoleAssignment).new(
                  assigned_to: real_id,
                  role_id: binding["role"]["id"],
                  scope_type: "CUSTOMER"
                )
# XXX guard this mess
                MU.log "Binding directory role #{(binding["role"]["name"] || binding["role"]["id"])} to #{entity_type} #{entity_id} in #{dir}", details: assign_obj
                MU::Cloud::Google.admin_directory(credentials: credentials).insert_role_assignment(
                  customer,
                  assign_obj
                )

              }
            end
          }

# XXX whattabout GSuite-tier roles?
        end
bindToIAM(role_id, entity_type, entity_id, scope_type, scope_id, credentials: nil, debug: false) click to toggle source

Attach a role to an entity @param role_id [String]: The cloud identifier of the role to which we're binding @param entity_type [String]: The kind of entity to bind; typically user, group, or domain @param entity_id [String]: The cloud identifier of the entity @param scope_type [String]: The kind of scope in which this binding will be valid; typically project, folder, or organization @param scope_id [String]: The cloud identifier of the scope in which this binding will be valid @param credentials [String]:

# File modules/mu/providers/google/role.rb, line 183
def self.bindToIAM(role_id, entity_type, entity_id, scope_type, scope_id, credentials: nil, debug: false)
  loglevel = debug ? MU::NOTICE : MU::DEBUG

  MU.log "Google::Role.bindToIAM(role_id: #{role_id}, entity_type: #{entity_type}, entity_id: #{entity_id}, scope_type: #{scope_type}, scope_id: #{scope_id}, credentials: #{credentials})", loglevel

  # scope_id might actually be the name of a credential set; if so, we
  # map it back to an actual organization on the fly
  if scope_type == "organizations"
    my_org = MU::Cloud::Google.getOrg(scope_id)
    if my_org
      scope_id = my_org.name
    end
  end

  @@role_bind_semaphore.synchronize {
    @@role_bind_scope_semaphores[scope_id] ||= Mutex.new
  }

  @@role_bind_scope_semaphores[scope_id].synchronize {
    entity = entity_type.sub(/s$/, "")+":"+entity_id
    policy = if scope_type == "organizations"
      MU::Cloud::Google.resource_manager(credentials: credentials).get_organization_iam_policy(scope_id)
    elsif scope_type == "folders"
      MU::Cloud::Google.resource_manager(credentials: credentials).get_folder_iam_policy(scope_id)
    elsif scope_type == "projects"
      if !scope_id
        raise MuError, "Google::Role.bindToIAM was called without a scope_id"
      elsif scope_id.is_a?(Hash)
        if scope_id["id"]
          scope_id = scope_id["id"]
        else
          raise MuError, "Google::Role.bindToIAM was called with a scope_id Ref hash that has no id field"
        end
      end
      MU::Cloud::Google.resource_manager(credentials: credentials).get_project_iam_policy(scope_id.sub(/^projects\//, ""))
    else
      puts "WTF DO WIT #{scope_type}"
    end

    saw_role = false
    policy.bindings.each { |binding|
      if binding.role == role_id 
        saw_role = true
        if binding.members.include?(entity)
          return # it's already bound, nothing needs doing
        else
          binding.members << entity
        end
      end
    }
    if !saw_role
      policy.bindings <<  MU::Cloud::Google.resource_manager(:Binding).new(
        role: role_id,
        members: [entity]
      )
    end
    MU.log "Granting #{role_id} to #{entity} in #{scope_id}", MU::NOTICE
    req_obj = MU::Cloud::Google.resource_manager(:SetIamPolicyRequest).new(
      policy: policy
    )
    if scope_type == "organizations"
      MU::Cloud::Google.resource_manager(credentials: credentials).set_organization_iam_policy(
        scope_id,
        req_obj
      )
    elsif scope_type == "folders"
      MU::Cloud::Google.resource_manager(credentials: credentials).set_folder_iam_policy(
        scope_id,
        req_obj
      )
    elsif scope_type == "projects"
      MU::Cloud::Google.resource_manager(credentials: credentials).set_project_iam_policy(
        scope_id,
        req_obj
      )
    end
  }
end
canLiveIn() click to toggle source

Return the list of “container” resource types in which this resource can reside. The list will include an explicit nil if this resource can exist outside of any container. @return [Array<Symbol,nil>]

# File modules/mu/providers/google/role.rb, line 454
def self.canLiveIn
  [nil, :Habitat, :Folder]
end
cleanup(noop: false, deploy_id: MU.deploy_id, ignoremaster: false, credentials: nil, flags: {}) click to toggle source

Remove all roles associated with the currently loaded deployment. @param noop [Boolean]: If true, will only print what would be done @param ignoremaster [Boolean]: If true, will remove resources not flagged as originating from this Mu server @return [void]

# File modules/mu/providers/google/role.rb, line 468
        def self.cleanup(noop: false, deploy_id: MU.deploy_id, ignoremaster: false, credentials: nil, flags: {})
          customer = MU::Cloud::Google.customerID(credentials)
          my_org = MU::Cloud::Google.getOrg(credentials)

          filter = %Q{(labels.mu-id = "#{MU.deploy_id.downcase}")}
          if !ignoremaster and MU.mu_public_ip
            filter += %Q{ AND (labels.mu-master-ip = "#{MU.mu_public_ip.gsub(/\./, "_")}")}
          end
          MU.log "Placeholder: Google Role artifacts do not support labels, so ignoremaster cleanup flag has no effect", MU::DEBUG, details: filter

          if flags['known']
            flags['known'].each { |id|
              next if id.nil?
              # GCP roles don't have a useful field for packing in our deploy
              # id, so if we have metadata to leverage for this, use it. For
              # directory roles, we try to make it into the name field, so
              # we'll check that later, but for org and project roles this is
              # our only option.
              if my_org and id.is_a?(Integer) or id.match(/^\d+/)
                begin
                  resp = MU::Cloud::Google.admin_directory(credentials: credentials).get_role(customer, id)
                rescue ::Google::Apis::ClientError => e
                  next if e.message.match(/notFound/)
                  raise e
                end
                if resp
                  MU.log "Deleting directory role #{resp.role_name}"
                  if !noop
                    MU::Cloud::Google.admin_directory(credentials: credentials).delete_role(customer, id)
                  end
                end
              elsif id.match(/^projects\/.*?\/roles\//)
                begin
                  resp = MU::Cloud::Google.iam(credentials: credentials).get_project_role(id)
                rescue ::Google::Apis::ClientError => e
                  next if e.message.match(/notFound/)
                  raise e
                end
                if resp
                  MU.log "Deleting project role #{resp.name}"
                  if !noop
                    MU::Cloud::Google.iam(credentials: credentials).delete_project_role(id)
                  end
                end
              elsif id.match(/^organizations\//)
                begin
                  resp = MU::Cloud::Google.iam(credentials: credentials).get_organization_role(id)
                rescue ::Google::Apis::ClientError => e
#MU.log e.message, MU::ERR, details: id
#next
                  next if e.message.match(/notFound/)
                  raise e
                end
                if resp
                  MU.log "Deleting organization role #{resp.name}"
                  if !noop
                    MU::Cloud::Google.iam(credentials: credentials).delete_organization_role(id)
                  end
                end
              end
            }
          end

          if my_org and MU.deploy_id and !MU.deploy_id.empty?
            resp = MU::Cloud::Google.admin_directory(credentials: credentials).list_roles(customer)
            if resp and resp.items
              resp.items.each { |role|
                if role.role_name.match(/^#{Regexp.quote(MU.deploy_id)}/)
                  MU.log "Deleting directory role #{role.role_name}"
                  if !noop
                    MU::Cloud::Google.admin_directory(credentials: credentials).delete_role(customer, role.role_id)
                  end
                end
              }
            end
          end

        end
entityBindingsToSchema(roles, credentials: nil) click to toggle source

Convert a list of bindings of the type returned by {MU::Cloud::Google::Role.getAllBindings} into valid configuration language. @param roles [Hash] @param credentials [String] @return [Hash]

# File modules/mu/providers/google/role.rb, line 963
def self.entityBindingsToSchema(roles, credentials: nil)
  my_org = MU::Cloud::Google.getOrg(credentials)
  role_cfg = []

  roles.each_pair { |role, scopes|
    rolemap = { }
    rolemap["role"] = if !role.is_a?(Integer) and role.match(/^roles\//)
      # generally referring to a canned GCP role
      { "id" => role.to_s }
    elsif role.is_a?(Integer) or role.match(/^\d+$/)
      # If this is a GSuite/Cloud Identity system role, reference it by
      # its human-readable name intead of its numeric id
      role_desc = MU::Cloud::Google::Role.find(cloud_id: role, credentials: credentials).values.first
      if role_desc.is_system_role
        { "id" => role_desc.role_name }
      else
        MU::Config::Ref.get(
          id: role,
          cloud: "Google",
          credentials: credentials,
          type: "roles"
        )
      end
    else
      # Possi-probably something we're declaring elsewhere in this
      # adopted Mu stack
      MU::Config::Ref.get(
        id: role,
        cloud: "Google",
        credentials: credentials,
        type: "roles"
      )
    end
    scopes.each_pair { |scopetype, places|
      if places.size > 0
        rolemap[scopetype] = []
        if scopetype == "organizations" or scopetype == "directories"
          places.each { |org|
            rolemap[scopetype] << ((org == my_org.name and credentials) ? credentials : org)
          }
        else
          places.each { |place|
            mu_type = scopetype == "projects" ? "habitats" : scopetype
            rolemap[scopetype] << MU::Config::Ref.get(
              id: place,
              cloud: "Google",
              credentials: credentials,
              type: mu_type
            )
          }
        end
      end
    }
    role_cfg << rolemap
  }

  role_cfg
end
find(**args) click to toggle source

Locate and return cloud provider descriptors of this resource type which match the provided parameters, or all visible resources if no filters are specified. At minimum, implementations of find must honor credentials and cloud_id arguments. We may optionally support other search methods, such as tag_key and tag_value, or cloud-specific arguments like project. See also {MU::MommaCat.findStray}. @param args [Hash]: Hash of named arguments passed via Ruby's double-splat @return [Hash<String,OpenStruct>]: The cloud provider's complete descriptions of matching resources

# File modules/mu/providers/google/role.rb, line 555
        def self.find(**args)
          credcfg = MU::Cloud::Google.credConfig(args[:credentials])
          customer = MU::Cloud::Google.customerID(args[:credentials])
          my_org = MU::Cloud::Google.getOrg(args[:credentials])

          found = {}
          args[:project] ||= args[:habitat]

          if args[:project]
            canned = Hash[MU::Cloud::Google.iam(credentials: args[:credentials]).list_roles.roles.map { |r| [r.name, r] }]
            begin
              MU::Cloud.resourceClass("Google", "Habitat").bindings(args[:project], credentials: args[:credentials]).each { |binding|
                found[binding.role] = canned[binding.role]
              }
            rescue ::Google::Apis::ClientError => e
              raise e if !e.message.match(/forbidden: /)
            end

            resp = begin
              MU::Cloud::Google.iam(credentials: args[:credentials]).list_project_roles("projects/"+args[:project])
            rescue ::Google::Apis::ClientError => e
              raise e if !e.message.match(/forbidden: /)
            end
            if resp and resp.roles
              resp.roles.each { |role|
                found[role.name] = role
              }
            end
            if args[:cloud_id]
              found.reject! { |k, _v| k != args[:cloud_id] }
            end

            # Now go get everything that's bound here
            bindings = MU::Cloud::Google::Role.getAllBindings(args[:credentials])
            if bindings and bindings['by_scope'] and
               bindings['by_scope']['projects'] and
               bindings['by_scope']['projects'][args[:project]]
              bindings['by_scope']['projects'][args[:project]].keys.each { |r|
                if r.match(/^roles\//)
                  begin
                    role = MU::Cloud::Google.iam(credentials: args[:credentials]).get_role(r)
                    found[role.name] = role
                  rescue ::Google::Apis::ClientError => e
                    raise e if !e.message.match(/(?:forbidden|notFound): /)
                    MU.log "Failed  MU::Cloud::Google.iam(credentials: #{args[:credentials]}).get_role(#{r})", MU::WARN, details: e.message
                  end
                elsif !found[r]
#                  MU.log "NEED TO GET #{r}", MU::WARN
                end
              }
            end
          else
            if credcfg['masquerade_as']
              if args[:cloud_id]
                begin
                  resp = MU::Cloud::Google.admin_directory(credentials: args[:credentials]).get_role(customer, args[:cloud_id].to_i)
                  if resp
                    found[args[:cloud_id].to_s] = resp
                  end
                rescue ::Google::Apis::ClientError => e
                  raise e if !e.message.match(/(?:forbidden|notFound): /)
                end
              else
                resp = MU::Cloud::Google.admin_directory(credentials: args[:credentials]).list_roles(customer)
                if resp and resp.items
                  resp.items.each { |role|
                    found[role.role_id.to_s] = role
                  }
                end
              end

            end
#            These are the canned roles
            resp = begin
              MU::Cloud::Google.iam(credentials: args[:credentials]).list_roles
            rescue ::Google::Apis::ClientError => e
              raise e if !e.message.match(/forbidden: /)
            end
            if resp
              resp.roles.each { |role|
                found[role.name] = role
              }
            end

            if my_org
              resp = begin
                MU::Cloud::Google.iam(credentials: args[:credentials]).list_organization_roles(my_org.name)
              rescue ::Google::Apis::ClientError => e
                raise e if !e.message.match(/forbidden: /)
              end
              if resp and resp.roles
                resp.roles.each { |role|
                  found[role.name] = role
                }
              end
            end
          end

          found
        end
getAllBindings(credentials = nil, refresh: false) click to toggle source

Retrieve IAM role bindings for all entities throughout our organization, map them in useful ways, and cache the result.

# File modules/mu/providers/google/role.rb, line 856
        def self.getAllBindings(credentials = nil, refresh: false)
          my_org = MU::Cloud::Google.getOrg(credentials)
          @@binding_semaphore.synchronize {
            if @@bindings_by_role.size > 0 and !refresh
              return {
                "by_role" => @@bindings_by_role.dup,
                "by_scope" => @@bindings_by_scope.dup,
                "by_entity" => @@bindings_by_entity.dup
              }
            end

            def self.insertBinding(scopetype, scope, binding = nil, member_type: nil, member_id: nil, role_id: nil)
              role_id = binding.role if binding
              @@bindings_by_scope[scopetype] ||= {}
              @@bindings_by_scope[scopetype][scope] ||= {}
              @@bindings_by_scope[scopetype][scope][role_id] ||= {}
              @@bindings_by_role[role_id] ||= {}
              @@bindings_by_role[role_id][scopetype] ||= {}
              @@bindings_by_role[role_id][scopetype][scope] ||= {}

              do_binding = Proc.new { |type, id|
                @@bindings_by_role[role_id][scopetype][scope][type] ||= []
                @@bindings_by_role[role_id][scopetype][scope][type] << id
                @@bindings_by_scope[scopetype][scope][role_id][type] ||= []
                @@bindings_by_scope[scopetype][scope][role_id][type] << id
                @@bindings_by_entity[type] ||= {}
                @@bindings_by_entity[type][id] ||= {}
                @@bindings_by_entity[type][id][role_id] ||= {}
                @@bindings_by_entity[type][id][role_id][scopetype] ||= []
                @@bindings_by_entity[type][id][role_id][scopetype] << scope
              }

              if binding
                binding.members.each { |member|
                  member_type, member_id = member.split(/:/)
                  do_binding.call(member_type, member_id)
                }
              elsif member_type and member_id
                do_binding.call(member_type, member_id)
              end

            end

            if my_org
              resp = MU::Cloud::Google.admin_directory(credentials: credentials).list_role_assignments(MU::Cloud::Google.customerID(credentials))

              resp.items.each { |binding|

                begin
                  user = MU::Cloud::Google.admin_directory(credentials: credentials).get_user(binding.assigned_to)
                  insertBinding("directories", my_org.name, member_id: user.primary_email, member_type: "user", role_id: binding.role_id.to_s)
                  next
                rescue ::Google::Apis::ClientError # notFound
                end

                begin
                  group = MU::Cloud::Google.admin_directory(credentials: credentials).get_group(binding.assigned_to)
                  MU.log "GROUP", MU::NOTICE, details: group
#                  insertBinding("directories", my_org.name, member_id: group.primary_email, member_type: "group", role_id: binding.role_id.to_s)
                  next
                rescue ::Google::Apis::ClientError # notFound
                end

                role = MU::Cloud::Google.admin_directory(credentials: credentials).get_role(MU::Cloud::Google.customerID(credentials), binding.role_id)
                MU.log "Failed to find entity #{binding.assigned_to} referenced in GSuite/Cloud Identity binding to role #{role.role_name}", MU::DEBUG, details: role
              }

              resp = MU::Cloud::Google.resource_manager(credentials: credentials).get_organization_iam_policy(my_org.name)
              resp.bindings.each { |binding|
                insertBinding("organizations", my_org.name, binding)
              }

              MU::Cloud.resourceClass("Google", "Folder").find(credentials: credentials).keys.each { |folder|
                folder_bindings = MU::Cloud.resourceClass("Google", "Folder").bindings(folder, credentials: credentials)
                next if !folder_bindings
                folder_bindings.each { |binding|
                  insertBinding("folders", folder, binding)
                }
              }
            end
            MU::Cloud::Google.listHabitats(credentials).each { |project|
              begin
                MU::Cloud.resourceClass("Google", "Habitat").bindings(project, credentials: credentials).each { |binding|
                  insertBinding("projects", project, binding)
                }
              rescue ::Google::Apis::ClientError => e
                if e.message.match(/forbidden: /)
                  MU.log "Do not have permissions to retrieve bindings in project #{project}, skipping", MU::WARN
                else
                  raise e
                end
              end

            }

            return {
              "by_role" => @@bindings_by_role.dup,
              "by_scope" => @@bindings_by_scope.dup,
              "by_entity" => @@bindings_by_entity.dup
            }
          }
        end
id_map_recurse(items, parent_name = nil) click to toggle source
# File modules/mu/providers/google/role.rb, line 1175
              def self.id_map_recurse(items, parent_name = nil)
                id_to_name = {}
                name_to_id = {}
                id_to_privs = {}

                items.each { |p|
                  svcname = p.service_name || parent_name
                  if svcname
                    id_to_name[p.service_id] ||= svcname
                    name_to_id[svcname] ||= p.service_id
                  else
#                    MU.log "FREAKING #{p.service_id} HAS NO NAME", MU::WARN
                  end
                  id_to_privs[p.service_id] ||= []
                  id_to_privs[p.service_id] << p.privilege_name
                  if p.child_privileges
                    ids, names, privs = id_map_recurse(p.child_privileges, svcname)
                    id_to_name.merge!(ids)
                    name_to_id.merge!(names)
                    privs.each_pair { |id, childprivs|
                      id_to_privs[id] ||= []
                      id_to_privs[id].concat(childprivs)
                    }
                  end
                }

                [id_to_name, name_to_id, id_to_privs]
              end
insertBinding(scopetype, scope, binding = nil, member_type: nil, member_id: nil, role_id: nil) click to toggle source
# File modules/mu/providers/google/role.rb, line 867
def self.insertBinding(scopetype, scope, binding = nil, member_type: nil, member_id: nil, role_id: nil)
  role_id = binding.role if binding
  @@bindings_by_scope[scopetype] ||= {}
  @@bindings_by_scope[scopetype][scope] ||= {}
  @@bindings_by_scope[scopetype][scope][role_id] ||= {}
  @@bindings_by_role[role_id] ||= {}
  @@bindings_by_role[role_id][scopetype] ||= {}
  @@bindings_by_role[role_id][scopetype][scope] ||= {}

  do_binding = Proc.new { |type, id|
    @@bindings_by_role[role_id][scopetype][scope][type] ||= []
    @@bindings_by_role[role_id][scopetype][scope][type] << id
    @@bindings_by_scope[scopetype][scope][role_id][type] ||= []
    @@bindings_by_scope[scopetype][scope][role_id][type] << id
    @@bindings_by_entity[type] ||= {}
    @@bindings_by_entity[type][id] ||= {}
    @@bindings_by_entity[type][id][role_id] ||= {}
    @@bindings_by_entity[type][id][role_id][scopetype] ||= []
    @@bindings_by_entity[type][id][role_id][scopetype] << scope
  }

  if binding
    binding.members.each { |member|
      member_type, member_id = member.split(/:/)
      do_binding.call(member_type, member_id)
    }
  elsif member_type and member_id
    do_binding.call(member_type, member_id)
  end

end
isGlobal?() click to toggle source

Does this resource type exist as a global (cloud-wide) artifact, or is it localized to a region/zone? @return [Boolean]

# File modules/mu/providers/google/role.rb, line 446
def self.isGlobal?
  true
end
map_directory_privileges(roles, credentials: nil) click to toggle source

Convert a list of shorthand GSuite/Cloud Identity privileges into RolePrivilege objects for consumption by other API calls @param roles [Array<String>]: @param credentials [String]: @return [Array<Google::Apis::AdminDirectoryV1::DirectoryService::Role::RolePrivilege>]

# File modules/mu/providers/google/role.rb, line 1216
def self.map_directory_privileges(roles, credentials: nil)
  rolepriv_objs = []
  notfound = []
  if roles
    ids, names, privlist = MU::Cloud::Google::Role.privilege_service_to_name(credentials)
    roles.each { |p|
      service, privilege = p.split(/\//)
      if !names[service] and !ids[service]
        notfound << service
      elsif !privlist[names[service]].include?(privilege)
        notfound << p
      elsif names[service]
        rolepriv_objs << MU::Cloud::Google.admin_directory(:Role)::RolePrivilege.new(
          privilege_name: privilege,
          service_id: names[service]
        )
      else
        rolepriv_objs << MU::Cloud::Google.admin_directory(:Role)::RolePrivilege.new(
          privilege_name: privilege,
          service_id: service
        )
      end
    }
  end
  [rolepriv_objs, notfound.uniq.sort]
end
new(**args) click to toggle source

Initialize this cloud resource object. Calling super will invoke the initializer defined under {MU::Cloud}, which should set the attribtues listed in {MU::Cloud::PUBLIC_ATTRS} as well as applicable dependency shortcuts, like @vpc, for us. @param args [Hash]: Hash of named arguments passed via Ruby's double-splat

Calls superclass method
# File modules/mu/providers/google/role.rb, line 23
def initialize(**args)
  super

  @mu_name ||= if !@config['scrub_mu_isms']
    @deploy.getResourceName(@config["name"])
  else
    @config['name']
  end

  # If we're being reverse-engineered from a cloud descriptor, use that
  # to determine what sort of account we are.
  if args[:from_cloud_desc]
    require 'google/apis/admin_directory_v1'
    @cloud_desc_cache = args[:from_cloud_desc]
    if args[:from_cloud_desc].class == ::Google::Apis::AdminDirectoryV1::Role
      @config['role_source'] = "directory"
    elsif args[:from_cloud_desc].name.match(/^roles\/(.*)/) or
          (@cloud_id and @cloud_id.match(/^roles\/(.*)/))
      @config['role_source'] = "canned"
      @config['name'] = Regexp.last_match[1]
    elsif args[:from_cloud_desc].name.match(/^organizations\/\d+\/roles\/(.*)/) or
          (@cloud_id and @cloud_id.match(/^organizations\/\d+\/roles\/(.*)/))
      @config['role_source'] = "org"
      @config['name'] = Regexp.last_match[1]
    elsif args[:from_cloud_desc].name.match(/^projects\/([^\/]+?)\/roles\/(.*)/) or
          (@cloud_id and @cloud_id.match(/^projects\/\d+\/roles\/(.*)/))
      @config['project'] = Regexp.last_match[1]
      @config['name'] = Regexp.last_match[2]
      @project_id = @config['project']
      @config['role_source'] = "project"
    else
      MU.log "I don't know what to do with this #{args[:from_cloud_desc].class.name}", MU::ERR, details: args[:from_cloud_desc]
      raise MuError, "I don't know what to do with this #{args[:from_cloud_desc].class.name}"
    end
  end
end
privilege_service_to_name(credentials = nil) click to toggle source

Generate lookup tables mapping between hex service role identifiers, human-readable names of services, and the privileges associated with those roles. @param credentials [String] @return [Array<Hash,Hash,Hash>]

# File modules/mu/providers/google/role.rb, line 1163
        def self.privilege_service_to_name(credentials = nil)

          customer = MU::Cloud::Google.customerID(credentials)
          @@service_name_map_semaphore.synchronize {
            if !@@service_id_to_name[credentials] or
               !@@service_id_to_privs[credentials] or
               !@@service_name_to_id[credentials]
              @@service_id_to_name[credentials] ||= {}
              @@service_id_to_privs[credentials] ||= {}
              @@service_name_to_id[credentials] ||= {}
              resp = MU::Cloud::Google.admin_directory(credentials: credentials).list_privileges(customer)

              def self.id_map_recurse(items, parent_name = nil)
                id_to_name = {}
                name_to_id = {}
                id_to_privs = {}

                items.each { |p|
                  svcname = p.service_name || parent_name
                  if svcname
                    id_to_name[p.service_id] ||= svcname
                    name_to_id[svcname] ||= p.service_id
                  else
#                    MU.log "FREAKING #{p.service_id} HAS NO NAME", MU::WARN
                  end
                  id_to_privs[p.service_id] ||= []
                  id_to_privs[p.service_id] << p.privilege_name
                  if p.child_privileges
                    ids, names, privs = id_map_recurse(p.child_privileges, svcname)
                    id_to_name.merge!(ids)
                    name_to_id.merge!(names)
                    privs.each_pair { |id, childprivs|
                      id_to_privs[id] ||= []
                      id_to_privs[id].concat(childprivs)
                    }
                  end
                }

                [id_to_name, name_to_id, id_to_privs]
              end

              @@service_id_to_name[credentials], @@service_id_to_privs[credentials], @@service_name_to_id[credentials] = self.id_map_recurse(resp.items)
            end

            return [@@service_id_to_name[credentials], @@service_id_to_privs[credentials], @@service_name_to_id[credentials]]
          }
        end
quality() click to toggle source

Denote whether this resource implementation is experiment, ready for testing, or ready for production use.

# File modules/mu/providers/google/role.rb, line 460
def self.quality
  MU::Cloud::RELEASE
end
ref_schema() click to toggle source

Schema used by user and group entities to reference role assignments and their scopes. @return [<Hash>]

# File modules/mu/providers/google/role.rb, line 824
def self.ref_schema
  {
    "type" => "object",
    "description" => "One or more Google IAM roles to associate with this entity. IAM roles in Google can be associated at the project (+Habitat+), folder, or organization level, so we must specify not only role, but each container in which it is granted to the entity in question.",
    "properties" => {
      "role" => MU::Config::Ref.schema(type: "roles"),
      "projects" => {
        "type" => "array",
        "items" => MU::Config::Ref.schema(type: "habitats")
      },
      "folders" => {
        "type" => "array",
        "items" => MU::Config::Ref.schema(type: "folders")
      },
      "organizations" => {
        "type" => "array",
        "items" => {
          "type" => "string",
          "description" => "Either an organization cloud identifier, like +organizations/123456789012+, or the name of set of Mu credentials listed in +mu.yaml+, which can be used as an alias to the organization to which they authenticate."
        }
      }
    }
  }
end
removeBindings(entity_type, entity_id, credentials: nil, noop: false) click to toggle source

Remove all bindings for the specified entity @param entity_type [String]: The kind of entity to bind; typically user, group, or domain @param entity_id [String]: The cloud identifier of the entity @param credentials [String]: @param noop [Boolean]: Just say what we'd do without doing it

# File modules/mu/providers/google/role.rb, line 267
        def self.removeBindings(entity_type, entity_id, credentials: nil, noop: false)

          scopes = {}

          my_org = MU::Cloud::Google.getOrg(credentials)
          if my_org
            scopes["organizations"] = [my_org.name]
            folders = MU::Cloud.resourceClass("Google", "Folder").find(credentials: credentials)
            if folders and folders.size > 0
              scopes["folders"] = folders.keys
            end
          end

          projects = MU::Cloud.resourceClass("Google", "Habitat").find(credentials: credentials)
          if projects and projects.size > 0
            scopes["projects"] = projects.keys
          end

          scopes.each_pair { |scope_type, scope_ids|
            scope_ids.each { |scope_id|
              @@role_bind_semaphore.synchronize {
                @@role_bind_scope_semaphores[scope_id] ||= Mutex.new
              }

              @@role_bind_scope_semaphores[scope_id].synchronize {
                entity = entity_type.sub(/s$/, "")+":"+entity_id
                policy = if scope_type == "organizations"
                  MU::Cloud::Google.resource_manager(credentials: credentials).get_organization_iam_policy(my_org.name)
                elsif scope_type == "folders"
                  MU::Cloud::Google.resource_manager(credentials: credentials).get_folder_iam_policy(scope_id)
                elsif scope_type == "projects"
                  MU::Cloud::Google.resource_manager(credentials: credentials).get_project_iam_policy(scope_id)
                end

                need_update = false
                policy.bindings.each { |binding|
                  if binding.members.include?(entity)
                    MU.log "Unbinding #{binding.role} from #{entity} in #{scope_id}"
                    need_update = true
                    binding.members.delete(entity)
                  end
                }
# XXX maybe drop bindings with 0 members?
                next if !need_update or noop
                req_obj = MU::Cloud::Google.resource_manager(:SetIamPolicyRequest).new(
                  policy: policy
                )

                if scope_type == "organizations"
                  MU::Cloud::Google.resource_manager(credentials: credentials).set_organization_iam_policy(
                    scope_id,
                    req_obj
                  )
                elsif scope_type == "folders"
                  MU::Cloud::Google.resource_manager(credentials: credentials).set_folder_iam_policy(
                    scope_id,
                    req_obj
                  )
                elsif scope_type == "projects"
                  MU::Cloud::Google.resource_manager(credentials: credentials).set_project_iam_policy(
                    scope_id,
                    req_obj
                  )
                end
              }

            }
          }
        end
schema(_config) click to toggle source

Cloud-specific configuration properties. @param _config [MU::Config]: The calling MU::Config object @return [Array<Array,Hash>]: List of required fields, and json-schema Hash of cloud-specific configuration parameters for this resource

# File modules/mu/providers/google/role.rb, line 1025
        def self.schema(_config)
          toplevel_required = []
          schema = {
            "name" => {
              "pattern" => '^[a-zA-Z0-9\-\.\/]+$'
            },
            "display_name" => {
              "type" => "string",
              "description" => "A human readable name for this role. If not specified, will default to our long-form deploy-generated name."
            },
            "role_source" => {
              "type" => "string",
              "description" => "Google effectively has four types of roles:
              
+directory+: An admin role in GSuite or Cloud Identity

+org+: A custom organization-level IAM role. Note that these are only valid in GSuite or Cloud Identity environments

+project+: A custom project-level IAM role.

+canned+: A reference to one of the standard pre-defined IAM roles, usually only declared to apply {bindings} to other artifacts.

If this value is not specified, and the role name matches the name of an existing +canned+ role, we will assume it should be +canned+. If it does not, and we have credentials which map to a valid organization, we will assume +org+; if the credentials do not map to an organization, we will assume +project+.",
              "enum" => ["directory", "org", "project", "canned"]
            },
            "description" => {
              "type" => "string",
              "description" => "Detailed human-readable description of this role's purpose"
            },
            "bindings" => {
              "type" => "array",
              "items" => {
                "type" => "object",
                "description" => "One or more entities (+user+, +group+, etc) to associate with this role. IAM roles in Google can be associated at the project (+Habitat+), folder, or organization level, so we must specify not only the target entity, but each container in which it is granted to the entity in question.",
                "properties" => {
                  "entity" => MU::Config::Ref.schema,
                  "projects" => {
                    "type" => "array",
                    "items" => MU::Config::Ref.schema(type: "habitats")
                  },
                  "folders" => {
                    "type" => "array",
                    "items" => MU::Config::Ref.schema(type: "folders")
                  },
                  "organizations" => {
                    "type" => "array",
                    "items" => {
                      "type" => "string",
                      "description" => "Either an organization cloud identifier, like +organizations/123456789012+, or the name of set of Mu credentials, which can be used as an alias to the organization to which they authenticate."
                    }
                  }
                }
              }
            }
          }
          [toplevel_required, schema]
        end
validateConfig(role, configurator) click to toggle source

Cloud-specific pre-processing of {MU::Config::BasketofKittens::roles}, bare and unvalidated. @param role [Hash]: The resource to process and validate @param configurator [MU::Config]: The overall deployment configurator of which this resource is a member @return [Boolean]: True if validation succeeded, False otherwise

# File modules/mu/providers/google/role.rb, line 1087
def self.validateConfig(role, configurator)
  ok = true

  MU::Cloud::Google.credConfig(role['credentials'])

  my_org = MU::Cloud::Google.getOrg(role['credentials'])
  if !role['role_source']
    begin
      lookup_name = role['name'].dup
      if !lookup_name.match(/^roles\//)
        lookup_name = "roles/"+lookup_name
      end
      MU::Cloud::Google.iam(credentials: role['credentials']).get_role(lookup_name)
      MU.log "Role #{role['name']} appears to be a referenced to canned role #{role.name} (#{role.title})", MU::NOTICE
      role['role_source'] = "canned"
    rescue ::Google::Apis::ClientError
      role['role_source'] = my_org ? "org" : "project"
    end
  end

  if role['role_source'] == "canned"
    if role['bindings'].nil? or role['bindings'].empty?
      MU.log "Role #{role['name']} appears to refer to a canned role, but does not have any bindings declared- this will effectively do nothing.", MU::WARN
    end
  end

  if role['role_source'] == "directory" 

    if role['import'] and role['import'].size > 0
      mappings, missing = map_directory_privileges(role['import'], credentials: role['credentials'])
      if mappings.size == 0
        MU.log "None of the directory service privileges available to credentials #{role['credentials']} map to the ones declared for role #{role['name']}", MU::ERR, details: role['import'].sort
        ok = false
      elsif missing.size > 0
        MU.log "Some directory service privileges declared for role #{role['name']} aren't available to credentials #{role['credentials']}, will skip", MU::DEBUG, details: missing
      end
    end
  end

  if role['role_source'] == "directory" or role['role_source'] == "org"
    if !my_org
      MU.log "Role #{role['name']} requires an organization/directory, but credential set #{role['credentials']} doesn't appear to have access to one", MU::ERR
      ok = false
    end
  end

  if role['role_source'] == "project"
    role['project'] ||= MU::Cloud::Google.defaultProject(role['credentials'])
    if configurator.haveLitterMate?(role['project'], "habitats")
      MU::Config.addDependency(role, role['project'], "habitat")
    end
  end

  if role['bindings']
    role['bindings'].each { |binding|
      if binding['entity'] and binding['entity']['name'] and 
         configurator.haveLitterMate?(binding['entity']['name'], binding['entity']['type'])
        MU::Config.addDependency(role, binding['entity']['name'], binding['entity']['type'])
      end
    }
    role['bindings'].uniq!
  end

  ok
end

Public Instance Methods

bindToIAM(entity_type, entity_id, scope_type, scope_id) click to toggle source

Wrapper for #{MU::Cloud::Google::Role.bindToIAM}

# File modules/mu/providers/google/role.rb, line 169
def bindToIAM(entity_type, entity_id, scope_type, scope_id)
  MU::Cloud::Google::Role.bindToIAM(@cloud_id, entity_type, entity_id, scope_type, scope_id, credentials: @config['credentials'])
end
cloud_desc(use_cache: true) click to toggle source

Return the cloud descriptor for the Role @return [Google::Apis::Core::Hashable]

# File modules/mu/providers/google/role.rb, line 140
def cloud_desc(use_cache: true)
  return @cloud_desc_cache if @cloud_desc_cache and use_cache

  MU::Cloud::Google.getOrg(@config['credentials'])

  @cloud_desc_cache = if @config['role_source'] == "directory"
    MU::Cloud::Google.admin_directory(credentials: @config['credentials']).get_role(@customer, @cloud_id)
  elsif @config['role_source'] == "canned"
    MU::Cloud::Google.iam(credentials: @config['credentials']).get_role(@cloud_id)
  elsif @config['role_source'] == "project"
    MU::Cloud::Google.iam(credentials: @config['credentials']).get_project_role(@cloud_id)
  elsif @config['role_source'] == "org"
    MU::Cloud::Google.iam(credentials: @config['credentials']).get_organization_role(@cloud_id)
  end

  @cloud_desc_cache
end
create() click to toggle source

Called automatically by {MU::Deploy#createResources}

# File modules/mu/providers/google/role.rb, line 61
def create
  @config['display_name'] ||= @mu_name
  if @config['role_source'] == "directory"
    role_obj = MU::Cloud::Google.admin_directory(:Role).new(
      role_name: @mu_name,
      role_description: @config['display_name'],
      role_privileges: MU::Cloud::Google::Role.map_directory_privileges(@config['import'], credentials: @credentials).first
    )
    MU.log "Creating directory role #{@mu_name}", details: role_obj

    resp = MU::Cloud::Google.admin_directory(credentials: @credentials).insert_role(@customer, role_obj)
    @cloud_id = resp.role_id.to_s

  elsif @config['role_source'] == "canned"
    @cloud_id = @config['name']
    if !@cloud_id.match(/^roles\//)
      @cloud_id = "roles/"+@cloud_id
    end
  else
    create_role_obj = MU::Cloud::Google.iam(:CreateRoleRequest).new(
      role: MU::Cloud::Google.iam(:Role).new(
        title: @config['display_name'],
        description: @config['description']
      ),
      role_id: MU::Cloud::Google.nameStr(@deploy.getResourceName(@config["name"], max_length: 64)).gsub(/[^a-zA-Z0-9_\.]/, "_")
    )

    resp = if @config['role_source'] == "org"
      my_org = MU::Cloud::Google.getOrg(@config['credentials'])
      MU.log "Creating IAM organization role #{@mu_name} in #{my_org.display_name}", details: create_role_obj
      MU::Cloud::Google.iam(credentials: @credentials).create_organization_role(my_org.name, create_role_obj)
    elsif @config['role_source'] == "project"
      if !@project_id
        raise MuError, "Role #{@mu_name} is supposed to be in project #{@config['project']}, but no such project was found"
      end
      MU.log "Creating IAM project role #{@mu_name} in #{@project_id}", details: create_role_obj
      MU::Cloud::Google.iam(credentials: @credentials).create_project_role("projects/"+@project_id, create_role_obj)
    end

    @cloud_id = resp.name

  end
end
groom() click to toggle source

Called automatically by {MU::Deploy#createResources}

# File modules/mu/providers/google/role.rb, line 106
        def groom
          if @config['role_source'] == "directory"
#            MU.log "Updating directory role #{@mu_name}", MU::NOTICE, details: role_obj
#            MU::Cloud::Google.admin_directory(credentials: @credentials).patch_role(@customer, @cloud_id, role_obj)
          elsif @config['role_source'] == "org"
          elsif @config['role_source'] == "project"
          elsif @config['role_source'] == "canned"
          end

          @config['bindings'].each { |binding|
            binding.keys.each { |scopetype|
              next if scopetype == "entity"
              binding[scopetype].each { |scope|
# XXX handle entity being a MU::Config::Ref
                entity_id = if binding["entity"]["name"]
                  sib = @deploy.findLitterMate(name: binding["entity"]["name"], type: binding["entity"]["type"])
                  raise MuError, "Failed to look up sibling #{binding["entity"]["type"]}:#{binding["entity"]["name"]}" if !sib
                  if binding["entity"]["type"] == "users" and sib.config["type"] == "service"
                    binding["entity"]["type"] = "serviceAccount"
                  end
                  sib.cloud_id
                else
                  binding["entity"]["id"]
                end
# XXX resolve scope as well, if it's named or a MU::Config::Ref
                bindToIAM(binding["entity"]["type"], entity_id.sub(/.*?\/([^\/]+)$/, '\1'), scopetype, scope["id"])
              }
            }
          }
        end
notify() click to toggle source

Return the metadata for this group configuration @return [Hash]

# File modules/mu/providers/google/role.rb, line 160
def notify
  base = MU.structToHash(cloud_desc)
  base.delete(:etag)
  base["cloud_id"] = @cloud_id

  base
end
toKitten(**args) click to toggle source

Reverse-map our cloud description into a runnable config hash. We assume that any values we have in +@config+ are placeholders, and calculate our own accordingly based on what's live in the cloud.

# File modules/mu/providers/google/role.rb, line 659
def toKitten(**args)

  bok = {
    "cloud" => "Google",
    "credentials" => @config['credentials'],
    "cloud_id" => @cloud_id
  }

  my_org = MU::Cloud::Google.getOrg(@config['credentials'])

  # This can happen if the role_source isn't set correctly. This logic
  # maybe belongs inside cloud_desc. XXX
  if cloud_desc.nil?
    if @cloud_id and @cloud_id.match(/^roles\/(.*)/)
      @config['role_source'] = "canned"
    elsif @cloud_id and @cloud_id.match(/^organizations\/\d+\/roles\/(.*)/)
      @config['role_source'] = "org"
    elsif @cloud_id and @cloud_id.match(/^projects\/\d+\/roles\/(.*)/)
      @config['role_source'] = "project"
    end
  end

  # GSuite or Cloud Identity role
  if cloud_desc.class == ::Google::Apis::AdminDirectoryV1::Role
    return nil if cloud_desc.is_system_role

    bok["name"] = @config['name'].gsub(/[^a-z0-9]/i, '-').downcase
    bok['role_source'] = "directory"
    bok["display_name"] = @config['name']
    if !cloud_desc.role_description.empty?
      bok["description"] = cloud_desc.role_description
    end
    if !cloud_desc.role_privileges.nil? and !cloud_desc.role_privileges.empty?
      bok['import'] = []
      ids, _names, _privs = MU::Cloud::Google::Role.privilege_service_to_name(@config['credentials'])
      cloud_desc.role_privileges.each { |priv|
        if !ids[priv.service_id]
          MU.log "Role privilege defined for a service id with no name I can find, writing with raw id", MU::DEBUG, details: priv
          bok["import"] << priv.service_id+"/"+priv.privilege_name
        else
          bok["import"] << ids[priv.service_id]+"/"+priv.privilege_name
        end
      }
      bok['import'].sort! # at least be legible
    end
  else # otherwise it's a GCP IAM role of some kind

    return nil if cloud_desc.stage == "DISABLED"
    if cloud_desc.name.match(/^roles\/([^\/]+)$/)
      name = Regexp.last_match[1]
      bok['name'] = name.gsub(/[^a-z0-9]/i, '-')
      bok['role_source'] = "canned"
    elsif cloud_desc.name.match(/^([^\/]+?)\/([^\/]+?)\/roles\/(.*)/)
      _junk, type, parent, name = Regexp.last_match.to_a
      bok['name'] = name.gsub(/[^a-z0-9]/i, '-')
      bok['role_source'] = type == "organizations" ? "org" : "project"
      if bok['role_source'] == "project"
        bok['project'] = parent
      end
      if cloud_desc.included_permissions and cloud_desc.included_permissions.size > 0
        bok['import'] = cloud_desc.included_permissions
      end

    else
      raise MuError, "I don't know how to parse GCP IAM role identifier #{cloud_desc.name}"
    end

    if !cloud_desc.description.nil? and !cloud_desc.description.empty?
      bok["description"] = cloud_desc.description
    end
    bok["display_name"] = cloud_desc.title

    bindings = MU::Cloud::Google::Role.getAllBindings(@config['credentials'])["by_role"][@cloud_id]

    if bindings
      refmap = {}
      bindings.keys.each { |scopetype|
        bindings[scopetype].each_pair { |scope_id, entity_types|
          # If we've been given a habitat filter, skip over bindings
          # that don't match it.
          if scopetype == "projects"
            if (args[:habitats] and !args[:habitats].empty? and
               !args[:habitats].include?(scope_id)) or
               !MU::Cloud::Google.listHabitats(@credentials).include?(scope_id)
              next
            end
          end

          entity_types.each_pair { |entity_type, entities|
            next if entity_type == "deleted"
            mu_entitytype = (entity_type == "serviceAccount" ? "user" : entity_type)+"s"
            entities.each { |entity|
              next if entity.nil?
              foreign = if entity_type == "serviceAccount" and entity.match(/@(.*?)\.iam\.gserviceaccount\.com/)
                !MU::Cloud::Google.listHabitats(@credentials).include?(Regexp.last_match[1])
              end

              entity_ref = if entity_type == "organizations"
                { "id" => ((org == my_org.name and @config['credentials']) ? @config['credentials'] : org) }
              elsif entity_type == "domain"
                { "id" => entity }
              else
                shortclass, _cfg_name, _cfg_plural, _classname = MU::Cloud.getResourceNames(mu_entitytype)
                if args[:types].include?(shortclass) and
                   !(entity_type == "serviceAccount" and
                     MU::Cloud::Google::User.cannedServiceAcctName?(entity))
                  MU.log "Role #{@cloud_id}: Skipping #{shortclass} binding for #{entity}; we are adopting that type and will set bindings from that resource", MU::DEBUG
                  next
                end

                MU::Config::Ref.get(
                  id: entity,
                  cloud: "Google",
                  type: mu_entitytype
                )
              end
              if entity_ref.nil?
                MU.log "I somehow ended up with a nil entity reference for #{entity_type} #{entity}", MU::ERR, details: [ bok, bindings ]
                next
              end
              refmap ||= {}
              refmap[entity_ref] ||= {}
              refmap[entity_ref][scopetype] ||= []
              mu_scopetype = scopetype == "projects" ? "habitats" : scopetype
              if scopetype == "organizations" or scopetype == "domains" # XXX singular? plural? barf
                refmap[entity_ref][scopetype] << ((scope_id == my_org.name and @config['credentials']) ? @config['credentials'] : scope_id)
              else
                refmap[entity_ref][scopetype] << MU::Config::Ref.get(
                  id: scope_id,
                  cloud: "Google",
                  type: mu_scopetype
                )
              end
              refmap[entity_ref][scopetype].uniq!
            }
          }
        }
      }

      bok["bindings"] ||= []
      refmap.each_pair { |entity, scopes|
        newbinding = { "entity" => entity }
        scopes.keys.each { |scopetype|
          newbinding[scopetype] = scopes[scopetype].sort
        }
        bok["bindings"] << newbinding
      }
    end
  end

  # Our only reason for declaring canned roles is so we can put their
  # bindings somewhere. If there aren't any, then we don't need
  # to bother with them.
  if bok['role_source'] == "canned" and
     (bok['bindings'].nil? or bok['bindings'].empty?)
    return nil
  end

  bok
end