Last active
May 19, 2016 12:09
-
-
Save runeb/440d3eb244ad8c33d67a to your computer and use it in GitHub Desktop.
Action rules enforcement
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# Main lib | |
class Object | |
@@_enforced_rules = {} | |
def self.enforce(*actions, &block) | |
@@_enforced_rules[enforcement_klass] ||= {} | |
actions.flatten.each do |action| | |
@@_enforced_rules[enforcement_klass][action] = block | |
end | |
end | |
def self.clear_enforcement!(&block) | |
@@_enforced_rules[enforcement_klass] = nil | |
end | |
def self.without_enforcement(&block) | |
if block_given? | |
Thread.current['_skip_enforcement'] ||= {} | |
Thread.current['_skip_enforcement'][enforcement_klass] = true | |
yield | |
Thread.current['_skip_enforcement'][enforcement_klass] = false | |
end | |
end | |
def allow?(action, user) | |
return true if bypass_enforcement? | |
if class_rules = @@_enforced_rules[enforcement_klass] | |
rule = class_rules[:any] | |
rule = class_rules[action] unless rule | |
return rule.call(self, user) if rule | |
end | |
true | |
end | |
private | |
def bypass_enforcement? | |
Thread.current['_skip_enforcement'] ||= {} | |
Thread.current['_skip_enforcement'][enforcement_klass] | |
end | |
def enforcement_klass | |
klass = self | |
klass = self.class unless klass.is_a?(Class) | |
klass | |
end | |
end | |
# Sinatra helpers | |
require 'sinatra/base' | |
module Sinatra | |
module AllowHelpers | |
def authorize!(object, action, user = allow_user) | |
halt 403 unless object.allow?(action, user) | |
end | |
def allow?(object, action, user = allow_user) | |
object.allow?(action, user) | |
end | |
def allow_user | |
@current_user | |
end | |
end | |
helpers AllowHelpers | |
end | |
# Start tests | |
if __FILE__ == $0 | |
class Post;end | |
require 'minitest/autorun' | |
class TestAllow < Minitest::Spec | |
before do | |
Post.enforce :destroy do |obj_or_class, user| | |
user == 'owner' | |
end | |
end | |
after do | |
Post.clear_enforcement! | |
end | |
it 'returns the class in the allow block when asked to a Class' do | |
Post.enforce :show do |obj_or_class, user| | |
assert_equal Post, obj_or_class | |
true | |
end | |
assert Post.allow?(:show, 'owner') | |
end | |
it 'returns the instance in the allow block when asked to an instance' do | |
post = Post.new | |
Post.enforce :show do |obj_or_class, user| | |
assert_equal post, obj_or_class | |
true | |
end | |
assert post.allow?(:show, 'owner') | |
end | |
it 'enforces rules directly on the object' do | |
post = Post.new | |
assert post.allow?(:destroy, 'owner') | |
refute post.allow?(:destroy, 'reader') | |
end | |
it 'enforces rules on the class level' do | |
assert Post.allow?(:destroy, 'owner') | |
refute Post.allow?(:destroy, 'reader') | |
end | |
it 'falls back to allowing unknown actions' do | |
assert Post.allow?(:juggle, 'someone') | |
end | |
it 'can set rules for multiple actions at the same time' do | |
Post.enforce :show, :update do |obj_or_class, user| | |
user == 'admin' | |
end | |
refute Post.allow?(:show, 'user') | |
refute Post.allow?(:update, 'user') | |
assert Post.allow?(:show, 'admin') | |
assert Post.allow?(:update, 'admin') | |
end | |
it 'can set a rule for any action, that overrides other rules' do | |
assert Post.allow?(:destroy, 'owner') | |
Post.enforce :any do |obj_or_class, user| | |
false | |
end | |
refute Post.allow?(:destroy, 'owner') | |
refute Post.allow?(:random, 'owner') | |
end | |
it 'works across threads' do | |
refute Post.allow?(:destroy, 'user') | |
Thread.new { refute Post.allow?(:destroy, 'user') }.join | |
end | |
it 'can clear rules permanently' do | |
refute Post.allow?(:destroy, 'user') | |
Post.clear_enforcement! | |
assert Post.allow?(:destroy, 'user') | |
end | |
it 'can clear rules temporarely with a block' do | |
refute Post.allow?(:destroy, 'user') | |
Post.without_enforcement do | |
assert Post.allow?(:destroy, 'user') | |
end | |
refute Post.allow?(:destroy, 'user') | |
end | |
it 'clearing rules temporarely is thread safe' do | |
refute Post.allow?(:destroy, 'user') | |
Post.without_enforcement do | |
assert Post.allow?(:destroy, 'user') | |
Thread.new { refute Post.allow?(:destroy, 'user') }.join | |
end | |
end | |
it 'does not bleed down to subclasses' do | |
class SubPost < Post;end | |
refute Post.allow?(:destroy, 'anyone') | |
assert SubPost.allow?(:destroy, 'anyone') | |
end | |
it 'does not bleed into random classes' do | |
String.enforce(:destroy) do |obj_or_class, user| | |
user == 'root' | |
end | |
assert 123.allow?(:destroy, 'anyone') | |
end | |
end | |
# Test sinatra helpers | |
ENV['RACK_ENV'] = 'test' | |
require 'sinatra/base' | |
require 'rack/test' | |
class TestApp < Sinatra::Base | |
helpers Sinatra::AllowHelpers | |
get '/unallow' do | |
@current_user = :user | |
post = Post.new | |
authorize! post, :show | |
'not ok' | |
end | |
get '/allow' do | |
@current_user = :admin | |
post = Post.new | |
authorize! post, :show | |
'ok' | |
end | |
end | |
class Post;end | |
class SinatraAllowTest < Minitest::Spec | |
include Rack::Test::Methods | |
before do | |
Post.enforce :show do |obj_or_class, user| | |
user == :admin | |
end | |
end | |
def app | |
TestApp | |
end | |
describe 'Sinatra helpers' do | |
it 'returns 403 Forbidden when not allowed' do | |
get '/unallow' | |
assert_equal 403, last_response.status | |
end | |
it 'returns 200 when allowed' do | |
get '/allow' | |
assert_equal 200, last_response.status | |
end | |
end | |
end | |
end |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment