allowy 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,5 @@
1
+ *.gem
2
+ .bundle
3
+ Gemfile.lock
4
+ pkg/*
5
+ *.swp
data/.rspec ADDED
@@ -0,0 +1,2 @@
1
+ --color
2
+ --tag @focus
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source "http://rubygems.org"
2
+
3
+ # Specify your gem's dependencies in allowy.gemspec
4
+ gemspec
@@ -0,0 +1,13 @@
1
+ # A sample Guardfile
2
+ # More info at https://github.com/guard/guard#readme
3
+
4
+ guard 'rspec', :version => 2 do
5
+ watch(%r{^spec/.+_spec\.rb$})
6
+ watch(%r{^lib/(.+)\.rb$}) { |m| "spec/#{m[1]}_spec.rb" }
7
+ watch('spec/spec_helper.rb') { "spec" }
8
+
9
+ watch(%r{^spec/support/(.+)\.rb$}) { "spec" }
10
+
11
+ callback([:start_begin, :reload_begin, :run_all_begin, :run_on_change_begin]) { system "clear" }
12
+ end
13
+
@@ -0,0 +1,202 @@
1
+ # Allowy - the simple authorization for Ruby (and/or Rails)
2
+
3
+ Allowy is the authorization library that doesn't enforce tight DSL on you.
4
+ It is very simple yet powerful.
5
+
6
+ If you have any questions please contact me [@dnagir](http://www.ApproachE.com).
7
+
8
+ ## Why another one?
9
+
10
+ I've been using really great [cancan](https://github.com/ryanb/cancan) gem by Ryan Bates for a long time.
11
+ It does its job amazingly well.
12
+
13
+ CanCan doesn't work very well for me when Ability definitions grow above 20 lines or so:
14
+
15
+ - it becomes **really** hard to track down why something was (or not) allowed.
16
+ - DSL enforces you to use ActiveRecord-like scopes or block to fall back. Those grow and it gets hard to maintain.
17
+ - The Ability class contains all the definitions for everything. Hard to test, hard to maintain.
18
+ - Implicit permission - CanCan tries to be very smart (and is indeed) using aliases such as `:manage` but it makes even harder to maintain.
19
+ - Implicit permission - you can use any symbol to check permissions. `:love_people` will do, even if you never defined it.
20
+ - A little bit tight to ORM. When using with database such as neo4j, some smalish things don't work. So I prefer to be explicit.
21
+
22
+ So I decided to put up allowy to solve those issue for me.
23
+
24
+ [Allowy](https://github.com/dnagir/allowy) better suites if you want more control over your authorization. It is inspired by CanCan, but was implemented with simplicity and explicitness in mind.
25
+
26
+
27
+ # Install
28
+
29
+ Add it to your Rails application's `Gemfile`:
30
+
31
+ ```ruby
32
+ gem 'alowy'
33
+ ```
34
+
35
+ Then `bundle install`.
36
+
37
+ Or use `allowy` gem any other way you are not in Rails.
38
+
39
+ # Usage
40
+
41
+ I will be assuming a CMS-like system in the examples below.
42
+ The `Page` class may be ActiveRecord, Mongoid or any other model of your choise. Doesn't matter.
43
+
44
+
45
+ ## Minimal setup
46
+
47
+ You define a set of permissions per class.
48
+ If you want to safeguard `Page` class then define `PageAccess` class:
49
+
50
+ ```ruby
51
+ class PageAccess
52
+ include Allowy::AccessControl
53
+
54
+ # This will allow you to ask: can? :view, page
55
+ # The truthy result of this function will grant access, otherwise not.
56
+ def view?(page)
57
+ page and page.published?
58
+ end
59
+
60
+ def edit?(page)
61
+ page and page.wiki?
62
+ end
63
+ end
64
+
65
+ # Then, in rails, you would use it:
66
+ can? :view, page
67
+ cannot? :edit, page
68
+ authorize! :view, page # raises Allowy::AccessDenied if can?(:view, page) return false
69
+ can? :love_people, page # Will fail because `love_people` is not defined on the Access Control class
70
+ ```
71
+
72
+ ## Context
73
+
74
+ You can access current user, request data etc using the `context` method.
75
+ In Rails, the context is set to the current controller, so you have full access to it (not only the current user!).
76
+
77
+
78
+ ```ruby
79
+ class PageAccess
80
+ include Allowy::AccessControl
81
+
82
+ def view?(page)
83
+ context.user_signed_in? and page.published?
84
+ end
85
+ end
86
+ ```
87
+
88
+ If you want to change the context in Rails then just override it on a single controller or globally on the `ApplicationController`:
89
+
90
+ ```ruby
91
+ class DefaultAccess
92
+ include Allowy::AccessControl
93
+ # This will give you methods without the need to go to context object
94
+ delegate :current_user, :to => :context
95
+ delegate :current_company, :to => :context
96
+ end
97
+ ```
98
+
99
+ ## More comprehensive example
100
+
101
+ You probably have multiple classes that you want to protect.
102
+ I recommend creating your own base class to provide common context and maybe some utility methods:
103
+
104
+ ```ruby
105
+ class DefaultAccess
106
+ include Allowy::AccessControl
107
+ end
108
+ ```
109
+
110
+ Then you can create multiple access control classes much easier:
111
+
112
+ ```ruby
113
+ class PageAccess < DefaultAccess
114
+ # can? :view, page
115
+ def view?(page)
116
+ page and page.published?
117
+ end
118
+
119
+ # can? :edit, page
120
+ def edit?(page)
121
+ view?(page) and page.wiki? # Notice how we can reuse other definitions!
122
+ end
123
+
124
+ # can? :create, WikiPage
125
+ def create?(page_class)
126
+ # We can do something with WikiPage here if we need to
127
+ # but can just ignore it and authorize based on current context only
128
+ current_user and current_user.admin?
129
+ end
130
+
131
+ # can? :search, Page, 'Ruby rocks!'
132
+ def search?(clazz, phrase)
133
+ # Apart from context, we can require to pass additional parameters
134
+ create?(Page) and (phrase || '').match /rocks/i
135
+ end
136
+ end
137
+ ```
138
+
139
+
140
+ # Testing with RSpec
141
+
142
+ To test the access control classes you can just instantiate those passing context as a parameter.
143
+ Most of the times you will stub out the context, so more isolated is a piece of cake.
144
+
145
+ You need to `require 'allowy/rspec'` to enable the RSpec matchers and Rails controller extensions.
146
+ It will give you RSpec matcher `be_able_to` and `ignore_authorization!` macro for controller specs.
147
+
148
+
149
+ ```ruby
150
+ # spec/models/page_access.rb
151
+ # Example spec for the PageAccess
152
+ describe PageAccess do
153
+ subject { PageAccess.new double(current_user: User.new.or_whatever) }
154
+ let(:page) { Page.new }
155
+
156
+ describe "#view" do
157
+ it { should_not be_able_to :view, page }
158
+
159
+ context "when published" do
160
+ before { page.publish! }
161
+ it { should be_able_to :view, page }
162
+ end
163
+ end
164
+
165
+ # and so on
166
+ end
167
+
168
+
169
+ # Example of a controller specs
170
+ describe PagesController do
171
+ # This will always grant access, so you don't have to create too many objects
172
+ # But make sure you test PageAccess separately as in the example above
173
+ ignore_authorization!
174
+
175
+ it "will always allow no matter what" do
176
+ post(:create).should be_success
177
+ end
178
+ end
179
+ ```
180
+
181
+ # Development
182
+
183
+
184
+ - Source hosted at [GitHub](https://github.com/dnagir/allowy)
185
+ - Report issues and feature requests to [GitHub Issues](https://github.com/dnagir/allowy/issues)
186
+ - Ping me on Twitter [@dnagir](https://twitter.com/#!/dnagir)
187
+
188
+
189
+ To start contributing (assuming you already cloned the repo in cd-d into it):
190
+
191
+ ```bash
192
+ bundle install
193
+ # Now run the Ruby specs
194
+ bundle exec rspec spec/
195
+ ```
196
+
197
+
198
+ Pull requests are very welcome, but please include the specs.
199
+
200
+ # License
201
+
202
+ [MIT] (http://www.opensource.org/licenses/mit-license.php)
@@ -0,0 +1 @@
1
+ require "bundler/gem_tasks"
@@ -0,0 +1,27 @@
1
+ # -*- encoding: utf-8 -*-
2
+ $:.push File.expand_path("../lib", __FILE__)
3
+ require "allowy/version"
4
+
5
+ Gem::Specification.new do |s|
6
+ s.name = "allowy"
7
+ s.version = Allowy::VERSION
8
+ s.authors = ["Dmytrii Nagirniak"]
9
+ s.email = ["dnagir@gmail.com"]
10
+ s.homepage = ""
11
+ s.summary = %q{Authorization with simplicity and explicitness in mind}
12
+ s.description = %q{Allowy provides CanCan-like way of checking permission but doesn't enforce a tight DSL giving you more control}
13
+
14
+ s.rubyforge_project = "allowy"
15
+
16
+ s.files = `git ls-files`.split("\n")
17
+ s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
18
+ s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
19
+ s.require_paths = ["lib"]
20
+
21
+ s.add_runtime_dependency "activesupport"
22
+
23
+ s.add_development_dependency "rspec"
24
+ s.add_development_dependency "pry"
25
+ s.add_development_dependency "guard"
26
+ s.add_development_dependency "guard-rspec"
27
+ end
@@ -0,0 +1,20 @@
1
+ require 'active_support/concern'
2
+ require "allowy/version"
3
+ require "allowy/access_control"
4
+ require "allowy/registry"
5
+ require "allowy/controller_extensions"
6
+
7
+ module Allowy
8
+ class UndefinedAccessControl < StandardError; end
9
+ class UndefinedAction < StandardError; end
10
+
11
+ class AccessDenied < StandardError
12
+ attr_reader :action, :subject
13
+
14
+ def initialize(message, action, subject)
15
+ @message = message
16
+ @action = action
17
+ @subject = subject
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,29 @@
1
+ module Allowy
2
+ module AccessControl
3
+ extend ActiveSupport::Concern
4
+ included do
5
+ attr_reader :context
6
+ end
7
+
8
+ module InstanceMethods
9
+ def initialize(ctx)
10
+ @context = ctx
11
+ end
12
+
13
+ def can?(action, *args)
14
+ m = "#{action}?"
15
+ raise UndefinedAction.new("The #{self.class.name} needs to have #{m} method. Please define it.") unless self.respond_to? m
16
+ send(m, *args)
17
+ end
18
+
19
+ def cannot?(*args)
20
+ not can?(*args)
21
+ end
22
+
23
+ def authorize!(*args)
24
+ raise AccessDenied.new("Not authorized", args.first, args[1]) unless can?(*args)
25
+ end
26
+ end
27
+
28
+ end
29
+ end
@@ -0,0 +1,37 @@
1
+ module Allowy
2
+
3
+ module ControllerExtensions
4
+ extend ActiveSupport::Concern
5
+ included do
6
+ helper_method :can?, :cannot?
7
+ end
8
+
9
+ module InstanceMethods
10
+
11
+ def allowy_context
12
+ self
13
+ end
14
+
15
+ def current_allowy
16
+ @current_allowy ||= ::Allowy::Registry.new(allowy_context)
17
+ end
18
+
19
+ def can?(action, subject, *args)
20
+ current_allowy.access_control_for!(subject).can?(action, subject, *args)
21
+ end
22
+
23
+ def cannot?(*args)
24
+ current_allowy.access_control_for!(subject).cannot?(action, subject, *args)
25
+ end
26
+
27
+ def authorize!(action, subject, *args)
28
+ current_allowy.access_control_for!(subject).authorize!(action, subject, *args)
29
+ end
30
+ end
31
+
32
+ end
33
+ end
34
+
35
+ if defined? ActionController
36
+ ActionController::Base.send(:include, Allowy::ControllerExtensions)
37
+ end
@@ -0,0 +1,44 @@
1
+ module Allowy
2
+ module Matchers
3
+
4
+ class AbleToMatcher
5
+ def initialize(action, subject=nil)
6
+ @action, @subject = action, subject
7
+ end
8
+
9
+ def say msg
10
+ "#{msg} #{@action} #{@subject.inspect}" + if @context
11
+ ' with ' + @context.inspect
12
+ else
13
+ ''
14
+ end
15
+ end
16
+
17
+ def matches?(access_control)
18
+ @context = access_control.context
19
+ access_control.can?(@action, @subject)
20
+ end
21
+
22
+ def description
23
+ say "be able to"
24
+ end
25
+
26
+ def failure_message
27
+ say "expected to be able to"
28
+ end
29
+
30
+ def negative_failure_message
31
+ say "expected NOT to be able to"
32
+ end
33
+ end
34
+
35
+ ::RSpec::Matchers.define :be_able_to do |*args|
36
+ m = AbleToMatcher.new(*args)
37
+ match {|a| m.matches?(a) }
38
+ failure_message_for_should { m.failure_message }
39
+ failure_message_for_should_not { m.negative_failure_message }
40
+ description { m.description }
41
+ end
42
+
43
+ end
44
+ end
@@ -0,0 +1,33 @@
1
+ module Allowy
2
+ class Registry
3
+ def initialize(ctx)
4
+ @context = ctx
5
+ @registry = {}
6
+ end
7
+
8
+ def access_control_for!(subject)
9
+ ac = access_control_for subject
10
+ raise UndefinedAccessControl.new("Please define Access Control class for #{subject.inspect}") unless ac
11
+ ac
12
+ end
13
+
14
+ def access_control_for(subject)
15
+ return unless subject
16
+ # Try subject as an object
17
+ clazz = class_for "#{subject.class.name}Access"
18
+
19
+ # Try subject as a class
20
+ clazz = class_for "#{subject.name}Access" if !clazz && subject.is_a?(Class)
21
+
22
+ return unless clazz # No luck this time
23
+ # create a new instance or return existing
24
+ @registry[clazz] ||= clazz.new(@context)
25
+ end
26
+
27
+ def class_for(name)
28
+ # TODO: Namespace it
29
+ return ::Object.const_get(name) if ::Object.const_defined?(name)
30
+ end
31
+
32
+ end
33
+ end
@@ -0,0 +1,20 @@
1
+ require 'allowy/matchers'
2
+
3
+ module Allowy
4
+
5
+ module ControllerAuthorizationMacros
6
+ def ignore_authorization!
7
+ before(:each) do
8
+ registry = double 'Registry'
9
+ registry.stub(:can? => true, :cannot? => false, :authorize! => nil, access_control_for!: registry)
10
+ @controller.stub(:current_allowy).and_return registry
11
+ end
12
+ end
13
+ end
14
+
15
+ end
16
+
17
+ RSpec.configure do |config|
18
+ config.extend Allowy::ControllerAuthorizationMacros, :type => :controller
19
+ end
20
+
@@ -0,0 +1,3 @@
1
+ module Allowy
2
+ VERSION = "0.1.0"
3
+ end
@@ -0,0 +1,49 @@
1
+ require 'spec_helper'
2
+
3
+ module Allowy
4
+ describe "checking permissions" do
5
+
6
+ let(:access) { SampleAccess.new(123) }
7
+ subject { access }
8
+
9
+ describe "#context as an arbitrary object" do
10
+ subject { access.context }
11
+ its(:to_s) { should == '123' }
12
+ its(:zero?) { should be_false }
13
+ it "should be able to access the context" do
14
+ access.should be_able_to :context_is_123
15
+ end
16
+ end
17
+
18
+ it "should allow" do
19
+ subject.should be_able_to :read, 'allow'
20
+ end
21
+
22
+ it "should deny" do
23
+ subject.should_not be_able_to :read, 'deny'
24
+ end
25
+
26
+ it "should raise if no permission defined" do
27
+ lambda { subject.can? :write, 'allow' }.should raise_error(UndefinedAction) {|err|
28
+ err.message.should include 'write?'
29
+ }
30
+ end
31
+
32
+
33
+ describe "#authorize!" do
34
+ it "shuold raise error" do
35
+ expect { subject.authorize! :read, 'deny' }.to raise_error AccessDenied do |err|
36
+ err.message.should_not be_blank
37
+ err.action.should == :read
38
+ err.subject.should == 'deny'
39
+ end
40
+ end
41
+
42
+ it "should not raise error" do
43
+ expect { subject.authorize! :read, 'allow' }.not_to raise_error AccessDenied
44
+ end
45
+ end
46
+
47
+ end
48
+
49
+ end
@@ -0,0 +1,36 @@
1
+ require 'spec_helper'
2
+
3
+ module Allowy
4
+ describe Registry do
5
+ let(:context) { 123 }
6
+ subject { Registry.new context }
7
+
8
+ describe "#access_control_for!" do
9
+
10
+ it "should find AC by appending Access to the subject" do
11
+ subject.access_control_for!(Sample.new).should be_a SampleAccess
12
+ end
13
+
14
+ it "should find AC when the subject is a class" do
15
+ subject.access_control_for!(Sample).should be_a SampleAccess
16
+ end
17
+
18
+ it "should raise when AC is not found by the subject" do
19
+ lambda { subject.access_control_for!(123) }.should raise_error(UndefinedAccessControl) {|err|
20
+ err.message.should include '123'
21
+ }
22
+ end
23
+
24
+ it "should raise when subject is nil" do
25
+ lambda { subject.access_control_for!(nil) }.should raise_error UndefinedAccessControl
26
+ end
27
+
28
+ it "should return the same AC instance" do
29
+ first = subject.access_control_for!(Sample)
30
+ secnd = subject.access_control_for!(Sample)
31
+ first.should === secnd
32
+ end
33
+
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,24 @@
1
+ require 'pry'
2
+ require 'allowy'
3
+ require 'allowy/matchers'
4
+
5
+ RSpec.configure do |c|
6
+ c.treat_symbols_as_metadata_keys_with_true_values = true
7
+ c.run_all_when_everything_filtered = true
8
+ end
9
+
10
+ class SampleAccess
11
+ include Allowy::AccessControl
12
+
13
+ def read?(str)
14
+ str == 'allow'
15
+ end
16
+
17
+ def context_is_123?(*whatever)
18
+ context === 123
19
+ end
20
+ end
21
+
22
+ class Sample
23
+ attr_accessor :name
24
+ end
metadata ADDED
@@ -0,0 +1,122 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: allowy
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Dmytrii Nagirniak
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2012-01-07 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: activesupport
16
+ requirement: &70137337531820 !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ! '>='
20
+ - !ruby/object:Gem::Version
21
+ version: '0'
22
+ type: :runtime
23
+ prerelease: false
24
+ version_requirements: *70137337531820
25
+ - !ruby/object:Gem::Dependency
26
+ name: rspec
27
+ requirement: &70137337531300 !ruby/object:Gem::Requirement
28
+ none: false
29
+ requirements:
30
+ - - ! '>='
31
+ - !ruby/object:Gem::Version
32
+ version: '0'
33
+ type: :development
34
+ prerelease: false
35
+ version_requirements: *70137337531300
36
+ - !ruby/object:Gem::Dependency
37
+ name: pry
38
+ requirement: &70137337530860 !ruby/object:Gem::Requirement
39
+ none: false
40
+ requirements:
41
+ - - ! '>='
42
+ - !ruby/object:Gem::Version
43
+ version: '0'
44
+ type: :development
45
+ prerelease: false
46
+ version_requirements: *70137337530860
47
+ - !ruby/object:Gem::Dependency
48
+ name: guard
49
+ requirement: &70137337530240 !ruby/object:Gem::Requirement
50
+ none: false
51
+ requirements:
52
+ - - ! '>='
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ type: :development
56
+ prerelease: false
57
+ version_requirements: *70137337530240
58
+ - !ruby/object:Gem::Dependency
59
+ name: guard-rspec
60
+ requirement: &70137337529580 !ruby/object:Gem::Requirement
61
+ none: false
62
+ requirements:
63
+ - - ! '>='
64
+ - !ruby/object:Gem::Version
65
+ version: '0'
66
+ type: :development
67
+ prerelease: false
68
+ version_requirements: *70137337529580
69
+ description: Allowy provides CanCan-like way of checking permission but doesn't enforce
70
+ a tight DSL giving you more control
71
+ email:
72
+ - dnagir@gmail.com
73
+ executables: []
74
+ extensions: []
75
+ extra_rdoc_files: []
76
+ files:
77
+ - .gitignore
78
+ - .rspec
79
+ - Gemfile
80
+ - Guardfile
81
+ - README.md
82
+ - Rakefile
83
+ - allowy.gemspec
84
+ - lib/allowy.rb
85
+ - lib/allowy/access_control.rb
86
+ - lib/allowy/controller_extensions.rb
87
+ - lib/allowy/matchers.rb
88
+ - lib/allowy/registry.rb
89
+ - lib/allowy/rspec.rb
90
+ - lib/allowy/version.rb
91
+ - spec/access_control_spec.rb
92
+ - spec/registry_spec.rb
93
+ - spec/spec_helper.rb
94
+ homepage: ''
95
+ licenses: []
96
+ post_install_message:
97
+ rdoc_options: []
98
+ require_paths:
99
+ - lib
100
+ required_ruby_version: !ruby/object:Gem::Requirement
101
+ none: false
102
+ requirements:
103
+ - - ! '>='
104
+ - !ruby/object:Gem::Version
105
+ version: '0'
106
+ required_rubygems_version: !ruby/object:Gem::Requirement
107
+ none: false
108
+ requirements:
109
+ - - ! '>='
110
+ - !ruby/object:Gem::Version
111
+ version: '0'
112
+ requirements: []
113
+ rubyforge_project: allowy
114
+ rubygems_version: 1.8.10
115
+ signing_key:
116
+ specification_version: 3
117
+ summary: Authorization with simplicity and explicitness in mind
118
+ test_files:
119
+ - spec/access_control_spec.rb
120
+ - spec/registry_spec.rb
121
+ - spec/spec_helper.rb
122
+ has_rdoc: