# Copyright:: Copyright (c) 2018 eGlobalTech, Inc., all rights reserved # # Licensed under the BSD-3 license (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License in the root of the project or at # # http://egt-labs.com/mu/LICENSE.html # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. module MU class Cloud class Azure # A user as configured in {MU::Config::BasketofKittens::roles} class Role < MU::Cloud::Role # 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 def initialize(**args) super if !mu_name.nil? @mu_name = mu_name @cloud_id = Id.new(cloud_desc.id) if @cloud_id else @mu_name ||= @deploy.getResourceName(@config["name"], max_length: 31) end end # Called automatically by {MU::Deploy#createResources} def create end # Called automatically by {MU::Deploy#createResources} def groom end # Return the metadata for this user configuration # @return [Hash] def notify description = MU.structToHash(cloud_desc) if description description.delete(:etag) return description end { } end # Does this resource type exist as a global (cloud-wide) artifact, or # is it localized to a region/zone? # @return [Boolean] def self.isGlobal? false end # Denote whether this resource implementation is experiment, ready for # testing, or ready for production use. def self.quality MU::Cloud::ALPHA end # Assign this role object to a given principal (create a RoleAssignment) # @param principal [MU::Cloud::Azure::Id] def assignTo(principal) MU::Cloud::Azure::Role.assignTo(principal_id, role_id: @cloud_id) end # Assign a role to a particular principal (create a RoleAssignment). We # support multiple ways of referring to a role # @param principal [MU::Cloud::Azure::Id] def self.assignTo(principal, role_name: nil, role_id: nil, credentials: nil) # XXX subscription might need extraction if !role_name and !role_id raise MuError, "Role.assignTo requries one of role_name, role_id, or permissions in order to look up roles for association" end existing = MU::Cloud::Azure.authorization(credentials: credentials).role_assignments.list() roles = MU::Cloud::Azure::Role.find(cloud_id: role_id, role_name: role_name, credentials: credentials) role = roles.values.first # XXX handle failures and multiples assign_obj = MU::Cloud::Azure.authorization(:RoleAssignmentCreateParameters, model_version: "V2018_09_01_preview").new assign_obj.principal_id = principal assign_obj.principal_type = "ServicePrincipal" assign_obj.role_definition_id = role.id # TODO this should defintiely be configurable, and for most Mu # deploy resources will be scoped to the resource group level scope = "/subscriptions/"+MU::Cloud::Azure.default_subscription(credentials) role_name = begin role.role_name rescue NoMethodError role.properties.role_name end used_ids = [] existing.each { |ext_assignment| used_ids << ext_assignment.name if ext_assignment.role_definition_id == role.id and ext_assignment.scope == scope and ext_assignment.principal_id == principal return end } guid = nil begin guid = MU::Cloud::Azure.genGUID end while used_ids.include?(guid) MU.log "Assigning role '#{role_name}' to principal #{principal}", details: assign_obj MU::Cloud::Azure.authorization(credentials: credentials).role_assignments.create( scope, guid, assign_obj ) end @@role_list_cache = {} @@role_list_semaphore = Mutex.new # 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]: The cloud provider's complete descriptions of matching resources def self.find(**args) found = {} sub_id = MU::Cloud::Azure.default_subscription(args[:credentials]) scope = "/subscriptions/"+sub_id if args[:cloud_id] id_str = args[:cloud_id].is_a?(MU::Cloud::Azure::Id) ? args[:cloud_id].name : args[:cloud_id] begin resp = MU::Cloud::Azure.authorization(credentials: args[:credentials]).role_definitions.get(scope, id_str) found[Id.new(resp.id)] = resp rescue MsRestAzure::AzureOperationError => e # this is fine, we're doing a blind search after all end else @@role_list_semaphore.synchronize { if !@@role_list_cache[scope] @@role_list_cache[scope] = Hash[MU::Cloud::Azure.authorization(credentials: args[:credentials]).role_definitions.list(scope).map { |r| [Id.new(r.id), r] }] end } if args[:role_name] @@role_list_cache[scope].each_pair { |key, role| begin if role.role_name == args[:role_name] found[Id.new(role.id)] = role break end rescue NoMethodError if role.properties.role_name == args[:role_name] found[Id.new(role.id)] = role break end end } else found = @@role_list_cache[scope].dup end end found end # Stub method. Azure resources are cleaned up by removing the parent # resource group. # @return [void] def self.cleanup(**args) end # Cloud-specific configuration properties. # @param config [MU::Config]: The calling MU::Config object # @return [Array]: List of required fields, and json-schema Hash of cloud-specific configuration parameters for this resource def self.schema(config) toplevel_required = [] schema = { } [toplevel_required, schema] end # 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 def self.validateConfig(role, configurator) ok = true role['region'] ||= MU::Cloud::Azure.myRegion(role['credentials']) ok end private end end end end