hydra-access-controls 6.3.4 → 6.4.0.pre1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: a66021a35f6755132cb22674e59e880d6980b571
4
- data.tar.gz: 4d82907106c6f1c80e63d13cf02aef92b8ab6faa
3
+ metadata.gz: d58a98abb5fa51ab9dcc8d38a80475d6bc9310cb
4
+ data.tar.gz: 23bc49074fa0715dc9515c1e9c7638484ba8040e
5
5
  SHA512:
6
- metadata.gz: dc5a599c3da37b374063f605ecc405db4ec68ea8b7b099a1c65507ff308459d58aea801589e05d98d6c52a33e33b31b2fc628de153071be8a451aba214659819
7
- data.tar.gz: d135be6d700d4aba21be4736d3e867c3327929a39f95b7eb63adaad205eb0a1792641511342e13aa96d4fb5ef26306c581387eb7670d5187dcaeb2389481a7e5
6
+ metadata.gz: 4b7e22c6d6e32dbdd5b39032af3d3caad5b3f6a885fbcf40b7852cc714ec1109aca94906b88f9044652375ca3ca81cb247fce852e58960ca728ab0678c06343c
7
+ data.tar.gz: dd23a59d0a72cd311011244ccba077cd8964cf0a295f5dace58e6487081411b35bfac55e767767ee3c5380b3be0fdf8b8bff0721f32e59af36ccce98e2266f56
data/.rspec ADDED
@@ -0,0 +1,2 @@
1
+ --color
2
+ --format progress
data/README.textile CHANGED
@@ -87,18 +87,14 @@ Object-level permissions and Policy-level permissions are combined to produce th
87
87
 
88
88
  To turn on Policy-based enforcement,
89
89
 
90
- * include the Hydra::PolicyAwareAbility module in your Ability class (Make sure to include it _after_ Hydra::Ability because it overrides some of the methods provided by that module.)
90
+ * include the Hydra::PolicyAwareAbility module in your Ability class (Make sure to remove `include Hydra::Ability`)
91
91
  * include the Hydra::PolicyAwareAccessControlsEnforcement module into any appropriate Controllers (or into ApplicationController)
92
92
 
93
93
 
94
94
  Example app/models/ability.rb
95
95
 
96
96
  <pre>
97
- # Allows you to use CanCan to control access to Models
98
- require 'cancan'
99
97
  class Ability
100
- include CanCan::Ability
101
- include Hydra::Ability
102
98
  include Hydra::PolicyAwareAbility
103
99
  end
104
100
  </pre>
