checkpoint 0.2.2 → 1.0.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 (57) hide show
  1. checksums.yaml +7 -0
  2. data/.envrc +1 -0
  3. data/.gitignore +18 -9
  4. data/.rspec +2 -0
  5. data/.rubocop.yml +30 -0
  6. data/.travis.yml +5 -0
  7. data/.yardopts +1 -0
  8. data/Gemfile +5 -1
  9. data/LICENSE.md +27 -0
  10. data/README.md +23 -0
  11. data/Rakefile +14 -0
  12. data/bin/console +18 -0
  13. data/bin/rake +21 -0
  14. data/bin/rspec +21 -0
  15. data/bin/sequel +21 -0
  16. data/bin/setup +8 -0
  17. data/bin/yard +21 -0
  18. data/bin/yardoc +21 -0
  19. data/checkpoint.gemspec +37 -19
  20. data/db/migrations/1_create_permits.rb +19 -0
  21. data/docs/Makefile +24 -0
  22. data/docs/_static/.gitkeep +0 -0
  23. data/docs/_templates/.gitkeep +0 -0
  24. data/docs/authentication.rst +18 -0
  25. data/docs/conf.py +46 -0
  26. data/docs/index.rst +28 -0
  27. data/docs/policies.rst +211 -0
  28. data/docs/requirements.txt +4 -0
  29. data/lib/checkpoint.rb +16 -2
  30. data/lib/checkpoint/agent.rb +93 -0
  31. data/lib/checkpoint/agent/resolver.rb +33 -0
  32. data/lib/checkpoint/agent/token.rb +52 -0
  33. data/lib/checkpoint/authority.rb +67 -0
  34. data/lib/checkpoint/credential.rb +82 -0
  35. data/lib/checkpoint/credential/permission.rb +27 -0
  36. data/lib/checkpoint/credential/resolver.rb +87 -0
  37. data/lib/checkpoint/credential/role.rb +26 -0
  38. data/lib/checkpoint/credential/token.rb +51 -0
  39. data/lib/checkpoint/db.rb +161 -0
  40. data/lib/checkpoint/db/permit.rb +24 -0
  41. data/lib/checkpoint/permission_mapper.rb +29 -0
  42. data/lib/checkpoint/permits.rb +133 -0
  43. data/lib/checkpoint/query.rb +42 -0
  44. data/lib/checkpoint/query/action_permitted.rb +40 -0
  45. data/lib/checkpoint/query/role_granted.rb +55 -0
  46. data/lib/checkpoint/railtie.rb +92 -71
  47. data/lib/checkpoint/resource.rb +138 -0
  48. data/lib/checkpoint/resource/all_of_any_type.rb +34 -0
  49. data/lib/checkpoint/resource/all_of_type.rb +50 -0
  50. data/lib/checkpoint/resource/any_entity.rb +25 -0
  51. data/lib/checkpoint/resource/any_entity_of_type.rb +29 -0
  52. data/lib/checkpoint/resource/resolver.rb +21 -0
  53. data/lib/checkpoint/resource/token.rb +65 -0
  54. data/lib/checkpoint/version.rb +3 -1
  55. data/lib/tasks/migrate.rake +75 -0
  56. metadata +260 -19
  57. data/Readme.markdown +0 -103
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Checkpoint
4
+ module DB
5
+ # Sequel model for permits
6
+ class Permit < Sequel::Model
7
+ # Instantiate a Permit from the constituent domain objects (agent,
8
+ # resource, credential).
9
+ def self.from(agent, credential, resource, zone: 'system')
10
+ new(
11
+ agent_type: agent.type, agent_id: agent.id, agent_token: agent.token,
12
+ credential_type: credential.type, credential_id: credential.id, credential_token: credential.token,
13
+ resource_type: resource.type, resource_id: resource.id, resource_token: resource.token,
14
+ zone_id: zone
15
+ )
16
+ end
17
+
18
+ # The default/system zone
19
+ def self.default_zone
20
+ '(all)'
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Checkpoint
4
+ # A PermissionMapper translates an action into a set of permissions and roles
5
+ # that would allow it. Commonly, the actions and permissions will share names
6
+ # for convenience and consistency, but this is not a requirement.
7
+ #
8
+ # For example, it may make sense in an application that one permission
9
+ # implies another, so an action may have multiple permissions that would
10
+ # allow it. In another application, it may be more convenient and
11
+ # understandable for users to have separate roles encapsulate that concept
12
+ # (such as an editor role having all of the permissions of a reader role and
13
+ # more).
14
+ #
15
+ # As a separate example, it may be more appropriate to implement permission
16
+ # inheritance directly in policy code (as by delegating to another check or
17
+ # policy), relying on the matching action and permission names with no roles
18
+ # resolved, as given by the default PermissionMapper. Checkpoint does not
19
+ # take an absolute position on the best pattern for a given application.
20
+ class PermissionMapper
21
+ def permissions_for(action)
22
+ [action.to_sym]
23
+ end
24
+
25
+ def roles_granting(_action)
26
+ []
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,133 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Note: we do not require db/permit because Sequel requires the connection
4
+ # to be set up before defining the model classes. The arrangment here
5
+ # assumes that DB.initialize! will have been called if the default model
6
+ # is to be used. In tests, that is done by spec/sequel_helper.rb. In an
7
+ # application, there should be an initializer that reads whatever appropriate
8
+ # configuration and does the initialization.
9
+
10
+ require 'checkpoint/db'
11
+
12
+ module Checkpoint
13
+ # The repository of permits -- a simple wrapper for the Sequel Datastore / permits table.
14
+ class Permits
15
+ def initialize(permits: Checkpoint::DB::Permit)
16
+ @permits = permits
17
+ end
18
+
19
+ def for(agents, credentials, resources)
20
+ where(agents, credentials, resources).select
21
+ end
22
+
23
+ def any?(agents, credentials, resources)
24
+ where(agents, credentials, resources).first != nil
25
+ end
26
+
27
+ private
28
+
29
+ def where(agents, credentials, resources)
30
+ Query.new(agents, credentials, resources, scope: permits)
31
+ end
32
+
33
+ attr_reader :permits
34
+
35
+ # A query object based on agents, credentials, and resources.
36
+ #
37
+ # This is a helper to capture a set of agents, credentials, and resources,
38
+ # and manage assembly of placeholder variables and binding expressions in
39
+ # the way Sequel expects them. It can take single items or arrays and
40
+ # converts them all to their tokens for query purposes.
41
+ class Query
42
+ attr_reader :agents, :credentials, :resources, :scope
43
+
44
+ def initialize(agents, credentials, resources, scope: Checkpoint::DB::Permit)
45
+ @agents = tokenize(agents)
46
+ @credentials = tokenize(credentials)
47
+ @resources = tokenize(resources)
48
+ @scope = scope
49
+ end
50
+
51
+ def query
52
+ scope.where(conditions)
53
+ end
54
+
55
+ def select
56
+ exec(:select)
57
+ end
58
+
59
+ def first
60
+ exec(:first)
61
+ end
62
+
63
+ def conditions
64
+ {
65
+ agent_token: agent_params.placeholders,
66
+ credential_token: credential_params.placeholders,
67
+ resource_token: resource_params.placeholders,
68
+ zone_id: :$zone_id
69
+ }
70
+ end
71
+
72
+ def parameters
73
+ (agent_params.values +
74
+ credential_params.values +
75
+ resource_params.values +
76
+ [[:zone_id, DB::Permit.default_zone]]).to_h
77
+ end
78
+
79
+ def agent_params
80
+ Params.new(agents, 'at')
81
+ end
82
+
83
+ def credential_params
84
+ Params.new(credentials, 'ct')
85
+ end
86
+
87
+ def resource_params
88
+ Params.new(resources, 'rt')
89
+ end
90
+
91
+ private
92
+
93
+ def exec(mode)
94
+ query.call(mode, parameters)
95
+ end
96
+
97
+ def tokenize(collection)
98
+ [collection].flatten.map(&:token)
99
+ end
100
+ end
101
+
102
+ # A helper for building placeholder variable names from items in a list and
103
+ # providing a corresponding hash of values. A prefix with some mnemonic
104
+ # corresponding to the column is recommended. For example, if the column is
105
+ # `agent_token`, using the prefix `at` will yield `$at_0`, `$at_1`, etc. for
106
+ # an IN clause.
107
+ class Params
108
+ attr_reader :items, :prefix
109
+
110
+ def initialize(items, prefix)
111
+ @items = [items].flatten
112
+ @prefix = prefix
113
+ end
114
+
115
+ def placeholders
116
+ 0.upto(items.size - 1).map do |i|
117
+ :"$#{prefix}_#{i}"
118
+ end
119
+ end
120
+
121
+ def values
122
+ items.map.with_index do |item, i|
123
+ value = if item.respond_to?(:sql_value)
124
+ item.sql_value
125
+ else
126
+ item.to_s
127
+ end
128
+ [:"#{prefix}_#{i}", value]
129
+ end
130
+ end
131
+ end
132
+ end
133
+ end
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'checkpoint/query/role_granted'
4
+ require 'checkpoint/query/action_permitted'
5
+
6
+ module Checkpoint
7
+ # The Query module is a container for the various types of checks or
8
+ # inquiries that an application might want to make.
9
+ #
10
+ # These classes provide a more expressive and object-oriented pattern than
11
+ # scattering the primitives and throughout the framework (and, more
12
+ # importantly, application) code base. They improve consistency and
13
+ # ergonomics in a similar way as named queries or scopes on a model class.
14
+ # That is, it's possible to query the authority directly (or model, by
15
+ # comparison) with primitives, but these classes will capture the semantics
16
+ # of a particular check, taking the conceptually pertinent parameters, and
17
+ # applying any defaults or conversion to authoriziation primitives needed,
18
+ # particularly around credential types.
19
+ #
20
+ # Despite modeling the semantics of a query in a convenient way, these
21
+ # objects do not assume a singleton authority. To make their usage truly
22
+ # convenient, they should be created from a factory method that binds them to
23
+ # an already-configured {Checkpoint::Authority}.
24
+ #
25
+ # NOTE: @botimer 2018-02-25: I suspect that we will build a convenience class
26
+ # that binds an authority, and has a factory method per query. This might end
27
+ # up being the main interface to Checkpoint; a wide-but-shallow adapter
28
+ # object that can be set up at initialization and made available to
29
+ # application policies (rather than using the authority directly). I also
30
+ # suspect that a shorthand adapter will appear for convenient aliasing in
31
+ # context. For example, a `can?` method on a base application policy that
32
+ # requires only an action parameter, binding its user and resource by
33
+ # default, would be a familiar and ergonomic way to call `action_permitted`.
34
+ # This would create and evaluate a new ActionPermitted instance bound to the
35
+ # parameters and the configured authority. A pattern like this would achieve
36
+ # the concurrent goals of maintaining the framework design and call-site
37
+ # simplicity, without relying on mixins -- the delegation to Checkpoint would
38
+ # be made explicit with some short boilerplate in application code that can
39
+ # be found and examined without digging into gems.
40
+ module Query
41
+ end
42
+ end
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Checkpoint
4
+ module Query
5
+ # ActionPermitted is a predicate query that captures the user, action,
6
+ # and target, and checks if the authority permits the action. It is likely
7
+ # to be the most commonly issued query in any given application.
8
+ class ActionPermitted
9
+ attr_reader :user, :action, :target
10
+
11
+ # @param user [<application actor>] the acting user/account
12
+ # @param action [String|Symbol] the action to be taken; this will be
13
+ # forced to a symbol
14
+ # @param target [<application entity>] the object or application resource
15
+ # to be acted upon; defaults to {Checkpoint::Resource.all} to ease
16
+ # checking for zone-/system-wide permission.
17
+ # @param authority [Checkpoint::Authority] the authority to ask about
18
+ # this permission
19
+ def initialize(
20
+ user,
21
+ action,
22
+ target = Checkpoint::Resource.all,
23
+ authority: Authority::RejectAll.new)
24
+
25
+ @user = user
26
+ @action = action.to_sym
27
+ @target = target
28
+ @authority = authority
29
+ end
30
+
31
+ def true?
32
+ authority.permits?(user, action, target)
33
+ end
34
+
35
+ private
36
+
37
+ attr_reader :authority
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,55 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Checkpoint
4
+ module Query
5
+ # RoleGranted is a predicate query that captures the user, role, and
6
+ # target, and checks if the authority recognizes the user as having the
7
+ # role.
8
+ #
9
+ # TODO: Extract-To-Manual
10
+ # There are two primary approaches to handling which actions are permitted
11
+ # for which roles:
12
+ #
13
+ # 1. Encoding the details directly in policy objects and checking for the
14
+ # appropriate roles within a given rule. This has the effect of placing
15
+ # the literal values within the body of a rule, making it quite easy
16
+ # to examine. Tests can validate system behavior at development time
17
+ # because it is static.
18
+ #
19
+ # 2. Implementing a {Checkpoint::Credential::Mapper} that maps backward
20
+ # from actions to named permissions and roles that would allow them.
21
+ # The policy rules would only authorize actions, leaving the mapping
22
+ # outside to accommodate configuration or runtime modification. This has
23
+ # the effect of being more flexible, while making the specifics of a
24
+ # rule more difficult to examine. Tests can only validate system
25
+ # behavior for a particular configuration -- whether an instance of the
26
+ # application is configured in a correct or expected way is not testable
27
+ # at development time.
28
+ class RoleGranted
29
+ attr_reader :user, :role, :target
30
+
31
+ # @param user [<application actor>] the acting user/account
32
+ # @param role [String|Symbol] the role to be checked; this will be
33
+ # forced to a symbol
34
+ # @param target [<application entity>] the object or application resource
35
+ # for which the user may have a role; defaults to {Checkpoint::Resource.all}
36
+ # to ease checking for zone-/system-wide roles.
37
+ # @param authority [Checkpoint::Authority] the authority to ask about
38
+ # this role-grant
39
+ def initialize(user, role, target = Resource.all, authority: Authority::RejectAll.new)
40
+ @user = user
41
+ @role = role.to_sym
42
+ @target = target
43
+ @authority = authority
44
+ end
45
+
46
+ def true?
47
+ authority.permits?(user, Credential::Role.new(role), target)
48
+ end
49
+
50
+ private
51
+
52
+ attr_reader :authority
53
+ end
54
+ end
55
+ end
@@ -1,84 +1,105 @@
1
- require 'rails'
1
+ # frozen_string_literal: true
2
2
 
3
- class Checkpoint::Railtie < ::Rails::Railtie
4
- config.before_initialize do
5
- class ::ActionController::Base
6
-
7
- def self.authorise_controllers_blocks
8
- if @authorise_controllers_blocks.nil?
9
- @authorise_controllers_blocks = {}
10
- end
11
- @authorise_controllers_blocks
3
+ module Checkpoint
4
+ # Railtie to hook Checkpoint into Rails applications.
5
+ #
6
+ # This does three things at present:
7
+ #
8
+ # 1. Loads our rake tasks, so you can run checkpoint:migrate from the app.
9
+ # 2. Pulls the Rails database information off of the ActiveRecord
10
+ # connection and puts it on Checkpoint::DB.config before any application
11
+ # initializers are run.
12
+ # 3. Sets up the Checkpoint database connection after application
13
+ # initializers have run, if it has not already been done and we are not
14
+ # running as a Rake task. This condition is key because when we are in
15
+ # rails server or console, we want to initialize!, but when we are in
16
+ # a rake task to update the database, we have to let it connect, but
17
+ # not initialize.
18
+ class Railtie < Rails::Railtie
19
+ railtie_name :checkpoint
20
+
21
+ class << self
22
+ # Register a callback to run before anything in 'config/initializers' runs.
23
+ # The block will get a reference to Checkpoint::DB.config as its only parameter.
24
+ def before_initializers(&block)
25
+ before_blocks << block
12
26
  end
13
-
14
- def self.authorise(arg1, &block)
15
-
16
- if block.nil?
17
- block = lambda {|c| true}
18
- end
19
-
20
- to_regexp = lambda do |pattern|
21
- if arg1.class.to_s == 'Regexp'
22
- arg1
23
- else
24
- Regexp.new('\A' + pattern.to_s.gsub(/[^\*]/){|char| Regexp.quote(char)}.gsub(/\*/){|| ".*?"} + '\Z')
25
- end
26
- end
27
-
28
- patterns = []
29
- if arg1.class.to_s == 'Array'
30
- arg1.each {|pattern| patterns.push to_regexp.call(pattern) }
31
- else
32
- patterns.push to_regexp.call(arg1)
33
- end
34
-
35
- authorise_controllers_blocks = ::ApplicationController.authorise_controllers_blocks
36
-
37
- patterns.each do |pattern|
38
- if authorise_controllers_blocks [pattern].nil?
39
- authorise_controllers_blocks[pattern] = []
40
- end
41
- authorise_controllers_blocks[pattern].push(block)
42
- end
27
+
28
+ # Register a callback to run after anything in 'config/initializers' runs.
29
+ # The block will get a reference to Checkpoint::DB.config as its only parameter.
30
+ # Checkpoint::DB.initialize! will not have been automatically called at this
31
+ # point, so this is an opportunity to do so if an initializer has not.
32
+ def after_initializers(&block)
33
+ after_blocks << block
43
34
  end
