checkpoint 1.0.3 → 1.1.0

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.
Files changed (36) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +15 -0
  3. data/.travis.yml +8 -0
  4. data/README.md +6 -0
  5. data/db/migrations/{1_create_permits.rb → 1_create_grants.rb} +1 -1
  6. data/docs/index.rst +6 -1
  7. data/lib/checkpoint/agent.rb +32 -31
  8. data/lib/checkpoint/agent/resolver.rb +45 -15
  9. data/lib/checkpoint/agent/token.rb +8 -3
  10. data/lib/checkpoint/authority.rb +126 -14
  11. data/lib/checkpoint/credential.rb +21 -8
  12. data/lib/checkpoint/credential/permission.rb +6 -5
  13. data/lib/checkpoint/credential/resolver.rb +63 -62
  14. data/lib/checkpoint/credential/role.rb +2 -3
  15. data/lib/checkpoint/credential/role_map_resolver.rb +65 -0
  16. data/lib/checkpoint/credential/token.rb +7 -2
  17. data/lib/checkpoint/db.rb +8 -1
  18. data/lib/checkpoint/db/cartesian_select.rb +71 -0
  19. data/lib/checkpoint/db/{permit.rb → grant.rb} +4 -4
  20. data/lib/checkpoint/db/params.rb +36 -0
  21. data/lib/checkpoint/db/query/ac.rb +47 -0
  22. data/lib/checkpoint/db/query/acr.rb +54 -0
  23. data/lib/checkpoint/db/query/ar.rb +47 -0
  24. data/lib/checkpoint/db/query/cr.rb +47 -0
  25. data/lib/checkpoint/grants.rb +116 -0
  26. data/lib/checkpoint/query/role_granted.rb +1 -1
  27. data/lib/checkpoint/resource.rb +16 -19
  28. data/lib/checkpoint/resource/all_of_any_type.rb +0 -8
  29. data/lib/checkpoint/resource/all_of_type.rb +0 -22
  30. data/lib/checkpoint/resource/resolver.rb +34 -11
  31. data/lib/checkpoint/resource/token.rb +7 -2
  32. data/lib/checkpoint/version.rb +1 -1
  33. metadata +12 -7
  34. data/docs/authentication.rst +0 -18
  35. data/lib/checkpoint/permission_mapper.rb +0 -29
  36. data/lib/checkpoint/permits.rb +0 -133
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 41834a2cddd2e33f15d474ebaa6d7e3212f8ef7c3f25a4af8e8b01f28c38121f
4
- data.tar.gz: 1c9204c8b037814616ad05ed652cdca8bc836b0c17f007e29eab83dcb0d637c2
3
+ metadata.gz: 4a736e9a0fc810c9323dcd7612aee0cb27af02521c59ef448c62c4e5dac3e15c
4
+ data.tar.gz: b4ab5365b4cae8e5f0040af209bea2c6b9da1e1e19f856d2ae088081188df1b9
5
5
  SHA512:
6
- metadata.gz: 1f4a80a00753ab9d20fcf4f75af644addafad7e5e487fdc2f2f35992899163a9db2513bb137835fe4c90f1dffb0516076bb2f0fd25762140486dd08a8e7ed4a7
7
- data.tar.gz: cd30847154a96d832e26a3c36b5f3ddb6008439bfccfdf5ba65aebfed67ff98c9efb7364c48ebcee3d8e07181c7406c0e34c09d2d70d6f36f3bfc45112593749
6
+ metadata.gz: b9f4e9ddda32c366b2402333cf8e43f52da41ad4c7d5ec5a77b66ec524d43c9a9ed521f533e28a27c5c8a96a5382f08a3212db1b4ebe38c12d8b6ea3042b9151
7
+ data.tar.gz: ec1130f73c436b07751d09b7a7f74e9975da9ac2d6fdd29908878150e5053e9e15f9ba8916861b82a784aa8dafc28f0aee2bc1a7fb99046f68868a6f5dc1873d
@@ -15,6 +15,9 @@ AllCops:
15
15
  - 'bin/**/*'
16
16
  - 'vendor/**/*'
17
17
 
18
+ Layout/EmptyLineAfterGuardClause:
19
+ Enabled: false
20
+
18
21
  Layout/MultilineMethodDefinitionBraceLayout:
19
22
  EnforcedStyle: same_line
20
23
 
@@ -26,5 +29,17 @@ Metrics/BlockLength:
26
29
  - '*.gemspec'
27
30
  ExcludedMethods: ['describe', 'context', 'xdescribe', 'xcontext']
28
31
 
32
+ Layout/SpaceInsideBlockBraces:
33
+ Enabled: false
34
+
35
+ Layout/IndentArray:
36
+ EnforcedStyle: consistent
37
+
38
+ Style/ClassAndModuleChildren:
39
+ Enabled: false
40
+
29
41
  Style/StringLiterals:
30
42
  Enabled: false
43
+
44
+ Style/SymbolArray:
45
+ EnforcedStyle: brackets
@@ -3,3 +3,11 @@ language: ruby
3
3
  rvm:
4
4
  - 2.4.2
5
5
  before_install: gem install bundler -v 1.16.0
6
+ before_script:
7
+ - curl -L https://codeclimate.com/downloads/test-reporter/test-reporter-latest-linux-amd64 > ./cc-test-reporter
8
+ - chmod +x ./cc-test-reporter
9
+ - ./cc-test-reporter before-build
10
+ script:
11
+ - bin/rspec
12
+ after_script:
13
+ - ./cc-test-reporter after-build --exit-code $TRAVIS_TEST_RESULT
data/README.md CHANGED
@@ -1,5 +1,6 @@
1
1
  [![Build Status](https://travis-ci.org/mlibrary/checkpoint.svg?branch=master)](https://travis-ci.org/mlibrary/checkpoint?branch=master)
2
2
  [![Coverage Status](https://coveralls.io/repos/github/mlibrary/checkpoint/badge.svg?branch=master)](https://coveralls.io/github/mlibrary/checkpoint?branch=master)
3
+ [![Documentation Status](https://readthedocs.org/projects/checkpoint/badge/?version=latest)](https://checkpoint.readthedocs.io/en/latest/?badge=latest)
3
4
 
4
5
  # Checkpoint
5
6
 
@@ -18,6 +19,11 @@ And then execute:
18
19
 
19
20
  $ bundle
20
21
 
22
+ ## Documentation
23
+
24
+ User documentation source is available in the `docs` directory, and in rendered format
25
+ on [readthedocs](https://checkpoint.readthedocs.io/en/latest/).
26
+
21
27
  ## License
22
28
 
23
29
  Checkpoint is licensed under the BSD-3-Clause license. See [LICENSE.md](LICENSE.md).
@@ -2,7 +2,7 @@
2
2
 
3
3
  Sequel.migration do
4
4
  change do
5
- create_table :permits do
5
+ create_table :grants do
6
6
  primary_key :id
7
7
  column :agent_type, String, size: 100, null: false
8
8
  column :agent_id, String, size: 100, null: false
@@ -17,12 +17,17 @@ where enterprise, legacy, and new systems must all interoperate.
17
17
  Checkpoint emphasizes the use of policies and object-oriented design, giving
18
18
  examples from very simple rules through complex group- and role-based scenarios.
19
19
 
20
+ Checkpoint does not handle authentication at all. See Keycard_ for a library
21
+ that does so and provides identity attributes that can be used as the basis for
22
+ grants and policies.
23
+
24
+
20
25
  Table of Contents
21
26
  -----------------
22
27
 
23
28
  .. toctree::
24
29
  :maxdepth: 2
25
30
 
26
- authentication
27
31
  policies
28
32
 
33
+ .. _Keycard: https://github.com/mlibrary/keycard
@@ -5,43 +5,46 @@ require 'checkpoint/agent/token'
5
5
 
6
6
  module Checkpoint
7
7
  # An Agent is an any person or entity that might be granted various
8
- # permission, such as a user, group, or institution.
8
+ # credentials, such as a user, group, or institution.
9
9
  #
10
10
  # The application objects that an agent represents may be of any type; this
11
11
  # is more of an interface or role than a base class. The important concept is
12
- # that permits are granted to agents, and that agents may be representative
12
+ # that credentials are granted to agents, and that agents may be representative
13
13
  # of multiple concrete actors, such as any person affiliated with a given
14
14
  # institution or any member of a given group.
15
+ #
16
+ # In an application, agents will typically be created by the
17
+ # {Agent::Resolver} registered with an {Checkpoint::Authority}. This keeps
18
+ # most of the application code decoupled from the Agent type, allowing the
19
+ # binding to happen in an isolated component. It will also generally not be
20
+ # required to subclass Agent, since it delegates to the concrete actor in
21
+ # flexible, well-defined ways, detailed on the individual methods here.
15
22
  class Agent
16
23
  attr_accessor :actor
17
24
 
18
- # Create an Agent. This should not be called externally; use {::from} instead.
25
+ # Create an Agent, wrapping a concrete actor.
26
+ #
27
+ # When retrieving the ID or type, we will delegate to the the actor at that
28
+ # time. See the {#id} and {#type} methods for exact semantics.
19
29
  def initialize(actor)
20
30
  @actor = actor
21
31
  end
22
32
 
23
- # Default conversion from an actor to an {Agent}.
33
+ # Convert this object to an Agent.
24
34
  #
25
- # If the actor implements #to_agent, we will delegate to it. Otherwise,
26
- # we check if the actor implements #agent_type or #agent_id; if so, we
27
- # use them as the type and id, respectively. If not, we use the actor's
28
- # class name as the type and call #id for the id. If the actor does not
29
- # implement any of the ways to supply an #id, an error will be raised.
30
- #
31
- # @return [Agent] the actor converted to an agent
32
- def self.from(actor)
33
- if actor.respond_to?(:to_agent)
34
- actor.to_agent
35
- else
36
- new(actor)
37
- end
35
+ # For Checkpoint-supplied Agents, this is an identity operation,
36
+ # but it allows consistent handling of the built-in types and
37
+ # application-supplied types that will either implement this interface or
38
+ # convert themselves to a built-in type. This removes the requirement to
39
+ # extend Checkpoint types or bind to a specific conversion method.
40
+ def to_agent
41
+ self
38
42
  end
39
43
 
40
- # Get the captive actor's type.
44
+ # Get the wrapped actor's type.
41
45
  #
42
- # If the entity implements `#to_agent`, we will call that and use the
43
- # returned agent's type. If not, but it implements `#agent_type`, we
44
- # will use that. Otherwise, we use the actors's class name.
46
+ # If the actor implements `#agent_type`, we will return that. Otherwise,
47
+ # we use the actors's class name.
45
48
  #
46
49
  # @return [String] the name of the actor's type after calling `#to_s` on it.
47
50
  def type
@@ -52,14 +55,13 @@ module Checkpoint
52
55
  end.to_s
53
56
  end
54
57
 
55
- # Get the captive actor's id.
58
+ # Get the wrapped actor's ID.
56
59
  #
57
- # If the entity implements `#to_agent`, we will call that and use the
58
- # returned agent's id. If not, but it implements `#agent_id`, we
59
- # will use that. Otherwise, we call `#id`. If the the actor does not
60
- # implement any of these methods, we raise a {NoIdentifierError}.
60
+ # If the actor implements `#agent_id`, we will call it and return that
61
+ # value. Otherwise, we call `#id`. If the the actor does not implement
62
+ # either of these methods, we raise a {NoIdentifierError}.
61
63
  #
62
- # @return [String] the name of the actor's type after calling `#to_s` on it.
64
+ # @return [String] the actor's ID after calling `#to_s` on it.
63
65
  def id
64
66
  if actor.respond_to?(:agent_id)
65
67
  actor.agent_id
@@ -82,12 +84,11 @@ module Checkpoint
82
84
  other.is_a?(Agent) && actor.eql?(other.actor)
83
85
  end
84
86
 
85
- # Check whether two Agents refer to the same concrete actor.
87
+ # Check whether two Agents refer to the same concrete actor by type and id.
86
88
  # @param other [Agent] Another Agent to compare with
87
- # @return [Boolean] true when the other Agent's actor is the same as
88
- # determined by comparing them with `==`.
89
+ # @return [Boolean] true when the other Agent's type and id are equal.
89
90
  def ==(other)
90
- other.is_a?(Agent) && actor == other.actor
91
+ other.is_a?(Agent) && type == other.type && id == other.id
91
92
  end
92
93
  end
93
94
  end
@@ -2,16 +2,24 @@
2
2
 
3
3
  module Checkpoint
4
4
  class Agent
5
- # An Agent Resolver takes a concrete user (or other account/actor) object and
6
- # resolves it into the set of {Agent}s that the user represents. This has the
7
- # effect of allowing a Permit to any of those agents to take effect when
8
- # authorizing an action by this user.
5
+ # An Agent Resolver is the bridge between a concrete user (or other
6
+ # account/actor) and {Agent}s that the user represents.
9
7
  #
10
- # This implementation only resolves the user into one agent, using the default
11
- # conversion.
8
+ # There are two basic operations:
12
9
  #
13
- # To extend the set of {Agent}s resolved, implement a specialized version
14
- # that returns an array of agents from #resolve. This customized
10
+ # - Conversion maps an actor to a single Agent
11
+ # - Expansion maps an actor to all of the Agents it represents
12
+ #
13
+ # These allow credentials to be granted, matched, or revoked with the
14
+ # appropriate semantics, depending on the operation. In general, a Grant
15
+ # is given to or revoked from a single Agent, while matching is applied
16
+ # to all Agents the actor represents.
17
+ #
18
+ # This implementation does not implement any expansion semantics other
19
+ # than to convert the actor into an Agent and return it as a list.
20
+ #
21
+ # To extend the set of {Agent}s resolved, implement a subclass
22
+ # that returns an array of agents from #expand. This customized
15
23
  # implementation would typically be injected to an application-wide
16
24
  # {Checkpoint::Authority}, rather than being used directly.
17
25
  #
@@ -19,14 +27,36 @@ module Checkpoint
19
27
  # the user is a member of, or IP address-based geographical regions or
20
28
  # organizational affiliations.
21
29
  class Resolver
22
- # Resolve an actor to a list of agents it represents.
30
+ # Expand an actor to a list of Agents it represents.
31
+ #
32
+ # This implementation simply converts the actor and wraps the resulting
33
+ # Agent in an array.
34
+ #
35
+ # If extending or overriding, you will likely want to call super or
36
+ # {#convert} on the concrete actor to make sure that the most specific
37
+ # Agent is included. It is acceptable to return subclasses of Agent,
38
+ # though that is generally unnecessary because of its design of
39
+ # delegating to actor methods.
40
+ #
41
+ # @return [Agent] an array of agents for this actor
42
+ def expand(actor)
43
+ [convert(actor)]
44
+ end
45
+
46
+ # Default conversion from an actor to an {Agent}.
47
+ #
48
+ # If the actor implements #to_agent, we will delegate to it. Otherwise,
49
+ # we will instantiate an {Agent} with the supplied actor.
50
+ #
51
+ # Override this method to use a different or conditional Agent type.
23
52
  #
24
- # If extending or overriding, you will most likely want to either call
25
- # super, or use the default conversion directly.
26
- # @return [[Checkpoint::Agent]] an array of agents for this actor
27
- # @see Checkpoint::Agent.from
28
- def resolve(actor)
29
- [Checkpoint::Agent.from(actor)]
53
+ # @return [Agent] the actor converted to an agent
54
+ def convert(actor)
55
+ if actor.respond_to?(:to_agent)
56
+ actor.to_agent
57
+ else
58
+ Agent.new(actor)
59
+ end
30
60
  end
31
61
  end
32
62
  end
@@ -3,9 +3,9 @@
3
3
  module Checkpoint
4
4
  class Agent
5
5
  # An Agent::Token is an identifier object for an Agent. It
6
- # includes a type and an identifier. A {Permit} can be granted for a Token.
6
+ # includes a type and an identifier. A {Grant} can be created for a Token.
7
7
  # Concrete actors are resolved into a number of agents, and those agents'
8
- # tokens will be checked for matching permits.
8
+ # tokens will be checked for matching grants.
9
9
  class Token
10
10
  attr_reader :type, :id
11
11
 
@@ -34,7 +34,7 @@ module Checkpoint
34
34
  self
35
35
  end
36
36
 
37
- # @return [String] a token string suitable for granting or matching permits for this agent
37
+ # @return [String] a token string suitable for granting or matching grants for this agent
38
38
  def to_s
39
39
  "#{type}:#{id}"
40
40
  end
@@ -45,6 +45,11 @@ module Checkpoint
45
45
  other.is_a?(Token) && type == other.type && id == other.id
46
46
  end
47
47
 
48
+ # @return [Integer] hash code based on to_s
49
+ def hash
50
+ to_s.hash
51
+ end
52
+
48
53
  alias == eql?
49
54
  alias inspect uri
50
55
  end
@@ -3,36 +3,52 @@
3
3
  require 'checkpoint/agent/resolver'
4
4
  require 'checkpoint/credential/resolver'
5
5
  require 'checkpoint/resource/resolver'
6
- require 'checkpoint/permits'
6
+ require 'checkpoint/grants'
7
7
 
8
8
  module Checkpoint
9
9
  # An Authority is the central point of contact for authorization questions in
10
- # Checkpoint. It checks whether there are permits that would allow a given
10
+ # Checkpoint. It checks whether there are grants that would allow a given
11
11
  # action to be taken.
12
12
  class Authority
13
13
  def initialize(
14
14
  agent_resolver: Agent::Resolver.new,
15
15
  credential_resolver: Credential::Resolver.new,
16
16
  resource_resolver: Resource::Resolver.new,
17
- permits: Permits.new)
17
+ grants: Grants.new)
18
18
 
19
19
  @agent_resolver = agent_resolver
20
20
  @credential_resolver = credential_resolver
21
21
  @resource_resolver = resource_resolver
22
- @permits = permits
22
+ @grants = grants
23
23
  end
24
24
 
25
- def permits?(agent, credential, resource)
25
+ # Check whether there are any matching grants that would allow this actor
26
+ # to take the action on the target entity.
27
+ #
28
+ # The parameters are generally intended to be the most convenient forms for
29
+ # the application. For example, user and resource model objects would be
30
+ # typical in a Rails application, for the actor and entity, respectively.
31
+ # Using a symbol for a named action is typical.
32
+ #
33
+ # Each of these will be converted and expanded by the corresponding
34
+ # resolver to sets of {Agent}s, {Credential}s, and {Resource}s. In the case
35
+ # where you already have an Agent, Credential, or Resource, it can be
36
+ # passed; the expectation is that those types have an identity conversion.
37
+ #
38
+ # @param actor [Object|Agent] The person/account taking the action.
39
+ # @param action [Symbol|String|Credential] The action to authorize or
40
+ # Credential to check for.
41
+ # @param entity [Object|Resource] The entity/resource to be acted upon.
42
+ def permits?(actor, action, entity)
26
43
  # Conceptually equivalent to:
27
- # can?(agent, action, target)
28
- # can?(current_user, 'edit', @listing)
44
+ # can?(current_user, :edit, @listing)
29
45
 
30
46
  # user => agent tokens
31
47
  # action => credential tokens
32
48
  # target => resource tokens
33
49
 
34
- # Permit.where(agent: agents, credential: credentials, resource: resources)
35
- # SELECT * FROM permits
50
+ # Grant.where(agent: agents, credential: credentials, resource: resources)
51
+ # SELECT * FROM grants
36
52
  # WHERE agent IN('user:gkostin', 'account-type:umich', 'affiliation:lib-staff')
37
53
  # AND credential IN('permission:edit', 'role:editor')
38
54
  # AND resource IN('listing:17', 'type:listing')
@@ -46,13 +62,109 @@ module Checkpoint
46
62
  # ^^^ ^^^^ ^^^^
47
63
  # if current_user has at least one row in each of of these columns,
48
64
  # they have been "granted permission"
49
- permits.for(
50
- agent_resolver.resolve(agent),
51
- credential_resolver.resolve(credential),
52
- resource_resolver.resolve(resource)
65
+ grants.for(
66
+ agent_resolver.expand(actor),
67
+ credential_resolver.expand(action),
68
+ resource_resolver.expand(entity)
53
69
  ).any?
54
70
  end
55
71
 
72
+ # Find agents who have grants to take an action on an entity.
73
+ #
74
+ # The action and entity are expanded for matching more general grants.
75
+ #
76
+ # @return [Array<Agent::Token>] The distinct set of tokens for agents permitted to
77
+ # take the given action on the given entity
78
+ def who(action, entity)
79
+ credentials = credential_resolver.expand(action)
80
+ resources = resource_resolver.expand(entity)
81
+
82
+ grants.who(credentials, resources).map do |grant|
83
+ Agent::Token.new(grant.agent_type, grant.agent_id)
84
+ end.uniq
85
+ end
86
+
87
+ # Find credentials granted to an actor on an entity.
88
+ #
89
+ # The actor and entity are expanded for matching more general grants.
90
+ #
91
+ # @return [Array<Credential::Token>] The distinct set of tokens for credentials
92
+ # that the actor is granted on the entity
93
+ def what(actor, entity)
94
+ agents = agent_resolver.expand(actor)
95
+ resources = resource_resolver.expand(entity)
96
+
97
+ grants.what(agents, resources).map do |grant|
98
+ Credential::Token.new(grant.credential_type, grant.credential_id)
99
+ end.uniq
100
+ end
101
+
102
+ # Find resources on which the actor is permitted to take the given action.
103
+ #
104
+ # The actor and action are expanded for matching more general grants.
105
+ #
106
+ # @return [Array<Resource::Token>] The distinct set of tokens for resources
107
+ # on which the actor is permitted to take the given action
108
+ def which(actor, action)
109
+ agents = agent_resolver.expand(actor)
110
+ credentials = credential_resolver.expand(action)
111
+
112
+ grants.which(agents, credentials).map do |grant|
113
+ Resource::Token.new(grant.resource_type, grant.resource_id)
114
+ end.uniq
115
+ end
116
+
117
+ # Grant a single credential to a specific actor on an entity.
118
+ #
119
+ # The parameters are converted to Agent, Credential, and Resource types,
120
+ # but not expanded. This allows very specific grants to be made. The
121
+ # default conversion of a symbol or string as the action is to a
122
+ # {Credential::Permission} of the same name.
123
+ #
124
+ # If you want to use more general grants (for example, for an account type
125
+ # rather than for a given user), you should pass a more general Agent or an
126
+ # object that will be converted to one. Another example would be using a
127
+ # wildcard Resource as the entity to grant the credential for all objects
128
+ # of some given type.
129
+ #
130
+ # @param actor [Object|Agent] The actor to whom the grant should be made.
131
+ # @param action [Symbol|String|Credential] The action or Credential to grant.
132
+ # @param entity [Object|Resource] The entity or Resource to which the
133
+ # grant will apply.
134
+ # @return [Boolean] True if the grant was made; false if it failed.
135
+ def grant!(actor, action, entity)
136
+ grant = grants.grant!(
137
+ agent_resolver.convert(actor),
138
+ credential_resolver.convert(action),
139
+ resource_resolver.convert(entity)
140
+ )
141
+
142
+ !grant.nil?
143
+ end
144
+
145
+ # Revoke a credential from a specific actor on an entity.
146
+ #
147
+ # Like {#permit!}, the parameters are converted to Agent, Credential, and
148
+ # Resource types, but not expanded. This means that specific grants can be
149
+ # revoked without revoking more general ones. For example, if a user was
150
+ # granted read permission on an object, and then granted the same credential
151
+ # on all objects of that type, the more specific grant could be revoked
152
+ # individually.
153
+ #
154
+ # @param actor [Object|Agent] The actor from whom the grant should be revoked.
155
+ # @param action [Symbol|String|Credential] The action or Credential to revoke.
156
+ # @param entity [Object|Resource] The entity or Resource upon which the
157
+ # @return [Boolean] True if any grants were revoked; false if none were revoked.
158
+ def revoke!(actor, action, entity)
159
+ revoked = grants.revoke!(
160
+ agent_resolver.convert(actor),
161
+ credential_resolver.convert(action),
162
+ resource_resolver.convert(entity)
163
+ )
164
+
165
+ revoked.positive?
166
+ end
167
+
56
168
  # Dummy authority that rejects everything
57
169
  class RejectAll
58
170
  def permits?(*)
@@ -62,6 +174,6 @@ module Checkpoint
62
174
 
63
175
  private
64
176
 
65
- attr_reader :agent_resolver, :credential_resolver, :resource_resolver, :permits
177
+ attr_reader :agent_resolver, :credential_resolver, :resource_resolver, :grants
66
178
  end
67
179
  end