checkpoint 1.0.3 → 1.1.0

Sign up to get free protection for your applications and to get access to all the features.
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