protector 0.0.2 → 0.0.4

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 098c77da509ed0a347c0a22df9dd6c6a8d34289a
4
- data.tar.gz: b9ae1d35809445399fda713c3f0276ac91114659
3
+ metadata.gz: 5196d6460c7c9550f4be2cef9135916d900dcdc0
4
+ data.tar.gz: 08886a665e9f57db43dd49d064c23b569e01106b
5
5
  SHA512:
6
- metadata.gz: d0cc601d414b036f8eb6db9381c9f30a93bc1bdb5bd92a946165f32ea97f9229be9fa76210a7b98ec878da9b0476f6dacffb6bf7e24200c496bf8e6106a6b4bf
7
- data.tar.gz: 9994bc1d0e0a3a93f81f4ee07fabf7cf1faf7c02c958952ec8cd80a6452349c20cd8fc7a79bf51665da2cbccdef179ce85963cc4ab0528b039b6b21a735b85bc
6
+ metadata.gz: 87cd3b93a9b8b3ca0b86d034234385111cd8aa9c793e6440908fc6362a6160e21a7d6010b6c89018a4245a44e1cb043c0086a023cf88ce50e501be7a21cece95
7
+ data.tar.gz: 2fffcf64bc2669bfb3292e432e52408d1a9207263085a6cac74dde4200882dc645523c93320449ffe9320f76aa94389caf3ce2c7c993ddfc56333e2426d1d5c8
data/.travis.yml ADDED
@@ -0,0 +1,6 @@
1
+ before_install:
2
+ - gem install bundler
3
+ rvm:
4
+ - 1.9.3
5
+ - jruby-19mode
6
+ - 2.0.0
data/Appraisals ADDED
@@ -0,0 +1,9 @@
1
+ appraise "AR_3.2" do
2
+ gem "activerecord", "3.2", require: "active_record"
3
+ gem "activerecord-jdbcsqlite3-adapter", platform: :jruby, github: "jruby/activerecord-jdbc-adapter"
4
+ end
5
+
6
+ appraise "AR_4" do
7
+ gem "activerecord", "4.0.0.rc1", require: "active_record"
8
+ gem "activerecord-jdbcsqlite3-adapter", platform: :jruby, github: "jruby/activerecord-jdbc-adapter"
9
+ end
data/Gemfile CHANGED
@@ -7,4 +7,7 @@ gem 'rspec'
7
7
  gem 'guard'
8
8
  gem 'guard-rspec'
9
9
 
10
- gem 'activerecord', '>= 3.0'
10
+ gem 'appraisal'
11
+
12
+ gem 'sqlite3', platform: :ruby
13
+ gem 'jdbc-sqlite3', platform: :jruby
data/README.md CHANGED
@@ -1,6 +1,125 @@
1
1
  # Protector
2
2
 
