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,28 @@
1
+ .. title:: Checkpoint, policy-based authorization in Ruby
2
+
3
+ Checkpoint Documentation
4
+ ========================
5
+
6
+ Checkpoint is both a Ruby library and an abstract model for authorizing user
7
+ activity within an application.
8
+
9
+ It takes its name from the concept of a physical security checkpoint, where
10
+ someone (or some device) verifies identity and credentials before granting
11
+ access to some space or resource. This documentation covers a range of topics
12
+ related to authentication and authorization, primarily focused on how to
13
+ implement secure, understandable, and maintainable business rules in web
14
+ applications built in Ruby. It comes out of the University of Michigan Library,
15
+ where enterprise, legacy, and new systems must all interoperate.
16
+
17
+ Checkpoint emphasizes the use of policies and object-oriented design, giving
18
+ examples from very simple rules through complex group- and role-based scenarios.
19
+
20
+ Table of Contents
21
+ -----------------
22
+
23
+ .. toctree::
24
+ :maxdepth: 2
25
+
26
+ authentication
27
+ policies
28
+
@@ -0,0 +1,211 @@
1
+ Policies
2
+ ========
3
+
4
+ A single, consistent model for authorizing user actions is helpful in
5
+ developing secure, correct applications. One basic approach to this is to use
6
+ what are called **Policy Objects** (or simply **policies**). This is somewhat
7
+ different than declarative or hybrid systems like CanCan, for example.
8
+
9
+ With CanCan, there is a convenient method for declaring that a user "can do"
10
+ something, building up a map of the users' permitted actions on each request.
11
+ This breaks down somewhat when dynamic roles and object state must be
12
+ considered. The typical solution is to provide a block that serves as a boolean
13
+ predicate for a given operation. This leads to a hybrid of statements and
14
+ predicates, with no clear pattern for what should be placed where.
15
+
16
+ The policy approach is slightly different in that it assumes there will be an
17
+ object that can answer "yes" or "no" to the question of whether a given user
18
+ can take a given action. This **rule** may need detailed contextual information
19
+ like two objects that the user would like to associate, but the "answer" is not
20
+ precomputed. This consistency is useful because object-oriented principles can
21
+ be applied readily, giving handy tools for organizing and composing the domain
22
+ rules.
23
+
24
+ There is some variation in how policies are constructed, particularly in
25
+ whether they are resource- or operation-oriented. Resource-oriented policies
26
+ tend to have one object covering multiple REST/CRUD operations with associative
27
+ operations being covered on a separate policy for logical resources (e.g., a
28
+ "membership"). Operation-oriented policies tend to cover one domain action per
29
+ object, though this is a flexible design decision where a policy may cover a
30
+ number of related actions.
31
+
32
+ For the sake of the following discussion, a rule should be considered as
33
+ applying for a specific action, without regard for whether it is the only rule
34
+ of a policy or one of multiple rules on a broader policy.
35
+
36
+
37
+ Rules vs. Facts
38
+ ---------------
39
+
40
+ One important distinction is that of **Rules** versus **Facts**. A given rule
41
+ will generally be static but may consider dynamic facts. For example, a rule
42
+ may state that a user may only delete documents they own. The rule considers
43
+ dynamic facts of who is acting and the owner of the object acted upon; it has a
44
+ context of two parameters.
45
+
46
+ Types of Rules
47
+ --------------
48
+
49
+ Rules sit on a continuum of how dynamic their context is; that is, how much
50
+ external information is needed to make the decision, and where that information
51
+ comes from. The extent to which a rule uses dynamic information can inform how
52
+ it is constructed.
53
+
54
+ In general, rules that consider few and simple facts should be implemented as
55
+ directly as possible, in the source code, and without calling out to additional
56
+ collaborators. This facilitates understanding and testing by limiting the scope
57
+ of what could result in different answers.
58
+
59
+ Rules that consider more facts and those that are complicated to derive deserve
60
+ some indirection as to avoid coupling to complexity that does not belong to the
61
+ rule itself, as well as duplication of conceptual elements (such as how role
62
+ hierarchy should imply additional permissions).
63
+
64
+ Some patterns can be applied to the rule code and fact data to avoid repeating
65
+ design activity and incidental variation that is unhelpful across multiple
66
+ applications in an organization. These examples are not necessarily exhaustive,
67
+ but aim to provide a representative range with extremes identified.
68
+
69
+ Totally Static
70
+ ~~~~~~~~~~~~~~
71
+
72
+ The rule is encoded in the application and will yield the same answer until the
73
+ source code is changed. This may be used a placeholder for a permission check
74
+ for a feature not yet enabled or as a deprecation tool to disable an action
75
+ while source-level invocations are removed. This type of rule is uncommon, only
76
+ really useful in transition to richer implementation or removal.
77
+
78
+ Configuration-Only
79
+ ~~~~~~~~~~~~~~~~~~
80
+
81
+ The rule considers only a system configuration value and no other contextual
82
+ information. This is functionally equivalent but semantically preferable to a
83
+ "feature flag" because the conceptual focus remains on whether the user is
84
+ permitted to take some action. The policy becomes a simple indirection point,
85
+ behind which the implementation of making the decision (flag, role, etc.) can
86
+ vary without changing the calling code.
87
+
88
+ An example of a user-oriented (but not user-specific) setting might be "delete
89
+ items", set in a system configuration file. Whether the action is permitted
90
+ should be conditioned by a rule that consults the setting. This type of rule
91
+ often evolves to account for concerns like whether the user has administrator
92
+ access or owns an object. Especially if the permission would be checked in
93
+ multiple places, a rule is preferable to maintain consistency of authorization
94
+ and discourage "authorization in disguise", such as scattered checks against a
95
+ flag or whether the user is an administrator.
96
+
97
+ Note that feature flags are appropriate to consult directly when system
98
+ behavior should be conditional, but is unlikely to vary based on the acting
99
+ user. An example of this might be whether the implementors have decided to
100
+ enable "social widgets" or PDF export across an entire web application. This is
101
+ not a matter of authorization, so it should not be implemented as a rule.
102
+
103
+ User and Resource
104
+ ~~~~~~~~~~~~~~~~~
105
+
106
+ The rule considers the acting user in combination with a target resource or
107
+ multiple resources. This is a very common rule type for which the policy
108
+ approach shines in comparison to the declarative or hybrid approaches. Rules of
109
+ this type will often check that the user owns the resource or a container, a
110
+ user attribute like "is administrator", object state such as whether a document
111
+ is in draft mode, or conditions such as publication or retraction dates versus
112
+ the current time.
113
+
114
+ This type of rule is usually quite concise in its expression in source code,
115
+ which helps clarify behavior so that it can be implemented, verified,
116
+ communicated, and maintained with confidence.
117
+
118
+ User, Resource, and Context
119
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~
120
+
121
+ The rule considers the user, the target resource, and contextual information
122
+ such as region as determined by IP address, usage quotas, or configuration. A
123
+ small extension to *User and Resource* rule type, the additional context is not
124
+ usually supplied by the calling code (as are the user and resource), but
125
+ retrieved from the environment or usage session information.
126
+
127
+ System Role and Resource
128
+ ~~~~~~~~~~~~~~~~~~~~~~~~
129
+
130
+ Rules of this type are similar to *User and Resource* rules, but they also
131
+ consider the user's system-wide role, typically an attribute of the user from a
132
+ fixed set of application roles. An administrator flag is commonly converted to
133
+ a named role when using this model.
134
+
135
+ For this discussion, a role implies a fixed set of permitted actions. For example, an
136
+ "editor" may be able to update items, but not delete them. This would be encoded
137
+ directly in the rule, only changing when the application changes.
138
+
139
+ Agent and Resource
140
+ ~~~~~~~~~~~~~~~~~~
141
+
142
+ The rule considers the acting user and the target resource, in light of
143
+ permitted agents. An **agent** is an abstraction of a permitted user or group.
144
+ There is some semantic overlap in common usage of the terms group and role. For
145
+ this discussion, a group serves as a simple set of users, not directly implying
146
+ any permitted actions like a role would. A group could serve as a container for
147
+ resources or be permitted to take actions on a resource by a property such as
148
+ "shared with". The agent abstraction allows actions to be permitted to single
149
+ users or groups without spreading conditional handling for both cases
150
+ throughout an application.
151
+
152
+ A rule of this type will commonly check the equivalent of information like
153
+ whether the user owns the resource, if the resource is shared directly with the
154
+ user, or if any of the groups to which the user belongs matches any of the
155
+ groups that the resource is shared with.
156
+
157
+ The agent abstraction reduces this type of check to whether the agent owns the
158
+ resource or if the resource is shared with the agent. The rules can remain
159
+ simple with the consistent semantics of "user or group" externalized.
160
+
161
+ Agent, Resource, Context, Role, and Permissions
162
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
163
+
164
+ The rule considers all of the above information in addition to dynamic
165
+ permissions granted to roles. The roles may be fixed by the application or
166
+ configurable. The permissions implied by a role are not fixed and must be
167
+ resolved at runtime. The configuration of the permissions may occur as a
168
+ customization of a deployed source file, a system configuration file read at
169
+ startup, or persisted elsewhere.
170
+
171
+ Inspecting rules of this type is much more abstract and requires significantly
172
+ more knowledge of the deployment infrastructure and configuration model,
173
+ especially if the permissions are granted in a database. Tests can only verify
174
+ that the right authorization questions are asked and answered as expected for
175
+ example configurations; they are no longer effective for verifying that an
176
+ application will behave as desired in production. The implementors hold the
177
+ responsibility for ensuring that the configuration is correct.
178
+
179
+ Significant tooling is typically built to allow runtime inspection or
180
+ modification of permissions within applications with this level of flexibility.
181
+
182
+ "One Rule"
183
+ ~~~~~~~~~~
184
+
185
+ In scenarios where groups, roles, permissions, and actions must be allowed to be
186
+ defined at runtime, the rules tend to become very generic while the "fact" data
187
+ becomes very detailed. Almost no literal values will be used in a rule, relying
188
+ on assembly of a set of attributes and requirements from persisted data. Taken
189
+ far enough, this approach sometimes reduces the set of business rules to "one
190
+ rule" serving as a first-order logic solver over a fully dynamic set of facts.
191
+ All parts of the application must make authorization requests in a generic
192
+ format to a single point of control.
193
+
194
+ Systems needing this level of flexibility are rare. For example, it is
195
+ sometimes seen in the ERP and CMS spaces where installers build workflows,
196
+ modules, and whole business applications in that environment at runtime -- the
197
+ developers of the base application implement a development environment as much
198
+ as a specific application. Inspection or modification of rules and facts in a
199
+ system of this complexity requires extensive operational tooling and expertise.
200
+
201
+ Enterprise Authorization
202
+ ~~~~~~~~~~~~~~~~~~~~~~~~
203
+
204
+ Some enterprises externalize application rules to policy systems. The
205
+ applications formulate requests in a standardized format, being explicit about
206
+ the subject, resources, and action in terms of enterprise identifiers. These
207
+ requests are then validated against policies managed at the enterprise level.
208
+ This approach provides consistency across applications and services at an
209
+ enormous complexity and operational cost, hence it will not be under further
210
+ discussion here.
211
+
@@ -0,0 +1,4 @@
1
+ sphinx>=1.6.5,<1.7.0
2
+ sphinx-autobuild>=0.7.1,<0.8.0
3
+ guzzle_sphinx_theme>=0.7.11,<0.8.0
4
+ recommonmark>=0.4.0,<0.5.0
@@ -1,7 +1,21 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require "checkpoint/version"
2
- require "checkpoint/railtie"
3
4
 
5
+ require 'sequel'
6
+ require 'mysql2'
7
+ require 'ettin'
4
8
 
9
+ # All of the Checkpoint components are contained within this top-level module.
5
10
  module Checkpoint
6
- # Your code goes here...
11
+ # An error raised if there is no callable identifier on an entity when
12
+ # attempting to convert it to a resource.
13
+ class NoIdentifierError < StandardError; end
7
14
  end
15
+
16
+ require 'checkpoint/agent'
17
+ require 'checkpoint/credential'
18
+ require 'checkpoint/resource'
19
+ require 'checkpoint/authority'
20
+ require 'checkpoint/query'
21
+ require 'checkpoint/railtie' if defined?(Rails)
@@ -0,0 +1,93 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'checkpoint/agent/resolver'
4
+ require 'checkpoint/agent/token'
5
+
6
+ module Checkpoint
7
+ # An Agent is an any person or entity that might be granted various
8
+ # permission, such as a user, group, or institution.
9
+ #
10
+ # The application objects that an agent represents may be of any type; this
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
13
+ # of multiple concrete actors, such as any person affiliated with a given
14
+ # institution or any member of a given group.
15
+ class Agent
16
+ attr_accessor :actor
17
+
18
+ # Create an Agent. This should not be called externally; use {::from} instead.
19
+ def initialize(actor)
20
+ @actor = actor
21
+ end
22
+
23
+ # Default conversion from an actor to an {Agent}.
24
+ #
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
38
+ end
39
+
40
+ # Get the captive actor's type.
41
+ #
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.
45
+ #
46
+ # @return [String] the name of the actor's type after calling `#to_s` on it.
47
+ def type
48
+ if actor.respond_to?(:agent_type)
49
+ actor.agent_type
50
+ else
51
+ actor.class
52
+ end.to_s
53
+ end
54
+
55
+ # Get the captive actor's id.
56
+ #
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}.
61
+ #
62
+ # @return [String] the name of the actor's type after calling `#to_s` on it.
63
+ def id
64
+ if actor.respond_to?(:agent_id)
65
+ actor.agent_id
66
+ elsif actor.respond_to?(:id)
67
+ actor.id
68
+ else
69
+ raise NoIdentifierError, "No usable identifier on actor of type: #{actor.class}"
70
+ end.to_s
71
+ end
72
+
73
+ def token
74
+ @token ||= Token.new(type, id)
75
+ end
76
+
77
+ # Check whether two Agents refer to the same concrete actor.
78
+ # @param other [Agent] Another Agent to compare with
79
+ # @return [Boolean] true when the other Agent's actor is the same as
80
+ # determined by comparing them with `#eql?`.
81
+ def eql?(other)
82
+ other.is_a?(Agent) && actor.eql?(other.actor)
83
+ end
84
+
85
+ # Check whether two Agents refer to the same concrete actor.
86
+ # @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
+ def ==(other)
90
+ other.is_a?(Agent) && actor == other.actor
91
+ end
92
+ end
93
+ end
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Checkpoint
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.
9
+ #
10
+ # This implementation only resolves the user into one agent, using the default
11
+ # conversion.
12
+ #
13
+ # To extend the set of {Agent}s resolved, implement a specialized version
14
+ # that returns an array of agents from #resolve. This customized
15
+ # implementation would typically be injected to an application-wide
16
+ # {Checkpoint::Authority}, rather than being used directly.
17
+ #
18
+ # For example, a custom resolver might add a group agent for each group that
19
+ # the user is a member of, or IP address-based geographical regions or
20
+ # organizational affiliations.
21
+ class Resolver
22
+ # Resolve an actor to a list of agents it represents.
23
+ #
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)]
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,52 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Checkpoint
4
+ class Agent
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.
7
+ # Concrete actors are resolved into a number of agents, and those agents'
8
+ # tokens will be checked for matching permits.
9
+ class Token
10
+ attr_reader :type, :id
11
+
12
+ # Create a new Agent Token representing an actor in an application.
13
+ #
14
+ # @param type [String] the application-determined type of this agent. This
15
+ # will commonly be 'user' or 'group', but may be anything that identifies
16
+ # a type of authentication attribute, such as 'account-type'. The type
17
+ # will be converted to a String if something else is supplied.
18
+ # @param id [String] the application-resolvable identifier for this agent.
19
+ # This will commonly be username or group ID, but may be any value of an
20
+ # attribute of this type used to qualify an actor (user). The id
21
+ # will be converted to a String if something else is supplied.
22
+ def initialize(type, id)
23
+ @type = type.to_s
24
+ @id = id.to_s
25
+ end
26
+
27
+ # @return [String] a URI for this agent, including its type and id
28
+ def uri
29
+ "agent://#{type}/#{id}"
30
+ end
31
+
32
+ # @return [Token] self; for convenience of taking an Agent or token
33
+ def token
34
+ self
35
+ end
36
+
37
+ # @return [String] a token string suitable for granting or matching permits for this agent
38
+ def to_s
39
+ "#{type}:#{id}"
40
+ end
41
+
42
+ # Compare with another Agent for equality. Consider them to represent
43
+ # the same resource if `other` is an Agent, has the same type, and same id.
44
+ def eql?(other)
45
+ other.is_a?(Token) && type == other.type && id == other.id
46
+ end
47
+
48
+ alias == eql?
49
+ alias inspect uri
50
+ end
51
+ end
52
+ end