diff --git a/README.md b/README.md index 6e724615..394eeee8 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,7 @@ This module manages firewalld, the userland interface that replaces iptables and ships with RHEL7+. The module manages firewalld itself as well as providing types and providers for managing firewalld zones, -ports, and rich rules. +policies, ports, and rich rules. ## Compatibility @@ -48,6 +48,7 @@ class { 'firewalld': } * `log_denied`: Optional, (firewalld-0.4.3.2-8+) Log denied packets, can be one of `off`, `all`, `multicast`, `unicast`, `broadcast` (default: undef) * `zones`: A hash of [firewalld zones](#firewalld-zones) to configure +* `policies`: A hash of [firewalld policies](#firewalld-policies) to configure * `ports`: A hash of [firewalld ports](#firewalld-ports) to configure * `services`: A hash of [firewalld services](#firewalld-service) to configure * `rich_rules`: A hash of [firewalld rich rules](#firewalld-rich-rules) to configure @@ -78,6 +79,7 @@ changes. This module supports a number of resource types * [firewalld_zone](#firewalld-zones) +* [firewalld_policy](#firewalld-policies) * [firewalld_port](#firewalld-ports) * [firewalld_service](#firewalld-service) * [firewalld_ipset](#firewalld-ipsets) @@ -146,14 +148,71 @@ firewalld::zones: includes the default ssh port. If you fail to specify the appropriate port, rich rule, or service, you will lock yourself out. +### Firewalld policies + +Firewalld policies can be managed with the `firewalld_policy` resource type. + +_Example in Class_: + +```puppet + firewalld_policy { 'anytorestricted': + ensure => present, + target => '%%REJECT%%', + ingress_zones => ['ANY'], + egress_zones => ['restricted'], + purge_rich_rules => true, + purge_services => true, + purge_ports => true, + } +``` + +_Example in Hiera_: + +```yaml +firewalld::policies: + anytorestricted: + ensure: present + target: '%%REJECT%%' + ingress_zones: + - 'ANY' + egress_zones: + - 'restricted' + purge_rich_rules: true + purge_services: true + purge_ports: true +``` + +#### Parameters (Firewalld policies) + +* `target`: Specify the target of the policy. +* `ingress_zones`: An array of ingress zones for this policy. +* `egress_zones`: An array of egress zones for this policy. +* `priority`: A non zero integer specifying the priority of this + policy, policies with negative priorities apply before rules in + zones, policies with positive priorities, after. Defaults to -1. +* `icmp_blocks`: An array of ICMP blocks for the policy +* `masquerade`: If set to `true` or `false` specifies whether or not + to add masquerading to the policy +* `purge_rich_rules`: Optional, and defaulted to false. When true any + configured rich rules found in the policy that do not match what is in + the Puppet catalog will be purged. +* `purge_services`: Optional, and defaulted to false. When true any + configured services found in the policy that do not match what is in + the Puppet catalog will be purged. +* `purge_ports`: Optional, and defaulted to false. When true any + configured ports found in the policy that do not match what is in the + Puppet catalog will be purged. + ### Firewalld Rich Rules Firewalld rich rules are managed using the `firewalld_rich_rule` resource type +Exactly one of the `zone` or `policy` parameters must be given + firewalld_rich_rules will `autorequire` the firewalld_zone specified -in the `zone` parameter so there is no need to add dependencies for -this +in the `zone` parameter or the firewalld_policy specified in the +`policy` parameter so there is no need to add dependencies for this _Example in Class_: @@ -181,7 +240,9 @@ firewalld::rich_rules: #### Parameters (Firewalld Rich Rules) -* `zone`: Name of the zone this rich rule belongs to +* `zone`: (Optional) Name of the zone this rich rule belongs to + +* `policy`: (Optional) Name of the policy this rich rule belongs to * `family`: Protocol family, defaults to `ipv4` @@ -403,6 +464,8 @@ will produce: The `firewalld_service` type is used to add or remove both built in and custom services from zones. +Exactly one of the `zone` or `policy` parameters must be given. + firewalld_service will `autorequire` the firewalld_zone specified in the `zone` parameter and the firewalld::custom_service specified in the `service` parameter, so there is no need to add dependencies for @@ -445,6 +508,10 @@ firewalld::services: defaults to parameter `default_service_zone` of class `firewalld` if specified. +* `policy`: Name of the policy in which you want to manage the + service. Make sure to set `zone` to `unset` if you use this and have + specified `default_service_zone` for class `firewalld`. + * `ensure`: Whether to add (`present`) or remove the service (`absent`), defaults to `present`. @@ -488,6 +555,8 @@ options of an ipset you must delete the existing ipset first. Firewalld ports can be managed with the `firewalld_port` resource type. +Exactly one of the `zone` or `policy` parameters must be given. + firewalld_port will `autorequire` the firewalld_zone specified in the `zone` parameter so there is no need to add dependencies for this @@ -518,6 +587,10 @@ firewalld::ports: * `zone`: Name of the zone this port belongs to, defaults to parameter `default_port_zone` of class `firewalld` if specified. +* `policy`: Name of the policy this port belongs to. Make sure to set + `zone` to `unset` if you use this and have specified + `default_port_zone` for class `firewalld`. + * `port`: The port to manage, defaults to the resource name. * `protocol`: The protocol this port uses, e.g. `tcp` or `udp`, diff --git a/lib/puppet/provider/firewalld.rb b/lib/puppet/provider/firewalld.rb index 017da166..c3b74916 100644 --- a/lib/puppet/provider/firewalld.rb +++ b/lib/puppet/provider/firewalld.rb @@ -27,7 +27,7 @@ def check_running_state def self.check_running_state debug("Executing --state command - current value #{@state}") - ret = execute_firewall_cmd(['--state'], nil, false, false, false) + ret = execute_firewall_cmd(['--state'], nil, nil, false, false, false) ret.exitstatus.zero? rescue Puppet::MissingCommand # This exception is caught in case the module is being run before @@ -43,7 +43,7 @@ def self.check_running_state end # v3.0.0 - def self.execute_firewall_cmd(args, zone = nil, perm = true, failonfail = true, check_online = true) + def self.execute_firewall_cmd(args, zone = nil, policy = nil, perm = true, failonfail = true, check_online = true) if check_online && !online? shell_cmd = 'firewall-offline-cmd' perm = false @@ -53,6 +53,7 @@ def self.execute_firewall_cmd(args, zone = nil, perm = true, failonfail = true, cmd_args = [] cmd_args << '--permanent' if perm cmd_args << ['--zone', zone] unless zone.nil? + cmd_args << ['--policy', policy] unless policy.nil? # Add the arguments to our command string, removing any quotes, the command # provider will sort the quotes out. @@ -74,7 +75,11 @@ def self.execute_firewall_cmd(args, zone = nil, perm = true, failonfail = true, end def execute_firewall_cmd(args, zone = @resource[:zone], perm = true, failonfail = true) - self.class.execute_firewall_cmd(args, zone, perm, failonfail) + self.class.execute_firewall_cmd(args, zone, nil, perm, failonfail) + end + + def execute_firewall_cmd_policy(args, policy = @resource[:policy], perm = true, failonfail = true) + self.class.execute_firewall_cmd(args, nil, policy, perm, failonfail) end # Arguments should be parsed as separate array entities, but quoted arg diff --git a/lib/puppet/provider/firewalld_policy/firewall_cmd.rb b/lib/puppet/provider/firewalld_policy/firewall_cmd.rb new file mode 100644 index 00000000..a01d887e --- /dev/null +++ b/lib/puppet/provider/firewalld_policy/firewall_cmd.rb @@ -0,0 +1,221 @@ +require 'puppet' +require 'puppet/type' +require File.join(File.dirname(__FILE__), '..', 'firewalld.rb') + +Puppet::Type.type(:firewalld_policy).provide( + :firewall_cmd, + parent: Puppet::Provider::Firewalld +) do + desc 'Interact with firewall-cmd' + + def exists? + @resource[:policy] = @resource[:name] + execute_firewall_cmd_policy(['--get-policies'], nil).split(' ').include?(@resource[:name]) + end + + def create + debug("Creating new policy #{@resource[:name]} with target: '#{@resource[:target]}'") + execute_firewall_cmd_policy(['--new-policy', @resource[:name]], nil) + + self.target = (@resource[:target]) if @resource[:target] + self.ingress_zones = @resource[:ingress_zones] + self.egress_zones = @resource[:egress_zones] + self.priority = @resource[:priority] if @resource[:priority] + self.icmp_blocks = (@resource[:icmp_blocks]) if @resource[:icmp_blocks] + self.description = (@resource[:description]) if @resource[:description] + self.short = (@resource[:short]) if @resource[:short] + end + + def destroy + debug("Deleting policy #{@resource[:name]}") + execute_firewall_cmd_policy(['--delete-policy', @resource[:name]], nil) + end + + def target + policy_target = execute_firewall_cmd_policy(['--get-target']).chomp + # The firewall-cmd may or may not return the target surrounded by + # %% depending on the version. See: + # https://github.com/crayfishx/puppet-firewalld/issues/111 + return @resource[:target] if @resource[:target].delete('%') == policy_target + policy_target + end + + def target=(_t) + debug("Setting target for policy #{@resource[:name]} to #{@resource[:target]}") + execute_firewall_cmd_policy(['--set-target', @resource[:target]]) + end + + def ingress_zones + execute_firewall_cmd_policy(['--list-ingress-zones']).chomp.split(' ') || [] + end + + def ingress_zones=(new_ingress_zones) + new_ingress_zones ||= [] + cur_ingress_zones = ingress_zones + # Current zones that the policy is not applied to in the catalog + # must be removed first or we might end up trying to add a regular + # zone to a policy that currently applies to ANY or HOST. + (cur_ingress_zones - new_ingress_zones).each do |i| + debug("Removing ingress zone '#{i}' from policy #{@resource[:name]}") + execute_firewall_cmd_policy(['--remove-ingress-zone', i]) + end + (new_ingress_zones - cur_ingress_zones).each do |i| + debug("Adding ingress zone '#{i}' to policy #{@resource[:name]}") + execute_firewall_cmd_policy(['--add-ingress-zone', i]) + end + end + + def egress_zones + execute_firewall_cmd_policy(['--list-egress-zones']).chomp.split(' ') || [] + end + + def egress_zones=(new_egress_zones) + new_egress_zones ||= [] + cur_egress_zones = egress_zones + # Current zones that the policy is not applied to in the catalog + # must be removed first or we might end up trying to add a regular + # zone to a policy that currently applies to ANY or HOST. + (cur_egress_zones - new_egress_zones).each do |i| + debug("Removing egress zone '#{i}' from policy #{@resource[:name]}") + execute_firewall_cmd_policy(['--remove-egress-zone', i]) + end + (new_egress_zones - cur_egress_zones).each do |i| + debug("Adding egress zone '#{i}' to policy #{@resource[:name]}") + execute_firewall_cmd_policy(['--add-egress-zone', i]) + end + end + + def priority + execute_firewall_cmd_policy(['--get-priority']).chomp + end + + def priority=(new_priority) + execute_firewall_cmd_policy(['--set-priority', new_priority]) + end + + def masquerade + if execute_firewall_cmd_policy(['--query-masquerade'], @resource[:name], true, false).chomp == 'yes' + :true + else + :false + end + end + + def masquerade=(bool) + case bool + when :true + execute_firewall_cmd_policy(['--add-masquerade']) + when :false + execute_firewall_cmd_policy(['--remove-masquerade']) + end + end + + def icmp_blocks + get_icmp_blocks + end + + def icmp_blocks=(i) + set_blocks = [] + remove_blocks = [] + + icmp_types = get_icmp_types + + case i + when Array then + get_icmp_blocks.each do |remove_block| + unless i.include?(remove_block) + debug("removing block #{remove_block} from policy #{@resource[:name]}") + remove_blocks.push(remove_block) + end + end + + i.each do |block| + raise Puppet::Error, 'parameter icmp_blocks must be a string or array of strings!' unless block.is_a?(String) + if icmp_types.include?(block) + debug("adding block #{block} to policy #{@resource[:name]}") + set_blocks.push(block) + else + valid_types = icmp_types.join(', ') + raise Puppet::Error, "#{block} is not a valid icmp type on this system! Valid types are: #{valid_types}" + end + end + when String then + get_icmp_blocks.reject { |x| x == i }.each do |remove_block| + debug("removing block #{remove_block} from policy #{@resource[:name]}") + remove_blocks.push(remove_block) + end + if icmp_types.include?(i) + debug("adding block #{i} to policy #{@resource[:name]}") + set_blocks.push(i) + else + valid_types = icmp_types.join(', ') + raise Puppet::Error, "#{i} is not a valid icmp type on this system! Valid types are: #{valid_types}" + end + else + raise Puppet::Error, 'parameter icmp_blocks must be a string or array of strings!' + end + unless remove_blocks.empty? + remove_blocks.each do |block| + execute_firewall_cmd_policy(['--remove-icmp-block', block]) + end + end + unless set_blocks.empty? # rubocop:disable Style/GuardClause + set_blocks.each do |block| + execute_firewall_cmd_policy(['--add-icmp-block', block]) + end + end + end + + # rubocop:disable Style/AccessorMethodName + def get_rules + perm = execute_firewall_cmd_policy(['--list-rich-rules']).split(%r{\n}) + curr = execute_firewall_cmd_policy(['--list-rich-rules'], @resource[:name], false).split(%r{\n}) + [perm, curr].flatten.uniq + end + + def get_services + perm = execute_firewall_cmd_policy(['--list-services']).split(' ') + curr = execute_firewall_cmd_policy(['--list-services'], @resource[:name], false).split(' ') + [perm, curr].flatten.uniq + end + + def get_ports + perm = execute_firewall_cmd_policy(['--list-ports']).split(' ') + curr = execute_firewall_cmd_policy(['--list-ports'], @resource[:name], false).split(' ') + + [perm, curr].flatten.uniq.map do |entry| + port, protocol = entry.split(%r{/}) + debug("get_ports() Found port #{port} protocol #{protocol}") + { 'port' => port, 'protocol' => protocol } + end + end + + def get_icmp_blocks + execute_firewall_cmd_policy(['--list-icmp-blocks']).split(' ').sort + end + + def get_icmp_types + execute_firewall_cmd_policy(['--get-icmptypes'], nil).split(' ') + end + # rubocop:enable Style/AccessorMethodName + + def description + execute_firewall_cmd_policy(['--get-description'], @resource[:name], true, false) + end + + def description=(new_description) + execute_firewall_cmd_policy(['--set-description', new_description], @resource[:name], true, false) + end + + def short + execute_firewall_cmd_policy(['--get-short'], @resource[:name], true, false) + end + + def short=(new_short) + execute_firewall_cmd_policy(['--set-short', new_short], @resource[:name], true, false) + end + + def flush + reload_firewall + end +end diff --git a/lib/puppet/provider/firewalld_port/firewall_cmd.rb b/lib/puppet/provider/firewalld_port/firewall_cmd.rb index 9e56457c..32032329 100644 --- a/lib/puppet/provider/firewalld_port/firewall_cmd.rb +++ b/lib/puppet/provider/firewalld_port/firewall_cmd.rb @@ -11,7 +11,16 @@ def exists? @rule_args ||= build_port_rule - output = execute_firewall_cmd(['--query-port', @rule_args], @resource[:zone], true, false) + + output = if @resource[:zone] == :unset + execute_firewall_cmd_policy(['--query-port', @rule_args], + @resource[:policy], + true, false) + else + execute_firewall_cmd(['--query-port', @rule_args], + @resource[:policy], + true, false) + end output.exitstatus.zero? end @@ -32,10 +41,18 @@ def build_port_rule end def create - execute_firewall_cmd(['--add-port', build_port_rule]) + if @resource[:zone] == :unset + execute_firewall_cmd_policy(['--add-port', build_port_rule]) + else + execute_firewall_cmd(['--add-port', build_port_rule]) + end end def destroy - execute_firewall_cmd(['--remove-port', build_port_rule]) + if @resource[:zone] == :unset + execute_firewall_cmd_policy(['--remove-port', build_port_rule]) + else + execute_firewall_cmd(['--remove-port', build_port_rule]) + end end end diff --git a/lib/puppet/provider/firewalld_rich_rule/firewall_cmd.rb b/lib/puppet/provider/firewalld_rich_rule/firewall_cmd.rb index 3112f55b..46abab74 100644 --- a/lib/puppet/provider/firewalld_rich_rule/firewall_cmd.rb +++ b/lib/puppet/provider/firewalld_rich_rule/firewall_cmd.rb @@ -11,7 +11,15 @@ def exists? @rule_args ||= build_rich_rule - output = execute_firewall_cmd(['--query-rich-rule', @rule_args], @resource[:zone], true, false) + output = if @resource[:zone] == :unset + execute_firewall_cmd_policy(['--query-rich-rule', @rule_args], + @resource[:policy], + true, false) + else + execute_firewall_cmd(['--query-rich-rule', @rule_args], + @resource[:zone], + true, false) + end output.exitstatus.zero? end @@ -124,10 +132,18 @@ def build_rich_rule end def create - execute_firewall_cmd(['--add-rich-rule', build_rich_rule]) + if @resource[:zone] == :unset + execute_firewall_cmd_policy(['--add-rich-rule', build_rich_rule]) + else + execute_firewall_cmd(['--add-rich-rule', build_rich_rule]) + end end def destroy - execute_firewall_cmd(['--remove-rich-rule', build_rich_rule]) + if @resource[:zone] == :unset + execute_firewall_cmd_policy(['--remove-rich-rule', build_rich_rule]) + else + execute_firewall_cmd(['--remove-rich-rule', build_rich_rule]) + end end end diff --git a/lib/puppet/provider/firewalld_service/firewall_cmd.rb b/lib/puppet/provider/firewalld_service/firewall_cmd.rb index 8e98aa97..06ccf4b4 100644 --- a/lib/puppet/provider/firewalld_service/firewall_cmd.rb +++ b/lib/puppet/provider/firewalld_service/firewall_cmd.rb @@ -8,12 +8,20 @@ desc 'Interact with firewall-cmd' def exists? - execute_firewall_cmd(['--list-services']).split(' ').include?(@resource[:service]) + if @resource[:zone] == :unset + execute_firewall_cmd_policy(['--list-services']).split(' ').include?(@resource[:service]) + else + execute_firewall_cmd(['--list-services']).split(' ').include?(@resource[:service]) + end end def create debug("Adding new service to firewalld: #{@resource[:service]}") - execute_firewall_cmd(['--add-service', @resource[:service]]) + if @resource[:zone] == :unset + execute_firewall_cmd_policy(['--add-service', @resource[:service]]) + else + execute_firewall_cmd(['--add-service', @resource[:service]]) + end reload_firewall end @@ -26,7 +34,11 @@ def destroy '--remove-service-from-zone' end - execute_firewall_cmd([flag, @resource[:service]]) + if @resource[:zone] == :unset + execute_firewall_cmd_policy([flag, @resource[:service]]) + else + execute_firewall_cmd([flag, @resource[:service]]) + end reload_firewall end end diff --git a/lib/puppet/type/firewalld_policy.rb b/lib/puppet/type/firewalld_policy.rb new file mode 100644 index 00000000..b5f88a50 --- /dev/null +++ b/lib/puppet/type/firewalld_policy.rb @@ -0,0 +1,308 @@ +require 'puppet' +require 'puppet/parameter/boolean' + +Puppet::Type.newtype(:firewalld_policy) do + # Reference the types here so we know they are loaded + # + Puppet::Type.type(:firewalld_rich_rule) + Puppet::Type.type(:firewalld_service) + Puppet::Type.type(:firewalld_port) + + desc <<-DOC + @summary + Creates and manages firewalld policies. + + Creates and manages firewalld policies. + + Note that setting `ensure => 'absent'` to the built in firewalld + policies will not work, and will generate an error. This is a + limitation of firewalld itself, not the module. + + @example Create a policy called `anytorestricted` + firewalld_policy { 'anytorestricted': + ensure => present, + target => '%%REJECT%%', + ingress_zones => ['ANY'], + egress_zones => ['restricted'], + purge_rich_rules => true, + purge_services => true, + purge_ports => true, + icmp_blocks => 'router-advertisement' + } + DOC + + ensurable do + defaultvalues + defaultto :present + end + + # When set to 1 these variables cause the purge_* options to + # indicate to Puppet that we are in a changed state + # + attr_reader :rich_rules_purgable + attr_reader :services_purgable + attr_reader :ports_purgable + + def generate + return [] unless Puppet::Provider::Firewalld.available? + purge_rich_rules if self[:purge_rich_rules] == :true + purge_services if self[:purge_services] == :true + purge_ports if self[:purge_ports] == :true + [] + end + + newparam(:name) do + desc 'Name of the rule resource in Puppet' + isnamevar + end + + newparam(:policy) do + desc 'Name of the policy' + end + + newparam(:description) do + desc 'Description of the policy to add' + end + + newparam(:short) do + desc 'Short description of the policy to add' + end + + newproperty(:target) do + desc 'Specify the target for the policy' + end + + newproperty(:ingress_zones, array_matching: :all) do + desc 'Specify the ingress zones for the policy' + + validate do |value| + return if value == :unset + + if value.length == 0 + raise Puppet::Error, 'parameter ingress_zones must contain at least one zone' + end + + return if value.length == 1 + + if value.include?('HOST') or value.include?('ANY') + raise Puppet::Error, 'parameter ingress_zones must contain a single symbolic zone or one or more regular zones' + end + end + + def insync?(is) + case should + when String then should.lines.sort == is.sort + when Array then should.sort == is.sort + else raise Puppet::Error, 'parameter ingress_zones must be a string or array of strings!' + end + end + end + + newproperty(:egress_zones, array_matching: :all) do + desc 'Specify the egress zones for the policy' + + validate do |value| + return if value == :unset + + if value.length == 0 + raise Puppet::Error, 'egress egress_zones must contain at least one zone' + end + + return if value.length == 1 + + if value.include?('HOST') or value.include?('ANY') + raise Puppet::Error, 'parameter egress_zones must contain a single symbolic zone or one or more regular zones' + end + end + + def insync?(is) + case should + when String then should.lines.sort == is.sort + when Array then should.sort == is.sort + else raise Puppet::Error, 'parameter egress_zones must be a string or array of strings!' + end + end + end + + newproperty(:priority) do + desc 'The priority of the policy as an integer (default -1)' + + defaultto('-1') + + validate do |value| + begin + Integer(value) + rescue + raise Puppet::Error, 'parameter priority must be a non zero integer' + end + + if Integer(value) == 0 + raise Puppet::Error, 'parameter priority must be non zero' + end + end + end + + newproperty(:masquerade) do + desc 'Can be set to true or false, specifies whether to add or remove masquerading from the policy' + newvalue(:true) + newvalue(:false) + end + + newproperty(:icmp_blocks, array_matching: :all) do + desc "Specify the icmp-blocks for the policy. Can be a single string specifying one icmp type, + or an array of strings specifying multiple icmp types. Any blocks not specified here will be removed + " + def insync?(is) + case should + when String then should.lines.sort == is.sort + when Array then should.sort == is.sort + else raise Puppet::Error, 'parameter icmp_blocks must be a string or array of strings!' + end + end + end + + newproperty(:purge_rich_rules) do + desc "When set to true any rich_rules associated with this policy + that are not managed by Puppet will be removed. + " + newvalue(:false) + newvalue(:true) do + true + end + + def retrieve + return :false if @resource[:purge_rich_rules] == :false + provider.resource.rich_rules_purgable ? :purgable : :true + end + end + + newproperty(:purge_services) do + desc "When set to true any services associated with this policy + that are not managed by Puppet will be removed. + " + newvalue(:false) + newvalue(:true) do + true + end + + def retrieve + return :false if @resource[:purge_services] == :false + provider.resource.services_purgable ? :purgable : :true + end + end + + newproperty(:purge_ports) do + desc "When set to true any ports associated with this policy + that are not managed by Puppet will be removed." + newvalue :false + newvalue(:true) do + true + end + + def retrieve + return :false if @resource[:purge_ports] == :false + provider.resource.ports_purgable ? :purgable : :true + end + end + + validate do + [:policy, :name].each do |attr| + if self[attr] && (self[attr]).to_s.length > 17 + raise(Puppet::Error, "Policy identifier '#{attr}' must be less than 18 characters long") + end + end + end + + autorequire(:service) do + ['firewalld'] + end + + autorequire(:firewalld_zone) do + (self[:ingress_zones] != :unset ? self[:ingress_zones] : []) + (self[:egress_zones] != :unset ? self[:egress_zones] : []) + end + + def purge_resource(res_type) + if Puppet.settings[:noop] || self[:noop] + Puppet.debug "Would have purged #{res_type.ref}, (noop)" + else + Puppet.debug "Purging #{res_type.ref}" + res_type.provider.destroy if res_type.provider.exists? + end + end + + def purge_rich_rules + return [] unless provider.exists? + puppet_rules = [] + catalog.resources.select { |r| r.is_a?(Puppet::Type::Firewalld_rich_rule) }.each do |fwr| + if fwr[:policy] == self[:name] + debug("not purging puppet controlled rich rule #{fwr[:name]}") + puppet_rules << fwr.provider.build_rich_rule + end + end + provider.get_rules.reject { |p| puppet_rules.include?(p) }.each do |purge| + debug("should purge rich rule #{purge}") + res_type = Puppet::Type.type(:firewalld_rich_rule).new( + name: purge, + raw_rule: purge, + ensure: :absent, + policy: self[:name] + ) + + # If the rule exists in --permanent then we should purge it + # + purge_resource(res_type) + + # Even if it doesn't exist, it may be a running rule, so we + # flag purge_rich_rules as changed so Puppet will reload + # the firewall and drop orphaned running rules + # + @rich_rules_purgable = true + end + end + + def purge_services + return [] unless provider.exists? + puppet_services = [] + catalog.resources.select { |r| r.is_a?(Puppet::Type::Firewalld_service) }.each do |fws| + if fws[:policy] == self[:name] + debug("not purging puppet controlled service #{fws[:service]}") + puppet_services << (fws[:service]).to_s + end + end + provider.get_services.reject { |p| puppet_services.include?(p) }.each do |purge| + debug("should purge service #{purge}") + res_type = Puppet::Type.type(:firewalld_service).new( + name: "#{self[:name]}-#{purge}", + ensure: :absent, + service: purge, + policy: self[:name] + ) + + purge_resource(res_type) + @services_purgable = true + end + end + + def purge_ports + return [] unless provider.exists? + puppet_ports = [] + catalog.resources.select { |r| r.is_a?(Puppet::Type::Firewalld_port) }.each do |fwp| + if fwp[:policy] == self[:name] + debug("Not purging puppet controlled port #{fwp[:port]}") + puppet_ports << { 'port' => fwp[:port], 'protocol' => fwp[:protocol] } + end + end + provider.get_ports.reject { |p| puppet_ports.include?(p) }.each do |purge| + debug("Should purge port #{purge['port']} proto #{purge['protocol']}") + res_type = Puppet::Type.type(:firewalld_port).new( + name: "#{self[:name]}-#{purge['port']}-#{purge['protocol']}-purge", + port: purge['port'], + ensure: :absent, + protocol: purge['protocol'], + policy: self[:name] + ) + purge_resource(res_type) + @ports_purgable = true + end + end +end diff --git a/lib/puppet/type/firewalld_port.rb b/lib/puppet/type/firewalld_port.rb index 8d25f852..a8b423a3 100644 --- a/lib/puppet/type/firewalld_port.rb +++ b/lib/puppet/type/firewalld_port.rb @@ -2,7 +2,10 @@ Puppet::Type.newtype(:firewalld_port) do @doc = "Assigns a port to a specific firewalld zone. - firewalld_port will autorequire the firewalld_zone specified in the zone parameter so there is no need to add dependencies for this + + firewalld_port will autorequire the firewalld_zone specified in + the zone parameter or the firewalld_policy specified in the policy + parameter so there is no need to add dependencies for this Example: @@ -31,7 +34,15 @@ end newparam(:zone) do - desc 'Name of the zone to which you want to add the port' + desc 'Name of the zone to which you want to add the port, exactly one of zone and policy must be supplied' + + defaultto(:unset) + end + + newparam(:policy) do + desc 'Name of the policy to which you want to add the port, exactly one of zone and policy must be supplied' + + defaultto(:unset) end newparam(:port) do @@ -44,8 +55,22 @@ desc 'Specify the element as a protocol' end + validate do + if self[:zone] != :unset and self[:policy] != :unset + raise Puppet::Error, "only one of the parameters zone and policy may be supplied" + end + + if self[:zone] == :unset and self[:policy] == :unset + raise Puppet::Error, "one of the parameters zone and policy must be supplied" + end + end + autorequire(:firewalld_zone) do - self[:zone] + self[:zone] if self[:zone] != :unset + end + + autorequire(:firewalld_policy) do + self[:policy] if self[:policy] != :unset end autorequire(:service) do diff --git a/lib/puppet/type/firewalld_rich_rule.rb b/lib/puppet/type/firewalld_rich_rule.rb index ff3f383e..48fca958 100644 --- a/lib/puppet/type/firewalld_rich_rule.rb +++ b/lib/puppet/type/firewalld_rich_rule.rb @@ -1,7 +1,9 @@ Puppet::Type.newtype(:firewalld_rich_rule) do @doc = "Manages firewalld rich rules. - firewalld_rich_rules will autorequire the firewalld_zone specified in the zone parameter so there is no need to add dependencies for this + firewalld_rich_rules will autorequire the firewalld_zone specified + in the zone parameter or the firewalld_policy specified in the + policy parameter so there is no need to add dependencies for this Example: @@ -26,7 +28,15 @@ end newparam(:zone) do - desc 'Name of the zone' + desc 'Name of the zone to attach the rich rule to, exactly one of zone and policy must be supplied' + + defaultto(:unset) + end + + newparam(:policy) do + desc 'Name of the policy to attach the rich rule to, exactly one of zone and policy must be supplied' + + defaultto(:unset) end newparam(:family) do @@ -114,8 +124,9 @@ def _validate_action(value) end newparam(:raw_rule) do - desc "Manage the entire rule as one string - this is used internally by firwalld_zone to - handle pruning of rules" + desc "Manage the entire rule as one string - this is used + internally by firwalld_zone and firewalld_policy to handle + pruning of rules" end def elements @@ -125,10 +136,22 @@ def elements validate do errormsg = "Only one element (#{elements.join(',')}) may be specified." raise errormsg if elements.select { |e| self[e] }.size > 1 + + if self[:zone] != :unset and self[:policy] != :unset + raise Puppet::Error, "only one of the parameters zone and policy may be supplied" + end + + if self[:zone] == :unset and self[:policy] == :unset + raise Puppet::Error, "one of the parameters zone and policy must be supplied" + end end autorequire(:firewalld_zone) do - self[:zone] + self[:zone] if self[:zone] != :unset + end + + autorequire(:firewalld_policy) do + self[:policy] if self[:policy] != :unset end autorequire(:ipset) do diff --git a/lib/puppet/type/firewalld_service.rb b/lib/puppet/type/firewalld_service.rb index 3cc6bffb..ae983d4f 100644 --- a/lib/puppet/type/firewalld_service.rb +++ b/lib/puppet/type/firewalld_service.rb @@ -7,9 +7,11 @@ Assigns a service to a specific firewalld zone. - `firewalld_service` will autorequire the `firewalld_zone` specified in the - `zone` parameter and the `firewalld::custom_service` specified in the `service` - parameter. There is no need to manually add dependencies for this. + `firewalld_service` will autorequire the `firewalld_zone` specified + in the `zone` parameter or the `firewalld_policy` specified in the + `policy` parameter and the `firewalld::custom_service` specified in + the `service` parameter. There is no need to manually add + dependencies for this. @example Allowing SSH firewalld_service {'Allow SSH in the public Zone': @@ -41,11 +43,33 @@ end newparam(:zone) do - desc 'Name of the zone to which you want to add the service' + desc 'Name of the zone to which you want to add the service, exactly one of zone and policy must be supplied' + + defaultto(:unset) + end + + newparam(:policy) do + desc 'Name of the policy to which you want to add the service, exactly one of zone and policy must be supplied' + + defaultto(:unset) + end + + validate do + if self[:zone] != :unset and self[:policy] != :unset + raise Puppet::Error, "only one of the parameters zone and policy may be supplied" + end + + if self[:zone] == :unset and self[:policy] == :unset + raise Puppet::Error, "one of the parameters zone and policy must be supplied" + end end autorequire(:firewalld_zone) do - self[:zone] + self[:zone] if self[:zone] != :unset + end + + autorequire(:firewalld_policy) do + self[:policy] if self[:policy] != :unset end autorequire(:service) do diff --git a/manifests/init.pp b/manifests/init.pp index 9446e6b2..89b55832 100644 --- a/manifests/init.pp +++ b/manifests/init.pp @@ -33,6 +33,7 @@ Boolean $install_gui = false, Boolean $service_enable = true, Hash $zones = {}, + Hash $policies = {}, Hash $ports = {}, Hash $services = {}, Hash $rich_rules = {}, @@ -99,6 +100,13 @@ } } + #...policies + $policies.each | String $key, Hash $attrs| { + firewalld_policy { $key: + * => $attrs, + } + } + #...services Firewalld_service { zone => $default_service_zone, diff --git a/spec/unit/puppet/provider/firewalld_policy_spec.rb b/spec/unit/puppet/provider/firewalld_policy_spec.rb new file mode 100644 index 00000000..71ac35f9 --- /dev/null +++ b/spec/unit/puppet/provider/firewalld_policy_spec.rb @@ -0,0 +1,72 @@ +require 'spec_helper' + +provider_class = Puppet::Type.type(:firewalld_policy).provider(:firewall_cmd) + +describe provider_class do + let(:resource) do + @resource = Puppet::Type.type(:firewalld_policy).new( + ensure: :present, + name: 'public2restricted', + description: 'Public to restricted', + ingress_zones: ['public'], + egress_zones: ['restricted'], + provider: described_class.name + ) + end + let(:provider) { resource.provider } + + before do + # rubocop:disable RSpec/AnyInstance + provider.class.stubs(:execute_firewall_cmd_policy).returns(Object.any_instance.stubs(exitstatus: 0)) + provider.class.stubs(:execute_firewall_cmd_policy).with(['--list-ingress-zones']).returns(Object.any_instance.stubs(exitstatus: 0, chomp: '')) + provider.class.stubs(:execute_firewall_cmd_policy).with(['--list-egress-zones']).returns(Object.any_instance.stubs(exitstatus: 0, chomp: '')) + # rubocop:enable RSpec/AnyInstance + end + + describe 'when creating' do + context 'with name public2restricted' do + it 'executes firewall_cmd with new-policy' do + resource.expects(:[]).with(:name).returns('public2restricted').at_least_once + resource.expects(:[]).with(:target).returns(nil).at_least_once + resource.expects(:[]).with(:ingress_zones).returns(['public']).at_least_once + resource.expects(:[]).with(:egress_zones).returns(['restricted']).at_least_once + resource.expects(:[]).with(:priority).returns(nil).at_least_once + resource.expects(:[]).with(:icmp_blocks).returns(nil).at_least_once + resource.expects(:[]).with(:description).returns(nil).at_least_once + resource.expects(:[]).with(:short).returns('public2restricted').at_least_once + provider.expects(:execute_firewall_cmd_policy).with(['--list-ingress-zones']) + provider.expects(:execute_firewall_cmd_policy).with(['--list-egress-zones']) + provider.expects(:execute_firewall_cmd_policy).with(['--add-ingress-zone', 'public']) + provider.expects(:execute_firewall_cmd_policy).with(['--add-egress-zone', 'restricted']) + provider.expects(:execute_firewall_cmd_policy).with(['--new-policy', 'public2restricted'], nil) + provider.expects(:execute_firewall_cmd_policy).with(['--set-short', 'public2restricted'], 'public2restricted', true, false) + provider.create + end + end + end + + describe 'when modifying' do + context 'type' do + it 'stores updated description' do + resource.expects(:[]).with(:name).returns('public2restricted').at_least_once + resource.expects(:[]).with(:target).returns(nil).at_least_once + resource.expects(:[]).with(:ingress_zones).returns(['public']).at_least_once + resource.expects(:[]).with(:egress_zones).returns(['restricted']).at_least_once + resource.expects(:[]).with(:priority).returns(nil).at_least_once + resource.expects(:[]).with(:icmp_blocks).returns(nil).at_least_once + resource.expects(:[]).with(:description).returns(nil).at_least_once + resource.expects(:[]).with(:short).returns('public2restricted').at_least_once + provider.expects(:execute_firewall_cmd_policy).with(['--list-ingress-zones']) + provider.expects(:execute_firewall_cmd_policy).with(['--list-egress-zones']) + provider.expects(:execute_firewall_cmd_policy).with(['--add-ingress-zone', 'public']) + provider.expects(:execute_firewall_cmd_policy).with(['--add-egress-zone', 'restricted']) + provider.expects(:execute_firewall_cmd_policy).with(['--new-policy', 'public2restricted'], nil) + provider.expects(:execute_firewall_cmd_policy).with(['--set-short', 'public2restricted'], 'public2restricted', true, false) + provider.expects(:execute_firewall_cmd_policy).with(['--set-description', :"Better description"], 'public2restricted', true, false) + provider.create + + provider.description = :'Better description' + end + end + end +end diff --git a/spec/unit/puppet/type/firewalld_policy_spec.rb b/spec/unit/puppet/type/firewalld_policy_spec.rb new file mode 100644 index 00000000..00f58e58 --- /dev/null +++ b/spec/unit/puppet/type/firewalld_policy_spec.rb @@ -0,0 +1,166 @@ +require 'spec_helper' + +describe Puppet::Type.type(:firewalld_policy) do + before do + Puppet::Provider::Firewalld.any_instance.stubs(:state).returns(:true) # rubocop:disable RSpec/AnyInstance + end + + describe 'type' do + context 'with no params' do + describe 'when validating attributes' do + [ + :name + ].each do |param| + it "should have a #{param} parameter" do + expect(described_class.attrtype(param)).to eq(:param) + end + end + + [:target, :ingress_zones, :egress_zones, :priority, :icmp_blocks, :purge_rich_rules, :purge_services, :purge_ports].each do |param| + it "should have a #{param} parameter" do + expect(described_class.attrtype(param)).to eq(:property) + end + end + end + end + end + + ## Provider tests for the firewalld_policy type + # + describe 'provider' do + context 'with standard parameters' do + let(:resource) do + described_class.new( + name: 'public2restricted', + target: '%%REJECT%%', + ingress_zones: ['public'], + egress_zones: ['restricted'], + icmp_blocks: ['redirect', 'router-advertisment'], + ) + end + let(:provider) do + resource.provider + end + + it 'checks if it exists' do + provider.expects(:execute_firewall_cmd_policy).with(['--get-policies'], nil).returns('public2restricted other') + expect(provider).to be_exists + end + + it 'checks if it doesnt exist' do + provider.expects(:execute_firewall_cmd_policy).with(['--get-policies'], nil).returns('wrong other') + expect(provider).not_to be_exists + end + + it 'evalulates target' do + provider.expects(:execute_firewall_cmd_policy).with(['--get-target']).returns('%%REJECT%%') + expect(provider.target).to eq('%%REJECT%%') + end + + it 'evalulates target correctly when not surrounded with %%' do + provider.expects(:execute_firewall_cmd_policy).with(['--get-target']).returns('REJECT') + expect(provider.target).to eq('%%REJECT%%') + end + + it 'creates' do + provider.expects(:execute_firewall_cmd_policy).with(['--new-policy', 'public2restricted'], nil) + provider.expects(:execute_firewall_cmd_policy).with(['--set-target', '%%REJECT%%']) + provider.expects(:execute_firewall_cmd_policy).with(['--set-priority', '-1']) + + provider.expects(:icmp_blocks=).with(['redirect', 'router-advertisment']) + + + provider.expects(:ingress_zones).returns([]) + provider.expects(:execute_firewall_cmd_policy).with(['--add-ingress-zone', 'public']) + + provider.expects(:egress_zones).returns([]) + provider.expects(:execute_firewall_cmd_policy).with(['--add-egress-zone', 'restricted']) + provider.create + end + + it 'removes' do + provider.expects(:execute_firewall_cmd_policy).with(['--delete-policy', 'public2restricted'], nil) + provider.destroy + end + + it 'sets target' do + provider.expects(:execute_firewall_cmd_policy).with(['--set-target', '%%REJECT%%']) + provider.target = '%%REJECT%%' + end + + it 'gets ingress zones' do + provider.expects(:execute_firewall_cmd_policy).with(['--list-ingress-zones']).returns('public') + expect(provider.ingress_zones).to eq(['public']) + end + + it 'gets egress zones' do + provider.expects(:execute_firewall_cmd_policy).with(['--list-egress-zones']).returns('restricted') + expect(provider.egress_zones).to eq(['restricted']) + end + + it 'gets icmp_blocks' do + provider.expects(:execute_firewall_cmd_policy).with(['--list-icmp-blocks']).returns('val') + expect(provider.icmp_blocks).to eq(['val']) + end + + it 'lists icmp types' do + provider.expects(:execute_firewall_cmd_policy).with(['--get-icmptypes'], nil).returns('echo-reply echo-request') + expect(provider.get_icmp_types).to eq(['echo-reply', 'echo-request']) + end + end + + context 'when specifiying masquerade' do + let(:resource) do + described_class.new( + name: 'public2restricted', + ensure: :present, + masquerade: true, + ingress_zones: ['public'], + egress_zones: ['restricted'], + ) + end + let(:provider) do + resource.provider + end + + it 'sets masquerading' do + provider.expects(:execute_firewall_cmd_policy).with(['--add-masquerade']) + provider.masquerade = :true + end + + it 'disables masquerading' do + provider.expects(:execute_firewall_cmd_policy).with(['--remove-masquerade']) + provider.masquerade = :false + end + + it 'gets masquerading state as false when not set' do + provider.expects(:execute_firewall_cmd_policy).with(['--query-masquerade'], 'public2restricted', true, false).returns("no\n") + expect(provider.masquerade).to eq(:false) + end + it 'gets masquerading state as true when set' do + provider.expects(:execute_firewall_cmd_policy).with(['--query-masquerade'], 'public2restricted', true, false).returns("yes\n") + expect(provider.masquerade).to eq(:true) + end + end + end + + context 'autorequires' do + # rubocop:disable RSpec/InstanceVariable + before do + firewalld_service = Puppet::Type.type(:service).new(name: 'firewalld') + @catalog = Puppet::Resource::Catalog.new + @catalog.add_resource(firewalld_service) + end + + it 'autorequires the firewalld service' do + resource = described_class.new( + name: 'test', + ingress_zones: :unset, + egress_zones: :unset) + @catalog.add_resource(resource) + + expect(resource.autorequire.map { |rp| rp.source.to_s }).to include('Service[firewalld]') + end + # rubocop:enable RSpec/InstanceVariable + end +end diff --git a/spec/unit/puppet/type/firewalld_port_spec.rb b/spec/unit/puppet/type/firewalld_port_spec.rb index 7eaf1545..a4096db9 100644 --- a/spec/unit/puppet/type/firewalld_port_spec.rb +++ b/spec/unit/puppet/type/firewalld_port_spec.rb @@ -30,7 +30,7 @@ end it 'autorequires the firewalld service' do - resource = described_class.new(name: 'test', port: 1234) + resource = described_class.new(name: 'test', port: 1234, zone: 'test') @catalog.add_resource(resource) expect(resource.autorequire.map { |rp| rp.source.to_s }).to include('Service[firewalld]') diff --git a/spec/unit/puppet/type/firewalld_service_spec.rb b/spec/unit/puppet/type/firewalld_service_spec.rb index 5dd0a53f..4ec8e0e5 100644 --- a/spec/unit/puppet/type/firewalld_service_spec.rb +++ b/spec/unit/puppet/type/firewalld_service_spec.rb @@ -30,7 +30,7 @@ end it 'autorequires the firewalld service' do - resource = described_class.new(name: 'test', service: 'test') + resource = described_class.new(name: 'test', service: 'test', zone: 'test') @catalog.add_resource(resource) expect(resource.autorequire.map { |rp| rp.source.to_s }).to include('Service[firewalld]')