entitlements-app 0.1.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
 - data/VERSION +1 -0
 - data/bin/deploy-entitlements +18 -0
 - data/lib/entitlements/auditor/base.rb +163 -0
 - data/lib/entitlements/backend/base_controller.rb +171 -0
 - data/lib/entitlements/backend/base_provider.rb +55 -0
 - data/lib/entitlements/backend/dummy/controller.rb +89 -0
 - data/lib/entitlements/backend/dummy.rb +3 -0
 - data/lib/entitlements/backend/ldap/controller.rb +188 -0
 - data/lib/entitlements/backend/ldap/provider.rb +128 -0
 - data/lib/entitlements/backend/ldap.rb +4 -0
 - data/lib/entitlements/backend/member_of/controller.rb +203 -0
 - data/lib/entitlements/backend/member_of.rb +3 -0
 - data/lib/entitlements/cli.rb +121 -0
 - data/lib/entitlements/data/groups/cached.rb +120 -0
 - data/lib/entitlements/data/groups/calculated/base.rb +478 -0
 - data/lib/entitlements/data/groups/calculated/filters/base.rb +93 -0
 - data/lib/entitlements/data/groups/calculated/filters/member_of_group.rb +32 -0
 - data/lib/entitlements/data/groups/calculated/modifiers/base.rb +38 -0
 - data/lib/entitlements/data/groups/calculated/modifiers/expiration.rb +56 -0
 - data/lib/entitlements/data/groups/calculated/ruby.rb +137 -0
 - data/lib/entitlements/data/groups/calculated/rules/base.rb +35 -0
 - data/lib/entitlements/data/groups/calculated/rules/group.rb +129 -0
 - data/lib/entitlements/data/groups/calculated/rules/username.rb +41 -0
 - data/lib/entitlements/data/groups/calculated/text.rb +337 -0
 - data/lib/entitlements/data/groups/calculated/yaml.rb +171 -0
 - data/lib/entitlements/data/groups/calculated.rb +290 -0
 - data/lib/entitlements/data/groups.rb +13 -0
 - data/lib/entitlements/data/people/combined.rb +197 -0
 - data/lib/entitlements/data/people/dummy.rb +71 -0
 - data/lib/entitlements/data/people/ldap.rb +142 -0
 - data/lib/entitlements/data/people/yaml.rb +102 -0
 - data/lib/entitlements/data/people.rb +58 -0
 - data/lib/entitlements/extras/base.rb +40 -0
 - data/lib/entitlements/extras/ldap_group/base.rb +20 -0
 - data/lib/entitlements/extras/ldap_group/filters/member_of_ldap_group.rb +50 -0
 - data/lib/entitlements/extras/ldap_group/rules/ldap_group.rb +69 -0
 - data/lib/entitlements/extras/orgchart/base.rb +32 -0
 - data/lib/entitlements/extras/orgchart/logic.rb +171 -0
 - data/lib/entitlements/extras/orgchart/person_methods.rb +55 -0
 - data/lib/entitlements/extras/orgchart/rules/direct_report.rb +62 -0
 - data/lib/entitlements/extras/orgchart/rules/management.rb +59 -0
 - data/lib/entitlements/extras.rb +82 -0
 - data/lib/entitlements/models/action.rb +82 -0
 - data/lib/entitlements/models/group.rb +280 -0
 - data/lib/entitlements/models/person.rb +149 -0
 - data/lib/entitlements/plugins/dummy.rb +22 -0
 - data/lib/entitlements/plugins/group_of_names.rb +28 -0
 - data/lib/entitlements/plugins/posix_group.rb +46 -0
 - data/lib/entitlements/plugins.rb +13 -0
 - data/lib/entitlements/rule/base.rb +74 -0
 - data/lib/entitlements/service/ldap.rb +405 -0
 - data/lib/entitlements/util/mirror.rb +42 -0
 - data/lib/entitlements/util/override.rb +64 -0
 - data/lib/entitlements/util/util.rb +219 -0
 - data/lib/entitlements.rb +606 -0
 - metadata +343 -0
 
| 
         @@ -0,0 +1,337 @@ 
     | 
|
| 
      
 1 
     | 
    
         
            +
            # frozen_string_literal: true
         
     | 
| 
      
 2 
     | 
    
         
            +
            # Interact with rules that are stored in a simplified text file.
         
     | 
| 
      
 3 
     | 
    
         
            +
             
     | 
| 
      
 4 
     | 
    
         
            +
            require "yaml"
         
     | 
| 
      
 5 
     | 
    
         
            +
            require_relative "../../../util/util"
         
     | 
| 
      
 6 
     | 
    
         
            +
             
     | 
| 
      
 7 
     | 
    
         
            +
            module Entitlements
         
     | 
| 
      
 8 
     | 
    
         
            +
              class Data
         
     | 
| 
      
 9 
     | 
    
         
            +
                class Groups
         
     | 
| 
      
 10 
     | 
    
         
            +
                  class Calculated
         
     | 
| 
      
 11 
     | 
    
         
            +
                    class Text < Entitlements::Data::Groups::Calculated::Base
         
     | 
| 
      
 12 
     | 
    
         
            +
                      include ::Contracts::Core
         
     | 
| 
      
 13 
     | 
    
         
            +
                      C = ::Contracts
         
     | 
| 
      
 14 
     | 
    
         
            +
             
     | 
| 
      
 15 
     | 
    
         
            +
                      SEMICOLON_PREDICATES = %w[expiration]
         
     | 
| 
      
 16 
     | 
    
         
            +
             
     | 
| 
      
 17 
     | 
    
         
            +
                      # Standard interface: Calculate the members of this group.
         
     | 
| 
      
 18 
     | 
    
         
            +
                      #
         
     | 
| 
      
 19 
     | 
    
         
            +
                      # Takes no arguments.
         
     | 
| 
      
 20 
     | 
    
         
            +
                      #
         
     | 
| 
      
 21 
     | 
    
         
            +
                      # Returns a Set[String] with DN's of the people in the group.
         
     | 
| 
      
 22 
     | 
    
         
            +
                      Contract C::None => C::Or[:calculating, C::SetOf[Entitlements::Models::Person]]
         
     | 
| 
      
 23 
     | 
    
         
            +
                      def members
         
     | 
| 
      
 24 
     | 
    
         
            +
                        @members ||= begin
         
     | 
| 
      
 25 
     | 
    
         
            +
                          Entitlements.logger.debug "Calculating members from #{filename}"
         
     | 
| 
      
 26 
     | 
    
         
            +
                          members_from_rules(rules)
         
     | 
| 
      
 27 
     | 
    
         
            +
                        end
         
     | 
| 
      
 28 
     | 
    
         
            +
                      end
         
     | 
| 
      
 29 
     | 
    
         
            +
             
     | 
