allowy 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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: