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
@@ -1,10 +1,10 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'checkpoint/credential/resolver'
4
+ require 'checkpoint/credential/role_map_resolver'
4
5
  require 'checkpoint/credential/role'
5
6
  require 'checkpoint/credential/permission'
6
7
  require 'checkpoint/credential/token'
7
- require 'checkpoint/permission_mapper'
8
8
 
9
9
  module Checkpoint
10
10
  # A Credential is the permission to take a particular action, or any
@@ -13,8 +13,8 @@ module Checkpoint
13
13
  #
14
14
  # Credentials are abstract; that is, they are not attached to a particular
15
15
  # actor or resource to be acted upon. A credential can be granted to an
16
- # {Agent}, optionally applying to a particular resource, by way of a Permit.
17
- # In other words, a credential can be likened to a class, while a permit can
16
+ # {Agent}, optionally applying to a particular resource, by way of a Grant.
17
+ # In other words, a credential can be likened to a class, while a grant can
18
18
  # be likened to an instance of that class, bound to a given agent and
19
19
  # possibly bound to a {Resource}.
20
20
  class Credential
@@ -47,23 +47,36 @@ module Checkpoint
47
47
  # This is an extension mechanism for application authors needing to
48
48
  # implement hierarchical or virtual credentials and wanting to do so in
49
49
  # an object-oriented way. The default implementation is to simply return
50
- # the credential itself in an array but, for example, an a custom
50
+ # the credential itself in an array but, for example, a custom
51
51
  # permission type could provide for aliasing by including itself and
52
52
  # another instance for the synonym. Another example is modeling permissions
53
53
  # granted by particular roles; this might be static, as defined in the
54
54
  # source files, or dynamic, as impacted by configuration or runtime data.
55
55
  #
56
- # As an alternative, these rules could be implemented under a
57
- # {PermissionMapper} in an application that prefers to model its credentials
58
- # as strings or symbols, rather than more specialized objects.
56
+ # As an alternative, these rules could be implemented by using the rather
57
+ # straightforward {RoleMapResolver} or a custom {Credential::Resolver}.
59
58
  #
60
- # @see Checkpoint::PermissionMapper
61
59
  # @return [Array<Credential>] the expanded list of credentials that would
62
60
  # grant this one
63
61
  def granted_by
64
62
  [self]
65
63
  end
66
64
 
65
+ # Convert this object to a Credential.
66
+ #
67
+ # For Checkpoint-supplied Credential types, this is an identity operation,
68
+ # but it allows consistent handling of the built-in types and
69
+ # application-supplied types that will either implement this interface or
70
+ # convert themselves to a built-in type. This removes the requirement to
71
+ # extend Checkpoint types and, in combination with `#granted_by`, allows
72
+ # design of an object-oriented permission model that can interoperate
73
+ # seamlessly with the Checkpoint constructs.
74
+ #
75
+ # @return [Credential] this credential
76
+ def to_credential
77
+ self
78
+ end
79
+
67
80
  # @return [Token] a token for this credential
68
81
  def token
69
82
  @token ||= Token.new(type, id)
@@ -11,11 +11,12 @@ module Checkpoint
11
11
  # action is permitted for a user. However, Permission could be extended or
12
12
  # modified to implement aliasing or hierarchy, for example.
13
13
  #
14
- # More likely, though, is advising the resolution of Permissions through a
15
- # {Checkpoint::PermissionMapper} or implementing a custom
16
- # {Checkpoint::Credential::Resolver}. Subclassing or monkey-patching Permission
17
- # should only be necessary if the application needs to extend the actual
18
- # behavior of the Permission objects, rather than just which ones are resolved.
14
+ # More likely, though, is advising the resolution of Permissions with a
15
+ # role map and {Checkpoint::Credential::RoleMapResolver} or implementing a
16
+ # custom {Checkpoint::Credential::Resolver}. Subclassing or monkey-patching
17
+ # Permission should only be necessary if the application needs to extend
18
+ # the actual behavior of the Permission objects, rather than just which
19
+ # ones are resolved.
19
20
  class Permission < Credential
20
21
  TYPE = 'permission'
21
22
 
@@ -1,87 +1,88 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'checkpoint/permission_mapper'
4
-
5
3
  module Checkpoint
6
4
  class Credential
7
- # A Credential Resolver takes a concrete action name and resolves it into any
8
- # {Credential}s that would permit the action.
5
+ # A Credential Resolver is the bridge between application action names
6
+ # and {Credential}s that would permit those actions.
9
7
  #
10
8
  # Checkpoint makes no particular demand on the credential model for an