@@ -0,0 +1,9 @@
1
+ module Hydra
2
+ module AccessControls
3
+ extend ActiveSupport::Autoload
4
+ autoload :AccessRight
5
+ autoload :WithAccessRight
6
+ autoload :Visibility
7
+ autoload :Permissions
8
+ end
9
+ end
@@ -0,0 +1,82 @@
1
+ module Hydra
2
+ module AccessControls
3
+ class AccessRight
4
+ # What these groups are called in the Hydra rightsMetadata XML:
5
+ PERMISSION_TEXT_VALUE_PUBLIC = 'public'.freeze
6
+ PERMISSION_TEXT_VALUE_AUTHENTICATED = 'registered'.freeze
7
+
8
+ # The values that get drawn to the page
9
+ VISIBILITY_TEXT_VALUE_PUBLIC = 'open'.freeze
10
+ VISIBILITY_TEXT_VALUE_EMBARGO = 'open_with_embargo_release_date'.freeze
11
+ VISIBILITY_TEXT_VALUE_AUTHENTICATED = 'authenticated'.freeze
12
+ VISIBILITY_TEXT_VALUE_PRIVATE = 'restricted'.freeze
13
+
14
+ # @param permissionable [#visibility, #permissions]
15
+ # @example
16
+ # file = GenericFile.find('sufia:1234')
17
+ # access = Sufia::AccessRight.new(file)
18
+ def initialize(permissionable)
19
+ @permissionable = permissionable
20
+ end
21
+
22
+ attr_reader :permissionable
23
+ delegate :persisted?, :permissions, :visibility, to: :permissionable
24
+ protected :persisted?, :permissions, :visibility
25
+
26
+
27
+ def open_access?
28
+ return true if has_visibility_text_for?(VISIBILITY_TEXT_VALUE_PUBLIC)
29
+ # We don't want to know if its under embargo, simply does it have a date.
30
+ # In this way, we can properly inform the label input
31
+ persisted_open_access_permission? && !permissionable.embargo_release_date.present?
32
+ end
33
+
34
+ def open_access_with_embargo_release_date?
35
+ return false unless permissionable_is_embargoable?
36
+ return true if has_visibility_text_for?(VISIBILITY_TEXT_VALUE_EMBARGO)
37
+ # We don't want to know if its under embargo, simply does it have a date.
38
+ # In this way, we can properly inform the label input
39
+ persisted_open_access_permission? && permissionable.embargo_release_date.present?
40
+ end
41
+
42
+ def authenticated_only?
43
+ return false if open_access?
44
+ has_permission_text_for?(PERMISSION_TEXT_VALUE_AUTHENTICATED) ||
45
+ has_visibility_text_for?(VISIBILITY_TEXT_VALUE_AUTHENTICATED)
46
+ end
47
+
48
+ def private?
49
+ return false if open_access?
50
+ return false if authenticated_only?
51
+ return false if open_access_with_embargo_release_date?
52
+ true
53
+ end
54
+
55
+ private
56
+
57
+ def persisted_open_access_permission?
58
+ if persisted?
59
+ has_permission_text_for?(PERMISSION_TEXT_VALUE_PUBLIC)
60
+ else
61
+ visibility.to_s == ''
62
+ end
63
+ end
64
+
65
+ def on_or_after_any_embargo_release_date?
66
+ return true unless permissionable.embargo_release_date
67
+ permissionable.embargo_release_date.to_date < Date.today
68
+ end
69
+
70
+ def permissionable_is_embargoable?
71
+ permissionable.respond_to?(:embargo_release_date)
72
+ end
73
+
74
+ def has_visibility_text_for?(text)
75
+ visibility == text
76
+ end
77
+ def has_permission_text_for?(text)
78
+ !!permissions.detect { |perm| perm[:name] == text }
79
+ end
80
+ end
81
+ end
82
+ end
@@ -0,0 +1,13 @@
1
+ module Hydra
2
+ module AccessControls
3
+ module Permissions
4
+ extend ActiveSupport::Concern
5
+ include Hydra::ModelMixins::RightsMetadata
6
+ include Hydra::AccessControls::Visibility
7
+
8
+ included do
9
+ has_metadata "rightsMetadata", type: Hydra::Datastream::RightsMetadata
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,59 @@
1
+ module Hydra
2
+ module AccessControls
3
+ module Visibility
4
+ extend ActiveSupport::Concern
5
+
6
+ def visibility=(value)
7
+ return if value.nil?
8
+ # only set explicit permissions
9
+ case value
10
+ when Hydra::AccessControls::AccessRight::VISIBILITY_TEXT_VALUE_PUBLIC
11
+ public_visibility!
12
+ when Hydra::AccessControls::AccessRight::VISIBILITY_TEXT_VALUE_AUTHENTICATED
13
+ registered_visibility!
14
+ when Hydra::AccessControls::AccessRight::VISIBILITY_TEXT_VALUE_PRIVATE
15
+ private_visibility!
16
+ else
17
+ raise ArgumentError, "Invalid visibility: #{value.inspect}"
18
+ end
19
+ end
20
+
21
+ def visibility
22
+ if read_groups.include? Hydra::AccessControls::AccessRight::PERMISSION_TEXT_VALUE_PUBLIC
23
+ Hydra::AccessControls::AccessRight::VISIBILITY_TEXT_VALUE_PUBLIC
24
+ elsif read_groups.include? Hydra::AccessControls::AccessRight::PERMISSION_TEXT_VALUE_AUTHENTICATED
25
+ Hydra::AccessControls::AccessRight::VISIBILITY_TEXT_VALUE_AUTHENTICATED
26
+ else
27
+ Hydra::AccessControls::AccessRight::VISIBILITY_TEXT_VALUE_PRIVATE
28
+ end
29
+ end
30
+
31
+ def visibility_changed?
32
+ @visibility_will_change
33
+ end
34
+
35
+ private
36
+ def visibility_will_change!
37
+ @visibility_will_change = true
38
+ end
39
+
40
+ def public_visibility!
41
+ visibility_will_change! unless visibility == Hydra::AccessControls::AccessRight::VISIBILITY_TEXT_VALUE_PUBLIC
42
+ self.datastreams["rightsMetadata"].permissions({:group=>Hydra::AccessControls::AccessRight::PERMISSION_TEXT_VALUE_PUBLIC}, "read")
43
+ end
44
+
45
+ def registered_visibility!
46
+ visibility_will_change! unless visibility == Hydra::AccessControls::AccessRight::VISIBILITY_TEXT_VALUE_AUTHENTICATED
47
+ self.datastreams["rightsMetadata"].permissions({:group=>Hydra::AccessControls::AccessRight::PERMISSION_TEXT_VALUE_AUTHENTICATED}, "read")
48
+ self.datastreams["rightsMetadata"].permissions({:group=>Hydra::AccessControls::AccessRight::PERMISSION_TEXT_VALUE_PUBLIC}, "none")
49
+ end
50
+
51
+ def private_visibility!
52
+ visibility_will_change! unless visibility == Hydra::AccessControls::AccessRight::VISIBILITY_TEXT_VALUE_PRIVATE
53
+ self.datastreams["rightsMetadata"].permissions({:group=>Hydra::AccessControls::AccessRight::PERMISSION_TEXT_VALUE_AUTHENTICATED}, "none")
54
+ self.datastreams["rightsMetadata"].permissions({:group=>Hydra::AccessControls::AccessRight::PERMISSION_TEXT_VALUE_PUBLIC}, "none")
55
+ end
56
+
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,20 @@
1
+ module Hydra
2
+ module AccessControls
3
+ module WithAccessRight
4
+ extend ActiveSupport::Concern
5
+
6
+ def under_embargo?
7
+ @under_embargo ||= rightsMetadata.under_embargo?
8
+ end
9
+
10
+ delegate :open_access?, :open_access_with_embargo_release_date?,
11
+ :authenticated_only_access?, :private_access?, to: :access_rights
12
+
13
+ protected
14
+ def access_rights
15
+ @access_rights ||= AccessRight.new(self)
16
+ end
17
+
18
+ end
19
+ end
20
+ end
@@ -6,6 +6,7 @@ require 'rails'
6
6
 
