sinatra_resource 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.document +5 -0
- data/.gitignore +7 -0
- data/LICENSE +20 -0
- data/README.mdown +28 -0
- data/Rakefile +34 -0
- data/VERSION +1 -0
- data/examples/datacatalog/Rakefile +23 -0
- data/examples/datacatalog/app.rb +15 -0
- data/examples/datacatalog/config/config.rb +66 -0
- data/examples/datacatalog/config/config.yml +11 -0
- data/examples/datacatalog/config.ru +6 -0
- data/examples/datacatalog/lib/base.rb +9 -0
- data/examples/datacatalog/lib/resource.rb +73 -0
- data/examples/datacatalog/lib/roles.rb +15 -0
- data/examples/datacatalog/models/categorization.rb +31 -0
- data/examples/datacatalog/models/category.rb +28 -0
- data/examples/datacatalog/models/source.rb +33 -0
- data/examples/datacatalog/models/user.rb +51 -0
- data/examples/datacatalog/resources/categories.rb +32 -0
- data/examples/datacatalog/resources/sources.rb +35 -0
- data/examples/datacatalog/resources/users.rb +26 -0
- data/examples/datacatalog/tasks/db.rake +29 -0
- data/examples/datacatalog/tasks/test.rake +16 -0
- data/examples/datacatalog/test/helpers/assertions/assert_include.rb +17 -0
- data/examples/datacatalog/test/helpers/assertions/assert_not_include.rb +17 -0
- data/examples/datacatalog/test/helpers/lib/model_factories.rb +49 -0
- data/examples/datacatalog/test/helpers/lib/model_helpers.rb +30 -0
- data/examples/datacatalog/test/helpers/lib/request_helpers.rb +53 -0
- data/examples/datacatalog/test/helpers/model_test_helper.rb +5 -0
- data/examples/datacatalog/test/helpers/resource_test_helper.rb +5 -0
- data/examples/datacatalog/test/helpers/shared/api_keys.rb +48 -0
- data/examples/datacatalog/test/helpers/shared/common_body_responses.rb +15 -0
- data/examples/datacatalog/test/helpers/shared/status_codes.rb +61 -0
- data/examples/datacatalog/test/helpers/test_cases/model_test_case.rb +6 -0
- data/examples/datacatalog/test/helpers/test_cases/resource_test_case.rb +36 -0
- data/examples/datacatalog/test/helpers/test_helper.rb +36 -0
- data/examples/datacatalog/test/models/categorization_test.rb +40 -0
- data/examples/datacatalog/test/models/category_test.rb +35 -0
- data/examples/datacatalog/test/models/source_test.rb +37 -0
- data/examples/datacatalog/test/models/user_test.rb +77 -0
- data/examples/datacatalog/test/resources/categories/categories_delete_test.rb +112 -0
- data/examples/datacatalog/test/resources/categories/categories_get_many_test.rb +58 -0
- data/examples/datacatalog/test/resources/categories/categories_get_one_test.rb +75 -0
- data/examples/datacatalog/test/resources/categories/categories_post_test.rb +135 -0
- data/examples/datacatalog/test/resources/categories/categories_put_test.rb +140 -0
- data/examples/datacatalog/test/resources/sources/sources_delete_test.rb +112 -0
- data/examples/datacatalog/test/resources/sources/sources_get_many_test.rb +58 -0
- data/examples/datacatalog/test/resources/sources/sources_get_one_test.rb +74 -0
- data/examples/datacatalog/test/resources/sources/sources_post_test.rb +184 -0
- data/examples/datacatalog/test/resources/sources/sources_put_test.rb +227 -0
- data/examples/datacatalog/test/resources/users/users_delete_test.rb +134 -0
- data/examples/datacatalog/test/resources/users/users_get_many_test.rb +111 -0
- data/examples/datacatalog/test/resources/users/users_get_one_test.rb +75 -0
- data/examples/datacatalog/test/resources/users/users_post_test.rb +142 -0
- data/examples/datacatalog/test/resources/users/users_put_test.rb +171 -0
- data/lib/builder/helpers.rb +319 -0
- data/lib/builder/mongo_helpers.rb +70 -0
- data/lib/builder.rb +84 -0
- data/lib/exceptions.rb +10 -0
- data/lib/resource.rb +171 -0
- data/lib/roles.rb +163 -0
- data/lib/sinatra_resource.rb +6 -0
- data/notes/keywords.mdown +1 -0
- data/notes/permissions.mdown +181 -0
- data/notes/questions.mdown +18 -0
- data/notes/see_also.mdown +3 -0
- data/notes/synonyms.mdown +7 -0
- data/notes/to_do.mdown +7 -0
- data/notes/uniform_interface.mdown +22 -0
- data/sinatra_resource.gemspec +183 -0
- data/spec/sinatra_resource_spec.rb +7 -0
- data/spec/spec_helper.rb +9 -0
- data/tasks/spec.rake +13 -0
- data/tasks/yard.rake +13 -0
- 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
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?
|