11
- # application, but offers a useful default implementation supporting
9
+ # application, but offers two useful default implementations supporting
12
10
  # permissions and roles. There are no default rules in Checkpoint as to which
13
11
  # permissions or roles exist and, therefore, it has no default mapping of
14
12
  # roles to permissions.
15
13
  #
16
- # This default resolver can be advised about an application model using roles
17
- # and permissions customized by using one or both of two extension points:
14
+ # This default resolver is useful for applications that only deal with
15
+ # permissions or model credentials in an object-oriented way (explained
16
+ # below). The {RoleMapResolver} supports a basic mapping pattern between
17
+ # named roles and permissions those roles would grant.
18
+ #
19
+ # More sophisticated mappings, such as those reading a configuration file
20
+ # or with dynamic roles and permissions defined in a database, can be
21
+ # implemented in a custom resolver.
18
22
  #
19
- # 1. Supplying a {PermissionMapper} gives a way to map action names to any
20
- # "larger" permissions (e.g., "manage" being a shorthand for all CRUD
21
- # operations on a Resource type) or roles that would grant a given
22
- # permission. This affords a rather straightforward mapping of strings
23
- # or symbols, short of building customized Credential types.
23
+ # Using this default resolver, it is possible to implement your own
24
+ # {Credential} types to model an application's credentials in an
25
+ # object-oriented way. If the resolver receives a {Credential} (rather than
26
+ # a string or symbol), it will call `#granted_by` on it to expand it. The
27
+ # Credential should be sure to include itself in the array it returns
28
+ # unless it is virtual and should never be considered as granted directly.
24
29
  #
25
- # 2. Implementing your own {Credential} types gives a way to model an
26
- # application's credentials in an object-oriented way. If the resolver
27
- # receives a {Credential} (rather than a string or symbol), it will call
28
- # `#granted_by` on it to expand it. The Credential should be sure to
29
- # include itself in the array it returns unless it is virtual and should
30
- # never be considered as granted directly.
30
+ # An example of this type of modeling could be implementing named action
31
+ # classes that have information like labels and descriptions for display
32
+ # purposes in a permission management interface or messages when a user
33
+ # does not have sufficient permission to take that action. There might also
34
+ # be some inheritance such that granting something like "manage" would be a
35
+ # shorthand for all CRUD operations on a Resource type.
31
36
  #
37
+ # Another example of a custom Credential type could be a license that would
38
+ # be an application object in its own right, carrying information such as
39
+ # the licensor and expiration date. By implementing the Credential
40
+ # interface, a license could be granted directly with the Checkpoint
41
+ # {Authority} and enforced at the policies.
42
+ #
43
+ # @see {RoleMapResolver}
32
44
  class Resolver
33
- def initialize(permission_mapper: PermissionMapper.new)
34
- @permission_mapper = permission_mapper
35
- end
36
-
37
- # Resolve an action into all {Credential}s that would permit it.
45
+ # Expand an action into all {Credential}s that would permit it.
38
46
  #
39
- # When supplied a string or symbol, we call `permissions_for` and
40
- # `roles_granting` on the {PermissionMapper} creating a {Permission} or
41
- # {Role} for every result, returning them all in an array.
47
+ # Expansion first converts the action to a Credential, and then calls
48
+ # `#granted_by` on it.
42
49
  #
43
- # When supplied a Credential, we call `#granted_by` on it and bypass the
44
- # PermissionMapper. More precisely, we only check that the object responds to
45
- # `#granted_by?`, but it would generally be a Credential subclass. The
46
- # Credential should return an array, but we ensure the return type by
47
- # wrapping and flattening.
50
+ # Note that the parameter name is `action`, though it can accept a
51
+ # Credential. This is to promote the most common and recommended model:
52
+ # authorizing based on named application actions. However, the
53
+ # polymorphic and hierarchical nature of credentials means that there can
54
+ # be cases where expanding something like a {Role} is intentional. As an
55
+ # example, an administrative interface for managing roles granted to
56
+ # users might need to expand the roles to show inheritance, rather than
57
+ # checking whether a given user would be permitted to take some action.
48
58
  #