7
7
  module Hydra
8
8
  extend ActiveSupport::Autoload
9
+ autoload :AccessControls
9
10
  autoload :User
10
11
  autoload :AccessControlsEnforcement
11
12
  autoload :PolicyAwareAccessControlsEnforcement
@@ -19,6 +20,9 @@ module Hydra
19
20
  autoload :PermissionsCache
20
21
  autoload :PermissionsSolrDocument
21
22
  class Engine < Rails::Engine
23
+ config.autoload_paths += %W(
24
+ #{config.root}/app/models/concerns
25
+ )
22
26
  end
23
27
 
24
28
  module ModelMixins
@@ -30,5 +34,4 @@ module Hydra
30
34
  # This usually happens within a call to AccessControlsEnforcement#enforce_access_controls but can be
31
35
  # raised manually.
32
36
  class AccessDenied < ::CanCan::AccessDenied; end
33
-
34
37
  end
@@ -1,5 +1,7 @@
1
1
  # Repeats access controls evaluation methods, but checks against a governing "Policy" object (or "Collection" object) that provides inherited access controls.
2
2
  module Hydra::PolicyAwareAbility
3
+ extend ActiveSupport::Concern
4
+ include Hydra::Ability
3
5
 
4
6
  # Extends Hydra::Ability.test_edit to try policy controls if object-level controls deny access
5
7
  def test_edit(pid)
@@ -27,7 +27,7 @@ module Hydra::PolicyAwareAccessControlsEnforcement
27
27
  # Grant access based on user id & role
28
28
  user_access_filters += apply_policy_role_permissions(discovery_permissions)
29
29
  user_access_filters += apply_policy_individual_permissions(discovery_permissions)
