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,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