49
- # Note that the parameter name to `resolve` is `action`. This isn't a perfect
50
- # name, but credentials are polymorphic such a way that there really is no
51
- # better application-side term (cf. actor -> Agent, entity -> Resource). It
52
- # would be something like `action_or_role`, `permission_or_role`, or a generic
53
- # `credential`. Part of the naming intent here was to distinguish from the
54
- # action and the ability to perform it. This inheritance relationship
55
- # permissions and roles are both credential types is a distinguishing feature
56
- # of Checkpoint, as opposed to models that treat permissions and roles as
57
- # distinct concepts that must be granted in very different ways. A better
58
- # name for this parameter may emerge over time, but it seems unlikely. The
59
- # name `action` was selected because the most common and appropriate concrete
60
- # thing to look for is a permission to take a named application action.
59
+ # @param action [String|Symbol|Credential] the action name or Credential
60
+ # to expand
61
+ # @return [Array<Credential>] the set of Credentials, any of which would
62
+ # allow the action
63
+ def expand(action)
64
+ convert(action).granted_by
65
+ end
66
+
67
+ # Convert an action to a Credential.
68
+ #
69
+ # This conversion is basic, assuming that actions should convert directly
70
+ # to permissions. For example, if `:read` or `'read'` is passed, a
71
+ # {Credential::Permission} with id `'read'` is returned. If the action
72
+ # implements `#to_credential`, that will be called and returned; the
73
+ # object must either extend {Credential} or implement its public methods.
61
74
  #
62
75
  # @param action [String|Symbol|Credential] the action name or Credential
63
- # to expand into any Credential that would grant it.
64
- def resolve(action)
65
- if action.respond_to?(:granted_by)
66
- [action.granted_by].flatten
76
+ # to convert
77
+ # @return Credential a Credential object that would specifically allow
78
+ # the action supplied
79
+ def convert(action)
80
+ if action.respond_to?(:to_credential)
81
+ action.to_credential
67
82
  else
68
- permissions_for(action) + roles_granting(action)
83
+ Permission.new(action)
69
84
  end
70
85
  end
71
-
72
- private
73
-
74
- def permissions_for(action)
75
- perms = permission_mapper.permissions_for(action)
76
- perms.map { |perm| Credential::Permission.new(perm) }
77
- end
78
-
79
- def roles_granting(action)
80
- roles = permission_mapper.roles_granting(action)
81
- roles.map { |role| Credential::Role.new(role) }
82
- end
83
-
84
- attr_reader :permission_mapper
85
86
  end
86
87
  end
87
88
  end
@@ -8,10 +8,9 @@ module Checkpoint
8
8
  # The most common use from outside Checkpoint will be by way of
9
9
  # {Checkpoint::Query::RoleGranted}, which will ask whether a given named
10
10
  # role is granted for a user. However, Role could be extended or modified
11
- # to implement aliasing or hierarchy, for example
11
+ # to implement aliasing or hierarchy, for example.
12
12
  #
13
- # More likely, though, is advising the resolution of Roles through a
14
- # {Checkpoint::PermissionMapper} or implementing a custom
13
+ # More likely, though, is implementing a custom
15
14
  # {Checkpoint::Credential::Resolver}. Subclassing or monkey-patching Role
16
15
  # should only be necessary if the application needs to extend the actual
17
16
  # behavior of the Role objects, rather than just which ones are resolved.
@@ -0,0 +1,65 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Checkpoint::Credential
4
+ # Credential Resolver that supports a basic role map model.
5
+ #
6
+ # The role map should be a hash containing all of the roles and each key
7
+ # should be an array of the permissions that role would grant. For example:
8
+ #
9
+ # ```
10
+ # {
11
+ # admin: [:read, :create, :edit, :delete],
12
+ # guest: [:read]
13
+ # }
14
+ # ```
15
+ #
16
+ # Note that this example is not a recommendation of how to model an
17
+ # application's permissions; it is only to show the expected format of the
18
+ # hash and that there is no inheritance of permissions between roles (:read
19
+ # is included in both roles). Any more sophisticated rules should be
20
+ # implemented in a custom Resolver, or custom Credential types.
21
+ #
22
+ # Actions convert to Permissions according to the base {Resolver} and expand
23
+ # according to the map.
24
+ class RoleMapResolver < Resolver
25
+ attr_reader :role_map, :permission_map
26
+
27
+ def initialize(role_map)
28
+ @role_map = role_map
29
+ @permission_map = invert_role_map
30
+ end
31
+
32
+ # Expand an action name into the matching permission and any roles that
33
+ # would grant it.
34
+ #
35
+ # @return [Array<Credential>]
36
+ def expand(action)
37
+ permissions_for(action) + roles_granting(action)
38
+ end
39
+
40
+ private
41
+
42
+ def permissions_for(action)
43
+ [Permission.new(action)]
44
+ end
45
+
46
+ def roles_granting(action)
47
+ if permission_map.key?(action)
48
+ permission_map[action].map {|role| Role.new(role) }
49
+ else
50
+ []
51
+ end
52
+ end
53
+
54
+ def invert_role_map
55
+ {}.tap do |hash|
56
+ role_map.each do |role, permissions|
57
+ permissions.each do |permission|
58
+ hash[permission] ||= []
59
+ hash[permission] << role
60
+ end
61
+ end
62
+ end
63
+ end
64
+ end
65
+ end
@@ -3,9 +3,9 @@
3
3
  module Checkpoint