30
- result = policy_class.find_with_conditions( user_access_filters.join(" OR "), :fl => "id" )
30
+ result = policy_class.find_with_conditions( user_access_filters.join(" OR "), :fl => "id", :rows => policy_class.count )
31
31
  logger.debug "get policies: #{result}\n\n"
32
32
  result.map {|h| h['id']}
33
33
  end
data/spec/spec_helper.rb CHANGED
@@ -1,6 +1,8 @@
1
1
  ENV["environment"] ||= "test"
2
2
 
3
3
  require 'rspec/mocks'
4
+ require 'rspec/autorun'
5
+ require 'hydra-access-controls'
4
6
 
5
7
  module Hydra
6
8
  # Stubbing Hydra.config[:policy_aware] so Hydra::PolicyAwareAbility will be loaded for tests.
@@ -12,6 +14,7 @@ end
12
14
 
13
15
  $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
14
16
  $LOAD_PATH.unshift(File.dirname(__FILE__))
17
+ Hydra::Engine.config.autoload_paths.each { |path| $LOAD_PATH.unshift path }
15
18
 
16
19
  if ENV['COVERAGE'] and RUBY_VERSION =~ /^1.9/
17
20
  require 'simplecov'
@@ -21,8 +24,6 @@ if ENV['COVERAGE'] and RUBY_VERSION =~ /^1.9/
21
24
  SimpleCov.start
22
25
  end
23
26
 
24
- require 'rspec/autorun'
25
- require 'hydra-access-controls'
26
27
  require 'support/mods_asset'
27
28
  require 'support/solr_document'
28
29
  require "support/user"
@@ -0,0 +1,57 @@
1
+ require 'spec_helper'
2
+
3
+ describe Hydra::AccessControls::AccessRight do
4
+ [
5
+ [false, Hydra::AccessControls::AccessRight::PERMISSION_TEXT_VALUE_PUBLIC, nil, nil, true, false, false, false],
6
+ [false, Hydra::AccessControls::AccessRight::PERMISSION_TEXT_VALUE_AUTHENTICATED, nil, nil, true, false, false, false],
7
+ [false, nil, nil, nil, true, false, false, false],
8
+ [false, nil, nil, 2.days.from_now, false, false, false, true],
9
+ [false, nil, nil, 2.days.ago, false, false, false, true],
10
+ [false, nil, Hydra::AccessControls::AccessRight::VISIBILITY_TEXT_VALUE_PUBLIC, nil, true, false, false, false],
11
+ [false, nil, Hydra::AccessControls::AccessRight::VISIBILITY_TEXT_VALUE_AUTHENTICATED, nil, false, true, false, false],
12
+ [false, nil, Hydra::AccessControls::AccessRight::VISIBILITY_TEXT_VALUE_PRIVATE, nil, false, false, true, false],
13
+ [false, nil, Hydra::AccessControls::AccessRight::VISIBILITY_TEXT_VALUE_EMBARGO, nil, false, false, false, true],
14
+ [true, Hydra::AccessControls::AccessRight::PERMISSION_TEXT_VALUE_PUBLIC, nil, nil, true, false, false, false],
15
+ [true, Hydra::AccessControls::AccessRight::PERMISSION_TEXT_VALUE_PUBLIC, nil, 2.days.from_now, false, false, false, true],
16
+ [true, Hydra::AccessControls::AccessRight::PERMISSION_TEXT_VALUE_PUBLIC, nil, 2.days.ago, false, false, false, true],
17
+ [true, Hydra::AccessControls::AccessRight::PERMISSION_TEXT_VALUE_AUTHENTICATED, nil, nil, false, true, false, false],
18
+ [true, nil, nil, nil, false, false, true, false],
19
+ [true, nil, Hydra::AccessControls::AccessRight::VISIBILITY_TEXT_VALUE_PUBLIC, nil, true, false, false, false],
20
+ [true, nil, Hydra::AccessControls::AccessRight::VISIBILITY_TEXT_VALUE_AUTHENTICATED, nil, false, true, false, false],
21
+ [true, nil, Hydra::AccessControls::AccessRight::VISIBILITY_TEXT_VALUE_PRIVATE, nil, false, false, true, false],
22
+ [true, nil, Hydra::AccessControls::AccessRight::VISIBILITY_TEXT_VALUE_EMBARGO, nil, false, false, false, true],
23
+ ].each do |given_persisted, givin_permission, given_visibility, given_embargo_release_date, expected_open_access, expected_authentication_only, expected_private, expected_open_access_with_embargo_release_date|
24
+ spec_text = <<-TEXT
25
+
26
+ GIVEN: {
27
+ persisted: #{given_persisted.inspect},
28
+ permission: #{givin_permission.inspect},
29
+ visibility: #{given_visibility.inspect},
30
+ embargo_release_date: #{given_embargo_release_date}
31
+ },
32
+ EXPECTED: {
33
+ open_access: #{expected_open_access.inspect},
34
+ restricted: #{expected_authentication_only.inspect},
35
+ private: #{expected_private.inspect},
36
+ open_access_with_embargo_release_date: #{expected_open_access_with_embargo_release_date}
37
+ },
38
+ TEXT
39
+
40
+ it spec_text do
41
+ permissions = [{access: :edit, name: givin_permission}]
42
+ permissionable = double(
43
+ 'permissionable',
44
+ permissions: permissions,
45
+ visibility: given_visibility,
46
+ persisted?: given_persisted,
47
+ embargo_release_date: given_embargo_release_date
48
+ )
49
+ access_right = Hydra::AccessControls::AccessRight.new(permissionable)
50
+
51
+ expect(access_right.open_access?).to eq(expected_open_access)
52
+ expect(access_right.authenticated_only?).to eq(expected_authentication_only)
53
+ expect(access_right.private?).to eq(expected_private)
54
+ expect(access_right.open_access_with_embargo_release_date?).to eq(expected_open_access_with_embargo_release_date)
55
+ end
56
+ end
57
+ end
@@ -129,7 +129,16 @@ describe Hydra::AdminPolicy do
129
129
  @user = FactoryGirl.build(:martia_morocco)