3
- TODO: Write a gem description
3
+ [![Build Status](https://travis-ci.org/inossidabile/protector.png?branch=master)](https://travis-ci.org/inossidabile/protector)
4
+ [![Code Climate](https://codeclimate.com/github/inossidabile/protector.png)](https://codeclimate.com/github/inossidabile/protector)
5
+
6
+ Protector is a Ruby ORM extension for managing security restrictions on a field level. The gem favors white-listing over black-listing (everything is disallowed by default), convention over configuration and is duck-type compatible with most of existing code.
7
+
8
+ Currently Protector supports the following ORM adapters:
9
+
10
+ * [ActiveRecord](http://guides.rubyonrails.org/active_record_querying.html) (>= 3.2)
11
+
12
+ We are working hard to extend the list with:
13
+
14
+ * [Sequel](http://sequel.rubyforge.org/)
15
+ * [DataMapper](http://datamapper.org/)
16
+ * [Mongoid](http://mongoid.org/en/mongoid/index.html)
17
+
18
+ ## Basics
19
+
20
+ DSL of Protector is a Ruby block (or several) describing ACL separated into contexts (authorized user is a very typical example of a context). Each time the context of model changes, DSL blocks reevaluate internally to get an actual ACL that is then utilized internally to cut restricted actions.
21
+
22
+ Protector follows nondestructive blocking strategy. It returns `nil` when the forbidden field is requested and only checks creation (modification) capability during persisting. Even more: the latter is implemented as a model validation so it will seamlessly integrate into your typical workflow.
23
+
24
+ This example is based on ActiveRecord but the code is mostly identical for any supported adapter.
25
+
26
+ ```ruby
27
+ class Article < ActiveRecord::Base # Fields: title, text, user_id, hidden
28
+ protect do |user| # `user` is a context of security
29
+
30
+ unless user.admin?
31
+ scope { where(hidden: false) } # Non-admins can only read insecure data
32
+
33
+ can :view # Allow to read any field
34
+ if user.nil? # User is unknown and therefore not authenticated
35
+ cannot :view, :text # Guests can't read the text
36
+ end
37
+
38
+ can :create, %w(title text) # Non-admins can't set `hidden` flag
39
+ can :create, user_id: lamda{|x| # ... and should correctly fill
40
+ x == user.id # ... the `user_id` association
41
+ }
42
+
43
+ # In this setup non-admins can not destroy or update existing records.
44
+ else
45
+ scope { all } # Admins can retrieve anything
46
+
47
+ can :view # ... and view anything
48
+ can :create # ... and create anything
49
+ can :update # ... and update anything
50
+ can :destroy # ... and they can delete
51
+ end
52
+ end
53
+ end
54
+ ```
55
+
56
+ Now that we have ACL described we can enable it as easy as:
57
+
58
+ ```ruby
59
+ article.restrict!(current_user) # Assuming article is an instance of Article
60
+ ```
61
+
62
+ To make model unsafe again call:
63
+
64
+ ```ruby
65
+ article.unrestrict!
66
+ ```
67
+
68
+ **Both methods are chainable!**
69
+
70
+ ## Scopes
71
+
72
+ Besides the `can` and `cannot` directives Protector also handles relations visibility. In the previous sample the following block is responsible to make hidden articles actually hide:
73
+
74
+ ```ruby
75
+ scope { where(hidden: false) } # Non-admins can only read unsecure data
76
+ ````
77
+
78
+ Make sure to write the block content of the `scope` directive in the notation of your ORM library.
79
+
80
+ To finally utilize this function use the same `restrict!` method on a level of Class or Relation. Like this:
81
+
82
+ ```ruby
83
+ Article.restrict!(current_user).where(...)
84
+ # OR
85
+ Article.where(...).restrict!(current_user)
86
+ ```
87
+
88
+ Note that you don't need to explicitly restrict models you get from a restricted scope – they born restricted.
89
+
90
+ ## Self-aware conditions
91
+
92
+ Sometimes an access decision depends on the object we restrict. `protect` block accepts second argument to fulfill these cases. Keep in mind however that it's not always accessible: we don't have any instance for the restriction of relation and therefore `nil` is passed.
93
+
94
+ The following example extends Article to allow users edit their own posts:
95
+
96
+ ```ruby
97
+ class Article < ActiveRecord::Base # Fields: title, text, user_id, hidden
98
+ protect do |user, article|
99
+ if user
100
+ if article.try(:user_id) == user.id # Checks belonging keeping possible nil in mind
101
+ can :update, %w(title text) # Allow authors to modify posts
102
+ end
103
+ end
104
+ end
105
+ end
106
+ ```
107
+
108
+ ## Associations
109
+
110
+ Protector is aware of associations. All the associations retrieved from restricted instance will automatically be restricted to the same context. Therefore you don't have to do anything special – it will respect proper scopes out of the box.
111
+
112
+ The access to `belongs_to` kind of association depends on corresponding foreign key readability.
113
+
114
+ ## Ideology
115
+
116
+ Protector is a successor to [Heimdallr](https://github.com/inossidabile/heimdallr). The latter being a proof-of-concept appeared to be way too paranoid and incompatible with the rest of the world. Protector re-implements same idea keeping the Ruby way:
117
+
118
+ * it works inside of the model instead of wrapping it into a proxy: that's why it's compatible with every other extension you use
119
+ * it secures persistence and not object properties: you can modify any properties you want but it's not going to let you save what you can not save
120
+ * it respects the differentiation between business-logic layer and SQL layer: protection is validation so any method that skips validation will also avoid the security check
121
+
122
+ **The last thing is really important to understand. No matter if you can read a field or not, methods like `.pluck` are still capable of reading any of your fields and if you tell your model to skip validation it will also skip an ACL check.**
4
123
 
5
124
  ## Installation
6
125
 
@@ -16,9 +135,13 @@ Or install it yourself as:
16
135
 
17
136
  $ gem install protector
18
137
 
19
- ## Usage
138
+ As long as you load Protector after an ORM library it is supposed to activate itself automatically. Otherwise you can enable required adapter manually:
139
+
140
+ ```ruby
141
+ Protector::Adapters::ActiveRecord.activate!
142
+ ```
20
143
 
21
- TODO: Write usage instructions here
144
+ Where "ActiveRecord" is the adapter you are about to use. It can be "Sequel", "DataMapper", "Mongoid".
22
145
 
23
146
  ## Contributing
24
147
 
data/Rakefile CHANGED
@@ -1,8 +1,13 @@
1
1
  require 'bundler/setup'
2
2
  require 'bundler/gem_tasks'
3
3
  require 'rspec/core/rake_task'
4
+ require 'appraisal'
4
5
 
5
6
  RSpec::Core::RakeTask.new(:spec)
6
7
 
7
- desc "Default: run the unit tests."
8
- task :default => :spec
8
+ task :default => :all
9
+
10
+ desc 'Test the plugin under all supported Rails versions.'
11
+ task :all => ["appraisal:install"] do |t|
12
+ exec('rake appraisal spec')
13
+ end
@@ -0,0 +1,16 @@
1
+ # This file was generated by Appraisal
2
+
3
+ source "https://rubygems.org"
4
+
5
+ gem "rake"
6
+ gem "pry"
7
+ gem "rspec"
8
+ gem "guard"
9
+ gem "guard-rspec"
10
+ gem "appraisal"
11
+ gem "sqlite3", :platform=>:ruby
12
+ gem "jdbc-sqlite3", :platform=>:jruby
13
+ gem "activerecord", "3.2", :require=>"active_record"
14
+ gem "activerecord-jdbcsqlite3-adapter", :platform=>:jruby, :github=>"jruby/activerecord-jdbc-adapter"
15
+
16
+ gemspec :path=>"../"
@@ -0,0 +1,104 @@
1
+ GIT
2
+ remote: git://github.com/jruby/activerecord-jdbc-adapter.git
3
+ revision: b1cb2cb59496a7c3ae22799f88c8c4789e6a8cce
4
+ specs:
5
+ activerecord-jdbcsqlite3-adapter (1.3.0.DEV)
6
+ activerecord-jdbc-adapter (~> 1.3.0.DEV)
7
+ jdbc-sqlite3 (~> 3.7.2)
8
+
9
+ PATH
10
+ remote: /Users/inossidabile/Repos/protector
11
+ specs:
12
+ protector (0.0.3)
13
+ activesupport
14
+ i18n
15
+
16
+ GEM
17
+ remote: https://rubygems.org/
18
+ specs:
19
+ activemodel (3.2.0)
20
+ activesupport (= 3.2.0)
21
+ builder (~> 3.0.0)
22
+ activerecord (3.2.0)
23
+ activemodel (= 3.2.0)
24
+ activesupport (= 3.2.0)
25
+ arel (~> 3.0.0)
26
+ tzinfo (~> 0.3.29)
27
+ activerecord-jdbc-adapter (1.3.0.beta1)
28
+ activesupport (3.2.0)
29
+ i18n (~> 0.6)
30
+ multi_json (~> 1.0)
31
+ appraisal (0.5.2)
32
+ bundler
33
+ rake
34
+ arel (3.0.2)
35
+ builder (3.0.4)
36
+ coderay (1.0.9)
37
+ diff-lcs (1.2.4)
38
+ ffi (1.8.1)
39
+ ffi (1.8.1-java)
40
+ formatador (0.2.4)
41
+ guard (1.8.0)
42
+ formatador (>= 0.2.4)
43
+ listen (>= 1.0.0)
44
+ lumberjack (>= 1.0.2)
45
+ pry (>= 0.9.10)
46
+ thor (>= 0.14.6)
47
+ guard-rspec (3.0.0)
48
+ guard (>= 1.8)
49
+ rspec (~> 2.13)
50
+ i18n (0.6.4)
51
+ jdbc-sqlite3 (3.7.2)
52
+ listen (1.1.3)
53
+ rb-fsevent (>= 0.9.3)
54
+ rb-inotify (>= 0.9)
55
+ rb-kqueue (>= 0.2)
56
+ lumberjack (1.0.3)
57
+ method_source (0.8.1)
58
+ multi_json (1.7.3)
59
+ pry (0.9.12.2)
60
+ coderay (~> 1.0.5)
61
+ method_source (~> 0.8)
62
+ slop (~> 3.4)
63
+ pry (0.9.12.2-java)
64
+ coderay (~> 1.0.5)
65
+ method_source (~> 0.8)
66
+ slop (~> 3.4)
67
+ spoon (~> 0.0)
68
+ rake (10.0.4)
69
+ rb-fsevent (0.9.3)
70
+ rb-inotify (0.9.0)
71
+ ffi (>= 0.5.0)
72
+ rb-kqueue (0.2.0)
73
+ ffi (>= 0.5.0)
74
+ rspec (2.13.0)
75
+ rspec-core (~> 2.13.0)
76
+ rspec-expectations (~> 2.13.0)
77
+ rspec-mocks (~> 2.13.0)
78
+ rspec-core (2.13.1)
79
+ rspec-expectations (2.13.0)
80
+ diff-lcs (>= 1.1.3, < 2.0)
81
+ rspec-mocks (2.13.1)
82
+ slop (3.4.5)
83
+ spoon (0.0.4)
84
+ ffi
85
+ sqlite3 (1.3.7)
86
+ thor (0.18.1)
87
+ tzinfo (0.3.37)
88
+
89
+ PLATFORMS
90
+ java
91
+ ruby
92
+
93
+ DEPENDENCIES
94
+ activerecord (= 3.2)
95
+ activerecord-jdbcsqlite3-adapter!
96
+ appraisal
97
+ guard
98
+ guard-rspec
99
+ jdbc-sqlite3
100
+ protector!
101
+ pry
102
+ rake
103
+ rspec
104
+ sqlite3
@@ -0,0 +1,16 @@
1
+ # This file was generated by Appraisal
2
+
3
+ source "https://rubygems.org"
4
+
5
+ gem "rake"
6
+ gem "pry"
7
+ gem "rspec"
8
+ gem "guard"
9
+ gem "guard-rspec"
10
+ gem "appraisal"
11
+ gem "sqlite3", :platform=>:ruby
12
+ gem "jdbc-sqlite3", :platform=>:jruby
13
+ gem "activerecord", "4.0.0.rc1", :require=>"active_record"
14
+ gem "activerecord-jdbcsqlite3-adapter", :platform=>:jruby, :github=>"jruby/activerecord-jdbc-adapter"
15
+
16
+ gemspec :path=>"../"
@@ -0,0 +1,113 @@
1
+ GIT
2
+ remote: git://github.com/jruby/activerecord-jdbc-adapter.git
3
+ revision: b1cb2cb59496a7c3ae22799f88c8c4789e6a8cce
4
+ specs:
5
+ activerecord-jdbcsqlite3-adapter (1.3.0.DEV)
6
+ activerecord-jdbc-adapter (~> 1.3.0.DEV)
7
+ jdbc-sqlite3 (~> 3.7.2)
8
+
9
+ PATH
10
+ remote: /Users/inossidabile/Repos/protector
11
+ specs:
12
+ protector (0.0.3)
13
+ activesupport
14
+ i18n
15
+
16
+ GEM
17
+ remote: https://rubygems.org/
18
+ specs:
19
+ activemodel (4.0.0.rc1)
20
+ activesupport (= 4.0.0.rc1)
21
+ builder (~> 3.1.0)
22
+ activerecord (4.0.0.rc1)
23
+ activemodel (= 4.0.0.rc1)
24
+ activerecord-deprecated_finders (~> 1.0.2)
25
+ activesupport (= 4.0.0.rc1)
26
+ arel (~> 4.0.0)
27
+ activerecord-deprecated_finders (1.0.2)
28
+ activerecord-jdbc-adapter (1.3.0.beta1)
29
+ activesupport (4.0.0.rc1)
30
+ i18n (~> 0.6, >= 0.6.4)
31
+ minitest (~> 4.2)
32
+ multi_json (~> 1.3)
33
+ thread_safe (~> 0.1)
34
+ tzinfo (~> 0.3.37)
35
+ appraisal (0.5.2)
36
+ bundler
37
+ rake
38
+ arel (4.0.0)
39
+ atomic (1.1.9)
40
+ atomic (1.1.9-java)
41
+ builder (3.1.4)
42
+ coderay (1.0.9)
43
+ diff-lcs (1.2.4)
44
+ ffi (1.8.1)
45
+ ffi (1.8.1-java)
46
+ formatador (0.2.4)
47
+ guard (1.8.0)
48
+ formatador (>= 0.2.4)
49
+ listen (>= 1.0.0)
50
+ lumberjack (>= 1.0.2)
51
+ pry (>= 0.9.10)
52
+ thor (>= 0.14.6)
53
+ guard-rspec (3.0.0)
54
+ guard (>= 1.8)
55
+ rspec (~> 2.13)
56
+ i18n (0.6.4)
57
+ jdbc-sqlite3 (3.7.2)
58
+ listen (1.1.3)
59
+ rb-fsevent (>= 0.9.3)
60
+ rb-inotify (>= 0.9)
61
+ rb-kqueue (>= 0.2)
62
+ lumberjack (1.0.3)
63
+ method_source (0.8.1)
64
+ minitest (4.7.4)
65
+ multi_json (1.7.3)
66
+ pry (0.9.12.2)
67
+ coderay (~> 1.0.5)
68
+ method_source (~> 0.8)
69
+ slop (~> 3.4)
70
+ pry (0.9.12.2-java)
71
+ coderay (~> 1.0.5)
72
+ method_source (~> 0.8)
73
+ slop (~> 3.4)
74
+ spoon (~> 0.0)
75
+ rake (10.0.4)
76
+ rb-fsevent (0.9.3)
77
+ rb-inotify (0.9.0)
78
+ ffi (>= 0.5.0)
79
+ rb-kqueue (0.2.0)
80
+ ffi (>= 0.5.0)
81
+ rspec (2.13.0)
82
+ rspec-core (~> 2.13.0)
83
+ rspec-expectations (~> 2.13.0)
84
+ rspec-mocks (~> 2.13.0)
85
+ rspec-core (2.13.1)
86
+ rspec-expectations (2.13.0)
87
+ diff-lcs (>= 1.1.3, < 2.0)
88
+ rspec-mocks (2.13.1)
89
+ slop (3.4.5)
90
+ spoon (0.0.4)
91
+ ffi
92
+ sqlite3 (1.3.7)
93
+ thor (0.18.1)
94
+ thread_safe (0.1.0)
95
+ atomic
96
+ tzinfo (0.3.37)
97
+
98
+ PLATFORMS
99
+ java
100
+ ruby
101
+
102
+ DEPENDENCIES
103
+ activerecord (= 4.0.0.rc1)
104
+ activerecord-jdbcsqlite3-adapter!
105
+ appraisal
106
+ guard
107
+ guard-rspec
108
+ jdbc-sqlite3
109
+ protector!
110
+ pry
111
+ rake
112
+ rspec
113
+ sqlite3
@@ -0,0 +1,29 @@
1
+ module Protector
2
+ module Adapters
3
+ module ActiveRecord
4
+ module Association
5
+ extend ActiveSupport::Concern
6
+
7
+ included do
8
+ alias_method_chain :scope, :protector
9
+ end
10
+
11
+ def scope_with_protector(*args)
12
+ scope_without_protector(*args).restrict!(owner.protector_subject)
13
+ end
14
+ end
15
+
16
+ module PreloaderAssociation
17
+ extend ActiveSupport::Concern
18
+
19
+ included do
20
+ alias_method_chain :scope, :protector
21
+ end
22
+
23
+ def scope_with_protector
24
+ scope_without_protector
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,100 @@
1
+ module Protector
2
+ module Adapters
3
+ module ActiveRecord
4
+ module Base
5
+ extend ActiveSupport::Concern
6
+
7
+ included do
8
+ include Protector::DSL::Base
9
+ include Protector::DSL::Entry
10
+
11
+ validate(on: :create) do
12
+ return unless @protector_subject
13
+ errors[:base] << I18n.t('protector.invalid') unless creatable?
14
+ end
15
+
16
+ validate(on: :update) do
17
+ return unless @protector_subject
18
+ errors[:base] << I18n.t('protector.invalid') unless updatable?
19
+ end
20
+
21
+ before_destroy do
22
+ return true unless @protector_subject
23
+ destroyable?
24
+ end
25
+
26
+ if Gem::Version.new(::ActiveRecord::VERSION::STRING) < Gem::Version.new('4.0.0.rc1')
27
+ def self.restrict!(subject)
28
+ scoped.restrict!(subject)
29
+ end
30
+ else
31
+ def self.restrict!(subject)
32
+ all.restrict!(subject)
33
+ end
34
+ end
35
+
36
+ def [](name)
37
+ if !@protector_subject || name == self.class.primary_key || protector_meta.readable?(name)
38
+ read_attribute(name)
39
+ else
40
+ nil
41
+ end
42
+ end
43
+ end
44
+
45
+ module ClassMethods
46
+ def define_method_attribute(name)
47
+ super
48
+
49
+ unless (primary_key == name || (primary_key.is_a?(Array) && primary_key.include?(name)))
50
+ generated_attribute_methods.module_eval <<-STR, __FILE__, __LINE__ + 1
51
+ alias_method #{"#{name}_unprotected".inspect}, #{name.inspect}
52
+
53
+ def #{name}
54
+ if !@protector_subject || protector_meta.readable?(#{name.inspect})
55
+ #{name}_unprotected
56
+ else
57
+ nil
58
+ end
59
+ end
60
+ STR
61
+ end
62
+ end
63
+ end
64
+
65
+ def protector_meta
66
+ unless @protector_subject
67
+ raise "Unprotected entity detected: use `restrict` method to protect it."
68
+ end
69
+
70
+ self.class.protector_meta.evaluate(
71
+ self.class,
72
+ self.class.column_names,
73
+ @protector_subject,
74
+ self
75
+ )
76
+ end
77
+
78
+ def visible?
79
+ protector_meta.relation.where(
80
+ self.class.primary_key => id
81
+ ).any?
82
+ end
83
+
84
+ def creatable?
85
+ fields = HashWithIndifferentAccess[changed.map{|x| [x, read_attribute(x)]}]
86
+ protector_meta.creatable?(fields)
87
+ end
88
+
89
+ def updatable?
90
+ fields = HashWithIndifferentAccess[changed.map{|x| [x, read_attribute(x)]}]
91
+ protector_meta.updatable?(fields)
92
+ end
93
+
94
+ def destroyable?
95
+ protector_meta.destroyable?
96
+ end
97
+ end
98
+ end
99
+ end
100
+ end
@@ -0,0 +1,49 @@
1
+ module Protector
2
+ module Adapters
3
+ module ActiveRecord
4
+ module Relation
5
+ extend ActiveSupport::Concern
6
+
7
+ included do
8
+ include Protector::DSL::Base
9
+
10
+ alias_method_chain :exec_queries, :protector
11
+ end
12
+
13
+ def protector_meta
14
+ @klass.protector_meta.evaluate(@klass, @klass.column_names, @protector_subject)
15
+ end
16
+
17
+ def unscoped
18
+ super.restrict!(@protector_subject)
19
+ end
20
+
21
+ def count(*args)
22
+ super || 0
23
+ end
24
+
25
+ def sum(*args)
26
+ super || 0
27
+ end
28
+
29
+ def calculate(*args)
30
+ return super unless @protector_subject
31
+ merge(protector_meta.relation).unrestrict!.calculate *args
32
+ end
33
+
34
+ def exists?(*args)
35
+ return super unless @protector_subject
36
+ merge(protector_meta.relation).unrestrict!.exists? *args
37
+ end
38
+
39
+ def exec_queries_with_protector(*args)
40
+ return exec_queries_without_protector unless @protector_subject
41
+
42
+ subject = @protector_subject
43
+ relation = merge(protector_meta.relation).unrestrict!
44
+ @records = relation.send(:exec_queries).each{|r| r.restrict!(subject)}
45
+ end
46
+ end
47
+ end
48
+ end
49
+ end