4
4
  class Credential
5
5
  # A Credential::Token is an identifier object for a Credential. It includes
6
- # a type and an identifier. A {Permit} can be granted for a Token. Concrete
6
+ # a type and an identifier. A {Grant} can be created for a Token. Concrete
7
7
  # actions are resolved into a number of credentials, and those credentials'
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
 
@@ -44,6 +44,11 @@ module Checkpoint
44
44
  other.is_a?(Token) && type == other.type && id == other.id
45
45
  end
46
46
 
47
+ # @return [Integer] hash code based on to_s
48
+ def hash
49
+ to_s.hash
50
+ end
51
+
47
52
  alias == eql?
48
53
  alias inspect uri
49
54
  end
@@ -4,6 +4,13 @@ require 'ostruct'
4
4
  require 'logger'
5
5
  require 'yaml'
6
6
 
7
+ require_relative 'db/cartesian_select'
8
+ require_relative 'db/params'
9
+ require_relative 'db/query/acr'
10
+ require_relative 'db/query/ac'
11
+ require_relative 'db/query/ar'
12
+ require_relative 'db/query/cr'
13
+
7
14
  module Checkpoint
8
15
  # Module for everything related to the Checkpoint database.
9
16
  module DB
@@ -110,7 +117,7 @@ module Checkpoint
110
117
 
111
118
  def model_files
112
119
  [
113
- 'db/permit'
120
+ 'db/grant'
114
121
  ]
115
122
  end
116
123
 
@@ -0,0 +1,71 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Checkpoint
4
+ module DB
5
+ # Helper for querying by cross-products across sets of parameters,
6
+ # especially for grants.
7
+ #
8
+ # This class is called CartesianSelect because the logical search space is
9
+ # the Cartesian product, for example, of agents X credentials X resources.
10
+ # All grants in that space would be selected.
11
+ #
12
+ # This is a base class to support convenient variations for searching in
13
+ # different scenarios. It is unlikely to be very useful in its own right,
14
+ # but provides structure for specific subclasses. For example, {Query::ACR}
15
+ # searches for grants when agents, credentials, and resources are all
16
+ # known, as when checking authorization. When seeking to list agents that
17
+ # could take a given action on a resource, {Query::CR} would be useful.
18
+ #
19
+ # Subclasses should extends the conditions and parameters methods to supply
20
+ # the placeholders and matching values. The {Params} class is helpful for
21
+ # that purpose.
22
+ #
23
+ # The queries are ultimately implemented with an IN clause for each key in
24
+ # the conditions with binding expressions in the way Sequel expects them.
25
+ class CartesianSelect
26
+ attr_reader :scope
27
+
28
+ def initialize(scope: Grant)
29
+ @scope = scope
30
+ end
31
+
32
+ def query
33
+ scope.where(conditions)
34
+ end
35
+
36
+ def all
37
+ exec(:select)
38
+ end
39
+
40
+ def first
41
+ exec(:first)
42
+ end
43
+
44
+ def delete
45
+ exec(:delete)
46
+ end
47
+
48
+ def conditions
49
+ {
50
+ zone_id: :$zone_id
51
+ }
52
+ end
53
+
54
+ def parameters
55
+ {
56
+ zone_id: Grant.default_zone
57
+ }
58
+ end
59
+
60
+ private
61
+
62
+ def exec(mode)
63
+ query.call(mode, parameters)
64
+ end
65
+
66
+ def tokenize(collection)
67
+ [collection].flatten.map(&:token)
68
+ end
69
+ end
70
+ end
71
+ end
@@ -2,11 +2,11 @@
2
2
 
3
3
  module Checkpoint
4
4
  module DB
5
- # Sequel model for permits
6
- class Permit < Sequel::Model(DB.db)
7
- # Instantiate a Permit from the constituent domain objects (agent,
5
+ # Sequel model for grants
6
+ class Grant < Sequel::Model(DB.db)
7
+ # Instantiate a Grant from the constituent domain objects (agent,
8
8
  # resource, credential).
9
- def self.from(agent, credential, resource, zone: 'system')
9
+ def self.from(agent, credential, resource, zone: default_zone)
10
10
  new(
11
11
  agent_type: agent.type, agent_id: agent.id, agent_token: agent.token,
12
12
  credential_type: credential.type, credential_id: credential.id, credential_token: credential.token,