130
130
  RoleMapper.stub(:roles).with(@user.user_key).and_return(@user.roles)
131
131
  end
132
- subject { Ability.new(@user) }
132
+ before(:all) do
133
+ class TestAbility
134
+ include Hydra::PolicyAwareAbility
135
+ end
136
+ end
137
+
138
+ after(:all) do
139
+ Object.send(:remove_const, :TestAbility)
140
+ end
141
+ subject { TestAbility.new(@user) }
133
142
  context "Given a policy grants read access to a group I belong to" do
134
143
  before do
135
144
  @policy = Hydra::AdminPolicy.new
@@ -21,8 +21,6 @@ describe Hydra::PolicyAwareAbility do
21
21
  end
22
22
  before(:all) do
23
23
  class PolicyAwareClass
24
- include CanCan::Ability
25
- include Hydra::Ability
26
24
  include Hydra::PolicyAwareAbility
27
25
  end
28
26
  @policy = Hydra::AdminPolicy.new
@@ -40,7 +38,11 @@ describe Hydra::PolicyAwareAbility do
40
38
  @asset.admin_policy = @policy
41
39
  @asset.save
42
40
  end
43
- after(:all) { @policy.delete; @asset.delete }
41
+ after(:all) do
42
+ @policy.delete
43
+ @asset.delete
44
+ Object.send(:remove_const, :PolicyAwareClass)
45
+ end
44
46
  subject { PolicyAwareClass.new( User.new ) }
45
47
 
46
48
  describe "policy_pid_for" do
@@ -65,6 +65,14 @@ describe Hydra::PolicyAwareAccessControlsEnforcement do
65
65
  policy8.save
66
66
  @sample_policies << policy8
67
67
 
68
+ # user discover policies for testing that all are applied when over 10 are applicable
69
+ (9..11).each do |i|
70
+ policy = Hydra::AdminPolicy.create(:pid => "test:policy#{i}")
71
+ policy.default_permissions = [{:type=>"user", :access=>"discover", :name=>"sara_student"}]
72
+ policy.save
73
+ @sample_policies << policy
74
+ end
75
+
68
76
  # no access
69
77
  policy_no_access = Hydra::AdminPolicy.create(:pid=>"test:policy_no_access")
70
78
  @sample_policies << policy_no_access
