Skip to content

Instantly share code, notes, and snippets.

@runeb
Last active May 19, 2016 12:09
Show Gist options
  • Save runeb/440d3eb244ad8c33d67a to your computer and use it in GitHub Desktop.
Save runeb/440d3eb244ad8c33d67a to your computer and use it in GitHub Desktop.
Action rules enforcement
# 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