checkpoint 0.2.2 → 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.envrc +1 -0
- data/.gitignore +18 -9
- data/.rspec +2 -0
- data/.rubocop.yml +30 -0
- data/.travis.yml +5 -0
- data/.yardopts +1 -0
- data/Gemfile +5 -1
- data/LICENSE.md +27 -0
- data/README.md +23 -0
- data/Rakefile +14 -0
- data/bin/console +18 -0
- data/bin/rake +21 -0
- data/bin/rspec +21 -0
- data/bin/sequel +21 -0
- data/bin/setup +8 -0
- data/bin/yard +21 -0
- data/bin/yardoc +21 -0
- data/checkpoint.gemspec +37 -19
- data/db/migrations/1_create_permits.rb +19 -0
- data/docs/Makefile +24 -0
- data/docs/_static/.gitkeep +0 -0
- data/docs/_templates/.gitkeep +0 -0
- data/docs/authentication.rst +18 -0
- data/docs/conf.py +46 -0
- data/docs/index.rst +28 -0
- data/docs/policies.rst +211 -0
- data/docs/requirements.txt +4 -0
- data/lib/checkpoint.rb +16 -2
- data/lib/checkpoint/agent.rb +93 -0
- data/lib/checkpoint/agent/resolver.rb +33 -0
- data/lib/checkpoint/agent/token.rb +52 -0
- data/lib/checkpoint/authority.rb +67 -0
- data/lib/checkpoint/credential.rb +82 -0
- data/lib/checkpoint/credential/permission.rb +27 -0
- data/lib/checkpoint/credential/resolver.rb +87 -0
- data/lib/checkpoint/credential/role.rb +26 -0
- data/lib/checkpoint/credential/token.rb +51 -0
- data/lib/checkpoint/db.rb +161 -0
- data/lib/checkpoint/db/permit.rb +24 -0
- data/lib/checkpoint/permission_mapper.rb +29 -0
- data/lib/checkpoint/permits.rb +133 -0
- data/lib/checkpoint/query.rb +42 -0
- data/lib/checkpoint/query/action_permitted.rb +40 -0
- data/lib/checkpoint/query/role_granted.rb +55 -0
- data/lib/checkpoint/railtie.rb +92 -71
- data/lib/checkpoint/resource.rb +138 -0
- data/lib/checkpoint/resource/all_of_any_type.rb +34 -0
- data/lib/checkpoint/resource/all_of_type.rb +50 -0
- data/lib/checkpoint/resource/any_entity.rb +25 -0
- data/lib/checkpoint/resource/any_entity_of_type.rb +29 -0
- data/lib/checkpoint/resource/resolver.rb +21 -0
- data/lib/checkpoint/resource/token.rb +65 -0
- data/lib/checkpoint/version.rb +3 -1
- data/lib/tasks/migrate.rake +75 -0
- metadata +260 -19
- data/Readme.markdown +0 -103
@@ -0,0 +1,138 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'checkpoint/resource/token'
|
4
|
+
require 'checkpoint/resource/all_of_type'
|
5
|
+
require 'checkpoint/resource/all_of_any_type'
|
6
|
+
require 'checkpoint/resource/any_entity'
|
7
|
+
require 'checkpoint/resource/any_entity_of_type'
|
8
|
+
|
9
|
+
module Checkpoint
|
10
|
+
# A Resource is any application object that should be considered for
|
11
|
+
# restricted access.
|
12
|
+
#
|
13
|
+
# Most commonly, these will be the core domain objects that are created by
|
14
|
+
# users ("model instances", to use Rails terminology), but this is not a
|
15
|
+
# requirement. A Resource can represent a fixed item in the system such as
|
16
|
+
# the administrative password, where there might be a single 'update'
|
17
|
+
# permission to change various elements of configuration. It might also be
|
18
|
+
# something like a section of a site as set up in a config file.
|
19
|
+
#
|
20
|
+
# In modeling an application, it is not always obvious whether a concept
|
21
|
+
# should be a {Credential} or a {Resource}, so take care to evaluate the
|
22
|
+
# options. As an example, consider access to derivatives of a high-quality
|
23
|
+
# media object based on subscription level. It may make more sense for a
|
24
|
+
# given application to model access to a fixed set of profiles (e.g., mobile,
|
25
|
+
# standard, premium) as credentials and named concepts that will appear
|
26
|
+
# throughout the codebase. For an application where the profiles are more
|
27
|
+
# dynamic, it may make more sense to model them as resources that can be
|
28
|
+
# listed and updated by configuration or at runtime, with a fixed set of
|
29
|
+
# permissions (e.g., preview, stream, download).
|
30
|
+
#
|
31
|
+
# Checkpoint does not force this decision to be made in one way for every
|
32
|
+
# application, but provides the concepts of permission mapping and resource
|
33
|
+
# resolution to accommodate whatever fixed, dynamic, or inherited modeling is
|
34
|
+
# most appropriate for the credentials and resources of an application.
|
35
|
+
class Resource
|
36
|
+
attr_reader :entity
|
37
|
+
|
38
|
+
# Special string to be used when permitting or searching for permits on all
|
39
|
+
# types or all resources
|
40
|
+
ALL = '(all)'
|
41
|
+
|
42
|
+
# Creates a Resource for this entity. Prefer the factory method {::from},
|
43
|
+
# which applies default conversion rules. This constructor does not
|
44
|
+
# consider whether the entity can covert itself with #to_resource.
|
45
|
+
def initialize(entity)
|
46
|
+
@entity = entity
|
47
|
+
end
|
48
|
+
|
49
|
+
# Default conversion from an entity to a Resource. Prefer this to creating
|
50
|
+
# new instances by hand.
|
51
|
+
#
|
52
|
+
# If the entity implements #to_resource, we will delegate to it. Otherwise,
|
53
|
+
# we will return a Resource for this entity.
|
54
|
+
def self.from(entity)
|
55
|
+
if entity.respond_to?(:to_resource)
|
56
|
+
entity.to_resource
|
57
|
+
else
|
58
|
+
new(entity)
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
# Covenience factory method to get a Resource that will match all entities
|
63
|
+
# of any type.
|
64
|
+
#
|
65
|
+
# @return [AllOfAnyType] a wildcard resource instance
|
66
|
+
def self.all
|
67
|
+
AllOfAnyType.new
|
68
|
+
end
|
69
|
+
|
70
|
+
# Get the resource type.
|
71
|
+
#
|
72
|
+
# Note that this is not necessarily a class/model type name. It can be
|
73
|
+
# whatever type name is most useful for building tokens and inspecting
|
74
|
+
# permits for this types. For example, there may be objects that have
|
75
|
+
# subtypes that are not modeled as objects, decorators, or collection
|
76
|
+
# objects (like a specialized type for the root of a tree) that should
|
77
|
+
# be treated as the element type.
|
78
|
+
#
|
79
|
+
# If the entity implements `#resource_type`, we will use that. Otherwise,
|
80
|
+
# we use the entity's class name.
|
81
|
+
#
|
82
|
+
# @return [String] the name of the entity's type after calling `#to_s` on it.
|
83
|
+
def type
|
84
|
+
if entity.respond_to?(:resource_type)
|
85
|
+
entity.resource_type
|
86
|
+
else
|
87
|
+
entity.class
|
88
|
+
end.to_s
|
89
|
+
end
|
90
|
+
|
91
|
+
# Get the resource ID.
|
92
|
+
#
|
93
|
+
# If the entity implements `#resource_id`, we will use that. Otherwise we
|
94
|
+
# call `#id`. If the the entity does not implement either of these methods,
|
95
|
+
# we raise a {NoIdentifierError}.
|
96
|
+
#
|
97
|
+
# @return [String] the entity's ID after calling `#to_s` on it.
|
98
|
+
def id
|
99
|
+
if entity.respond_to?(:resource_id)
|
100
|
+
entity.resource_id
|
101
|
+
elsif entity.respond_to?(:id)
|
102
|
+
entity.id
|
103
|
+
else
|
104
|
+
raise NoIdentifierError, "No usable identifier on entity of type: #{entity.class}"
|
105
|
+
end.to_s
|
106
|
+
end
|
107
|
+
|
108
|
+
# @return [Resource::Token] The token for this resource
|
109
|
+
def token
|
110
|
+
@token ||= Token.new(type, id)
|
111
|
+
end
|
112
|
+
|
113
|
+
# Convert this Resource into a wildcard representing all resources of this
|
114
|
+
# type.
|
115
|
+
#
|
116
|
+
# @see Resource::AllOfType
|
117
|
+
# @return [Resource] A Resource of the same type, but for all members
|
118
|
+
def all_of_type
|
119
|
+
Resource::AllOfType.new(type)
|
120
|
+
end
|
121
|
+
|
122
|
+
# Check whether two Resources refer to the same entity.
|
123
|
+
# @param other [Resource] Another Resource to compare with
|
124
|
+
# @return [Boolean] true when the other Resource's entity is the same as
|
125
|
+
# determined by comparing them with `#eql?`.
|
126
|
+
def eql?(other)
|
127
|
+
other.is_a?(Resource) && entity.eql?(other.entity)
|
128
|
+
end
|
129
|
+
|
130
|
+
# Check whether two Resources refer to the same entity.
|
131
|
+
# @param other [Resource] Another Resource to compare with
|
132
|
+
# @return [Boolean] true when the other Resource's entity is the same as
|
133
|
+
# determined by comparing them with `#==`.
|
134
|
+
def ==(other)
|
135
|
+
other.is_a?(Resource) && entity == other.entity
|
136
|
+
end
|
137
|
+
end
|
138
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Checkpoint
|
4
|
+
class Resource
|
5
|
+
# Specialized Resource type to represent all entities of a any/all types.
|
6
|
+
# This is used for zone-/system-wide grants or checks.
|
7
|
+
class AllOfAnyType < Resource
|
8
|
+
# Create a wildcard Resource.
|
9
|
+
#
|
10
|
+
# Because type and ID are static, this takes no parameters
|
11
|
+
def initialize
|
12
|
+
@entity = AnyEntity.new
|
13
|
+
end
|
14
|
+
|
15
|
+
# Create a wildcard Resource "from" an entity. The entity disregarded and
|
16
|
+
# {AnyEntity} is substituted.
|
17
|
+
#
|
18
|
+
# @return [AllOfAnyType] a wildcard Resource instance
|
19
|
+
def self.from(_entity)
|
20
|
+
new
|
21
|
+
end
|
22
|
+
|
23
|
+
# The special ALL type
|
24
|
+
def type
|
25
|
+
Resource::ALL
|
26
|
+
end
|
27
|
+
|
28
|
+
# The special ALL id
|
29
|
+
def id
|
30
|
+
Resource::ALL
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,50 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Checkpoint
|
4
|
+
class Resource
|
5
|
+
# Specialized Resource type to represent all entities of a particular type.
|
6
|
+
class AllOfType < Resource
|
7
|
+
attr_reader :type
|
8
|
+
# Create a wildcard Resource for a given type
|
9
|
+
def initialize(type)
|
10
|
+
@type = type
|
11
|
+
end
|
12
|
+
|
13
|
+
# Create a type-specific wildcard Resource from a given entity
|
14
|
+
#
|
15
|
+
# When the entity implements to #to_resource, we convert it first and take
|
16
|
+
# the type from the result. Otherwise, when it implements #resource_type,
|
17
|
+
# we use that result. Otherwise, we take the class name of the entity.
|
18
|
+
# Regardless of the source, the type is forced to a string.
|
19
|
+
def self.from(entity)
|
20
|
+
new(type_of(entity))
|
21
|
+
end
|
22
|
+
|
23
|
+
# This is always the special ALL resource ID
|
24
|
+
def id
|
25
|
+
Resource::ALL
|
26
|
+
end
|
27
|
+
|
28
|
+
# Compares with another Resource
|
29
|
+
#
|
30
|
+
# @return [Boolean] true if `other` is a Resource and its #type matches.
|
31
|
+
def eql?(other)
|
32
|
+
other.is_a?(Resource) && type == other.type
|
33
|
+
end
|
34
|
+
|
35
|
+
# Private type name extraction
|
36
|
+
def self.type_of(entity)
|
37
|
+
if entity.respond_to?(:to_resource)
|
38
|
+
entity.to_resource.type
|
39
|
+
elsif entity.respond_to?(:resource_type)
|
40
|
+
entity.resource_type
|
41
|
+
else
|
42
|
+
entity.class
|
43
|
+
end.to_s
|
44
|
+
end
|
45
|
+
|
46
|
+
private_class_method :type_of
|
47
|
+
alias == eql?
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Checkpoint
|
4
|
+
class Resource
|
5
|
+
# Special class to represent any entity of any type. This is used for
|
6
|
+
# zone-/system-wide grants or checks. It is basically so {AllOfAnyType} can
|
7
|
+
# have an entity rather than a nil.
|
8
|
+
#
|
9
|
+
# Wildcards or null objects typically have somewhat strange semantics, and
|
10
|
+
# this is no exception. It will compare as eql? and == to any object.
|
11
|
+
class AnyEntity
|
12
|
+
# Always returns true; this wildcard is "equal" to any object.
|
13
|
+
# return [Boolean] true
|
14
|
+
def eql?(*)
|
15
|
+
true
|
16
|
+
end
|
17
|
+
|
18
|
+
# Always returns true; this wildcard is "equal" to any object.
|
19
|
+
# return [Boolean] true
|
20
|
+
def ==(*)
|
21
|
+
true
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Checkpoint
|
4
|
+
class Resource
|
5
|
+
# Special class to represent any entity of a type. This is used for
|
6
|
+
# type-wide grants or checks. It is basically so {AllOfType} can
|
7
|
+
# have an entity rather than a nil.
|
8
|
+
#
|
9
|
+
# Wildcards or null objects typically have somewhat strange semantics, and
|
10
|
+
# this is no exception. It will compare as eql? and == to any object that
|
11
|
+
# has the same type attribute.
|
12
|
+
class AnyEntityOfType
|
13
|
+
attr_reader :type
|
14
|
+
|
15
|
+
# Create a wildcard entity that will compare as equal to
|
16
|
+
def initialize(type)
|
17
|
+
@type = type
|
18
|
+
end
|
19
|
+
|
20
|
+
# Always returns true; this wildcard is "equal" to any object.
|
21
|
+
# return [Boolean] true
|
22
|
+
def eql?(other)
|
23
|
+
other.respond_to?(:type) && type == other.type
|
24
|
+
end
|
25
|
+
|
26
|
+
alias == eql?
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Checkpoint
|
4
|
+
class Resource
|
5
|
+
# A Resource Resolver takes a concrete object (like a model instance) and
|
6
|
+
# resolves it into all {Resource}s for which a permit would allow an action.
|
7
|
+
# For example, this can be used to grant a credential on all items of a given
|
8
|
+
# model class or to implement cascading permissions when all credentials for
|
9
|
+
# a container should apply to the contained objects.
|
10
|
+
#
|
11
|
+
# NOTE: This implementation currently always resolves to the entity and its
|
12
|
+
# type and nothing more. This needs some thought on an appropriate extension
|
13
|
+
# mechanism to mirror the {PermissionMapper}.
|
14
|
+
class Resolver
|
15
|
+
def resolve(target)
|
16
|
+
return [target] if target.is_a?(Resource)
|
17
|
+
[Resource.from(target), Resource::AllOfType.from(target)]
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,65 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'checkpoint/resource/resolver'
|
4
|
+
|
5
|
+
module Checkpoint
|
6
|
+
class Resource
|
7
|
+
# A Resource::Token is an identifier object for a Resource. It includes a
|
8
|
+
# type and an identifier. A {Permit} can be granted for a Token. Concrete
|
9
|
+
# entities are resolved into a number of resources, and those resources'
|
10
|
+
# tokens will be checked for matching permits.
|
11
|
+
class Token
|
12
|
+
attr_reader :type, :id
|
13
|
+
|
14
|
+
# Create a new Resource representing a domain entity or concept that would
|
15
|
+
# be acted upon.
|
16
|
+
#
|
17
|
+
# @param type [String] the application-determined type of this resource.
|
18
|
+
# This might correspond to a model class or other type of named concept
|
19
|
+
# in the application. The type is always coerced to String with `#to_s`
|
20
|
+
# in case something else is supplied.
|
21
|
+
#
|
22
|
+
# @param id [String] the application-resolvable identifier for this
|
23
|
+
# resource. For example, this might be the ID of a model object, the
|
24
|
+
# name of a section. The id is always coerced to String with `#to_s` in
|
25
|
+
# case something else is supplied.
|
26
|
+
def initialize(type, id)
|
27
|
+
@type = type.to_s
|
28
|
+
@id = id.to_s
|
29
|
+
end
|
30
|
+
|
31
|
+
# Get the special "all" Resource Token. This is a singleton that represents all
|
32
|
+
# resources of all types. It is used to grant permissions or roles within
|
33
|
+
# a zone, but not specific to a particular resource.
|
34
|
+
#
|
35
|
+
# @return [Resource::Token] the special "all" Resource Token
|
36
|
+
def self.all
|
37
|
+
@all ||= new(Resource::ALL, Resource::ALL).freeze
|
38
|
+
end
|
39
|
+
|
40
|
+
# @return [Token] self; for convenience of taking a Resource or token
|
41
|
+
def token
|
42
|
+
self
|
43
|
+
end
|
44
|
+
|
45
|
+
# @return [String] a URI for this resource, including its type and id
|
46
|
+
def uri
|
47
|
+
"resource://#{type}/#{id}"
|
48
|
+
end
|
49
|
+
|
50
|
+
# @return [String] a token suitable for granting or matching this resource
|
51
|
+
def to_s
|
52
|
+
"#{type}:#{id}"
|
53
|
+
end
|
54
|
+
|
55
|
+
# Compare with another Resource for equality. Consider them to represent
|
56
|
+
# the same resource if `other` is a Resource, has the same type, and same id.
|
57
|
+
def eql?(other)
|
58
|
+
other.is_a?(Resource::Token) && type == other.type && id == other.id
|
59
|
+
end
|
60
|
+
|
61
|
+
alias == eql?
|
62
|
+
alias inspect uri
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
data/lib/checkpoint/version.rb
CHANGED
@@ -0,0 +1,75 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'bundler/setup'
|
4
|
+
require 'checkpoint'
|
5
|
+
|
6
|
+
if defined?(Rails)
|
7
|
+
# When db:schema:dump is called directly, we can tack this on.
|
8
|
+
# If we do it unconditionally, db:migrate will try to dump before we have
|
9
|
+
# been able to migrate the checkpoint tables.
|
10
|
+
if Rake.application.top_level_tasks.include?('db:schema:dump')
|
11
|
+
Rake::Task['db:schema:dump'].enhance do
|
12
|
+
Rake::Task['checkpoint:schema:dump'].invoke
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
# Run our schema load to make sure that the version number is stored in
|
17
|
+
# schema_info, so migrations don't try to double-run. The actual table
|
18
|
+
# structure is handled by the Rails schema:dump and schema:load.
|
19
|
+
# A db:setup will trigger this, so we don't have to handle it separately.
|
20
|
+
Rake::Task['db:schema:load'].enhance do
|
21
|
+
Rake::Task['checkpoint:schema:load'].invoke
|
22
|
+
end
|
23
|
+
|
24
|
+
# We hook into db:migrate for convenience.
|
25
|
+
Rake::Task['db:migrate'].enhance do
|
26
|
+
Rake::Task['checkpoint:migrate'].invoke
|
27
|
+
end
|
28
|
+
|
29
|
+
end
|
30
|
+
|
31
|
+
namespace :checkpoint do
|
32
|
+
desc "Migrate the Checkpoint database to the latest version"
|
33
|
+
task :migrate do
|
34
|
+
if defined?(Rails)
|
35
|
+
# Load the 'environment', which does the full Rails initialization.
|
36
|
+
# The Railtie is smart enough to know whether we are in a Rake task,
|
37
|
+
# so it can avoid initializing and we can migrate safely before the
|
38
|
+
# models are loaded.
|
39
|
+
Rake::Task['environment'].invoke
|
40
|
+
end
|
41
|
+
|
42
|
+
# After migrating, we initialize here, even though it isn't strictly
|
43
|
+
# necessary, but it will ensure that migration does a small sanity check
|
44
|
+
# that at least all of the tables expected by model classes exist.
|
45
|
+
Checkpoint::DB.migrate!
|
46
|
+
Checkpoint::DB.initialize!
|
47
|
+
end
|
48
|
+
|
49
|
+
# We don't bother defining the schema:dump and schema:load tasks if we're
|
50
|
+
# not running under Rails. They exist only to cooperate with the dumps done
|
51
|
+
# by Rails, since schema.rb includes any Checkpoint tables in the same
|
52
|
+
# database as the application -- a convenient default mode.
|
53
|
+
if defined?(Rails)
|
54
|
+
namespace :schema do
|
55
|
+
desc "Dump the Checkpoint version to db/checkpoint.yml"
|
56
|
+
task :dump do
|
57
|
+
Rake::Task['environment'].invoke
|
58
|
+
Checkpoint::DB.dump_schema!
|
59
|
+
end
|
60
|
+
|
61
|
+
desc "Load the Checkpoint version from db/checkpoint.yml"
|
62
|
+
task :load do
|
63
|
+
Rake::Task['environment'].invoke
|
64
|
+
Checkpoint::DB.load_schema!
|
65
|
+
end
|
66
|
+
|
67
|
+
# When running under Rails, we dump the schema after migrating so
|
68
|
+
# everything stays synced up for db:setup against a new database.
|
69
|
+
# Rake::Task['checkpoint:schema:dump'].invoke
|
70
|
+
Rake::Task['checkpoint:migrate'].enhance do
|
71
|
+
Rake::Task['checkpoint:schema:dump'].invoke
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|