@@ -0,0 +1,19 @@
1
+ require 'spec_helper'
2
+
3
+ describe Hydra::AccessControls::Visibility do
4
+ module VisibilityOverride
5
+ extend ActiveSupport::Concern
6
+ include Hydra::AccessControls::Permissions
7
+ def visibility; super; end
8
+ def visibility=(value); super(value); end
9
+ end
10
+ class MockParent < ActiveFedora::Base
11
+ include VisibilityOverride
12
+ end
13
+
14
+ it 'allows for overrides of visibility' do
15
+ expect{
16
+ MockParent.new(visibility: Hydra::AccessControls::AccessRight::VISIBILITY_TEXT_VALUE_PRIVATE)
17
+ }.to_not raise_error
18
+ end
19
+ end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: hydra-access-controls
3
3
  version: !ruby/object:Gem::Version
4
- version: 6.3.4
4
+ version: 6.4.0.pre1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Chris Beer
@@ -10,7 +10,7 @@ authors:
10
10
  autorequire:
11
11
  bindir: bin
12
12
  cert_chain: []
13
- date: 2013-08-27 00:00:00.000000000 Z
13
+ date: 2013-09-27 00:00:00.000000000 Z
14
14
  dependencies:
15
15
  - !ruby/object:Gem::Dependency
16
16
  name: activesupport
@@ -117,9 +117,15 @@ executables: []
117
117
  extensions: []
118
118
  extra_rdoc_files: []
119
119
  files:
120
+ - .rspec
120
121
  - README.textile
121
122
  - Rakefile
122
123
  - app/models/ability.rb
124
+ - app/models/concerns/hydra/access_controls.rb
125
+ - app/models/concerns/hydra/access_controls/access_right.rb
126
+ - app/models/concerns/hydra/access_controls/permissions.rb
127
+ - app/models/concerns/hydra/access_controls/visibility.rb
128
+ - app/models/concerns/hydra/access_controls/with_access_right.rb
123
129
  - app/models/role_mapper.rb
124
130
  - config/fedora.yml
125
131
  - config/solr.yml
@@ -151,6 +157,7 @@ files:
151
157
  - spec/support/user.rb
152
158
  - spec/unit/ability_spec.rb
153
159
  - spec/unit/access_controls_enforcement_spec.rb
160
+ - spec/unit/access_right_spec.rb
154
161
  - spec/unit/admin_policy_spec.rb
155
162
  - spec/unit/hydra_rights_metadata_persistence_spec.rb
156
163
  - spec/unit/hydra_rights_metadata_spec.rb
@@ -159,6 +166,7 @@ files:
159
166
  - spec/unit/policy_aware_access_controls_enforcement_spec.rb
160
167
  - spec/unit/rights_metadata_spec.rb
161
168
  - spec/unit/role_mapper_spec.rb
169
+ - spec/unit/visibility_spec.rb
162
170
  - tasks/hydra-access-controls.rake
163
171
  homepage: http://projecthydra.org
164
172
  licenses:
@@ -175,9 +183,9 @@ required_ruby_version: !ruby/object:Gem::Requirement
175
183
  version: 1.9.3
176
184
  required_rubygems_version: !ruby/object:Gem::Requirement
177
185
  requirements:
178
- - - '>='
186
+ - - '>'
179
187
  - !ruby/object:Gem::Version
180
- version: '0'
188
+ version: 1.3.1
181
189
  requirements: []
182
190
  rubyforge_project:
183
191
  rubygems_version: 2.0.5
@@ -196,6 +204,7 @@ test_files:
196
204
  - spec/support/user.rb
197
205
  - spec/unit/ability_spec.rb
198
206
  - spec/unit/access_controls_enforcement_spec.rb
207
+ - spec/unit/access_right_spec.rb
199
208
  - spec/unit/admin_policy_spec.rb
200
209
  - spec/unit/hydra_rights_metadata_persistence_spec.rb
201
210
  - spec/unit/hydra_rights_metadata_spec.rb
@@ -204,4 +213,5 @@ test_files:
204
213
  - spec/unit/policy_aware_access_controls_enforcement_spec.rb
205
214
  - spec/unit/rights_metadata_spec.rb
206
215
  - spec/unit/role_mapper_spec.rb
216
+ - spec/unit/visibility_spec.rb
207
217
  has_rdoc: