authorizer 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- data/History.txt +4 -0
- data/Manifest.txt +17 -0
- data/PostInstall.txt +7 -0
- data/README.rdoc +160 -0
- data/Rakefile +26 -0
- data/app/models/object_role.rb +59 -0
- data/lib/authorizer.rb +7 -0
- data/lib/authorizer/admin.rb +112 -0
- data/lib/authorizer/application_controller.rb +65 -0
- data/lib/authorizer/base.rb +320 -0
- data/lib/authorizer/exceptions.rb +6 -0
- data/lib/authorizer/object_observer.rb +21 -0
- data/lib/authorizer/user_observer.rb +26 -0
- data/rails/init.rb +11 -0
- data/script/console +10 -0
- data/script/destroy +14 -0
- data/script/generate +14 -0
- metadata +149 -0
data/History.txt
ADDED
data/Manifest.txt
ADDED
@@ -0,0 +1,17 @@
|
|
1
|
+
History.txt
|
2
|
+
Manifest.txt
|
3
|
+
PostInstall.txt
|
4
|
+
README.rdoc
|
5
|
+
Rakefile
|
6
|
+
lib/authorizer.rb
|
7
|
+
script/console
|
8
|
+
script/destroy
|
9
|
+
script/generate
|
10
|
+
lib/authorizer/admin.rb
|
11
|
+
lib/authorizer/application_controller.rb
|
12
|
+
lib/authorizer/base.rb
|
13
|
+
lib/authorizer/exceptions.rb
|
14
|
+
lib/authorizer/object_observer.rb
|
15
|
+
lib/authorizer/user_observer.rb
|
16
|
+
app/models/object_role.rb
|
17
|
+
rails/init.rb
|
data/PostInstall.txt
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
Remaining installation steps: (1 and 2 already done!)
|
2
|
+
|
3
|
+
3. generate the migration by running "script/generate authorizer_migration"
|
4
|
+
4. run "rake db:migrate" to migrate your database
|
5
|
+
5. Add observers to 'config/environment.rb'
|
6
|
+
|
7
|
+
config.active_record.observers = "Authorizer::UserObserver", "Authorizer::ObjectObserver"
|
data/README.rdoc
ADDED
@@ -0,0 +1,160 @@
|
|
1
|
+
= authorizer
|
2
|
+
|
3
|
+
* https://github.com/cmdjohnson/authorizer
|
4
|
+
|
5
|
+
== DESCRIPTION:
|
6
|
+
|
7
|
+
Authorizer is a gem for Ruby (in conjunction with Rails 2.3) that does authorization for you on a per-object basis. What makes this gem different from e.g. declarative_authorization and cancan is they define one role for the entire application. With Authorizer, you define roles for different users on every Rails object.
|
8
|
+
|
9
|
+
Let's use a Dropbox analogy.
|
10
|
+
|
11
|
+
With Dropbox, you can choose which folder you want to share. For instance:
|
12
|
+
|
13
|
+
Al has a home folder with these subfolders in it:
|
14
|
+
- Music (shared with Bob)
|
15
|
+
- Pictures (shared with Casper and Bob)
|
16
|
+
- News (shared with no-one)
|
17
|
+
|
18
|
+
This causes Al to have all 3 folders in his Dropbox. Bob has 2 and Casper has only 1 folder called Pictures.
|
19
|
+
|
20
|
+
In other words, a user has access to a subset of the entire collection of folders. Bob has access to 2 of Al's folders, namely Music and Pictures. But he doesn't even see the News folder, nor can he download files from it.
|
21
|
+
|
22
|
+
Bob's access to the two folders is both read and write, so let's call that role "admin". Al is the owner of all 3 folders and has a role called "owner". This leads to the following Roles table:
|
23
|
+
|
24
|
+
folder_name user_name role
|
25
|
+
Music Al owner
|
26
|
+
Bob admin
|
27
|
+
Pictures Al owner
|
28
|
+
Bob admin
|
29
|
+
Casper admin
|
30
|
+
News Al owner
|
31
|
+
|
32
|
+
Now if we would allow Bob to also access the News folder but only read from it, we could add the role called "reader" to the table:
|
33
|
+
|
34
|
+
folder_name user_name role
|
35
|
+
News Bob reader
|
36
|
+
|
37
|
+
This is exactly what Authorizer does for your Rails application.
|
38
|
+
|
39
|
+
== FEATURES/PROBLEMS:
|
40
|
+
|
41
|
+
Handles authorization for you.
|
42
|
+
|
43
|
+
== SYNOPSIS:
|
44
|
+
|
45
|
+
Authorize a user on an object
|
46
|
+
|
47
|
+
Authorizer::Base.authorize_user( :object => object )
|
48
|
+
|
49
|
+
=> true/false
|
50
|
+
|
51
|
+
If you want to know if the current user is authorized on an object, use:
|
52
|
+
|
53
|
+
Authorizer::Base.user_is_authorized?( :object => object)
|
54
|
+
=> true/false
|
55
|
+
|
56
|
+
Remove authorization from an object
|
57
|
+
|
58
|
+
Authorizer::Base.remove_authorization( :object => object )
|
59
|
+
=> true/false
|
60
|
+
|
61
|
+
Find all objects that the current user is authorized to use
|
62
|
+
|
63
|
+
Authorizer::Base.find("Post", :all, { :conditions => { :name => "my_post" } }) # [ #<Post id: 1>, #<Post id: 2> ]
|
64
|
+
Authorizer::Base.find("Post", :first) #<Post id: 1>
|
65
|
+
Authorizer::Base.find("Post", [ 1, 6 ]) # [ #<Post id: 1>, #<Post id: 6> ]
|
66
|
+
|
67
|
+
If you are using inherited_resources, you can also use these filters in your controller class:
|
68
|
+
|
69
|
+
# own created objects so you can access them after creation
|
70
|
+
after_filter :own_created_object, :only => :create
|
71
|
+
# authorize entire controller
|
72
|
+
before_filter :authorize, :only => [ :show, :edit, :update, :destroy ]
|
73
|
+
|
74
|
+
This obviously works out of the box with resource-oriented controllers, but with anything different you'll have to make your own choices.
|
75
|
+
|
76
|
+
If you're just getting started with Authorizer but you already have a running app, you can have one user own all objects with this method:
|
77
|
+
|
78
|
+
Authorizer::Admin.create_brand_new_object_roles(:user => User.first)
|
79
|
+
|
80
|
+
This method will guess what objects to use by checking for descendants of ActiveRecord::Base.
|
81
|
+
|
82
|
+
If you just want to do this for the Post and Category classes, use:
|
83
|
+
|
84
|
+
Authorizer::Admin.create_brand_new_object_roles(:user => User.first, :objects => [ "Post", "Category" ])
|
85
|
+
|
86
|
+
Authorizer uses ActiveRecord observers to make sure it doesn't make any mess, for instance, when a user is deleted, all of his authorization objects are deleted as well. Should you want more control over this garbage collection process, or if you are a cleanfreak, use this to get rid of any stale authorization objects lying around in your database: (protip: embed into rake task!)
|
87
|
+
|
88
|
+
Authorizer::Admin.remove_all_unused_authorization_objects
|
89
|
+
|
90
|
+
== REQUIREMENTS:
|
91
|
+
|
92
|
+
- Ruby (this gem was tested with 1.8.7)
|
93
|
+
- Rails 2.3 (tested with 2.3.11 and 2.3.12)
|
94
|
+
- Authlogic (for authentication)
|
95
|
+
|
96
|
+
Optional:
|
97
|
+
- inherited_resources if you want to use the controller filters supplied with this gem. Otherwise, you'll have to check for authorization yourself.
|
98
|
+
|
99
|
+
== INSTALL:
|
100
|
+
|
101
|
+
Installation
|
102
|
+
===
|
103
|
+
|
104
|
+
1. sudo gem install authorizer
|
105
|
+
2. add "authorizer" to your Gemfile (I hope you've stopped using config.gem already even if you are on Rails 2.3?)
|
106
|
+
3. generate a migration for authorization objects:
|
107
|
+
|
108
|
+
script/generate migration CreateObjectRoles
|
109
|
+
|
110
|
+
Paste this code into the newly generated file:
|
111
|
+
|
112
|
+
def self.up
|
113
|
+
create_table :object_roles do |t|
|
114
|
+
t.string :klazz_name
|
115
|
+
t.integer :object_reference
|
116
|
+
t.references :user
|
117
|
+
t.string :role
|
118
|
+
|
119
|
+
t.timestamps
|
120
|
+
end
|
121
|
+
end
|
122
|
+
|
123
|
+
def self.down
|
124
|
+
drop_table :object_roles
|
125
|
+
end
|
126
|
+
|
127
|
+
4. run "rake db:migrate" to migrate your database
|
128
|
+
|
129
|
+
That's it!
|
130
|
+
|
131
|
+
== DEVELOPERS:
|
132
|
+
|
133
|
+
Reviews, patches and bug tickets are welcome!
|
134
|
+
|
135
|
+
{authorizer_project}[https://github.com/cmdjohnson/authorizer_project] is a sample project that shows the usage of the Authorizer gem. It also contains all tests for your testing pleasure.
|
136
|
+
|
137
|
+
== LICENSE:
|
138
|
+
|
139
|
+
(The MIT License)
|
140
|
+
|
141
|
+
Copyright (c) 2011 Commander Johnson <commanderjohnson@gmail.com>
|
142
|
+
|
143
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
144
|
+
a copy of this software and associated documentation files (the
|
145
|
+
'Software'), to deal in the Software without restriction, including
|
146
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
147
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
148
|
+
permit persons to whom the Software is furnished to do so, subject to
|
149
|
+
the following conditions:
|
150
|
+
|
151
|
+
The above copyright notice and this permission notice shall be
|
152
|
+
included in all copies or substantial portions of the Software.
|
153
|
+
|
154
|
+
THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
|
155
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
156
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
157
|
+
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
158
|
+
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
159
|
+
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
160
|
+
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/Rakefile
ADDED
@@ -0,0 +1,26 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
gem 'hoe', '>= 2.1.0'
|
3
|
+
require 'hoe'
|
4
|
+
require 'fileutils'
|
5
|
+
require './lib/authorizer'
|
6
|
+
|
7
|
+
Hoe.plugin :newgem
|
8
|
+
# Hoe.plugin :website
|
9
|
+
# Hoe.plugin :cucumberfeatures
|
10
|
+
|
11
|
+
# Generate all the Rake tasks
|
12
|
+
# Run 'rake -T' to see list of generated tasks (from gem root directory)
|
13
|
+
$hoe = Hoe.spec 'authorizer' do
|
14
|
+
self.developer 'CmdJohnson', 'commanderjohnson@gmail.com'
|
15
|
+
self.post_install_message = 'PostInstall.txt'
|
16
|
+
self.rubyforge_name = self.name
|
17
|
+
self.extra_deps = [['options_checker','>= 0.0.1']]
|
18
|
+
|
19
|
+
end
|
20
|
+
|
21
|
+
require 'newgem/tasks'
|
22
|
+
Dir['tasks/**/*.rake'].each { |t| load t }
|
23
|
+
|
24
|
+
# TODO - want other tests/tasks run by default? Add them to the list
|
25
|
+
# remove_task :default
|
26
|
+
# task :default => [:spec, :features]
|
@@ -0,0 +1,59 @@
|
|
1
|
+
# -*- encoding : utf-8 -*-
|
2
|
+
class ObjectRole < ActiveRecord::Base
|
3
|
+
##############################################################################
|
4
|
+
# class methods
|
5
|
+
##############################################################################
|
6
|
+
|
7
|
+
def self.roles
|
8
|
+
[ "owner" ]
|
9
|
+
end
|
10
|
+
|
11
|
+
# What ObjectRoles does this object have associated?
|
12
|
+
def self.find_all_by_object(object)
|
13
|
+
raise "Can only operate on ActiveRecord::Base objects." unless object.is_a?(ActiveRecord::Base)
|
14
|
+
raise "Can only operate on saved objects" if object.new_record?
|
15
|
+
|
16
|
+
klazz_name = object.class.to_s
|
17
|
+
object_reference = object.id
|
18
|
+
|
19
|
+
ObjectRole.find(:all, :conditions => { :klazz_name => klazz_name, :object_reference => object_reference } )
|
20
|
+
end
|
21
|
+
|
22
|
+
##############################################################################
|
23
|
+
# associations
|
24
|
+
##############################################################################
|
25
|
+
|
26
|
+
belongs_to :user
|
27
|
+
|
28
|
+
##############################################################################
|
29
|
+
# validations
|
30
|
+
##############################################################################
|
31
|
+
|
32
|
+
validates_presence_of :klazz_name, :object_reference, :user_id, :role
|
33
|
+
validates_numericality_of :object_reference, :only_integer => true
|
34
|
+
validates_inclusion_of :role, :in => ObjectRole.roles
|
35
|
+
|
36
|
+
##############################################################################
|
37
|
+
# constructor
|
38
|
+
##############################################################################
|
39
|
+
|
40
|
+
##############################################################################
|
41
|
+
# instance methods
|
42
|
+
##############################################################################
|
43
|
+
|
44
|
+
def description
|
45
|
+
"#{self.klazz_name} #{self.object_reference}"
|
46
|
+
end
|
47
|
+
|
48
|
+
def object
|
49
|
+
obj = nil
|
50
|
+
|
51
|
+
begin
|
52
|
+
klazz = eval(self.klazz_name)
|
53
|
+
obj = klazz.find(self.object_reference)
|
54
|
+
rescue
|
55
|
+
end
|
56
|
+
|
57
|
+
obj
|
58
|
+
end
|
59
|
+
end
|
data/lib/authorizer.rb
ADDED
@@ -0,0 +1,112 @@
|
|
1
|
+
# -*- encoding : utf-8 -*-
|
2
|
+
module Authorizer
|
3
|
+
############################################################################
|
4
|
+
# helper class that does administrative functions for you
|
5
|
+
############################################################################
|
6
|
+
class Admin < Base
|
7
|
+
# not everybody uses "User" as user class name. We do :o -=- :))) ffFF ww ppO :)))
|
8
|
+
@@user_class_name = "User"
|
9
|
+
|
10
|
+
############################################################################
|
11
|
+
# create_brand_new_object_roles
|
12
|
+
#
|
13
|
+
# For if you've been working without Authorizer and want to start using it.
|
14
|
+
# Obviously, if you don't have any ObjectRoles then you'll find yourself blocked out of your own app.
|
15
|
+
# This method will assign all objects listed in an array to a certain user.
|
16
|
+
# For instance:
|
17
|
+
#
|
18
|
+
# user = User.find(1)
|
19
|
+
# objects = [ "Post", "Category" ]
|
20
|
+
# Authorizer::Admin.create_brand_new_object_roles( :user => user, :objects => objects )
|
21
|
+
#
|
22
|
+
# If objects is not specified, Admin will look for all descendants of ActiveRecord::Base
|
23
|
+
# and exclude the ObjectRole and User classes.
|
24
|
+
#
|
25
|
+
# Authorizer::Admin.create_brand_new_object_roles( :user => user )
|
26
|
+
############################################################################
|
27
|
+
|
28
|
+
def self.create_brand_new_object_roles(options = {})
|
29
|
+
OptionsChecker.check(options, [ :user ])
|
30
|
+
|
31
|
+
objects = gather_direct_descendants_of_activerecord_base || options[:objects]
|
32
|
+
|
33
|
+
ret = false
|
34
|
+
|
35
|
+
raise "objects must be an Array" unless objects.is_a?(Array)
|
36
|
+
|
37
|
+
# Nothing to do ..
|
38
|
+
return ret if objects.blank?
|
39
|
+
|
40
|
+
begin
|
41
|
+
user_id = options[:user].id
|
42
|
+
rescue
|
43
|
+
end
|
44
|
+
|
45
|
+
unless user_id.nil?
|
46
|
+
for object in objects
|
47
|
+
evaled_klazz = nil
|
48
|
+
|
49
|
+
begin
|
50
|
+
evaled_klazz = eval(object)
|
51
|
+
rescue
|
52
|
+
end
|
53
|
+
|
54
|
+
unless evaled_klazz.nil?
|
55
|
+
# One is enough to return exit status OK.
|
56
|
+
ret = true
|
57
|
+
# Let's find all. This is the same as Post.all
|
58
|
+
if evaled_klazz.is_a?(Class)
|
59
|
+
collection = evaled_klazz.all
|
60
|
+
|
61
|
+
# Go
|
62
|
+
unless collection.blank?
|
63
|
+
for coll_ in collection
|
64
|
+
ObjectRole.create!( :klazz_name => object, :object_reference => coll_.id, :user_id => user_id, :role => "owner" )
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
ret
|
73
|
+
end
|
74
|
+
|
75
|
+
############################################################################
|
76
|
+
# remove_all_unused_authorization_objects
|
77
|
+
#############################################################################
|
78
|
+
# Remove all stale (non-object) authorization objects.
|
79
|
+
############################################################################
|
80
|
+
|
81
|
+
def self.remove_all_unused_authorization_objects options = {}
|
82
|
+
# no options
|
83
|
+
# ___
|
84
|
+
# Let's iterate all ObjectRoles
|
85
|
+
for object_role in ObjectRole.all
|
86
|
+
object_role.destroy if object_role.object.nil?
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
protected
|
91
|
+
|
92
|
+
############################################################################
|
93
|
+
# gather_direct_descendants_of_activerecord_base
|
94
|
+
############################################################################
|
95
|
+
|
96
|
+
def self.gather_direct_descendants_of_activerecord_base
|
97
|
+
ret = []
|
98
|
+
|
99
|
+
classes = ActiveRecord::Base.send(:subclasses)
|
100
|
+
|
101
|
+
# Go through it twice to determine direct descendants
|
102
|
+
for klazz in classes
|
103
|
+
# Push the class name, not the class object itself.
|
104
|
+
klazz_name = klazz.to_s
|
105
|
+
# May never use the ObjectRole or User class.
|
106
|
+
ret.push klazz_name if !klazz_name.eql?("ObjectRole") && !klazz_name.eql?(@@user_class_name)
|
107
|
+
end
|
108
|
+
|
109
|
+
ret
|
110
|
+
end
|
111
|
+
end
|
112
|
+
end
|
@@ -0,0 +1,65 @@
|
|
1
|
+
# -*- encoding : utf-8 -*-
|
2
|
+
# Use this file to add a couple of helper methods to ApplicationController
|
3
|
+
|
4
|
+
################################################################################
|
5
|
+
# Addendum to ApplicationController
|
6
|
+
################################################################################
|
7
|
+
# These methods are heavily dependent on InheritedResources, more specifically the 'resource' method.
|
8
|
+
#
|
9
|
+
# Otherwise there would be no predefined way of peeking into a controller's resource object.
|
10
|
+
################################################################################
|
11
|
+
|
12
|
+
# for user_not_authorized
|
13
|
+
require 'authorizer/exceptions'
|
14
|
+
|
15
|
+
class ApplicationController < ActionController::Base
|
16
|
+
helper_method :own_created_object, :authorize
|
17
|
+
|
18
|
+
private
|
19
|
+
|
20
|
+
##############################################################################
|
21
|
+
# authorizer
|
22
|
+
##############################################################################
|
23
|
+
|
24
|
+
def own_created_object
|
25
|
+
ret = true # always return true otherwise the filter chain would be blocked.
|
26
|
+
|
27
|
+
begin
|
28
|
+
r = resource
|
29
|
+
rescue
|
30
|
+
end
|
31
|
+
|
32
|
+
unless r.nil?
|
33
|
+
# only if this objet was successfully created will we do this.
|
34
|
+
unless r.new_record?
|
35
|
+
Authorizer::Base.authorize_user( :object => r )
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
ret
|
40
|
+
end
|
41
|
+
|
42
|
+
def authorize
|
43
|
+
ret = false # return false by default, effectively using a whitelist method.
|
44
|
+
|
45
|
+
begin
|
46
|
+
r = resource
|
47
|
+
rescue
|
48
|
+
end
|
49
|
+
|
50
|
+
unless r.nil?
|
51
|
+
auth = Authorizer::Base.user_is_authorized?( :object => r )
|
52
|
+
|
53
|
+
if auth.eql?(false)
|
54
|
+
raise Authorizer::UserNotAuthorized.new("You are not authorized to access this resource.")
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
ret
|
59
|
+
end
|
60
|
+
|
61
|
+
##############################################################################
|
62
|
+
# end authorizer
|
63
|
+
##############################################################################
|
64
|
+
end
|
65
|
+
|
@@ -0,0 +1,320 @@
|
|
1
|
+
# -*- encoding : utf-8 -*-
|
2
|
+
################################################################################
|
3
|
+
# Authorizer
|
4
|
+
#
|
5
|
+
# Authorizer is a Ruby class that authorizes using the ObjectRole record.
|
6
|
+
################################################################################
|
7
|
+
|
8
|
+
# for user_not_authorized
|
9
|
+
require 'authorizer/exceptions'
|
10
|
+
require 'authorizer/application_controller'
|
11
|
+
|
12
|
+
module Authorizer
|
13
|
+
class Base < ApplicationController
|
14
|
+
############################################################################
|
15
|
+
# authorize_user
|
16
|
+
#
|
17
|
+
# If no user is specified, authorizes the current user.
|
18
|
+
# If no role is specified, "owner" is used as role.
|
19
|
+
############################################################################
|
20
|
+
|
21
|
+
def self.authorize_user(options)
|
22
|
+
OptionsChecker.check(options, [ :object ])
|
23
|
+
|
24
|
+
ret = false
|
25
|
+
|
26
|
+
object = options[:object]
|
27
|
+
role = options[:role] || "owner"
|
28
|
+
user = options[:user] || get_current_user
|
29
|
+
|
30
|
+
return false if basic_check_fails?(options)
|
31
|
+
|
32
|
+
check_user(user)
|
33
|
+
# Checks done. Let's go.
|
34
|
+
|
35
|
+
or_ = find_object_role(object, user)
|
36
|
+
|
37
|
+
# This time, we want it to be nil.
|
38
|
+
if or_.nil? && !user.nil?
|
39
|
+
klazz_name = object.class.to_s
|
40
|
+
object_reference = object.id
|
41
|
+
|
42
|
+
ObjectRole.create!( :klazz_name => klazz_name, :object_reference => object_reference, :user => user, :role => role )
|
43
|
+
Rails.logger.debug("Authorizer: created authorization on #{object} for current_user with ID #{user.id} witih role #{role}")
|
44
|
+
ret = true
|
45
|
+
end
|
46
|
+
|
47
|
+
ret
|
48
|
+
end
|
49
|
+
|
50
|
+
############################################################################
|
51
|
+
# user_is_authorized?
|
52
|
+
#
|
53
|
+
# If no user is specified, current_user is used.
|
54
|
+
############################################################################
|
55
|
+
|
56
|
+
def self.user_is_authorized? options
|
57
|
+
OptionsChecker.check(options, [ :object ])
|
58
|
+
|
59
|
+
ret = false
|
60
|
+
|
61
|
+
check = basic_check_fails?(options)
|
62
|
+
return ret if check
|
63
|
+
|
64
|
+
object = options[:object]
|
65
|
+
user = options[:user] || get_current_user
|
66
|
+
|
67
|
+
# Checks
|
68
|
+
check_user(user)
|
69
|
+
# Checks done. Let's go.
|
70
|
+
|
71
|
+
or_ = find_object_role(object, user)
|
72
|
+
|
73
|
+
# Congratulations, you've been Authorized.
|
74
|
+
unless or_.nil?
|
75
|
+
ret = true
|
76
|
+
end
|
77
|
+
|
78
|
+
if ret
|
79
|
+
Rails.logger.debug("Authorizer: authorized current_user with ID #{user.id} to access #{or_.description} because of role #{or_.role}") unless user.nil? || or_.nil?
|
80
|
+
else
|
81
|
+
Rails.logger.debug("Authorizer: authorization failed for current_user with ID #{user.id} to access #{object.to_s}") unless user.nil? || object.nil?
|
82
|
+
end
|
83
|
+
|
84
|
+
ret
|
85
|
+
end
|
86
|
+
|
87
|
+
############################################################################
|
88
|
+
# remove_authorization
|
89
|
+
############################################################################
|
90
|
+
# Remove authorization a user has on a certain object.
|
91
|
+
############################################################################
|
92
|
+
|
93
|
+
def self.remove_authorization options = {}
|
94
|
+
OptionsChecker.check(options, [ :object ])
|
95
|
+
|
96
|
+
ret = false
|
97
|
+
|
98
|
+
return ret if basic_check_fails?(options)
|
99
|
+
|
100
|
+
object = options[:object]
|
101
|
+
user = options[:user] || get_current_user
|
102
|
+
|
103
|
+
# Check
|
104
|
+
check_user(user)
|
105
|
+
# Checks done. Let's go.
|
106
|
+
|
107
|
+
or_ = find_object_role(object, user)
|
108
|
+
|
109
|
+
unless or_.nil?
|
110
|
+
Rails.logger.debug("Authorizer: removed authorization for user ID #{user.id} on #{or_.description}")
|
111
|
+
|
112
|
+
or_.destroy
|
113
|
+
|
114
|
+
ret = true
|
115
|
+
end
|
116
|
+
|
117
|
+
ret
|
118
|
+
end
|
119
|
+
|
120
|
+
############################################################################
|
121
|
+
# find
|
122
|
+
############################################################################
|
123
|
+
# Out of the collection of all Posts, return the subset that belongs to the current user.
|
124
|
+
# External method that maps to the internal_find which is the generic find method.
|
125
|
+
#
|
126
|
+
# Arguments:
|
127
|
+
# - class_name: which class to use, e.g. "Post"
|
128
|
+
# - what: will be passed on to the ActiveRecord find function (e.g. Post.find(what))
|
129
|
+
# - find_options: will also be passed on (e.g. Post.find(what, find_options))
|
130
|
+
# - authorizer_options: options for authorizer, e.g. { :user => @user }
|
131
|
+
############################################################################
|
132
|
+
|
133
|
+
def self.find(class_name, what, find_options = {}, authorizer_options = {})
|
134
|
+
options = { :class_name => class_name, :what => what, :find_options => find_options }
|
135
|
+
my_options = authorizer_options.merge(options) # options overrides user-specified options.
|
136
|
+
|
137
|
+
internal_find(my_options)
|
138
|
+
end
|
139
|
+
|
140
|
+
############################################################################
|
141
|
+
# is_authorized?
|
142
|
+
#
|
143
|
+
# Checks if the corresponding role.eql?("owner")
|
144
|
+
############################################################################
|
145
|
+
|
146
|
+
def self.is_authorized? object
|
147
|
+
user_is_authorized? :object => object
|
148
|
+
end
|
149
|
+
|
150
|
+
############################################################################
|
151
|
+
# create_ownership
|
152
|
+
#
|
153
|
+
# ObjectRole.create!( :klazz_name => object.class.to_s, :object_reference => object.id, :user => current_user, :role => "owner" )
|
154
|
+
############################################################################
|
155
|
+
|
156
|
+
def self.create_ownership object
|
157
|
+
ret = false
|
158
|
+
|
159
|
+
return ret if basic_check_fails?(object)
|
160
|
+
|
161
|
+
ret = authorize_user( :object => object )
|
162
|
+
|
163
|
+
ret
|
164
|
+
end
|
165
|
+
|
166
|
+
protected
|
167
|
+
|
168
|
+
############################################################################
|
169
|
+
# get_current_user
|
170
|
+
############################################################################
|
171
|
+
# helper method to not be dependent on the current_user method
|
172
|
+
############################################################################
|
173
|
+
|
174
|
+
def self.get_current_user
|
175
|
+
ret = nil
|
176
|
+
|
177
|
+
begin
|
178
|
+
session = UserSession.find
|
179
|
+
ret = session.user
|
180
|
+
rescue
|
181
|
+
end
|
182
|
+
|
183
|
+
ret
|
184
|
+
end
|
185
|
+
|
186
|
+
############################################################################
|
187
|
+
# internal_find
|
188
|
+
############################################################################
|
189
|
+
# Extract some info from ObjectRole objects and then pass the info through
|
190
|
+
# to the ActiveRecord finder.
|
191
|
+
############################################################################
|
192
|
+
|
193
|
+
def self.internal_find(options = {})
|
194
|
+
# Options
|
195
|
+
OptionsChecker.check(options, [ :what, :class_name ])
|
196
|
+
|
197
|
+
# assign
|
198
|
+
class_name = options[:class_name]
|
199
|
+
what = options[:what]
|
200
|
+
find_options = options[:find_options] || {}
|
201
|
+
user = options[:user] || get_current_user
|
202
|
+
|
203
|
+
# We don't do the what checks anymore, ActiveRecord::Base.find does that for us now.
|
204
|
+
#what_checks = [ :all, :first, :last, :id ]
|
205
|
+
#raise "What must be one of #{what_checks.inspect}" unless what_checks.include?(what)
|
206
|
+
|
207
|
+
# Check userrrrrrrrrrrr --- =====================- ---= ===-=- *&((28 @((8
|
208
|
+
check_user(user)
|
209
|
+
# rrrr
|
210
|
+
ret = nil
|
211
|
+
# Checks
|
212
|
+
# Checks done. Let's go.
|
213
|
+
# Get the real klazz
|
214
|
+
klazz = nil
|
215
|
+
# Check it
|
216
|
+
begin
|
217
|
+
klazz = eval(class_name)
|
218
|
+
rescue
|
219
|
+
end
|
220
|
+
# oooo ooo ooo ___ --- === __- --_- ++_+_ =--- +- =+=-=- =-= <--- ice beam!
|
221
|
+
unless klazz.nil?
|
222
|
+
# now we know klazz really exists.
|
223
|
+
# let's find the object_role objects that match the user and klaz.
|
224
|
+
# Get the object_role objects
|
225
|
+
object_roles_conditions = { :klazz_name => class_name, :user_id => user.id }
|
226
|
+
object_roles = ObjectRole.find(:all, :conditions => object_roles_conditions )
|
227
|
+
# Get a list of IDs. These are objects that are owned by the current_user
|
228
|
+
object_role_ids = object_roles.collect { |or_| or_.object_reference } # [ 1, 1, 1, 1 ]
|
229
|
+
# Make it at least an array if object_role_ids returns nil
|
230
|
+
object_role_ids ||= []
|
231
|
+
# Try to emulate find as good as we can
|
232
|
+
# so don't skip this, try to always pass it on.
|
233
|
+
unless object_roles.nil?
|
234
|
+
# Prepare find_options
|
235
|
+
leading_find_options = {} # insert conventions here if needed
|
236
|
+
my_find_options = find_options.merge(leading_find_options)
|
237
|
+
# If the user passed an Array we should filter it with the list of available (authorized) objects.
|
238
|
+
#
|
239
|
+
# http://www.ruby-doc.org/core/classes/Array.html
|
240
|
+
# &
|
241
|
+
# Set Intersection—Returns a new array containing elements common to the two arrays, with no duplicates.
|
242
|
+
safe_what = what
|
243
|
+
if what.is_a?(Array)
|
244
|
+
safe_what = what & object_role_ids
|
245
|
+
end
|
246
|
+
# The big show. Let's call out F I N D !!!!!!
|
247
|
+
# INF FINFD FIWI FFIND IF FIND FIND FIND FIND FIND FIND FIND FIND
|
248
|
+
# FIND FIND FIND FIND FIND FIND FIND FIND FIND FIND FIND FIND FIND FIND
|
249
|
+
# FIND FIND FIND FIND FIND FIND FIND FIND FIND FIND FIND FIND FIND FIND
|
250
|
+
# FIND FIND FIND FIND FIND FIND FIND FIND FIND FIND FIND FIND FIND FIND
|
251
|
+
# FIND FIND FIND FIND FIND FIND FIND FIND FIND FIND FIND FIND FIND FIND
|
252
|
+
# FIND FIND FIND FIND FIND FIND FIND FIND FIND FIND FIND FIND FIND FIND
|
253
|
+
# FIND FIND FIND FIND FIND FIND FIND FIND FIND FIND FIND FIND FIND FIND
|
254
|
+
# FIND FIND FIND FIND FIND FIND FIND FIND FIND FIND FIND FIND FIND FIND
|
255
|
+
# FIND FIND FIND FIND FIND FIND FIND FIND FIND FIND FIND FIND FIND FIND
|
256
|
+
# FIND FIND FIND FIND FIND FIND FIND FIND FIND FIND FIND FIND FIND FIND
|
257
|
+
# FIND FIND FIND FIND FIND FIND FIND FIND FIND FIND FIND FIND FIND FIND
|
258
|
+
if safe_what.eql?(:all)
|
259
|
+
ret = klazz.find(:all, my_find_options)
|
260
|
+
elsif safe_what.eql?(:first)
|
261
|
+
ret = klazz.find(object_role_ids.first, my_find_options)
|
262
|
+
elsif safe_what.eql?(:last)
|
263
|
+
ret = klazz.find(object_role_ids.last, my_find_options)
|
264
|
+
else
|
265
|
+
ret = klazz.find(safe_what, my_find_options)
|
266
|
+
end
|
267
|
+
# SAFE WHAT???? SAFE WHAT???? SAFE WHAT???? SAFE WHAT???? SAFE WHAT????
|
268
|
+
# SAFE WHAT???? SAFE WHAT???? SAFE WHAT???? SAFE WHAT???? SAFE WHAT????
|
269
|
+
# SAFE WHAT???? SAFE WHAT???? SAFE WHAT???? SAFE WHAT???? SAFE WHAT????
|
270
|
+
# SAFE WHAT???? SAFE WHAT???? SAFE WHAT???? SAFE WHAT???? SAFE WHAT????
|
271
|
+
# SAFE WHAT???? SAFE WHAT???? SAFE WHAT???? SAFE WHAT???? SAFE WHAT????
|
272
|
+
# SAFE WHAT???? SAFE WHAT???? SAFE WHAT???? SAFE WHAT???? SAFE WHAT????
|
273
|
+
# SAFE WHAT???? SAFE WHAT???? SAFE WHAT???? SAFE WHAT???? SAFE WHAT????
|
274
|
+
# SAFE WHAT???? SAFE WHAT???? SAFE WHAT???? SAFE WHAT???? SAFE WHAT????
|
275
|
+
end
|
276
|
+
end
|
277
|
+
|
278
|
+
ret
|
279
|
+
end
|
280
|
+
|
281
|
+
def self.find_object_role(object, user)
|
282
|
+
return nil if object.nil? || user.nil?
|
283
|
+
|
284
|
+
# Check
|
285
|
+
check_user(user)
|
286
|
+
# Checks done. Let's go.
|
287
|
+
|
288
|
+
klazz_name = object.class.to_s
|
289
|
+
object_reference = object.id
|
290
|
+
|
291
|
+
unless user.nil?
|
292
|
+
or_ = ObjectRole.first( :conditions => { :klazz_name => klazz_name, :object_reference => object_reference, :user_id => user.id } )
|
293
|
+
end
|
294
|
+
|
295
|
+
or_
|
296
|
+
end
|
297
|
+
|
298
|
+
def self.basic_check_fails?(options)
|
299
|
+
ret = false
|
300
|
+
|
301
|
+
unless options[:object].nil?
|
302
|
+
if !options[:object].is_a?(ActiveRecord::Base) || options[:object].new_record?
|
303
|
+
raise "object must be subclass of ActiveRecord::Base and must also be saved."
|
304
|
+
end
|
305
|
+
end
|
306
|
+
|
307
|
+
ret
|
308
|
+
end
|
309
|
+
|
310
|
+
def self.check_user(user)
|
311
|
+
ret = true
|
312
|
+
|
313
|
+
raise "User cannot be nil" if user.nil?
|
314
|
+
raise "User must inherit from ActiveRecord::Base" unless user.is_a?(ActiveRecord::Base)
|
315
|
+
raise "User must be saved" if user.new_record?
|
316
|
+
|
317
|
+
ret
|
318
|
+
end
|
319
|
+
end
|
320
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
# -*- encoding : utf-8 -*-
|
2
|
+
module Authorizer
|
3
|
+
# Observes users and deleted any associated ObjectRole objects when the user gets deleted.
|
4
|
+
class ObjectObserver < ActiveRecord::Observer
|
5
|
+
# Observe this.
|
6
|
+
observe ActiveRecord::Base
|
7
|
+
|
8
|
+
# W DONT DO DIZ
|
9
|
+
# let's use before_destroy instead of after_destroy. More chance it will still have an ID >:)))))))))) :') :DDDDDDDDDDDDDDDDDDDDDDD
|
10
|
+
# W DONT DO DIZ
|
11
|
+
def after_destroy(object)
|
12
|
+
return nil if object.is_a?(User) # Users are covered by the other observer class.
|
13
|
+
# Find all ObjectRole records that point to this object.
|
14
|
+
object_roles = ObjectRole.find_all_by_object(object)
|
15
|
+
# Walk through 'em
|
16
|
+
for object_role in object_roles
|
17
|
+
object_role.destroy
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
# -*- encoding : utf-8 -*-
|
2
|
+
module Authorizer
|
3
|
+
# Observes users and deleted any associated ObjectRole objects when the user gets deleted.
|
4
|
+
class UserObserver < ActiveRecord::Observer
|
5
|
+
# Observe this.
|
6
|
+
observe :user
|
7
|
+
|
8
|
+
# W DONT DO DIZ
|
9
|
+
# let's use before_destroy instead of after_destroy. More chance it will still have an ID >:)))))))))) :') :DDDDDDDDDDDDDDDDDDDDDDD
|
10
|
+
# W DONT DO DIZ
|
11
|
+
def after_destroy(user)
|
12
|
+
# Default
|
13
|
+
object_roles = []
|
14
|
+
# Find all ObjectRole records that point to this user's ID.
|
15
|
+
begin
|
16
|
+
object_roles = ObjectRole.find_all_by_user_id(user.id)
|
17
|
+
rescue
|
18
|
+
end
|
19
|
+
# Walk through 'em
|
20
|
+
# Not executed if anything happens (array.size = 0)
|
21
|
+
for object_role in object_roles
|
22
|
+
object_role.destroy
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
data/rails/init.rb
ADDED
@@ -0,0 +1,11 @@
|
|
1
|
+
# -*- encoding : utf-8 -*-
|
2
|
+
################################################################################
|
3
|
+
# init.rb
|
4
|
+
#
|
5
|
+
# This file will load the Observers we need to prevent the database from becoming clogged with stale authorization objects.
|
6
|
+
################################################################################
|
7
|
+
|
8
|
+
config.after_initialize do
|
9
|
+
ActiveRecord::Base.observers << Authorizer::UserObserver
|
10
|
+
ActiveRecord::Base.observers << Authorizer::ObjectObserver
|
11
|
+
end
|
data/script/console
ADDED
@@ -0,0 +1,10 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# File: script/console
|
3
|
+
irb = RUBY_PLATFORM =~ /(:?mswin|mingw)/ ? 'irb.bat' : 'irb'
|
4
|
+
|
5
|
+
libs = " -r irb/completion"
|
6
|
+
# Perhaps use a console_lib to store any extra methods I may want available in the cosole
|
7
|
+
# libs << " -r #{File.dirname(__FILE__) + '/../lib/console_lib/console_logger.rb'}"
|
8
|
+
libs << " -r #{File.dirname(__FILE__) + '/../lib/authorizer.rb'}"
|
9
|
+
puts "Loading authorizer gem"
|
10
|
+
exec "#{irb} #{libs} --simple-prompt"
|
data/script/destroy
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
APP_ROOT = File.expand_path(File.join(File.dirname(__FILE__), '..'))
|
3
|
+
|
4
|
+
begin
|
5
|
+
require 'rubigen'
|
6
|
+
rescue LoadError
|
7
|
+
require 'rubygems'
|
8
|
+
require 'rubigen'
|
9
|
+
end
|
10
|
+
require 'rubigen/scripts/destroy'
|
11
|
+
|
12
|
+
ARGV.shift if ['--help', '-h'].include?(ARGV[0])
|
13
|
+
RubiGen::Base.use_component_sources! [:rubygems, :newgem, :newgem_theme, :test_unit]
|
14
|
+
RubiGen::Scripts::Destroy.new.run(ARGV)
|
data/script/generate
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
APP_ROOT = File.expand_path(File.join(File.dirname(__FILE__), '..'))
|
3
|
+
|
4
|
+
begin
|
5
|
+
require 'rubigen'
|
6
|
+
rescue LoadError
|
7
|
+
require 'rubygems'
|
8
|
+
require 'rubigen'
|
9
|
+
end
|
10
|
+
require 'rubigen/scripts/generate'
|
11
|
+
|
12
|
+
ARGV.shift if ['--help', '-h'].include?(ARGV[0])
|
13
|
+
RubiGen::Base.use_component_sources! [:rubygems, :newgem, :newgem_theme, :test_unit]
|
14
|
+
RubiGen::Scripts::Generate.new.run(ARGV)
|
metadata
ADDED
@@ -0,0 +1,149 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: authorizer
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
hash: 29
|
5
|
+
prerelease: false
|
6
|
+
segments:
|
7
|
+
- 0
|
8
|
+
- 0
|
9
|
+
- 1
|
10
|
+
version: 0.0.1
|
11
|
+
platform: ruby
|
12
|
+
authors:
|
13
|
+
- CmdJohnson
|
14
|
+
autorequire:
|
15
|
+
bindir: bin
|
16
|
+
cert_chain: []
|
17
|
+
|
18
|
+
date: 2011-07-26 00:00:00 +02:00
|
19
|
+
default_executable:
|
20
|
+
dependencies:
|
21
|
+
- !ruby/object:Gem::Dependency
|
22
|
+
name: options_checker
|
23
|
+
prerelease: false
|
24
|
+
requirement: &id001 !ruby/object:Gem::Requirement
|
25
|
+
none: false
|
26
|
+
requirements:
|
27
|
+
- - ">="
|
28
|
+
- !ruby/object:Gem::Version
|
29
|
+
hash: 29
|
30
|
+
segments:
|
31
|
+
- 0
|
32
|
+
- 0
|
33
|
+
- 1
|
34
|
+
version: 0.0.1
|
35
|
+
type: :runtime
|
36
|
+
version_requirements: *id001
|
37
|
+
- !ruby/object:Gem::Dependency
|
38
|
+
name: hoe
|
39
|
+
prerelease: false
|
40
|
+
requirement: &id002 !ruby/object:Gem::Requirement
|
41
|
+
none: false
|
42
|
+
requirements:
|
43
|
+
- - ">="
|
44
|
+
- !ruby/object:Gem::Version
|
45
|
+
hash: 47
|
46
|
+
segments:
|
47
|
+
- 2
|
48
|
+
- 8
|
49
|
+
- 0
|
50
|
+
version: 2.8.0
|
51
|
+
type: :development
|
52
|
+
version_requirements: *id002
|
53
|
+
description: |-
|
54
|
+
Authorizer is a gem for Ruby (in conjunction with Rails 2.3) that does authorization for you on a per-object basis. What makes this gem different from e.g. declarative_authorization and cancan is they define one role for the entire application. With Authorizer, you define roles for different users on every Rails object.
|
55
|
+
|
56
|
+
Let's use a Dropbox analogy.
|
57
|
+
|
58
|
+
With Dropbox, you can choose which folder you want to share. For instance:
|
59
|
+
|
60
|
+
Al has a home folder with these subfolders in it:
|
61
|
+
- Music (shared with Bob)
|
62
|
+
- Pictures (shared with Casper and Bob)
|
63
|
+
- News (shared with no-one)
|
64
|
+
|
65
|
+
This causes Al to have all 3 folders in his Dropbox. Bob has 2 and Casper has only 1 folder called Pictures.
|
66
|
+
|
67
|
+
In other words, a user has access to a subset of the entire collection of folders. Bob has access to 2 of Al's folders, namely Music and Pictures. But he doesn't even see the News folder, nor can he download files from it.
|
68
|
+
|
69
|
+
Bob's access to the two folders is both read and write, so let's call that role "admin". Al is the owner of all 3 folders and has a role called "owner". This leads to the following Roles table:
|
70
|
+
|
71
|
+
folder_name user_name role
|
72
|
+
Music Al owner
|
73
|
+
Bob admin
|
74
|
+
Pictures Al owner
|
75
|
+
Bob admin
|
76
|
+
Casper admin
|
77
|
+
News Al owner
|
78
|
+
|
79
|
+
Now if we would allow Bob to also access the News folder but only read from it, we could add the role called "reader" to the table:
|
80
|
+
|
81
|
+
folder_name user_name role
|
82
|
+
News Bob reader
|
83
|
+
|
84
|
+
This is exactly what Authorizer does for your Rails application.
|
85
|
+
email:
|
86
|
+
- commanderjohnson@gmail.com
|
87
|
+
executables: []
|
88
|
+
|
89
|
+
extensions: []
|
90
|
+
|
91
|
+
extra_rdoc_files:
|
92
|
+
- History.txt
|
93
|
+
- Manifest.txt
|
94
|
+
- PostInstall.txt
|
95
|
+
files:
|
96
|
+
- History.txt
|
97
|
+
- Manifest.txt
|
98
|
+
- PostInstall.txt
|
99
|
+
- README.rdoc
|
100
|
+
- Rakefile
|
101
|
+
- lib/authorizer.rb
|
102
|
+
- script/console
|
103
|
+
- script/destroy
|
104
|
+
- script/generate
|
105
|
+
- lib/authorizer/admin.rb
|
106
|
+
- lib/authorizer/application_controller.rb
|
107
|
+
- lib/authorizer/base.rb
|
108
|
+
- lib/authorizer/exceptions.rb
|
109
|
+
- lib/authorizer/object_observer.rb
|
110
|
+
- lib/authorizer/user_observer.rb
|
111
|
+
- app/models/object_role.rb
|
112
|
+
- rails/init.rb
|
113
|
+
has_rdoc: true
|
114
|
+
homepage: https://github.com/cmdjohnson/authorizer
|
115
|
+
licenses: []
|
116
|
+
|
117
|
+
post_install_message: PostInstall.txt
|
118
|
+
rdoc_options:
|
119
|
+
- --main
|
120
|
+
- README.rdoc
|
121
|
+
require_paths:
|
122
|
+
- lib
|
123
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
124
|
+
none: false
|
125
|
+
requirements:
|
126
|
+
- - ">="
|
127
|
+
- !ruby/object:Gem::Version
|
128
|
+
hash: 3
|
129
|
+
segments:
|
130
|
+
- 0
|
131
|
+
version: "0"
|
132
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
133
|
+
none: false
|
134
|
+
requirements:
|
135
|
+
- - ">="
|
136
|
+
- !ruby/object:Gem::Version
|
137
|
+
hash: 3
|
138
|
+
segments:
|
139
|
+
- 0
|
140
|
+
version: "0"
|
141
|
+
requirements: []
|
142
|
+
|
143
|
+
rubyforge_project: authorizer
|
144
|
+
rubygems_version: 1.3.7
|
145
|
+
signing_key:
|
146
|
+
specification_version: 3
|
147
|
+
summary: Authorizer is a gem for Ruby (in conjunction with Rails 2.3) that does authorization for you on a per-object basis
|
148
|
+
test_files: []
|
149
|
+
|