sinatra_resource 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (75) hide show
  1. data/.document +5 -0
  2. data/.gitignore +7 -0
  3. data/LICENSE +20 -0
  4. data/README.mdown +28 -0
  5. data/Rakefile +34 -0
  6. data/VERSION +1 -0
  7. data/examples/datacatalog/Rakefile +23 -0
  8. data/examples/datacatalog/app.rb +15 -0
  9. data/examples/datacatalog/config/config.rb +66 -0
  10. data/examples/datacatalog/config/config.yml +11 -0
  11. data/examples/datacatalog/config.ru +6 -0
  12. data/examples/datacatalog/lib/base.rb +9 -0
  13. data/examples/datacatalog/lib/resource.rb +73 -0
  14. data/examples/datacatalog/lib/roles.rb +15 -0
  15. data/examples/datacatalog/models/categorization.rb +31 -0
  16. data/examples/datacatalog/models/category.rb +28 -0
  17. data/examples/datacatalog/models/source.rb +33 -0
  18. data/examples/datacatalog/models/user.rb +51 -0
  19. data/examples/datacatalog/resources/categories.rb +32 -0
  20. data/examples/datacatalog/resources/sources.rb +35 -0
  21. data/examples/datacatalog/resources/users.rb +26 -0
  22. data/examples/datacatalog/tasks/db.rake +29 -0
  23. data/examples/datacatalog/tasks/test.rake +16 -0
  24. data/examples/datacatalog/test/helpers/assertions/assert_include.rb +17 -0
  25. data/examples/datacatalog/test/helpers/assertions/assert_not_include.rb +17 -0
  26. data/examples/datacatalog/test/helpers/lib/model_factories.rb +49 -0
  27. data/examples/datacatalog/test/helpers/lib/model_helpers.rb +30 -0
  28. data/examples/datacatalog/test/helpers/lib/request_helpers.rb +53 -0
  29. data/examples/datacatalog/test/helpers/model_test_helper.rb +5 -0
  30. data/examples/datacatalog/test/helpers/resource_test_helper.rb +5 -0
  31. data/examples/datacatalog/test/helpers/shared/api_keys.rb +48 -0
  32. data/examples/datacatalog/test/helpers/shared/common_body_responses.rb +15 -0
  33. data/examples/datacatalog/test/helpers/shared/status_codes.rb +61 -0
  34. data/examples/datacatalog/test/helpers/test_cases/model_test_case.rb +6 -0
  35. data/examples/datacatalog/test/helpers/test_cases/resource_test_case.rb +36 -0
  36. data/examples/datacatalog/test/helpers/test_helper.rb +36 -0
  37. data/examples/datacatalog/test/models/categorization_test.rb +40 -0
  38. data/examples/datacatalog/test/models/category_test.rb +35 -0
  39. data/examples/datacatalog/test/models/source_test.rb +37 -0
  40. data/examples/datacatalog/test/models/user_test.rb +77 -0
  41. data/examples/datacatalog/test/resources/categories/categories_delete_test.rb +112 -0
  42. data/examples/datacatalog/test/resources/categories/categories_get_many_test.rb +58 -0
  43. data/examples/datacatalog/test/resources/categories/categories_get_one_test.rb +75 -0
  44. data/examples/datacatalog/test/resources/categories/categories_post_test.rb +135 -0
  45. data/examples/datacatalog/test/resources/categories/categories_put_test.rb +140 -0
  46. data/examples/datacatalog/test/resources/sources/sources_delete_test.rb +112 -0
  47. data/examples/datacatalog/test/resources/sources/sources_get_many_test.rb +58 -0
  48. data/examples/datacatalog/test/resources/sources/sources_get_one_test.rb +74 -0
  49. data/examples/datacatalog/test/resources/sources/sources_post_test.rb +184 -0
  50. data/examples/datacatalog/test/resources/sources/sources_put_test.rb +227 -0
  51. data/examples/datacatalog/test/resources/users/users_delete_test.rb +134 -0
  52. data/examples/datacatalog/test/resources/users/users_get_many_test.rb +111 -0
  53. data/examples/datacatalog/test/resources/users/users_get_one_test.rb +75 -0
  54. data/examples/datacatalog/test/resources/users/users_post_test.rb +142 -0
  55. data/examples/datacatalog/test/resources/users/users_put_test.rb +171 -0
  56. data/lib/builder/helpers.rb +319 -0
  57. data/lib/builder/mongo_helpers.rb +70 -0
  58. data/lib/builder.rb +84 -0
  59. data/lib/exceptions.rb +10 -0
  60. data/lib/resource.rb +171 -0
  61. data/lib/roles.rb +163 -0
  62. data/lib/sinatra_resource.rb +6 -0
  63. data/notes/keywords.mdown +1 -0
  64. data/notes/permissions.mdown +181 -0
  65. data/notes/questions.mdown +18 -0
  66. data/notes/see_also.mdown +3 -0
  67. data/notes/synonyms.mdown +7 -0
  68. data/notes/to_do.mdown +7 -0
  69. data/notes/uniform_interface.mdown +22 -0
  70. data/sinatra_resource.gemspec +183 -0
  71. data/spec/sinatra_resource_spec.rb +7 -0
  72. data/spec/spec_helper.rb +9 -0
  73. data/tasks/spec.rake +13 -0
  74. data/tasks/yard.rake +13 -0
  75. metadata +253 -0
