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,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
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Checkpoint
2
- VERSION = "0.2.2"
4
+ VERSION = "1.0.0"
3
5
  end
@@ -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