checkpoint 0.2.2 → 1.0.0

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