heimdallr 1.0.3 → 1.0.4
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.
- data/README.md +9 -0
- data/heimdallr.gemspec +1 -1
- data/lib/heimdallr.rb +15 -0
- data/lib/heimdallr/model.rb +1 -1
- data/lib/heimdallr/proxy/collection.rb +75 -13
- data/lib/heimdallr/proxy/record.rb +33 -3
- metadata +10 -10
    
        data/README.md
    CHANGED
    
    | @@ -110,6 +110,15 @@ that means it will raise an exception for every insecure request. Calling `.impl | |
| 110 110 | 
             
            of proxy object switched to another strategy. With that it will silently return nil for every attribute
         | 
| 111 111 | 
             
            that is inaccessible.
         | 
| 112 112 |  | 
| 113 | 
            +
            There are several options which alter Heimdallr's behavior in security-sensitive ways. They are described
         | 
| 114 | 
            +
            in [Heimdallr](http://rubydoc.info/gems/heimdallr/master/Heimdallr).
         | 
| 115 | 
            +
             | 
| 116 | 
            +
            Rails notes
         | 
| 117 | 
            +
            -----------
         | 
| 118 | 
            +
             | 
| 119 | 
            +
            As of Rails 3.2.3 attr_accessible is in whitelist mode by default. That makes no sense when using Heimdallr. To 
         | 
| 120 | 
            +
            turn it off set the `config.active_record.whitelist_attributes` value to false at yours `application.rb`.
         | 
| 121 | 
            +
             | 
| 113 122 | 
             
            Typical cases
         | 
| 114 123 | 
             
            -------------
         | 
| 115 124 |  | 
    
        data/heimdallr.gemspec
    CHANGED
    
    | @@ -3,7 +3,7 @@ $:.push File.expand_path("../lib", __FILE__) | |
| 3 3 |  | 
| 4 4 | 
             
            Gem::Specification.new do |s|
         | 
| 5 5 | 
             
              s.name        = "heimdallr"
         | 
| 6 | 
            -
              s.version     = "1.0. | 
| 6 | 
            +
              s.version     = "1.0.4"
         | 
| 7 7 | 
             
              s.authors     = ["Peter Zotov", "Boris Staal"]
         | 
| 8 8 | 
             
              s.email       = ["whitequark@whitequark.org", "boris@roundlake.ru"]
         | 
| 9 9 | 
             
              s.homepage    = "http://github.com/roundlake/heimdallr"
         | 
    
        data/lib/heimdallr.rb
    CHANGED
    
    | @@ -25,9 +25,24 @@ module Heimdallr | |
| 25 25 | 
             
                #
         | 
| 26 26 | 
             
                # @return [Boolean]
         | 
| 27 27 | 
             
                attr_accessor :allow_insecure_associations
         | 
| 28 | 
            +
             | 
| 29 | 
            +
                # Allow unrestricted association fetching in case of eager loading.
         | 
| 30 | 
            +
                #
         | 
| 31 | 
            +
                # By default, associations are restricted with fetch scope either when
         | 
| 32 | 
            +
                # they are accessed or when they are eagerly loaded (with #includes).
         | 
| 33 | 
            +
                # Condition injection on eager loads are known to be quirky in some cases,
         | 
| 34 | 
            +
                # particularly deeply nested polymorphic associations, and if the layout
         | 
| 35 | 
            +
                # of your database guarantees that any data fetched through explicitly
         | 
| 36 | 
            +
                # eagerly loaded associations will be safe to view (or if you restrict
         | 
| 37 | 
            +
                # it manually), you can enable this setting to skip automatic condition
         | 
| 38 | 
            +
                # injection.
         | 
| 39 | 
            +
                #
         | 
| 40 | 
            +
                # @return [Boolean]
         | 
| 41 | 
            +
                attr_accessor :skip_eager_condition_injection
         | 
| 28 42 | 
             
              end
         | 
| 29 43 |  | 
| 30 44 | 
             
              self.allow_insecure_associations = false
         | 
| 45 | 
            +
              self.skip_eager_condition_injection = false
         | 
| 31 46 |  | 
| 32 47 | 
             
              # {PermissionError} is raised when a security policy prevents
         | 
| 33 48 | 
             
              # a called operation from being executed.
         | 
    
        data/lib/heimdallr/model.rb
    CHANGED
    
    | @@ -35,7 +35,7 @@ module Heimdallr | |
| 35 35 | 
             
                    if block
         | 
| 36 36 | 
             
                      @restrictions = Evaluator.new(self, block)
         | 
| 37 37 | 
             
                    else
         | 
| 38 | 
            -
                      Proxy::Collection.new(context, restrictions(context).request_scope, options)
         | 
| 38 | 
            +
                      Proxy::Collection.new(context, restrictions(context).request_scope(:fetch, self), options)
         | 
| 39 39 | 
             
                    end
         | 
| 40 40 | 
             
                  end
         | 
| 41 41 |  | 
| @@ -19,6 +19,7 @@ module Heimdallr | |
| 19 19 | 
             
                  @context, @scope, @options = context, scope, options
         | 
| 20 20 |  | 
| 21 21 | 
             
                  @restrictions = @scope.restrictions(context)
         | 
| 22 | 
            +
                  @options[:eager_loaded] ||= {}
         | 
| 22 23 | 
             
                end
         | 
| 23 24 |  | 
| 24 25 | 
             
                # Collections cannot be restricted with different context or options.
         | 
| @@ -40,7 +41,7 @@ module Heimdallr | |
| 40 41 | 
             
                def self.delegate_as_constructor(name, method)
         | 
| 41 42 | 
             
                  class_eval(<<-EOM, __FILE__, __LINE__)
         | 
| 42 43 | 
             
                  def #{name}(attributes={})
         | 
| 43 | 
            -
                    record = @restrictions.request_scope(:fetch).new.restrict(@context,  | 
| 44 | 
            +
                    record = @restrictions.request_scope(:fetch).new.restrict(@context, options_with_escape)
         | 
| 44 45 | 
             
                    record.#{method}(attributes.merge(@restrictions.fixtures[:create]))
         | 
| 45 46 | 
             
                    record
         | 
| 46 47 | 
             
                  end
         | 
| @@ -53,7 +54,7 @@ module Heimdallr | |
| 53 54 | 
             
                def self.delegate_as_scope(name)
         | 
| 54 55 | 
             
                  class_eval(<<-EOM, __FILE__, __LINE__)
         | 
| 55 56 | 
             
                  def #{name}(*args)
         | 
| 56 | 
            -
                    Proxy::Collection.new(@context, @scope.#{name}(*args),  | 
| 57 | 
            +
                    Proxy::Collection.new(@context, @scope.#{name}(*args), options_with_escape)
         | 
| 57 58 | 
             
                  end
         | 
| 58 59 | 
             
                  EOM
         | 
| 59 60 | 
             
                end
         | 
| @@ -75,7 +76,7 @@ module Heimdallr | |
| 75 76 | 
             
                def self.delegate_as_record(name)
         | 
| 76 77 | 
             
                  class_eval(<<-EOM, __FILE__, __LINE__)
         | 
| 77 78 | 
             
                  def #{name}(*args)
         | 
| 78 | 
            -
                    @scope.#{name}(*args).restrict(@context,  | 
| 79 | 
            +
                    @scope.#{name}(*args).restrict(@context, options_with_eager_load)
         | 
| 79 80 | 
             
                  end
         | 
| 80 81 | 
             
                  EOM
         | 
| 81 82 | 
             
                end
         | 
| @@ -87,7 +88,7 @@ module Heimdallr | |
| 87 88 | 
             
                  class_eval(<<-EOM, __FILE__, __LINE__)
         | 
| 88 89 | 
             
                  def #{name}(*args)
         | 
| 89 90 | 
             
                    @scope.#{name}(*args).map do |element|
         | 
| 90 | 
            -
                      element.restrict(@context,  | 
| 91 | 
            +
                      element.restrict(@context, options_with_eager_load)
         | 
| 91 92 | 
             
                    end
         | 
| 92 93 | 
             
                  end
         | 
| 93 94 | 
             
                  EOM
         | 
| @@ -113,9 +114,6 @@ module Heimdallr | |
| 113 114 | 
             
                delegate_as_scope :uniq
         | 
| 114 115 | 
             
                delegate_as_scope :where
         | 
| 115 116 | 
             
                delegate_as_scope :joins
         | 
| 116 | 
            -
                delegate_as_scope :includes
         | 
| 117 | 
            -
                delegate_as_scope :eager_load
         | 
| 118 | 
            -
                delegate_as_scope :preload
         | 
| 119 117 | 
             
                delegate_as_scope :lock
         | 
| 120 118 | 
             
                delegate_as_scope :limit
         | 
| 121 119 | 
             
                delegate_as_scope :offset
         | 
| @@ -154,6 +152,57 @@ module Heimdallr | |
| 154 152 | 
             
                delegate_as_records :to_a
         | 
| 155 153 | 
             
                delegate_as_records :to_ary
         | 
| 156 154 |  | 
| 155 | 
            +
                # A proxy for +includes+ which adds Heimdallr conditions for eager loaded
         | 
| 156 | 
            +
                # associations.
         | 
| 157 | 
            +
                def includes(*associations)
         | 
| 158 | 
            +
                  # Normalize association list to strict nested hash.
         | 
| 159 | 
            +
                  normalize = ->(list) {
         | 
| 160 | 
            +
                    if list.is_a? Array
         | 
| 161 | 
            +
                      list.map(&normalize).reduce(:merge)
         | 
| 162 | 
            +
                    elsif list.is_a? Symbol
         | 
| 163 | 
            +
                      { list => {} }
         | 
| 164 | 
            +
                    elsif list.is_a? Hash
         | 
| 165 | 
            +
                      hash = {}
         | 
| 166 | 
            +
                      list.each do |key, value|
         | 
| 167 | 
            +
                        hash[key] = normalize.(value)
         | 
| 168 | 
            +
                      end
         | 
| 169 | 
            +
                      hash
         | 
| 170 | 
            +
                    end
         | 
| 171 | 
            +
                  }
         | 
| 172 | 
            +
                  associations = normalize.(associations)
         | 
| 173 | 
            +
             | 
| 174 | 
            +
                  current_scope = @scope.includes(associations)
         | 
| 175 | 
            +
             | 
| 176 | 
            +
                  add_conditions = ->(associations, scope) {
         | 
| 177 | 
            +
                    associations.each do |association, nested|
         | 
| 178 | 
            +
                      reflection = scope.reflect_on_association(association)
         | 
| 179 | 
            +
                      if reflection && !reflection.options[:polymorphic]
         | 
| 180 | 
            +
                        associated_klass = reflection.klass
         | 
| 181 | 
            +
             | 
| 182 | 
            +
                        if associated_klass.respond_to? :restrict
         | 
| 183 | 
            +
                          nested_scope = associated_klass.restrictions(@context).request_scope(:fetch)
         | 
| 184 | 
            +
             | 
| 185 | 
            +
                          where_values = nested_scope.where_values
         | 
| 186 | 
            +
                          if where_values.any?
         | 
| 187 | 
            +
                            current_scope = current_scope.where(*where_values)
         | 
| 188 | 
            +
                          end
         | 
| 189 | 
            +
             | 
| 190 | 
            +
                          add_conditions.(nested, associated_klass)
         | 
| 191 | 
            +
                        end
         | 
| 192 | 
            +
                      end
         | 
| 193 | 
            +
                    end
         | 
| 194 | 
            +
                  }
         | 
| 195 | 
            +
             | 
| 196 | 
            +
                  unless Heimdallr.skip_eager_condition_injection
         | 
| 197 | 
            +
                    add_conditions.(associations, current_scope)
         | 
| 198 | 
            +
                  end
         | 
| 199 | 
            +
             | 
| 200 | 
            +
                  options = @options.merge(eager_loaded:
         | 
| 201 | 
            +
                    @options[:eager_loaded].merge(associations))
         | 
| 202 | 
            +
             | 
| 203 | 
            +
                  Proxy::Collection.new(@context, current_scope, options)
         | 
| 204 | 
            +
                end
         | 
| 205 | 
            +
             | 
| 157 206 | 
             
                # A proxy for +find+ which restricts the returned record or records.
         | 
| 158 207 | 
             
                #
         | 
| 159 208 | 
             
                # @return [Proxy::Record, Array<Proxy::Record>]
         | 
| @@ -162,10 +211,10 @@ module Heimdallr | |
| 162 211 |  | 
| 163 212 | 
             
                  if result.is_a? Enumerable
         | 
| 164 213 | 
             
                    result.map do |element|
         | 
| 165 | 
            -
                      element.restrict(@context,  | 
| 214 | 
            +
                      element.restrict(@context, options_with_eager_load)
         | 
| 166 215 | 
             
                    end
         | 
| 167 216 | 
             
                  else
         | 
| 168 | 
            -
                    result.restrict(@context,  | 
| 217 | 
            +
                    result.restrict(@context, options_with_eager_load)
         | 
| 169 218 | 
             
                  end
         | 
| 170 219 | 
             
                end
         | 
| 171 220 |  | 
| @@ -175,7 +224,7 @@ module Heimdallr | |
| 175 224 | 
             
                # @yieldparam [Proxy::Record] record
         | 
| 176 225 | 
             
                def each
         | 
| 177 226 | 
             
                  @scope.each do |record|
         | 
| 178 | 
            -
                    yield record.restrict(@context,  | 
| 227 | 
            +
                    yield record.restrict(@context, options_with_eager_load)
         | 
| 179 228 | 
             
                  end
         | 
| 180 229 | 
             
                end
         | 
| 181 230 |  | 
| @@ -183,12 +232,12 @@ module Heimdallr | |
| 183 232 | 
             
                def method_missing(method, *args)
         | 
| 184 233 | 
             
                  if method =~ /^find_all_by/
         | 
| 185 234 | 
             
                    @scope.send(method, *args).map do |element|
         | 
| 186 | 
            -
                      element.restrict(@context,  | 
| 235 | 
            +
                      element.restrict(@context, options_with_escape)
         | 
| 187 236 | 
             
                    end
         | 
| 188 237 | 
             
                  elsif method =~ /^find_by/
         | 
| 189 | 
            -
                    @scope.send(method, *args).restrict(@context,  | 
| 238 | 
            +
                    @scope.send(method, *args).restrict(@context, options_with_escape)
         | 
| 190 239 | 
             
                  elsif @scope.heimdallr_scopes && @scope.heimdallr_scopes.include?(method)
         | 
| 191 | 
            -
                    Proxy::Collection.new(@context, @scope.send(method, *args),  | 
| 240 | 
            +
                    Proxy::Collection.new(@context, @scope.send(method, *args), options_with_escape)
         | 
| 192 241 | 
             
                  elsif @scope.respond_to? method
         | 
| 193 242 | 
             
                    raise InsecureOperationError,
         | 
| 194 243 | 
             
                        "Potentially insecure method #{method} was called"
         | 
| @@ -232,5 +281,18 @@ module Heimdallr | |
| 232 281 | 
             
                def creatable?
         | 
| 233 282 | 
             
                  @restrictions.can? :create
         | 
| 234 283 | 
             
                end
         | 
| 284 | 
            +
             | 
| 285 | 
            +
                private
         | 
| 286 | 
            +
             | 
| 287 | 
            +
                # Return options hash to pass to children proxies.
         | 
| 288 | 
            +
                # Currently this checks only eagerly loaded collections, which
         | 
| 289 | 
            +
                # shouldn't be passed around blindly.
         | 
| 290 | 
            +
                def options_with_escape
         | 
| 291 | 
            +
                  @options.reject { |k,v| k == :eager_loaded }
         | 
| 292 | 
            +
                end
         | 
| 293 | 
            +
             | 
| 294 | 
            +
                def options_with_eager_load
         | 
| 295 | 
            +
                  @options
         | 
| 296 | 
            +
                end
         | 
| 235 297 | 
             
              end
         | 
| 236 298 | 
             
            end
         | 
| @@ -16,9 +16,10 @@ module Heimdallr | |
| 16 16 | 
             
                # @param object   proxified record
         | 
| 17 17 | 
             
                # @option options [Boolean] implicit proxy type
         | 
| 18 18 | 
             
                def initialize(context, record, options={})
         | 
| 19 | 
            -
                  @context, @record, @options = context, record, options
         | 
| 19 | 
            +
                  @context, @record, @options = context, record, options.dup
         | 
| 20 20 |  | 
| 21 21 | 
             
                  @restrictions = @record.class.restrictions(context, record)
         | 
| 22 | 
            +
                  @eager_loaded = @options.delete(:eager_loaded) || {}
         | 
| 22 23 | 
             
                end
         | 
| 23 24 |  | 
| 24 25 | 
             
                # @method decrement(field, by=1)
         | 
| @@ -40,6 +41,18 @@ module Heimdallr | |
| 40 41 | 
             
                # and thus is not considered as a potential security threat.
         | 
| 41 42 | 
             
                delegate :touch, :to => :@record
         | 
| 42 43 |  | 
| 44 | 
            +
                # @method model_name
         | 
| 45 | 
            +
                # @macro delegate
         | 
| 46 | 
            +
                delegate :model_name, :to => :@record
         | 
| 47 | 
            +
             | 
| 48 | 
            +
                # @method to_key
         | 
| 49 | 
            +
                # @macro delegate
         | 
| 50 | 
            +
                delegate :to_key, :to => :@record
         | 
| 51 | 
            +
             | 
| 52 | 
            +
                # @method to_param
         | 
| 53 | 
            +
                # @macro delegate
         | 
| 54 | 
            +
                delegate :to_param, :to => :@record
         | 
| 55 | 
            +
             | 
| 43 56 | 
             
                # A proxy for +attributes+ method which removes all attributes
         | 
| 44 57 | 
             
                # without +:view+ permission.
         | 
| 45 58 | 
             
                def attributes
         | 
| @@ -176,7 +189,7 @@ module Heimdallr | |
| 176 189 | 
             
                    suffix = nil
         | 
| 177 190 | 
             
                  end
         | 
| 178 191 |  | 
| 179 | 
            -
                  if ( | 
| 192 | 
            +
                  if (@record.is_a?(ActiveRecord::Reflection) &&
         | 
| 180 193 | 
             
                      association = @record.class.reflect_on_association(method)) ||
         | 
| 181 194 | 
             
                     (!@record.class.heimdallr_relations.nil? &&
         | 
| 182 195 | 
             
                      @record.class.heimdallr_relations.include?(normalized_method))
         | 
| @@ -185,7 +198,19 @@ module Heimdallr | |
| 185 198 | 
             
                    if referenced.nil?
         | 
| 186 199 | 
             
                      nil
         | 
| 187 200 | 
             
                    elsif referenced.respond_to? :restrict
         | 
| 188 | 
            -
                       | 
| 201 | 
            +
                      if @eager_loaded.include?(method)
         | 
| 202 | 
            +
                        options = @options.merge(eager_loaded: @eager_loaded[method])
         | 
| 203 | 
            +
                      else
         | 
| 204 | 
            +
                        options = @options
         | 
| 205 | 
            +
                      end
         | 
| 206 | 
            +
             | 
| 207 | 
            +
                      if association.collection? && @eager_loaded.include?(method)
         | 
| 208 | 
            +
                        # Don't re-restrict eagerly loaded collections to not
         | 
| 209 | 
            +
                        # discard preloaded data.
         | 
| 210 | 
            +
                        Proxy::Collection.new(@context, referenced, options)
         | 
| 211 | 
            +
                      else
         | 
| 212 | 
            +
                        referenced.restrict(@context, @options)
         | 
| 213 | 
            +
                      end
         | 
| 189 214 | 
             
                    elsif Heimdallr.allow_insecure_associations
         | 
| 190 215 | 
             
                      referenced
         | 
| 191 216 | 
             
                    else
         | 
| @@ -263,6 +288,11 @@ module Heimdallr | |
| 263 288 | 
             
                  }.merge(@restrictions.reflection)
         | 
| 264 289 | 
             
                end
         | 
| 265 290 |  | 
| 291 | 
            +
                def visible?
         | 
| 292 | 
            +
                  scope = @restrictions.request_scope(:fetch)
         | 
| 293 | 
            +
                  scope.where({ @record.class.primary_key => @record.to_key }).any?
         | 
| 294 | 
            +
                end
         | 
| 295 | 
            +
             | 
| 266 296 | 
             
                def creatable?
         | 
| 267 297 | 
             
                  @restrictions.can? :create
         | 
| 268 298 | 
             
                end
         | 
    
        metadata
    CHANGED
    
    | @@ -1,7 +1,7 @@ | |
| 1 1 | 
             
            --- !ruby/object:Gem::Specification
         | 
| 2 2 | 
             
            name: heimdallr
         | 
| 3 3 | 
             
            version: !ruby/object:Gem::Version
         | 
| 4 | 
            -
              version: 1.0. | 
| 4 | 
            +
              version: 1.0.4
         | 
| 5 5 | 
             
              prerelease: 
         | 
| 6 6 | 
             
            platform: ruby
         | 
| 7 7 | 
             
            authors:
         | 
| @@ -10,11 +10,11 @@ authors: | |
| 10 10 | 
             
            autorequire: 
         | 
| 11 11 | 
             
            bindir: bin
         | 
| 12 12 | 
             
            cert_chain: []
         | 
| 13 | 
            -
            date: 2012- | 
| 13 | 
            +
            date: 2012-06-01 00:00:00.000000000 Z
         | 
| 14 14 | 
             
            dependencies:
         | 
| 15 15 | 
             
            - !ruby/object:Gem::Dependency
         | 
| 16 16 | 
             
              name: activesupport
         | 
| 17 | 
            -
              requirement: & | 
| 17 | 
            +
              requirement: &70300668429320 !ruby/object:Gem::Requirement
         | 
| 18 18 | 
             
                none: false
         | 
| 19 19 | 
             
                requirements:
         | 
| 20 20 | 
             
                - - ! '>='
         | 
| @@ -22,10 +22,10 @@ dependencies: | |
| 22 22 | 
             
                    version: 3.0.0
         | 
| 23 23 | 
             
              type: :runtime
         | 
| 24 24 | 
             
              prerelease: false
         | 
| 25 | 
            -
              version_requirements: * | 
| 25 | 
            +
              version_requirements: *70300668429320
         | 
| 26 26 | 
             
            - !ruby/object:Gem::Dependency
         | 
| 27 27 | 
             
              name: activemodel
         | 
| 28 | 
            -
              requirement: & | 
| 28 | 
            +
              requirement: &70300668428840 !ruby/object:Gem::Requirement
         | 
| 29 29 | 
             
                none: false
         | 
| 30 30 | 
             
                requirements:
         | 
| 31 31 | 
             
                - - ! '>='
         | 
| @@ -33,10 +33,10 @@ dependencies: | |
| 33 33 | 
             
                    version: 3.0.0
         | 
| 34 34 | 
             
              type: :runtime
         | 
| 35 35 | 
             
              prerelease: false
         | 
| 36 | 
            -
              version_requirements: * | 
| 36 | 
            +
              version_requirements: *70300668428840
         | 
| 37 37 | 
             
            - !ruby/object:Gem::Dependency
         | 
| 38 38 | 
             
              name: rspec
         | 
| 39 | 
            -
              requirement: & | 
| 39 | 
            +
              requirement: &70300668428460 !ruby/object:Gem::Requirement
         | 
| 40 40 | 
             
                none: false
         | 
| 41 41 | 
             
                requirements:
         | 
| 42 42 | 
             
                - - ! '>='
         | 
| @@ -44,10 +44,10 @@ dependencies: | |
| 44 44 | 
             
                    version: '0'
         | 
| 45 45 | 
             
              type: :development
         | 
| 46 46 | 
             
              prerelease: false
         | 
| 47 | 
            -
              version_requirements: * | 
| 47 | 
            +
              version_requirements: *70300668428460
         | 
| 48 48 | 
             
            - !ruby/object:Gem::Dependency
         | 
| 49 49 | 
             
              name: activerecord
         | 
| 50 | 
            -
              requirement: & | 
| 50 | 
            +
              requirement: &70300668428000 !ruby/object:Gem::Requirement
         | 
| 51 51 | 
             
                none: false
         | 
| 52 52 | 
             
                requirements:
         | 
| 53 53 | 
             
                - - ! '>='
         | 
| @@ -55,7 +55,7 @@ dependencies: | |
| 55 55 | 
             
                    version: '0'
         | 
| 56 56 | 
             
              type: :development
         | 
| 57 57 | 
             
              prerelease: false
         | 
| 58 | 
            -
              version_requirements: * | 
| 58 | 
            +
              version_requirements: *70300668428000
         | 
| 59 59 | 
             
            description: ! "Heimdallr aims to provide an easy to configure and efficient object-
         | 
| 60 60 | 
             
              and field-level access\n control solution, reusing proven patterns from gems like
         | 
| 61 61 | 
             
              CanCan and allowing one to manage permissions in a very\n fine-grained manner."
         |