| 
      
 30 
     | 
    
         
            +
                      # Standard interface: Get the description of this group.
         
     | 
| 
      
 31 
     | 
    
         
            +
                      #
         
     | 
| 
      
 32 
     | 
    
         
            +
                      # Takes no arguments.
         
     | 
| 
      
 33 
     | 
    
         
            +
                      #
         
     | 
| 
      
 34 
     | 
    
         
            +
                      # Returns a String with the group description, or "" if undefined.
         
     | 
| 
      
 35 
     | 
    
         
            +
                      Contract C::None => String
         
     | 
| 
      
 36 
     | 
    
         
            +
                      def description
         
     | 
| 
      
 37 
     | 
    
         
            +
                        return "" unless parsed_data.key?("description")
         
     | 
| 
      
 38 
     | 
    
         
            +
             
     | 
| 
      
 39 
     | 
    
         
            +
                        if parsed_data["description"]["!="].any?
         
     | 
| 
      
 40 
     | 
    
         
            +
                          fatal_message("The description cannot use '!=' operator in #{filename}!")
         
     | 
| 
      
 41 
     | 
    
         
            +
                        end
         
     | 
| 
      
 42 
     | 
    
         
            +
             
     | 
| 
      
 43 
     | 
    
         
            +
                        unless parsed_data["description"]["="].size == 1
         
     | 
| 
      
 44 
     | 
    
         
            +
                          fatal_message("The description key is duplicated in #{filename}!")
         
     | 
| 
      
 45 
     | 
    
         
            +
                        end
         
     | 
| 
      
 46 
     | 
    
         
            +
             
     | 
| 
      
 47 
     | 
    
         
            +
                        parsed_data["description"]["="].first.fetch(:key)
         
     | 
| 
      
 48 
     | 
    
         
            +
                      end
         
     | 
| 
      
 49 
     | 
    
         
            +
             
     | 
| 
      
 50 
     | 
    
         
            +
                      # Files can support modifiers that act independently of rules.
         
     | 
| 
      
 51 
     | 
    
         
            +
                      # This returns the modifiers from the file as a hash.
         
     | 
| 
      
 52 
     | 
    
         
            +
                      #
         
     | 
| 
      
 53 
     | 
    
         
            +
                      # Takes no arguments.
         
     | 
| 
      
 54 
     | 
    
         
            +
                      #
         
     | 
| 
      
 55 
     | 
    
         
            +
                      # Returns Hash[<String>key => <Object>value]
         
     | 
| 
      
 56 
     | 
    
         
            +
                      Contract C::None => C::HashOf[String => C::Any]
         
     | 
| 
      
 57 
     | 
    
         
            +
                      def modifiers
         
     | 
| 
      
 58 
     | 
    
         
            +
                        parse_with_prefix("modifier_")
         
     | 
| 
      
 59 
     | 
    
         
            +
                      end
         
     | 
| 
      
 60 
     | 
    
         
            +
             
     | 
| 
      
 61 
     | 
    
         
            +
                      private
         
     | 
| 
      
 62 
     | 
    
         
            +
             
     | 
| 
      
 63 
     | 
    
         
            +
                      # Get a hash of the filters defined in the group.
         
     | 
| 
      
 64 
     | 
    
         
            +
                      #
         
     | 
| 
      
 65 
     | 
    
         
            +
                      # Takes no arguments.
         
     | 
| 
      
 66 
     | 
    
         
            +
                      #
         
     | 
| 
      
 67 
     | 
    
         
            +
                      # Returns a Hash[String => :all/:none/List of strings].
         
     | 
| 
      
 68 
     | 
    
         
            +
                      Contract C::None => C::HashOf[String => C::Or[:all, :none, C::ArrayOf[String]]]
         
     | 
| 
      
 69 
     | 
    
         
            +
                      def initialize_filters
         
     | 
| 
      
 70 
     | 
    
         
            +
                        result = Entitlements::Data::Groups::Calculated.filters_default
         
     | 
| 
      
 71 
     | 
    
         
            +
             
     | 
| 
      
 72 
     | 
    
         
            +
                        parsed_data.each do |raw_key, val|
         
     | 
| 
      
 73 
     | 
    
         
            +
                          if raw_key == "filter_"
         
     | 
| 
      
 74 
     | 
    
         
            +
                            fatal_message("In #{filename}, cannot have a key named \"filter_\"!")
         
     | 
| 
      
 75 
     | 
    
         
            +
                          end
         
     | 
| 
      
 76 
     | 
    
         
            +
             
     | 
| 
      
 77 
     | 
    
         
            +
                          next unless raw_key.start_with?("filter_")
         
     | 
| 
      
 78 
     | 
    
         
            +
                          key = raw_key.sub(/\Afilter_/, "")
         
     | 
| 
      
 79 
     | 
    
         
            +
             
     | 
| 
      
 80 
     | 
    
         
            +
                          unless result.key?(key)
         
     | 
| 
      
 81 
     | 
    
         
            +
                            fatal_message("In #{filename}, the key #{raw_key} is invalid!")
         
     | 
| 
      
 82 
     | 
    
         
            +
                          end
         
     | 
| 
      
 83 
     | 
    
         
            +
             
     | 
| 
      
 84 
     | 
    
         
            +
                          if val["!="].any?
         
     | 
| 
      
 85 
     | 
    
         
            +
                            fatal_message("The filter #{key} cannot use '!=' operator in #{filename}!")
         
     | 
| 
      
 86 
     | 
    
         
            +
                          end
         
     | 
| 
      
 87 
     | 
    
         
            +
             
     | 
| 
      
 88 
     | 
    
         
            +
                          values = val["="].reject { |v| expired?(v[:expiration], filename) }.map { |v| v[:key].strip }
         
     | 
| 
      
 89 
     | 
    
         
            +
                          if values.size == 1 && (values.first == "all" || values.first == "none")
         
     | 
| 
      
 90 
     | 
    
         
            +
                            result[key] = values.first.to_sym
         
     | 
| 
      
 91 
     | 
    
         
            +
                          elsif values.size > 1 && (values.include?("all") || values.include?("none"))
         
     | 
| 
      
 92 
     | 
    
         
            +
                            fatal_message("In #{filename}, #{raw_key} cannot contain multiple entries when 'all' or 'none' is used!")
         
     | 
| 
      
 93 
     | 
    
         
            +
                          elsif values.size == 0
         
     | 
| 
      
 94 
     | 
    
         
            +
                            # This could happen if all of the specified filters were deleted due to expiration.
         
     | 
| 
      
 95 
     | 
    
         
            +
                            # In that case make no changes so the default gets used.
         
     | 
| 
      
 96 
     | 
    
         
            +
                            next
         
     | 
| 
      
 97 
     | 
    
         
            +
                          else
         
     | 
| 
      
 98 
     | 
    
         
            +
                            result[key] = values
         
     | 
| 
      
 99 
     | 
    
         
            +
                          end
         
     | 
| 
      
 100 
     | 
    
         
            +
                        end
         
     | 
| 
      
 101 
     | 
    
         
            +
             
     | 
| 
      
 102 
     | 
    
         
            +
                        result
         
     | 
| 
      
 103 
     | 
    
         
            +
                      end
         
     | 
| 
      
 104 
     | 
    
         
            +
             
     | 
| 
      
 105 
     | 
    
         
            +
                      # Files can support metadata intended for consumption by things other than LDAP.
         
     | 
| 
      
 106 
     | 
    
         
            +
                      # This returns the metadata from the file as a hash.
         
     | 
| 
      
 107 
     | 
    
         
            +
                      #
         
     | 
| 
      
 108 
     | 
    
         
            +
                      # Takes no arguments.
         
     | 
| 
      
 109 
     | 
    
         
            +
                      #
         
     | 
| 
      
 110 
     | 
    
         
            +
                      # Returns Hash[<String>key => <Object>value]
         
     | 
| 
      
 111 
     | 
    
         
            +
                      Contract C::None => C::HashOf[String => C::Any]
         
     | 
| 
      
 112 
     | 
    
         
            +
                      def initialize_metadata
         
     | 
| 
      
 113 
     | 
    
         
            +
                        parse_with_prefix("metadata_")
         
     | 
| 
      
 114 
     | 
    
         
            +
                      end
         
     | 
| 
      
 115 
     | 
    
         
            +
             
     | 
| 
      
 116 
     | 
    
         
            +
                      # Metadata and modifiers are parsed with nearly identical logic. In DRY spirit, use
         
     | 
| 
      
 117 
     | 
    
         
            +
                      # a single parsing method.
         
     | 
| 
      
 118 
     | 
    
         
            +
                      #
         
     | 
| 
      
 119 
     | 
    
         
            +
                      # prefix - String with the prefix expected for the key.
         
     | 
| 
      
 120 
     | 
    
         
            +
                      #
         
     | 
| 
      
 121 
     | 
    
         
            +
                      # Returns Hash[<String>key => <Object>value]
         
     | 
| 
      
 122 
     | 
    
         
            +
                      Contract String => C::HashOf[String => C::Any]
         
     | 
| 
      
 123 
     | 
    
         
            +
                      def parse_with_prefix(prefix)
         
     | 
| 
      
 124 
     | 
    
         
            +
                        result = {}
         
     | 
| 
      
 125 
     | 
    
         
            +
                        parsed_data.each do |raw_key, val|
         
     | 
| 
      
 126 
     | 
    
         
            +
                          if raw_key == "#{prefix}"
         
     | 
| 
      
 127 
     | 
    
         
            +
                            raise "In #{filename}, cannot have a key named \"#{prefix}\"!"
         
     | 
| 
      
 128 
     | 
    
         
            +
                          end
         
     | 
| 
      
 129 
     | 
    
         
            +
             
     | 
| 
      
 130 
     | 
    
         
            +
                          next unless raw_key.start_with?(prefix)
         
     | 
| 
      
 131 
     | 
    
         
            +
                          key = raw_key.sub(/\A#{prefix}/, "")
         
     | 
| 
      
 132 
     | 
    
         
            +
             
     | 
| 
      
 133 
     | 
    
         
            +
                          if val["!="].any?
         
     | 
| 
      
 134 
     | 
    
         
            +
                            fatal_message("The key #{raw_key} cannot use '!=' operator in #{filename}!")
         
     | 
| 
      
 135 
     | 
    
         
            +
                          end
         
     | 
| 
      
 136 
     | 
    
         
            +
             
     | 
| 
      
 137 
     | 
    
         
            +
                          unless val["="].size == 1
         
     | 
| 
      
 138 
     | 
    
         
            +
                            fatal_message("In #{filename}, the key #{raw_key} is repeated!")
         
     | 
| 
      
 139 
     | 
    
         
            +
                          end
         
     | 
| 
      
 140 
     | 
    
         
            +
             
     | 
| 
      
 141 
     | 
    
         
            +
                          unless val["="].first.keys == [:key]
         
     | 
| 
      
 142 
     | 
    
         
            +
                            settings = (val["="].first.keys - [:key]).map { |i| i.to_s.inspect }.join(",")
         
     | 
| 
      
 143 
     | 
    
         
            +
                            fatal_message("In #{filename}, the key #{raw_key} cannot have additional setting(s) #{settings}!")
         
     | 
| 
      
 144 
     | 
    
         
            +
                          end
         
     | 
| 
      
 145 
     | 
    
         
            +
             
     | 
| 
      
 146 
     | 
    
         
            +
                          result[key] = val["="].first.fetch(:key)
         
     | 
| 
      
 147 
     | 
    
         
            +
                        end
         
     | 
| 
      
 148 
     | 
    
         
            +
                        result
         
     | 
| 
      
 149 
     | 
    
         
            +
                      end
         
     | 
| 
      
 150 
     | 
    
         
            +
             
     | 
| 
      
 151 
     | 
    
         
            +
                      # Obtain the rule set from the content of the file and convert it to an object.
         
     | 
| 
      
 152 
     | 
    
         
            +
                      #
         
     | 
| 
      
 153 
     | 
    
         
            +
                      # Takes no arguments.
         
     | 
| 
      
 154 
     | 
    
         
            +
                      #
         
     | 
| 
      
 155 
     | 
    
         
            +
                      # Returns a Hash.
         
     | 
| 
      
 156 
     | 
    
         
            +
                      Contract C::None => C::HashOf[String => C::Any]
         
     | 
| 
      
 157 
     | 
    
         
            +
                      def rules
         
     | 
| 
      
 158 
     | 
    
         
            +
                        @rules ||= begin
         
     | 
| 
      
 159 
     | 
    
         
            +
                          ignored_keys = %w[description]
         
     | 
| 
      
 160 
     | 
    
         
            +
             
     | 
| 
      
 161 
     | 
    
         
            +
                          relevant_entries = parsed_data.reject { |k, _| ignored_keys.include?(k) }
         
     | 
| 
      
 162 
     | 
    
         
            +
                          relevant_entries.reject! { |k, _| k.start_with?("metadata_", "filter_", "modifier_") }
         
     | 
| 
      
 163 
     | 
    
         
            +
             
     | 
| 
      
 164 
     | 
    
         
            +
                          # Review all entries
         
     | 
| 
      
 165 
     | 
    
         
            +
                          affirmative = []
         
     | 
| 
      
 166 
     | 
    
         
            +
                          mandatory = []
         
     | 
| 
      
 167 
     | 
    
         
            +
                          negative = []
         
     | 
| 
      
 168 
     | 
    
         
            +
                          relevant_entries.each do |k, v|
         
     | 
| 
      
 169 
     | 
    
         
            +
                            function = function_for(k)
         
     | 
| 
      
 170 
     | 
    
         
            +
                            unless whitelisted_methods.member?(function)
         
     | 
| 
      
 171 
     | 
    
         
            +
                              Entitlements.logger.fatal "The method #{k.inspect} is not allowed in #{filename}!"
         
     | 
| 
      
 172 
     | 
    
         
            +
                              raise "The method #{k.inspect} is not allowed in #{filename}!"
         
     | 
| 
      
 173 
     | 
    
         
            +
                            end
         
     | 
| 
      
 174 
     | 
    
         
            +
             
     | 
| 
      
 175 
     | 
    
         
            +
                            add_relevant_entries!(affirmative, function, v["="], filename)
         
     | 
| 
      
 176 
     | 
    
         
            +
                            add_relevant_entries!(mandatory, function, v["&="], filename)
         
     | 
| 
      
 177 
     | 
    
         
            +
                            add_relevant_entries!(negative, function, v["!="], filename)
         
     | 
| 
      
 178 
     | 
    
         
            +
                          end
         
     | 
| 
      
 179 
     | 
    
         
            +
             
     | 
| 
      
 180 
     | 
    
         
            +
                          # Expiration pre-processing: An entitlement that is expired as a whole should not
         
     | 
| 
      
 181 
     | 
    
         
            +
                          # raise an error about having no conditions.
         
     | 
| 
      
 182 
     | 
    
         
            +
                          if parsed_data.key?("modifier_expiration") && affirmative.empty?
         
     | 
| 
      
 183 
     | 
    
         
            +
                            exp_date = parsed_data.fetch("modifier_expiration").fetch("=").first.fetch(:key)
         
     | 
| 
      
 184 
     | 
    
         
            +
                            date = Entitlements::Util::Util.parse_date(exp_date)
         
     | 
| 
      
 185 
     | 
    
         
            +
                            return {"always" => false} if date <= Time.now.utc.to_date
         
     | 
| 
      
 186 
     | 
    
         
            +
                          end
         
     | 
| 
      
 187 
     | 
    
         
            +
             
     | 
| 
      
 188 
     | 
    
         
            +
                          # There has to be at least one affirmative condition, not just all negative ones.
         
     | 
| 
      
 189 
     | 
    
         
            +
                          # Override with `metadata_no_conditions_ok = true`.
         
     | 
| 
      
 190 
     | 
    
         
            +
                          if affirmative.empty?
         
     | 
| 
      
 191 
     | 
    
         
            +
                            return {"always" => false} if [true, "true"].include?(metadata["no_conditions_ok"])
         
     | 
| 
      
 192 
     | 
    
         
            +
                            fatal_message("No conditions were found in #{filename}!")
         
     | 
| 
      
 193 
     | 
    
         
            +
                          end
         
     | 
| 
      
 194 
     | 
    
         
            +
             
     | 
| 
      
 195 
     | 
    
         
            +
                          # Get base affirmative and negative rules.
         
     | 
| 
      
 196 
     | 
    
         
            +
                          result = affirmative_negative_rules(affirmative, negative)
         
     | 
| 
      
 197 
     | 
    
         
            +
             
     | 
| 
      
 198 
     | 
    
         
            +
                          # Apply any mandatory rules.
         
     | 
| 
      
 199 
     | 
    
         
            +
                          if mandatory.size == 1
         
     | 
| 
      
 200 
     | 
    
         
            +
                            old_result = result.dup
         
     | 
| 
      
 201 
     | 
    
         
            +
                            result = { "and" => [mandatory.first, old_result] }
         
     | 
| 
      
 202 
     | 
    
         
            +
                          elsif mandatory.size > 1
         
     | 
| 
      
 203 
     | 
    
         
            +
                            old_result = result.dup
         
     | 
| 
      
 204 
     | 
    
         
            +
                            result = { "and" => [{ "or" => mandatory }, old_result] }
         
     | 
| 
      
 205 
     | 
    
         
            +
                          end
         
     | 
| 
      
 206 
     | 
    
         
            +
             
     | 
| 
      
 207 
     | 
    
         
            +
                          # Return what we've got.
         
     | 
| 
      
 208 
     | 
    
         
            +
                          result
         
     | 
| 
      
 209 
     | 
    
         
            +
                        end
         
     | 
| 
      
 210 
     | 
    
         
            +
                      end
         
     | 
| 
      
 211 
     | 
    
         
            +
             
     | 
| 
      
 212 
     | 
    
         
            +
                      # Handle affirmative and negative rules.
         
     | 
| 
      
 213 
     | 
    
         
            +
                      #
         
     | 
| 
      
 214 
     | 
    
         
            +
                      # affirmative - An array of Hashes with rules.
         
     | 
| 
      
 215 
     | 
    
         
            +
                      # negative    - An array of Hashes with rules.
         
     | 
| 
      
 216 
     | 
    
         
            +
                      #
         
     | 
| 
      
 217 
     | 
    
         
            +
                      # Returns appropriate and / or hash.
         
     | 
| 
      
 218 
     | 
    
         
            +
                      Contract C::ArrayOf[Hash], C::ArrayOf[Hash] => C::HashOf[String => C::Any]
         
     | 
| 
      
 219 
     | 
    
         
            +
                      def affirmative_negative_rules(affirmative, negative)
         
     | 
| 
      
 220 
     | 
    
         
            +
                        if negative.empty?
         
     | 
| 
      
 221 
     | 
    
         
            +
                          # This is a simplified file. Just OR all the conditions together. (For
         
     | 
| 
      
 222 
     | 
    
         
            +
                          # something more complicated, use YAML or ruby formats.)
         
     | 
| 
      
 223 
     | 
    
         
            +
                          { "or" => affirmative }
         
     | 
| 
      
 224 
     | 
    
         
            +
                        else
         
     | 
| 
      
 225 
     | 
    
         
            +
                          # Each affirmative condition is OR'd, but any negative condition will veto.
         
     | 
| 
      
 226 
     | 
    
         
            +
                          # For something more complicated, use YAML or ruby formats.
         
     | 
| 
      
 227 
     | 
    
         
            +
                          {
         
     | 
| 
      
 228 
     | 
    
         
            +
                            "and" => [
         
     | 
| 
      
 229 
     | 
    
         
            +
                              { "or" => affirmative },
         
     | 
| 
      
 230 
     | 
    
         
            +
                              { "and" => negative.map { |condition| { "not" => condition } } }
         
     | 
| 
      
 231 
     | 
    
         
            +
                            ]
         
     | 
| 
      
 232 
     | 
    
         
            +
                          }
         
     | 
| 
      
 233 
     | 
    
         
            +
                        end
         
     | 
| 
      
 234 
     | 
    
         
            +
                      end
         
     | 
| 
      
 235 
     | 
    
         
            +
             
     | 
| 
      
 236 
     | 
    
         
            +
                      # Helper method to extract relevant entries from the parsed rules and concatenate them
         
     | 
| 
      
 237 
     | 
    
         
            +
                      # onto the given array.
         
     | 
| 
      
 238 
     | 
    
         
            +
                      #
         
     | 
| 
      
 239 
     | 
    
         
            +
                      # array_to_update - An Array which will have relevant rules concat'd to it.
         
     | 
| 
      
 240 
     | 
    
         
            +
                      # key             - String with the key.
         
     | 
| 
      
 241 
     | 
    
         
            +
                      # rule_items      - An Array of Hashes with the rules to evaluate.
         
     | 
| 
      
 242 
     | 
    
         
            +
                      # filename        - Filename where rule is defined (used for error printing).
         
     | 
| 
      
 243 
     | 
    
         
            +
                      #
         
     | 
| 
      
 244 
     | 
    
         
            +
                      # Updates and returns array_to_update.
         
     | 
| 
      
 245 
     | 
    
         
            +
                      Contract C::ArrayOf[C::HashOf[String => String]], String, C::ArrayOf[C::HashOf[Symbol => String]], String => C::ArrayOf[C::HashOf[String => String]]
         
     | 
| 
      
 246 
     | 
    
         
            +
                      def add_relevant_entries!(array_to_update, key, rule_items, filename)
         
     | 
| 
      
 247 
     | 
    
         
            +
                        new_items = rule_items.reject { |item| expired?(item[:expiration], filename) }.map { |item| { key => item[:key] } }
         
     | 
| 
      
 248 
     | 
    
         
            +
                        array_to_update.concat new_items
         
     | 
| 
      
 249 
     | 
    
         
            +
                      end
         
     | 
| 
      
 250 
     | 
    
         
            +
             
     | 
| 
      
 251 
     | 
    
         
            +
                      # Return the parsed data from the file. This is called on demand and cached.
         
     | 
| 
      
 252 
     | 
    
         
            +
                      #
         
     | 
| 
      
 253 
     | 
    
         
            +
                      # Takes no arguments.
         
     | 
| 
      
 254 
     | 
    
         
            +
                      #
         
     | 
| 
      
 255 
     | 
    
         
            +
                      # Returns a Hash.
         
     | 
| 
      
 256 
     | 
    
         
            +
                      Contract C::None => C::HashOf[String => C::HashOf[String, C::ArrayOf[C::HashOf[Symbol, String]]]]
         
     | 
| 
      
 257 
     | 
    
         
            +
                      def parsed_data
         
     | 
| 
      
 258 
     | 
    
         
            +
                        @parsed_data ||= begin
         
     | 
| 
      
 259 
     | 
    
         
            +
                          result = {}
         
     | 
| 
      
 260 
     | 
    
         
            +
                          filter_keywords = Entitlements::Data::Groups::Calculated.filters_index.keys
         
     | 
| 
      
 261 
     | 
    
         
            +
                          content = File.read(filename).split(/\n/)
         
     | 
| 
      
 262 
     | 
    
         
            +
                          content.each do |raw_line|
         
     | 
| 
      
 263 
     | 
    
         
            +
                            line = raw_line.strip
         
     | 
| 
      
 264 
     | 
    
         
            +
             
     | 
| 
      
 265 
     | 
    
         
            +
                            # Ignore comments and blank lines
         
     | 
| 
      
 266 
     | 
    
         
            +
                            next if line.start_with?("#") || line == ""
         
     | 
| 
      
 267 
     | 
    
         
            +
             
     | 
| 
      
 268 
     | 
    
         
            +
                            # Ensure valid lines
         
     | 
| 
      
 269 
     | 
    
         
            +
                            unless line =~ /\A([\w\-]+)\s*([&!]?=)\s*(.+?)\s*\z/
         
     | 
| 
      
 270 
     | 
    
         
            +
                              Entitlements.logger.fatal "Unparseable line #{line.inspect} in #{filename}!"
         
     | 
| 
      
 271 
     | 
    
         
            +
                              raise "Unparseable line #{line.inspect} in #{filename}!"
         
     | 
| 
      
 272 
     | 
    
         
            +
                            end
         
     | 
| 
      
 273 
     | 
    
         
            +
             
     | 
| 
      
 274 
     | 
    
         
            +
                            # Parsing
         
     | 
| 
      
 275 
     | 
    
         
            +
                            raw_key, operator, val = Regexp.last_match(1), Regexp.last_match(2), Regexp.last_match(3)
         
     | 
| 
      
 276 
     | 
    
         
            +
             
     | 
| 
      
 277 
     | 
    
         
            +
                            key = if filter_keywords.include?(raw_key)
         
     | 
| 
      
 278 
     | 
    
         
            +
                              "filter_#{raw_key}"
         
     | 
| 
      
 279 
     | 
    
         
            +
                            elsif MODIFIERS.include?(raw_key)
         
     | 
| 
      
 280 
     | 
    
         
            +
                              "modifier_#{raw_key}"
         
     | 
| 
      
 281 
     | 
    
         
            +
                            else
         
     | 
| 
      
 282 
     | 
    
         
            +
                              raw_key
         
     | 
| 
      
 283 
     | 
    
         
            +
                            end
         
     | 
| 
      
 284 
     | 
    
         
            +
             
     | 
| 
      
 285 
     | 
    
         
            +
                            # Contractor function is used internally but may not be specified in the file by the user.
         
     | 
| 
      
 286 
     | 
    
         
            +
                            if key == "contractor"
         
     | 
| 
      
 287 
     | 
    
         
            +
                              Entitlements.logger.fatal "The method #{key.inspect} is not permitted in #{filename}!"
         
     | 
| 
      
 288 
     | 
    
         
            +
                              raise "Rule Error: #{key} is not a valid function in #{filename}!"
         
     | 
| 
      
 289 
     | 
    
         
            +
                            end
         
     | 
| 
      
 290 
     | 
    
         
            +
             
     | 
| 
      
 291 
     | 
    
         
            +
                            result[key] ||= {}
         
     | 
| 
      
 292 
     | 
    
         
            +
                            result[key]["="] ||= []
         
     | 
| 
      
 293 
     | 
    
         
            +
                            result[key]["!="] ||= []
         
     | 
| 
      
 294 
     | 
    
         
            +
                            result[key]["&="] ||= []
         
     | 
| 
      
 295 
     | 
    
         
            +
             
     | 
| 
      
 296 
     | 
    
         
            +
                            # Semicolon predicates
         
     | 
| 
      
 297 
     | 
    
         
            +
                            if key == "description"
         
     | 
| 
      
 298 
     | 
    
         
            +
                              result[key][operator] << { key: val }
         
     | 
| 
      
 299 
     | 
    
         
            +
                            else
         
     | 
| 
      
 300 
     | 
    
         
            +
                              result[key][operator] << parsed_predicate(val)
         
     | 
| 
      
 301 
     | 
    
         
            +
                            end
         
     | 
| 
      
 302 
     | 
    
         
            +
                          end
         
     | 
| 
      
 303 
     | 
    
         
            +
             
     | 
| 
      
 304 
     | 
    
         
            +
                          result
         
     | 
| 
      
 305 
     | 
    
         
            +
                        end
         
     | 
| 
      
 306 
     | 
    
         
            +
                      end
         
     | 
| 
      
 307 
     | 
    
         
            +
             
     | 
| 
      
 308 
     | 
    
         
            +
                      # Parse predicate for a rule. Turn into a hash of { key: <String of Primary Value> + other keys in line }.
         
     | 
| 
      
 309 
     | 
    
         
            +
                      #
         
     | 
| 
      
 310 
     | 
    
         
            +
                      # val - The predicate string
         
     | 
| 
      
 311 
     | 
    
         
            +
                      #
         
     | 
| 
      
 312 
     | 
    
         
            +
                      # Returns a Hash.
         
     | 
| 
      
 313 
     | 
    
         
            +
                      Contract String => C::HashOf[Symbol, String]
         
     | 
| 
      
 314 
     | 
    
         
            +
                      def parsed_predicate(val)
         
     | 
| 
      
 315 
     | 
    
         
            +
                        v = val.sub(/\s*#.*\z/, "")
         
     | 
| 
      
 316 
     | 
    
         
            +
                        return { key: v } unless v.include?(";")
         
     | 
| 
      
 317 
     | 
    
         
            +
             
     | 
| 
      
 318 
     | 
    
         
            +
                        parts = v.split(/\s*;\s*/)
         
     | 
| 
      
 319 
     | 
    
         
            +
                        op_hash = { key: parts.shift }
         
     | 
| 
      
 320 
     | 
    
         
            +
                        parts.each do |part|
         
     | 
| 
      
 321 
     | 
    
         
            +
                          if part =~ /\A(\w+)\s*=\s*(\S+)\s*\z/
         
     | 
| 
      
 322 
     | 
    
         
            +
                            predicate_keyword, predicate_value = Regexp.last_match(1), Regexp.last_match(2)
         
     | 
| 
      
 323 
     | 
    
         
            +
                            unless SEMICOLON_PREDICATES.include?(predicate_keyword)
         
     | 
| 
      
 324 
     | 
    
         
            +
                              raise ArgumentError, "Rule Error: Invalid semicolon predicate #{predicate_keyword.inspect} in #{filename}!"
         
     | 
| 
      
 325 
     | 
    
         
            +
                            end
         
     | 
| 
      
 326 
     | 
    
         
            +
                            op_hash[predicate_keyword.to_sym] = predicate_value
         
     | 
| 
      
 327 
     | 
    
         
            +
                          else
         
     | 
| 
      
 328 
     | 
    
         
            +
                            raise ArgumentError, "Rule Error: Unparseable semicolon predicate #{part.inspect} in #{filename}!"
         
     | 
| 
      
 329 
     | 
    
         
            +
                          end
         
     | 
| 
      
 330 
     | 
    
         
            +
                        end
         
     | 
| 
      
 331 
     | 
    
         
            +
                        op_hash
         
     | 
| 
      
 332 
     | 
    
         
            +
                      end
         
     | 
| 
      
 333 
     | 
    
         
            +
                    end
         
     | 
| 
      
 334 
     | 
    
         
            +
                  end
         
     | 
| 
      
 335 
     | 
    
         
            +
                end
         
     | 
| 
      
 336 
     | 
    
         
            +
              end
         
     | 
| 
      
 337 
     | 
    
         
            +
            end
         
     | 
| 
         @@ -0,0 +1,171 @@ 
     | 
|
| 
      
 1 
     | 
    
         
            +
            # frozen_string_literal: true
         
     | 
| 
      
 2 
     | 
    
         
            +
            # Interact with rules that are stored in a YAML file.
         
     | 
| 
      
 3 
     | 
    
         
            +
             
     | 
| 
      
 4 
     | 
    
         
            +
            require "yaml"
         
     | 
| 
      
 5 
     | 
    
         
            +
             
     | 
| 
      
 6 
     | 
    
         
            +
            module Entitlements
         
     | 
| 
      
 7 
     | 
    
         
            +
              class Data
         
     | 
| 
      
 8 
     | 
    
         
            +
                class Groups
         
     | 
| 
      
 9 
     | 
    
         
            +
                  class Calculated
         
     | 
| 
      
 10 
     | 
    
         
            +
                    class YAML < Entitlements::Data::Groups::Calculated::Base
         
     | 
| 
      
 11 
     | 
    
         
            +
                      include ::Contracts::Core
         
     | 
| 
      
 12 
     | 
    
         
            +
                      C = ::Contracts
         
     | 
| 
      
 13 
     | 
    
         
            +
             
     | 
| 
      
 14 
     | 
    
         
            +
                      # Standard interface: Calculate the members of this group.
         
     | 
| 
      
 15 
     | 
    
         
            +
                      #
         
     | 
| 
      
 16 
     | 
    
         
            +
                      # Takes no arguments.
         
     | 
| 
      
 17 
     | 
    
         
            +
                      #
         
     | 
| 
      
 18 
     | 
    
         
            +
                      # Returns a Set[String] with DN's of the people in the group.
         
     | 
| 
      
 19 
     | 
    
         
            +
                      Contract C::None => C::Or[:calculating, C::SetOf[Entitlements::Models::Person]]
         
     | 
| 
      
 20 
     | 
    
         
            +
                      def members
         
     | 
| 
      
 21 
     | 
    
         
            +
                        @members ||= begin
         
     | 
| 
      
 22 
     | 
    
         
            +
                          Entitlements.logger.debug "Calculating members from #{filename}"
         
     | 
| 
      
 23 
     | 
    
         
            +
                          members_from_rules(rules)
         
     | 
| 
      
 24 
     | 
    
         
            +
                        end
         
     | 
| 
      
 25 
     | 
    
         
            +
                      end
         
     | 
| 
      
 26 
     | 
    
         
            +
             
     | 
| 
      
 27 
     | 
    
         
            +
                      # Standard interface: Get the description of this group.
         
     | 
| 
      
 28 
     | 
    
         
            +
                      #
         
     | 
| 
      
 29 
     | 
    
         
            +
                      # Takes no arguments.
         
     | 
| 
      
 30 
     | 
    
         
            +
                      #
         
     | 
| 
      
 31 
     | 
    
         
            +
                      # Returns a String with the group description, or "" if undefined.
         
     | 
| 
      
 32 
     | 
    
         
            +
                      Contract C::None => String
         
     | 
| 
      
 33 
     | 
    
         
            +
                      def description
         
     | 
| 
      
 34 
     | 
    
         
            +
                        parsed_data.fetch("description", "")
         
     | 
| 
      
 35 
     | 
    
         
            +
                      end
         
     | 
| 
      
 36 
     | 
    
         
            +
             
     | 
| 
      
 37 
     | 
    
         
            +
                      # Files can support modifiers that act independently of rules.
         
     | 
| 
      
 38 
     | 
    
         
            +
                      # This returns the modifiers from the file as a hash.
         
     | 
| 
      
 39 
     | 
    
         
            +
                      #
         
     | 
| 
      
 40 
     | 
    
         
            +
                      # Takes no arguments.
         
     | 
| 
      
 41 
     | 
    
         
            +
                      #
         
     | 
| 
      
 42 
     | 
    
         
            +
                      # Returns Hash[<String>key => <Object>value]
         
     | 
| 
      
 43 
     | 
    
         
            +
                      Contract C::None => C::HashOf[String => C::Any]
         
     | 
| 
      
 44 
     | 
    
         
            +
                      def modifiers
         
     | 
| 
      
 45 
     | 
    
         
            +
                        parsed_data.select { |k, _v| MODIFIERS.include?(k) }
         
     | 
| 
      
 46 
     | 
    
         
            +
                      end
         
     | 
| 
      
 47 
     | 
    
         
            +
             
     | 
| 
      
 48 
     | 
    
         
            +
                      private
         
     | 
| 
      
 49 
     | 
    
         
            +
             
     | 
| 
      
 50 
     | 
    
         
            +
                      # Get a hash of the filters defined in the group.
         
     | 
| 
      
 51 
     | 
    
         
            +
                      #
         
     | 
| 
      
 52 
     | 
    
         
            +
                      # Takes no arguments.
         
     | 
| 
      
 53 
     | 
    
         
            +
                      #
         
     | 
| 
      
 54 
     | 
    
         
            +
                      # Returns a Hash[String => :all/:none/List of strings].
         
     | 
| 
      
 55 
     | 
    
         
            +
                      Contract C::None => C::HashOf[String => C::Or[:all, :none, C::ArrayOf[String]]]
         
     | 
| 
      
 56 
     | 
    
         
            +
                      def initialize_filters
         
     | 
| 
      
 57 
     | 
    
         
            +
                        result = Entitlements::Data::Groups::Calculated.filters_default
         
     | 
| 
      
 58 
     | 
    
         
            +
                        return result unless parsed_data.key?("filters")
         
     | 
| 
      
 59 
     | 
    
         
            +
             
     | 
| 
      
 60 
     | 
    
         
            +
                        f = parsed_data["filters"]
         
     | 
| 
      
 61 
     | 
    
         
            +
                        unless f.is_a?(Hash)
         
     | 
| 
      
 62 
     | 
    
         
            +
                          raise ArgumentError, "For filters in #{filename}: expected Hash, got #{f.inspect}!"
         
     | 
| 
      
 63 
     | 
    
         
            +
                        end
         
     | 
| 
      
 64 
     | 
    
         
            +
             
     | 
| 
      
 65 
     | 
    
         
            +
                        f.each do |key, val|
         
     | 
| 
      
 66 
     | 
    
         
            +
                          unless result.key?(key)
         
     | 
| 
      
 67 
     | 
    
         
            +
                            raise ArgumentError, "Filter #{key} in #{filename} is invalid!"
         
     | 
| 
      
 68 
     | 
    
         
            +
                          end
         
     | 
| 
      
 69 
     | 
    
         
            +
             
     | 
| 
      
 70 
     | 
    
         
            +
                          values = if val.is_a?(String)
         
     | 
| 
      
 71 
     | 
    
         
            +
                            [val]
         
     | 
| 
      
 72 
     | 
    
         
            +
                          elsif val.is_a?(Array)
         
     | 
| 
      
 73 
     | 
    
         
            +
                            val
         
     | 
| 
      
 74 
     | 
    
         
            +
                          else
         
     | 
| 
      
 75 
     | 
    
         
            +
                            raise ArgumentError, "Value #{val.inspect} for #{key} in #{filename} is invalid!"
         
     | 
| 
      
 76 
     | 
    
         
            +
                          end
         
     | 
| 
      
 77 
     | 
    
         
            +
             
     | 
| 
      
 78 
     | 
    
         
            +
                          # Check for expiration
         
     | 
| 
      
 79 
     | 
    
         
            +
                          values.reject! { |v| v.is_a?(Hash) && expired?(v["expiration"].to_s, filename) }
         
     | 
| 
      
 80 
     | 
    
         
            +
                          values.map! { |v| v.is_a?(Hash) ? v.fetch("key") : v.strip }
         
     | 
| 
      
 81 
     | 
    
         
            +
             
     | 
| 
      
 82 
     | 
    
         
            +
                          if values.size == 1 && (values.first == "all" || values.first == "none")
         
     | 
| 
      
 83 
     | 
    
         
            +
                            result[key] = values.first.to_sym
         
     | 
| 
      
 84 
     | 
    
         
            +
                          elsif values.size > 1 && (values.include?("all") || values.include?("none"))
         
     | 
| 
      
 85 
     | 
    
         
            +
                            raise ArgumentError, "In #{filename}, #{key} cannot contain multiple entries when 'all' or 'none' is used!"
         
     | 
| 
      
 86 
     | 
    
         
            +
                          elsif values.size == 0
         
     | 
| 
      
 87 
     | 
    
         
            +
                            # This could happen if all of the specified filters were deleted due to expiration.
         
     | 
| 
      
 88 
     | 
    
         
            +
                            # In that case make no changes so the default gets used.
         
     | 
| 
      
 89 
     | 
    
         
            +
                            next
         
     | 
| 
      
 90 
     | 
    
         
            +
                          else
         
     | 
| 
      
 91 
     | 
    
         
            +
                            result[key] = values
         
     | 
| 
      
 92 
     | 
    
         
            +
                          end
         
     | 
| 
      
 93 
     | 
    
         
            +
                        end
         
     | 
| 
      
 94 
     | 
    
         
            +
             
     | 
| 
      
 95 
     | 
    
         
            +
                        result
         
     | 
| 
      
 96 
     | 
    
         
            +
                      end
         
     | 
| 
      
 97 
     | 
    
         
            +
             
     | 
| 
      
 98 
     | 
    
         
            +
                      # Files can support metadata intended for consumption by things other than LDAP.
         
     | 
| 
      
 99 
     | 
    
         
            +
                      # This returns the metadata from the file as a hash.
         
     | 
| 
      
 100 
     | 
    
         
            +
                      #
         
     | 
| 
      
 101 
     | 
    
         
            +
                      # Takes no arguments.
         
     | 
| 
      
 102 
     | 
    
         
            +
                      #
         
     | 
| 
      
 103 
     | 
    
         
            +
                      # Returns Hash[<String>key => <Object>value]
         
     | 
| 
      
 104 
     | 
    
         
            +
                      Contract C::None => C::HashOf[String => C::Any]
         
     | 
| 
      
 105 
     | 
    
         
            +
                      def initialize_metadata
         
     | 
| 
      
 106 
     | 
    
         
            +
                        return {} unless parsed_data.key?("metadata")
         
     | 
| 
      
 107 
     | 
    
         
            +
                        result = parsed_data["metadata"]
         
     | 
| 
      
 108 
     | 
    
         
            +
             
     | 
| 
      
 109 
     | 
    
         
            +
                        unless result.is_a?(Hash)
         
     | 
| 
      
 110 
     | 
    
         
            +
                          raise ArgumentError, "For metadata in #{filename}: expected Hash, got #{result.inspect}!"
         
     | 
| 
      
 111 
     | 
    
         
            +
                        end
         
     | 
| 
      
 112 
     | 
    
         
            +
             
     | 
| 
      
 113 
     | 
    
         
            +
                        result.each do |key, _|
         
     | 
| 
      
 114 
     | 
    
         
            +
                          next if key.is_a?(String)
         
     | 
| 
      
 115 
     | 
    
         
            +
                          raise ArgumentError, "For metadata in #{filename}: keys are expected to be strings, but #{key.inspect} is not!"
         
     | 
| 
      
 116 
     | 
    
         
            +
                        end
         
     | 
| 
      
 117 
     | 
    
         
            +
             
     | 
| 
      
 118 
     | 
    
         
            +
                        result
         
     | 
| 
      
 119 
     | 
    
         
            +
                      end
         
     | 
| 
      
 120 
     | 
    
         
            +
             
     | 
| 
      
 121 
     | 
    
         
            +
                      # Obtain the rule set from the YAML file and convert it to an object. Cache this the first
         
     | 
| 
      
 122 
     | 
    
         
            +
                      # time it happens, because this code is going to be called once per person!
         
     | 
| 
      
 123 
     | 
    
         
            +
                      #
         
     | 
| 
      
 124 
     | 
    
         
            +
                      # Takes no arguments.
         
     | 
| 
      
 125 
     | 
    
         
            +
                      #
         
     | 
| 
      
 126 
     | 
    
         
            +
                      # Returns a Hash.
         
     | 
| 
      
 127 
     | 
    
         
            +
                      Contract C::None => C::HashOf[String => C::Any]
         
     | 
| 
      
 128 
     | 
    
         
            +
                      def rules
         
     | 
| 
      
 129 
     | 
    
         
            +
                        @rules ||= begin
         
     | 
| 
      
 130 
     | 
    
         
            +
                          rules_hash = parsed_data["rules"]
         
     | 
| 
      
 131 
     | 
    
         
            +
                          unless rules_hash.is_a?(Hash)
         
     | 
| 
      
 132 
     | 
    
         
            +
                            raise "Expected to find 'rules' as a Hash in #{filename}, but got #{rules_hash.class}!"
         
     | 
| 
      
 133 
     | 
    
         
            +
                          end
         
     | 
| 
      
 134 
     | 
    
         
            +
                          remove_expired_rules(rules_hash)
         
     | 
| 
      
 135 
     | 
    
         
            +
                        end
         
     | 
| 
      
 136 
     | 
    
         
            +
                      end
         
     | 
| 
      
 137 
     | 
    
         
            +
             
     | 
| 
      
 138 
     | 
    
         
            +
                      # Remove expired rules from the rules hash.
         
     | 
| 
      
 139 
     | 
    
         
            +
                      #
         
     | 
| 
      
 140 
     | 
    
         
            +
                      # rules_hash - Hash of rules.
         
     | 
| 
      
 141 
     | 
    
         
            +
                      #
         
     | 
| 
      
 142 
     | 
    
         
            +
                      # Returns the updated hash that has no expired rules in it.
         
     | 
| 
      
 143 
     | 
    
         
            +
                      Contract C::HashOf[String => C::Any] => C::HashOf[String => C::Any]
         
     | 
| 
      
 144 
     | 
    
         
            +
                      def remove_expired_rules(rules_hash)
         
     | 
| 
      
 145 
     | 
    
         
            +
                        if rules_hash.keys.size == 1
         
     | 
| 
      
 146 
     | 
    
         
            +
                          if rules_hash.values.first.is_a?(Array)
         
     | 
| 
      
 147 
     | 
    
         
            +
                            return { rules_hash.keys.first => rules_hash.values.first.map { |v| remove_expired_rules(v) }.reject { |h| h.empty? } }
         
     | 
| 
      
 148 
     | 
    
         
            +
                          else
         
     | 
| 
      
 149 
     | 
    
         
            +
                            return rules_hash
         
     | 
| 
      
 150 
     | 
    
         
            +
                          end
         
     | 
| 
      
 151 
     | 
    
         
            +
                        end
         
     | 
| 
      
 152 
     | 
    
         
            +
             
     | 
| 
      
 153 
     | 
    
         
            +
                        expdate = rules_hash.delete("expiration")
         
     | 
| 
      
 154 
     | 
    
         
            +
                        return {} if expired?(expdate, filename)
         
     | 
| 
      
 155 
     | 
    
         
            +
                        rules_hash
         
     | 
| 
      
 156 
     | 
    
         
            +
                      end
         
     | 
| 
      
 157 
     | 
    
         
            +
             
     | 
| 
      
 158 
     | 
    
         
            +
                      # Return the parsed data from the file. This is called on demand and cached.
         
     | 
| 
      
 159 
     | 
    
         
            +
                      #
         
     | 
| 
      
 160 
     | 
    
         
            +
                      # Takes no arguments.
         
     | 
| 
      
 161 
     | 
    
         
            +
                      #
         
     | 
| 
      
 162 
     | 
    
         
            +
                      # Returns a Hash.
         
     | 
| 
      
 163 
     | 
    
         
            +
                      Contract C::None => C::HashOf[String => C::Any]
         
     | 
| 
      
 164 
     | 
    
         
            +
                      def parsed_data
         
     | 
| 
      
 165 
     | 
    
         
            +
                        @parsed_data ||= ::YAML.load(File.read(filename))
         
     | 
| 
      
 166 
     | 
    
         
            +
                      end
         
     | 
| 
      
 167 
     | 
    
         
            +
                    end
         
     | 
| 
      
 168 
     | 
    
         
            +
                  end
         
     | 
| 
      
 169 
     | 
    
         
            +
                end
         
     | 
| 
      
 170 
     | 
    
         
            +
              end
         
     | 
| 
      
 171 
     | 
    
         
            +
            end
         
     |