@@ -0,0 +1,70 @@
1
+ module SinatraResource
2
+
3
+ class Builder
4
+
5
+ module MongoHelpers
6
+
7
+ # Create a document from params. If not valid, returns 400.
8
+ #
9
+ # @return [MongoMapper::Document]
10
+ def create_document!
11
+ document = config[:model].new(params)
12
+ unless document.valid?
13
+ error 400, convert(body_for(:invalid_document, document))
14
+ end
15
+ unless document.save
16
+ error 400, convert(body_for(:internal_server_error))
17
+ end
18
+ document
19
+ end
20
+
21
+ # Delete a document with +id+.
22
+ #
23
+ # @param [String] id
24
+ #
25
+ # @return [MongoMapper::Document]
26
+ def delete_document!(id)
27
+ document = find_document!(id)
28
+ document.destroy
29
+ document
30
+ end
31
+
32
+ # Find a document with +id+. If not found, returns 404.
33
+ #
34
+ # @param [String] id
35
+ #
36
+ # @return [MongoMapper::Document]
37
+ def find_document!(id)
38
+ document = config[:model].find_by_id(id)
39
+ unless document
40
+ error 404, convert(body_for(:not_found))
41
+ end
42
+ document
43
+ end
44
+
45
+ # Find all +model+ documents.
46
+ #
47
+ # @param [Class] model
48
+ # a class that includes MongoMapper::Document
49
+ #
50
+ # @return [Array<MongoMapper::Document>]
51
+ def find_documents!
52
+ config[:model].find(:all)
53
+ end
54
+
55
+ # Update a document with +id+ from params. If not valid, returns 400.
56
+ #
57
+ # @return [MongoMapper::Document]
58
+ def update_document!(id)
59
+ document = config[:model].update(id, params)
60
+ unless document.valid?
61
+ error 400, convert(body_for(:invalid_document, document))
62
+ end
63
+ document
64
+ end
65
+
66
+ end
67
+
68
+ end
69
+
70
+ end
data/lib/builder.rb ADDED
@@ -0,0 +1,84 @@
1
+ module SinatraResource
2
+
3
+ class Builder
4
+
5
+ def initialize(klass)
6
+ @klass = klass
7
+ end
8
+
9
+ def build
10
+ build_get_one
11
+ build_get_many
12
+ build_post
13
+ build_put
14
+ build_delete
15
+ build_helpers
16
+ end
17
+
18
+ def build_get_one
19
+ @klass.get '/:id/?' do
20
+ id = params.delete("id")
21
+ role = get_role(id)
22
+ check_permission(:read, role)
23
+ check_params(:read, role)
24
+ document = find_document!(id)
25
+ resource = build_resource(role, document)
26
+ display(:read, resource)
27
+ end
28
+ end
29
+
30
+ def build_get_many
31
+ @klass.get '/?' do
32
+ role = get_role
33
+ check_permission(:read, role)
34
+ check_params(:read, role)
35
+ documents = find_documents!
36
+ resources = build_resources(documents)
37
+ display(:read, resources)
38
+ end
39
+ end
40
+
41
+ def build_post
42
+ @klass.post '/?' do
43
+ role = get_role
44
+ check_permission(:create, role)
45
+ check_params(:create, role)
46
+ document = create_document!
47
+ resource = build_resource(role, document)
48
+ display(:create, resource)
49
+ end
50
+ end
51
+
52
+ def build_put
53
+ @klass.put '/:id/?' do
54
+ id = params.delete("id")
55
+ role = get_role(id)
56
+ check_permission(:update, role)
57
+ check_params(:update, role)
58
+ document = update_document!(id)
59
+ resource = build_resource(role, document)
60
+ display(:update, resource)
61
+ end
62
+ end
63
+
64
+ def build_delete
65
+ @klass.delete '/:id/?' do
66
+ id = params.delete("id")
67
+ role = get_role(id)
68
+ check_permission(:delete, role)
69
+ check_params(:delete, role)
70
+ delete_document!(id)
71
+ display(:delete, "")
72
+ end
73
+ end
74
+
75
+ def build_helpers
76
+ @klass.helpers do
77
+ include Helpers
78
+ include MongoHelpers
79
+ end
80
+ end
81
+
82
+ end
83
+
84
+ end
data/lib/exceptions.rb ADDED
@@ -0,0 +1,10 @@
1
+ module SinatraResource
2
+
3
+ class Error < RuntimeError; end
4
+
5
+ class DefinitionError < Error; end
6
+ class NotImplemented < Error; end
7
+ class UndefinedRole < Error; end
8
+ class ValidationError < Error; end
9
+
10
+ end
data/lib/resource.rb ADDED
@@ -0,0 +1,171 @@
1
+ module SinatraResource
2
+
3
+ module Resource
4
+ def self.included(includee)
5
+ includee.extend ClassMethods
6
+ includee.setup
7
+ end
8
+
9
+ def config
10
+ self.class.instance_variable_get("@resource_config")
11
+ end
12
+
13
+ module ClassMethods
14
+
15
+ # Build the Sinatra actions based on the DSL statements in this class.
16
+ # You will want to do this last.
17
+ #
18
+ # If for some reason you reopen the class, you will need to call this
19
+ # method again. However, this usage has not been tested.
20
+ #
21
+ # @return [undefined]
22
+ def build
23
+ validate
24
+ Builder.new(self).build
25
+ end
26
+
27
+ # Specify the underlying +model+
28
+ #
29
+ # @example
30
+ # model User
31
+ #
32
+ # # which refers to, for example ...
33
+ # # class User
34
+ # # include MongoMapper::Document
35
+ # # ...
36
+ # # end
37
+ #
38
+ # @param [MongoMapper::Document] model
39
+ #
40
+ # @return [undefined]
41
+ def model(model)
42
+ if @resource_config[:model]
43
+ raise DefinitionError, "model already declared in #{self}"
44
+ end
45
+ @resource_config[:model] = model
46
+ default_properties
47
+ end
48
+
49
+ # Specify the minimal role needed to access this resource for reading
50
+ # or writing.
51
+ #
52
+ # @example
53
+ # permission :read => :basic
54
+ # permission :modify => :owner
55
+ #
56
+ # @param [Hash<Symbol => Symbol>] access_rules
57
+ # valid keys are :read or :modify
58
+ # values should be a role (such as :admin)
59
+ #
60
+ # @return [undefined]
61
+ def permission(access_rules)
62
+ access_rules.each_pair do |verb, role|
63
+ @resource_config[:permission][verb] = role
64
+ end
65
+ end
66
+
67
+ # Declare a property and its access rules.
68
+ #
69
+ # @example
70
+ # property :name, :r => :basic
71
+ # property :email, :r => :owner
72
+ # property :role, :r => :owner, :w => :admin
73
+ #
74
+ # @param [Symbol] name
75
+ #
76
+ # @param [Hash] access_rules
77
+ #
78
+ # @return [undefined]
79
+ def property(name, access_rules={}, &block)
80
+ if @resource_config[:properties][name]
81
+ raise DefinitionError, "property #{name} already declared in #{self}"
82
+ end
83
+ @resource_config[:properties][name] = {}
84
+ if block
85
+ @resource_config[:properties][name][:w] = :nobody
86
+ @resource_config[:properties][name][:read_proc] = block
87
+ else
88
+ access_rules.each_pair do |kind, role|
89
+ @resource_config[:properties][name][kind] = role
90
+ end
91
+ end
92
+ end
93
+
94
+ # Specify the role definitions for this resource.
95
+ #
96
+ # @example
97
+ # roles Roles
98
+ #
99
+ # # which refers to, for example ...
100
+ # # module Roles
101
+ # # include SinatraResource::Roles
102
+ # #
103
+ # # role :anonymous
104
+ # # role :basic => :anonymous
105
+ # # role :admin => :basic
106
+ # # end
107
+ #
108
+ # @param [Class] klass
109
+ #
110
+ # @return [undefined]
111
+ def roles(klass)
112
+ if @resource_config[:roles]
113
+ raise DefinitionError, "roles already declared in #{self}"
114
+ end
115
+ @resource_config[:roles] = klass
116
+ end
117
+
118
+ # For internal use. Initializes internal data structure.
119
+ def setup
120
+ @resource_config = {
121
+ :model => nil,
122
+ :permission => {},
123
+ :properties => {},
124
+ :roles => nil,
125
+ :path => default_path,
126
+ }
127
+ end
128
+
129
+ protected
130
+
131
+ # Return the default relative path for a resource.
132
+ #
133
+ # @return [String]
134
+ def default_path
135
+ self.to_s.split('::').last.downcase
136
+ end
137
+
138
+ # Define some default properties to mirror common keys in the
139
+ # model
140
+ #
141
+ # @return [undefined]
142
+ def default_properties
143
+ keys = @resource_config[:model].keys
144
+ if keys.include?("_id")
145
+ property :id, :w => :nobody
146
+ end
147
+
148
+ if keys.include?("created_at")
149
+ property :created_at, :w => :nobody
150
+ end
151
+
152
+ if keys.include?("updated_at")
153
+ property :updated_at, :w => :nobody
154
+ end
155
+ end
156
+
157
+ # Verifies correctness of resource.
158
+ #
159
+ # @raise [ValidationError] if invalid
160
+ #
161
+ # @return [undefined]
162
+ def validate
163
+ unless @resource_config[:model]
164
+ raise ValidationError, "model required"
165
+ end
166
+ end
167
+
168
+ end
169
+ end
170
+
171
+ end
data/lib/roles.rb ADDED
@@ -0,0 +1,163 @@
1
+ module SinatraResource
2
+
3
+ module Roles
4
+ def self.included(includee)
5
+ includee.extend ClassMethods
6
+ includee.setup
7
+ end
8
+
9
+ module ClassMethods
10
+
11
+ # High-level way to define a role. You can also specify what role it
12
+ # builds upon (its parent).
13
+ #
14
+ # For example:
15
+ # role :anonymous
16
+ # role :basic => :anonymous
17
+ # role :admin => :basic
18
+ #
19
+ # This means: admin > basic > anonymous
20
+ #
21
+ # The order of the role statements does not matter. Only the
22
+ # dependencies between a role and its parent are significant.
23
+ #
24
+ # Roles do not have to be a single linear ordering. You can have any
25
+ # number of roles, connected in a DAG (directed acyclic graph). For
26
+ # example:
27
+ #
28
+ # role :anonymous
29
+ # role :basic => :anonymous
30
+ # role :editor => :basic
31
+ # role :manager => :basic
32
+ # role :admin => [:editor, :manager]
33
+ #
34
+ # # which means:
35
+ # # * admin > manager > basic > anonymous
36
+ # # * admin > editor > basic > anonymous
37
+ # # * manager and editor cannot be compared
38
+ #
39
+ # @param [Symbol, Hash<Symbol => [Symbol, Array<Symbol>]>] arg
40
+ def role(arg)
41
+ if arg.is_a?(Symbol)
42
+ create_role(arg)
43
+ elsif arg.is_a?(Hash)
44
+ arg.each_pair do |name, parent_name|
45
+ create_role(name, parent_name)
46
+ end
47
+ else
48
+ raise ArgumentError
49
+ end
50
+ end
51
+
52
+ # Is +role+ at least as privileged as +minimum+?
53
+ #
54
+ # @example
55
+ # satisfies?(:anonymous, :basic) # => false
56
+ # satisfies?(:admin, :basic) # => true
57
+ # satisfies?(:basic, :basic) # => true
58
+ #
59
+ # @param [Symbol] role
60
+ # a role (such as :anonymous, :basic, or :admin)
61
+ #
62
+ # @param [Symbol] minimum
63
+ #
64
+ # @return [Boolean]
65
+ def satisfies?(role, minimum)
66
+ @satisfies_cache[[role, minimum]] ||= (
67
+ role == minimum || ancestors(role).include?(minimum)
68
+ )
69
+ end
70
+
71
+ def setup
72
+ @role_config = {}
73
+ @satisfies_cache = {}
74
+ end
75
+
76
+ # Halt if +role+ is undefined.
77
+ #
78
+ # @raise [UndefinedRole] if role undefined
79
+ def validate_role(role)
80
+ unless @role_config.include?(role)
81
+ raise UndefinedRole, "#{role.inspect} not defined"
82
+ end
83
+ end
84
+
85
+ protected
86
+
87
+ # Find the ancestors of +role+.
88
+ #
89
+ # @param [Symbol] role
90
+ #
91
+ # @return [Array<Symbol>]
92
+ def ancestors(role)
93
+ _ancestors([role])
94
+ end
95
+
96
+ # Find the ancestors of +roles+.
97
+ #
98
+ # Implementation details
99
+ #
100
+ # This is a recursive function. For each recursion, all of the parents
101
+ # of the list are found. A new list is created by merging the original
102
+ # list and the parents. The recursion stops when the new list is the
103
+ # same as the original list.
104
+ #
105
+ # If you have these roles ...
106
+ # role :anonymous
107
+ # role :basic => :anonymous
108
+ # role :owner => :basic
109
+ # role :curator => :basic
110
+ # role :admin => [:owner, :curator]
111
+ #
112
+ # ... and you do ...
113
+ # _ancestors([:owner, :curator])
114
+ #
115
+ # ... then the recursion unfolds like this:
116
+ # _ancestors([:owner, :curator, :basic])
117
+ # _ancestors([:owner, :curator, :basic, :anonymous])
118
+ #
119
+ # @param [Array<Symbol>] roles
120
+ #
121
+ # @return [Array<Symbol>]
122
+ def _ancestors(roles)
123
+ parents = roles.map { |role| parents(role) }.flatten
124
+ list = parents.concat(roles).uniq
125
+ return roles if list == roles
126
+ _ancestors(list)
127
+ end
128
+
129
+ # Low-level way to define a role. You can also specify what role it
130
+ # builds upon (+parent_name+).
131
+ #
132
+ # @param [Symbol] name
133
+ # The name of the role being defined
134
+ #
135
+ # @param [Symbol, nil] parent_name
136
+ # The name of the parent role
137
+ #
138
+ # @api private
139
+ def create_role(name, parent_name=nil)
140
+ @role_config[name] = parent_name
141
+ end
142
+
143
+ # Find the parents of +role+.
144
+ #
145
+ # @param [Symbol] role
146
+ # a role (such as :anonymous, :basic, or :admin)
147
+ #
148
+ # @return [Array<Symbol>]
149
+ def parents(role)
150
+ x = @role_config[role]
151
+ if x.is_a?(Enumerable)
152
+ x
153
+ elsif x
154
+ [x]
155
+ else
156
+ []
157
+ end
158
+ end
159
+
160
+ end
161
+ end
162
+
163
+ end
@@ -0,0 +1,6 @@
1
+ require File.dirname(__FILE__) + '/exceptions'
2
+ require File.dirname(__FILE__) + '/builder/helpers'
3
+ require File.dirname(__FILE__) + '/builder/mongo_helpers'
4
+ require File.dirname(__FILE__) + '/builder'
5
+ require File.dirname(__FILE__) + '/resource'
6
+ require File.dirname(__FILE__) + '/roles'
@@ -0,0 +1 @@
1
+ * role-based permissions
@@ -0,0 +1,181 @@
1
+ # Preface
2
+
3
+ Please consider this document in the context of building APIs in the Resource
4
+ Oriented Architecture style.
5
+
6
+ # Introduction
7
+
8
+ Permission systems may be relatively simple or relatively complex.
9
+
10
+ Simple permission systems are more rigid, less expressive, and hopefully
11
+ simpler to develop. Complex permission systems are more flexible, more
12
+ expressive, simpler (in theory) for an end-user to update, but harder to
13
+ develop.
14
+
15
+ It is not hard to envision reasonable permission systems that get moderately
16
+ complicated fairly quickly. Why?
17
+
18
+ * It may be easier to state permissions in terms of general rules and
19
+ exceptions to the general rule instead of spelling out every single
20
+ situation explicitly.
21
+ * Permissions are often multi-leveled.
22
+ * Permissions are rules, so sometimes the precedence is not obvious.
23
+ * Permissions are sometimes most conveniently stated in terms of
24
+ 'positive' and 'negative' rules. This create possibilities for
25
+ logical contradictions.
26
+
27
+ A relatively simple example of a permission system would:
28
+
29
+ * have single level
30
+ * would require every situation to be spelled out explicitly
31
+
32
+ A more complex, but still achievable permission system would:
33
+
34
+ * have a couple of levels
35
+ * would allow general rules to be stated
36
+ * would allow specific situations to be spelled out
37
+ * would allow more specific rules to override more general ones
38
+ * would detect conflicts
39
+
40
+ # Permissions can be stated at many levels:
41
+
42
+ * action level
43
+ * document level
44
+ * property level
45
+
46
+ # What are some example use cases?
47
+
48
+ ## Resource-level permission stories:
49
+
50
+ This stories illustrate when a user type either can or cannot access
51
+ a resource based just upon the action type.
52
+
53
+ :admin_user can :read any Source
54
+ :admin_user can :create a Source
55
+ :admin_user can :update any Source
56
+ :admin_user can :delete any Source
57
+
58
+ :basic_user can :read any Source
59
+ :basic_user can't :create any Source
60
+ :basic_user can't :update any Source
61
+ :basic_user can't :delete any Source
62
+
63
+ To extract the pattern:
64
+
65
+ UserType {can | can't} Action Resource
66
+
67
+ In other words, if you know the user type, action, and resource, you
68
+ know whether to allow or disallow.
69
+
70
+ def allow?(user_type, action, resource)
71
+ # logic depends solely on parameters
72
+ end
73
+
74
+ def disallow?(user_type, action, resource)
75
+ # logic depends solely on parameters
76
+ end
77
+
78
+ ## Document-level use cases:
79
+
80
+ In some cases, knowing the user type, action, and resource is not enough --
81
+ the relationship between the 'document at hand' and the 'user at hand' is also
82
+ needed. Note that the 'document at hand' is different from the 'resource' and
83
+ the 'user at hand' is different from the 'user type'.
84
+
85
+ :basic_user can't :read any Note # less useful
86
+ :basic_user can :read some Notes # less useful
87
+ :basic_user can :read an owned Note
88
+ :basic_user can't :read an unowned Note
89
+
90
+ :basic_user can :create a Note
91
+
92
+ :basic_user can't :update any Note # less useful
93
+ :basic_user can :update some Notes # less useful
94
+ :basic_user can :update an owned Note
95
+ :basic_user can't :update an unowned Note
96
+
97
+ :basic_user can't :delete any Note # less useful
98
+ :basic_user can :delete some Notes # less useful
99
+ :basic_user can :delete an owned Note
100
+ :basic_user can't :delete an unowned Note
101
+
102
+ These stories can be rewritten to make the pattern clearer:
103
+
104
+ :basic_user user can :read the Note n if n.owner == user
105
+ :basic_user user can't :read the Note n if n.owner != user
106
+ :basic_user user can :create a Note
107
+ :basic_user user can :update the Note n if n.owner == user
108
+ :basic_user user can't :update the Note n if n.owner != user
109
+ :basic_user user can :delete the Note n if n.owner == user
110
+ :basic_user user can't :delete the Note n if n.owner != user
111
+
112
+ To extract the pattern:
113
+
114
+ UserType {can | can't} Action
115
+
116
+ There is another way to see it, that uses "iff" ("if and only if"):
117
+
118
+ :basic_user user can :read the Note n iff n.owner == user
119
+ :basic_user user can :create a Note
120
+ :basic_user user can :update the Note n iff n.owner == user
121
+ :basic_user user can :delete the Note n iff n.owner == user
122
+
123
+ I'm not sure I prefer the "iff" form. It is more compact, but I get the
124
+ feeling that it makes it harder to have multilevel (cascading) permissions.
125
+
126
+ To extract the pattern:
127
+
128
+ UserType User Action Resource Instance Relation
129
+
130
+ UserType : :basic_user
131
+ User : 'user'
132
+ Action : [:read, :create, :update, :delete]
133
+ Resource : Note
134
+ Instance : 'n'
135
+ Relation : :owner
136
+
137
+ To back up (skipping a few steps, but I hope this is still clear) and remove
138
+ the "iff" we would give us this pattern:
139
+
140
+ UserType User {can | can't} Action Resource Instance Relation
141
+
142
+ The nice thing about the {can | can't} style is that it allows for one or both
143
+ 'sides' to be specified.
144
+
145
+ Which brings us back to the 'allow?' and 'disallow?' methods:
146
+
147
+ def allow?(user_type, user, action, resource, instance, relation)
148
+ # logic depends solely on parameters
149
+ end
150
+
151
+ def disallow?(user_type, user, action, resource, instance, relation)
152
+ # logic depends solely on parameters
153
+ end
154
+
155
+ I would expect that user_type can be derived from user, so we can simplify:
156
+
157
+ def allow?(user, action, resource, instance, relation)
158
+ # logic depends solely on parameters
159
+ end
160
+
161
+ def disallow?(user, action, resource, instance, relation)
162
+ # logic depends solely on parameters
163
+ end
164
+
165
+ It is tempting to try to simplify further. For example, why not assume that resource can be inferred from instance? That may be so, but I'm not so convinced that there is only one resource for each instance. For example,
166
+ what if we are talking about a "note" which can be exposed in two places:
167
+
168
+ * /sources/40/note/231
169
+ * /note/231
170
+
171
+ I skipped a key question: is this two resources or two representations?
172
+ (Sorry, I'm not going to answer this one right now.)
173
+
174
+ Depending on your answer, another question arises: is there a need to specify different permissions for each of these (resources | representations)?
175
+
176
+ ## Property-level use cases:
177
+
178
+ * read/write (a good default)
179
+ * writable only by a certain user type / permission (e.g. admin)
180
+ * writable only users that satisfy a relation (e.g. ownership)
181
+ * writable only on creation
@@ -0,0 +1,18 @@
1
+ # Questions:
2
+
3
+ * How to use MongoMapper?
4
+
5
+ * How to use ActiveRecord?
6
+
7
+ * How to use DataMapper?
8
+
9
+ * How to handle permissions?
10
+
11
+ * How to handle action-level permissions?
12
+
13
+ * How to handle document-level permissions?
14
+
15
+ * How to do complete overrides?
16
+
17
+ * How to have a resource define a "ratings" property if the underlying model
18
+ already has a :ratings association?