44
-
45
- #for our american friends
46
- def self.authorize(arg1, &block)
47
- authorise(arg1, &block)
35
+
36
+ # Register a callback to run when Checkpoint is ready and fully initialized.
37
+ # This will happen once in production, and on each request in development.
38
+ # If you need to do something once in development, you can choose between
39
+ # keeping a flag or using the after_initializers.
40
+ def when_checkpoint_is_ready(&block)
41
+ ready_blocks << block
48
42
  end
49
-
50
- def authorised?
51
- action = "#{self.class.to_s}::#{params[:action]}"
52
- ::ApplicationController.authorise_controllers_blocks.each do |pattern, blocks|
53
- if action.match pattern
54
- blocks.each do |block|
55
- if instance_eval(&block)
56
- return true
57
- end
58
- end
59
- end
60
- end
61
- false
43
+
44
+ def before_blocks
45
+ @before ||= []
62
46
  end
63
47
 
64
- def access_denied
65
- logger.info "\n\n-----------------------------------------------"
66
- logger.info " (401) Access Denied!"
67
- logger.info " * see the above request for more info"
68
- logger.info "-----------------------------------------------\n\n"
69
- render :text => "Access Denied", :status => 401
48
+ def after_blocks
49
+ @after ||= []
70
50
  end
71
51
 
