class MU::Cloud::Google::FirewallRule

A firewall ruleset as configured in {MU::Config::BasketofKittens::firewall_rules}

Constants

PROTOS

Firewall protocols supported by GCP as of early 2019

STD_PROTOS

Our default subset of supported firewall protocols

Attributes

rulesets[R]

Public Class Methods

cleanup(noop: false, deploy_id: MU.deploy_id, ignoremaster: false, credentials: nil, flags: {}) click to toggle source

Remove all security groups (firewall rulesets) 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/firewall_rule.rb, line 210
def self.cleanup(noop: false, deploy_id: MU.deploy_id, ignoremaster: false, credentials: nil, flags: {})
  flags["habitat"] ||= MU::Cloud::Google.defaultProject(credentials)
  return if !MU::Cloud.resourceClass("Google", "Habitat").isLive?(flags["habitat"], 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 FirewallRule artifacts do not support labels, so ignoremaster cleanup flag has no effect", MU::DEBUG, details: filter

  MU::Cloud::Google.compute(credentials: credentials).delete(
    "firewall",
    flags["habitat"],
    nil,
    noop
  )
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/firewall_rule.rb, line 174
def self.find(**args)
  args = MU::Cloud::Google.findLocationArgs(args)

  found = {}
  resp = begin
    MU::Cloud::Google.compute(credentials: args[:credentials]).list_firewalls(args[:project])
  rescue  ::Google::Apis::ClientError => e
    raise e if !e.message.match(/^(?:notFound|forbidden): /)
  end
  if resp and resp.items
    resp.items.each { |fw|
      next if !args[:cloud_id].nil? and fw.name != args[:cloud_id]
      found[fw.name] = fw
    }
  end

  found
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/firewall_rule.rb, line 196
def self.isGlobal?
  true
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/firewall_rule.rb, line 32
def initialize(**args)
  super

  if !@vpc.nil?
    @mu_name ||= @deploy.getResourceName(@config['name'], need_unique_string: true, max_length: 61)
  else
    @mu_name ||= @deploy.getResourceName(@config['name'], max_length: 61)
  end
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/firewall_rule.rb, line 202
def self.quality
  MU::Cloud::RELEASE
end
schema(_config = nil) 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/firewall_rule.rb, line 370
def self.schema(_config = nil)
  toplevel_required = []
  schema = {
    "rules" => {
      "items" => {
        "properties" => {
          "weight" => {
            "type" => "integer",
            "description" => "Explicitly set a priority for this firewall rule, between 0 and 65535, with lower numbered priority rules having greater precedence."
          },
          "deny" => {
            "type" => "boolean",
            "default" => false,
            "description" => "Set this rule to +DENY+ traffic instead of +ALLOW+"
          },
          "proto" => {
            "description" => "The protocol to allow with this rule. The +standard+ keyword will expand to a series of identical rules covering +icmp+, +tcp+, and +udp; the +all+ keyword will expand to a series of identical rules for all supported protocols.",
            "enum" => PROTOS + ["all", "standard"]
          },
          "source_tags" => {
            "type" => "array",
            "description" => "VMs with these tags, from which traffic will be allowed",
            "items" => {
              "type" => "string"
            }
          },
          "source_service_accounts" => {
            "type" => "array",
            "description" => "Resources using these service accounts, from which traffic will be allowed",
            "items" => {
              "type" => "string"
            }
          },
          "target_tags" => {
            "type" => "array",
            "description" => "VMs with these tags, to which traffic will be allowed",
            "items" => {
              "type" => "string"
            }
          },
          "target_service_accounts" => {
            "type" => "array",
            "description" => "Resources using these service accounts, to which traffic will be allowed",
            "items" => {
              "type" => "string"
            }
          }
        }
      }
    },
    "project" => {
      "type" => "string",
      "description" => "The project into which to deploy resources"
    }
  }
  [toplevel_required, schema]
end
validateConfig(acl, config) click to toggle source

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

# File modules/mu/providers/google/firewall_rule.rb, line 432
def self.validateConfig(acl, config)
  ok = true

  if acl['vpc']
    if !acl['vpc']['habitat']
      acl['vpc']['project'] ||= acl['project']
    elsif acl['vpc']['habitat'] and acl['vpc']['habitat']['id']
      acl['vpc']['project'] = acl['vpc']['habitat']['id']
    elsif acl['vpc']['habitat'] and acl['vpc']['habitat']['name']
      acl['vpc']['project'] = acl['vpc']['habitat']['name']
    end
    correct_vpc = MU::Cloud.resourceClass("Google", "VPC").pickVPC(
      acl['vpc'],
      acl,
      "firewall_rule",
      config
    )
    acl['vpc'] = correct_vpc if correct_vpc
  end

  acl['rules'] ||= []

  # Firewall entries without rules are illegal in GCP, so insert a
  # default-deny placeholder.
  if acl['rules'].empty?
    acl['rules'] << {
      "deny" => true,
      "proto" => "all",
      "hosts" => ["0.0.0.0/0"],
      "weight" => 65535
    }
  end

  # First, expand some of our protocol shorthand into a real list
  append = []
  delete = []
  acl['rules'].each { |r|
    if !r['egress']
      if !r['source_tags'] and !r['source_service_accounts'] and
         (!r['hosts'] or r['hosts'].empty?)
        r['hosts'] = ['0.0.0.0/0']
      end
    else
      if !r['destination_tags'] and !r['destination_service_accounts'] and
         (!r['hosts'] or r['hosts'].empty?)
        r['hosts'] = ['0.0.0.0/0']
      end
    end

    if r['proto'] == "standard"
      STD_PROTOS.each { |p|
        newrule = r.dup
        newrule['proto'] = p
        append << newrule
      }
      delete << r
    elsif r['proto'] == "all"
      PROTOS.each { |p|
        newrule = r.dup
        newrule['proto'] = p
        append << newrule
      }
      delete << r
    end

  }
  delete.each { |r|
    acl['rules'].delete(r)
  }
  acl['rules'].concat(append)

  # Next, bucket these by what combination of allow/deny and
  # ingress/egress rule they are. If we have more than one
  # classification
  rules_by_class = {
    "allow-ingress" => [],
    "allow-egress" => [],
    "deny-ingress" => [],
    "deny-egress" => [],
  }

  acl['rules'].each { |rule|
    if rule['deny']
      if rule['egress']
        rules_by_class["deny-egress"] << rule
      else
        rules_by_class["deny-ingress"] << rule
      end
    else
      if rule['egress']
        rules_by_class["allow-egress"] << rule
      else
        rules_by_class["allow-ingress"] << rule
      end
    end
  }

  rules_by_class.reject! { |_k, v| v.size == 0 }

  # Generate other firewall rule objects to cover the other behaviors
  # we've requested, if indeed we've done so.
  if rules_by_class.size > 1
    keep = rules_by_class.keys.first
    acl['rules'] = rules_by_class[keep]
    rules_by_class.delete(keep)
    rules_by_class.each_pair { |behaviors, rules|
      newrule = acl.dup
      newrule['name'] += "-"+behaviors
      newrule['rules'] = rules
      ok = false if !config.insertKitten(newrule, "firewall_rules")

    }
  end

  ok
end

Public Instance Methods

addRule(hosts, proto: "tcp", port: nil, egress: false, port_range: "0-65535") click to toggle source

Insert a rule into an existing security group.

@param hosts [Array<String>]: An array of CIDR network addresses to which this rule will apply. @param proto [String]: One of “tcp,” “udp,” or “icmp” @param port [Integer]: A port number. Only valid with udp or tcp. @param egress [Boolean]: Whether this is an egress ruleset, instead of ingress. @param port_range [String]: A port range descriptor (e.g. 0-65535). Only valid with udp or tcp. @return [void]

# File modules/mu/providers/google/firewall_rule.rb, line 163
def addRule(hosts, proto: "tcp", port: nil, egress: false, port_range: "0-65535")
end
create() click to toggle source

Called by {MU::Deploy#createResources}

# File modules/mu/providers/google/firewall_rule.rb, line 45
        def create
          @cloud_id = @mu_name.downcase.gsub(/[^-a-z0-9]/, "-")

          vpc_id = if @vpc
            @vpc.url if !@vpc.nil?
          else
            vpc_ref = MU::Config::Ref.get(@config['vpc'])
            if !vpc_ref.kitten
              MU.log "Failed to resolve VPC for FirewallRule #{@mu_name}", MU::ERR, details: @config['vpc']
              raise MuError, "Failed to resolve VPC for FirewallRule #{@mu_name}"
            else
              vpc_ref.kitten.url
            end
          end

          if vpc_id.nil?
            raise MuError, "Failed to resolve VPC for #{self}"
          end

          params = {
            :name => @cloud_id,
            :network => vpc_id
          }

          @config['rules'].each { |rule|
            srcs = []
            ruleobj = nil
# XXX 'all' and 'standard' keywords
            if ["tcp", "udp"].include?(rule['proto']) and (rule['port_range'] or rule['port'])
              ruleobj = MU::Cloud::Google.compute(:Firewall)::Allowed.new(
                ip_protocol: rule['proto'],
                ports: [rule['port_range'] || rule['port']]
              )
            else
              ruleobj = MU::Cloud::Google.compute(:Firewall)::Allowed.new(
                ip_protocol: rule['proto']
              )
            end
            if rule['hosts']
              rule['hosts'].each { |cidr| srcs << cidr }
            end

            dir = (rule["ingress"] or !rule["egress"]) ? "INGRESS" : "EGRESS"
            if params[:direction] and params[:direction] != dir
              MU.log "Google Cloud firewalls cannot mix ingress and egress rules", MU::ERR, details: @config['rules']
              raise MuError, "Google Cloud firewalls cannot mix ingress and egress rules"
            end

            params[:direction] = dir

            if @deploy
              params[:description] = @deploy.deploy_id
            end
            filters = if dir == "INGRESS"
              ['source_service_accounts', 'source_tags']
            else
              ['target_service_accounts', 'target_tags']
            end
            filters.each { |filter|
              if config[filter] and config[filter].size > 0
                params[filter.to_sym] = config[filter].dup
              end
            }
            action = rule['deny'] ? :denied : :allowed
            params[action] ||= []
            params[action] << ruleobj
            ipparam = dir == "INGRESS" ? :source_ranges : :destination_ranges
            params[ipparam] ||= []
            params[ipparam].concat(srcs)
            params[:priority] = rule['weight'] if rule['weight']
          }

          fwobj = MU::Cloud::Google.compute(:Firewall).new(params)
          MU.log "Creating firewall #{@cloud_id} in project #{@project_id}", details: fwobj
begin
  MU::Cloud::Google.compute(credentials: @config['credentials']).insert_firewall(@project_id, fwobj)
rescue ::Google::Apis::ClientError => e
  MU.log @config['project']+"/"+@config['name']+": "+@cloud_id, MU::ERR, details: @config['vpc']
  MU.log e.inspect, MU::ERR, details: fwobj
  if e.message.match(/Invalid value for field/)
    dependencies(use_cache: false, debug: true)
  end
  raise e
end
          # Make sure it actually got made before we move on
          desc = nil
          begin
            desc = MU::Cloud::Google.compute(credentials: @config['credentials']).get_firewall(@project_id, @cloud_id)
            sleep 1
          end while desc.nil?
          desc
        end
groom() click to toggle source

Called by {MU::Deploy#createResources}

# File modules/mu/providers/google/firewall_rule.rb, line 139
def groom
end
notify() click to toggle source

Log metadata about this ruleset to the currently running deployment

# File modules/mu/providers/google/firewall_rule.rb, line 143
def notify
  sg_data = MU.structToHash(
    MU::Cloud::Google::FirewallRule.find(cloud_id: @cloud_id, region: @config['region'])
  )
  sg_data ||= {}
  sg_data["group_id"] = @cloud_id
  sg_data["project_id"] = habitat_id
  sg_data["cloud_id"] = @cloud_id

  return sg_data
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/firewall_rule.rb, line 230
def toKitten(**_args)

  if cloud_desc.name.match(/^[a-f0-9]+$/)
    gke_ish = true
    cloud_desc.target_tags.each { |tag|
      gke_ish = false if !tag.match(/^gke-/)
    }
    if gke_ish
      MU.log "FirewallRule #{cloud_desc.name} appears to belong to a ContainerCluster, skipping adoption", MU::DEBUG
      return nil
    end
  end

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

  bok['rules'] = []
  bok['name'] = cloud_desc.name.dup
  bok['cloud_id'] = cloud_desc.name.dup


  cloud_desc.network.match(/(?:^|\/)projects\/(.*?)\/.*?\/networks\/([^\/]+)(?:$|\/)/)
  vpc_proj = Regexp.last_match[1]
  vpc_id = Regexp.last_match[2]

  if vpc_id == "default" and !@config['project']
    raise MuError, "FirewallRule toKitten: I'm in 'default' VPC but can't figure out what project I'm in"
  end

  # XXX make sure this is sane (that these rules come with default VPCs)
  if vpc_id == "default" and ["default-allow-icmp", "default-allow-http"].include?(cloud_desc.name)
    return nil
  end

  if vpc_id != "default"
    bok['vpc'] = MU::Config::Ref.get(
      id: vpc_id,
      habitat: MU::Config::Ref.get(
        id: vpc_proj,
        cloud: "Google",
        credentials: @credentials,
        type: "habitats"
      ),
      cloud: "Google",
      credentials: @config['credentials'],
      type: "vpcs"
    )
  end

  byport = {}

  rule_list = []
  is_deny = false
  if cloud_desc.denied
    rule_list = cloud_desc.denied
    is_deny = true
  else
    rule_list = cloud_desc.allowed
  end

  rule_list.each { |rule|
    hosts = if cloud_desc.direction == "INGRESS"
      cloud_desc.source_ranges ? cloud_desc.source_ranges : ["0.0.0.0/0"]
    else
      cloud_desc.destination_ranges ? cloud_desc.destination_ranges : ["0.0.0.0/0"]
    end
    hosts.map! { |h|
      h = h+"/32" if h.match(/^\d+\.\d+\.\d+\.\d+$/)
      h
    }
    proto = rule.ip_protocol ? rule.ip_protocol : "all"

    if rule.ports
      rule.ports.each { |ports|
        ports = "0-65535" if ["1-65535", "1-65536", "0-65536"].include?(ports)
        byport[ports] ||= {}
        byport[ports][hosts] ||= []
        byport[ports][hosts] << proto
      }
    else
      byport["0-65535"] ||= {}
      byport["0-65535"][hosts] ||= []
      byport["0-65535"][hosts] << proto
    end

  }

  byport.each_pair { |ports, hostlists|
    hostlists.each_pair { |hostlist, protos|
      protolist = if protos.sort.uniq == PROTOS.sort.uniq
        ["all"]
      elsif protos.sort.uniq == ["icmp", "tcp", "udp"]
        ["standard"]
      else
        protos
      end
      protolist.each { |proto|
        rule = {
          "proto" => proto,
          "hosts" => hostlist
        }
        rule["deny"] = true if is_deny
        if cloud_desc.priority and cloud_desc.priority != 1000
          rule["weight"] = cloud_desc.priority
        end
        if ports.match(/-/)
          rule["port_range"] = ports
        else
          rule["port"] = ports.to_i
        end
        if cloud_desc.source_service_accounts
          rule["source_service_accounts"] = cloud_desc.source_service_accounts
        end
        if cloud_desc.source_tags
          rule["source_tags"] = cloud_desc.source_tags
        end
        if cloud_desc.target_service_accounts
          rule["target_service_accounts"] = cloud_desc.target_service_accounts
        end
        if cloud_desc.target_tags
          rule["target_tags"] = cloud_desc.target_tags
        end
        if cloud_desc.direction == "EGRESS"
          rule['egress'] = true
          rule['ingress'] = false
        end
        bok['rules'] << rule
      }
    }
  }

  bok
end