72
- def check_authorized
73
- if !authorised?
74
- access_denied
75
- end
52
+ def ready_blocks
53
+ @ready ||= []
76
54
  end
77
-
78
- before_filter do |controller|
79
- check_authorized
55
+
56
+ def under_rake!
57
+ @rake = true
80
58
  end
81
-
59
+
60
+ def under_rake?
61
+ @rake ||= false
62
+ end
63
+ end
64
+
65
+ # This runs before anything in 'config/initializers' runs.
66
+ initializer "checkpoint.before_initializers", before: :load_config_initializers do
67
+ config = Checkpoint::DB.config
68
+ unless config.url
69
+ opts = ActiveRecord::Base.connection.instance_variable_get(:@config).dup
70
+ opts.delete(:flags)
71
+ config[:opts] = opts
72
+ end
73
+
74
+ Railtie.before_blocks.each do |block|
75
+ block.call(config.to_h)
76
+ end
77
+ end
78
+
79
+ # This runs after everything in 'config/initializers' runs.
80
+ initializer "checkpoint.after_initializers", after: :load_config_initializers do
81
+ config = Checkpoint::DB.config
82
+ Railtie.after_blocks.each do |block|
83
+ block.call(config.to_h)
84
+ end
85
+ end
86
+
87
+ # This runs before any block registered under a `config.to_prepare`, which
88
+ # could be in plugins or initializers that want to use a fully configured
89
+ # Checkpoint instance. The `to_prepare` hook is run once at the start of a
90
+ # production instance and for every request in development (unless caching
91
+ # is turned on so there is no reloading).
92
+ initializer "checkpoint.ready", after: :finisher_hook do
93
+ Checkpoint::DB.initialize! unless Railtie.under_rake?
94
+
95
+ Railtie.ready_blocks.each do |block|
96
+ block.call(Checkpoint::DB.db)
97
+ end
98
+ end
99
+
100
+ rake_tasks do
101
+ Railtie.under_rake!
102
+ load "tasks/migrate.rake"
82
103
  end
83
104
  end
84
105
  end