sinatra-bouncer 1.2.0 → 2.0.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
- SHA1:
3
- metadata.gz: 254ec098fccec946d8f3891767aec06c0b176ba1
4
- data.tar.gz: e9289acabf308082675f3176367c3016f3af791b
2
+ SHA256:
3
+ metadata.gz: f07c9c87e55563ac21610aecc8813d32ee3322d291382bb1bd5cca0ce0d55a12
4
+ data.tar.gz: fa55400409722c0f0f4ea83c6ee552654d0d8aee128d484f3a42c2c888e9b46b
5
5
  SHA512:
6
- metadata.gz: b42b9091612bc206cc384f346108eee042de851c8793b09772d8c9fc818df7595c4a9cd2dfe5443b9f61ab045748d49082677e77f0e901abd1c78d4fa8d232a8
7
- data.tar.gz: 34231f5cbb55f638c6d6b977c080dd9a77ed5f4a3e561d90221b85c413e3717330d433eb06c2564d6613e5b325dc92b02e0ad34ace65ebbb5502748e15abac3b
6
+ metadata.gz: 4fc6e73dcb5867a418dea6b555f74fee57f41b58d75ab76657925a71b1b2ff059295135c30646f4de83fe82a00bc816a6d668d54ecd64f035b63f27d68b80ef1
7
+ data.tar.gz: ca808e79a1f59743c17afc6e7475dd03a9a3186d8ed1ec3c6b4412ec96af2c38c0e454a0fada15c513e83c1070eb7b6f72c2a116be9e2ed7135592ce7f04ce70
data/.gitignore ADDED
@@ -0,0 +1,18 @@
1
+ # See http://help.github.com/ignore-files/ for more about ignoring files.
2
+ #
3
+ # If you find yourself ignoring temporary files generated by your text editor
4
+ # or operating system, you probably want to add a global ignore instead:
5
+ # git config --global core.excludesfile ~/.gitignore_global
6
+
7
+ # Ignore all logfiles and tempfiles.
8
+ *.log
9
+ tmp/*
10
+
11
+ # Rubymine stuff
12
+ .idea
13
+
14
+ # Ignore all gem files.
15
+ *.gem
16
+
17
+ # Ignore simplecov results
18
+ .coverage/
data/.rubocop.yml ADDED
@@ -0,0 +1,14 @@
1
+ inherit_from: ~/.config/rubocop/config.yml
2
+
3
+ AllCops:
4
+ Exclude:
5
+ - 'bin/*'
6
+ - 'benchmarks/*'
7
+
8
+ Layout/LineLength:
9
+ Exclude:
10
+ - 'spec/**/*.rb'
11
+
12
+ # setting to 6 to match RubyMine autoformat
13
+ Layout/FirstArrayElementIndentation:
14
+ IndentationWidth: 6
data/.ruby-version ADDED
@@ -0,0 +1 @@
1
+ ruby-3.1.4
data/.simplecov ADDED
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ SimpleCov.start do
4
+ coverage_dir '.coverage'
5
+ enable_coverage :branch
6
+
7
+ root __dir__
8
+
9
+ add_filter 'tests/'
10
+ # add_filter 'spec/'
11
+ end
@@ -0,0 +1,49 @@
1
+ # Contributor Code of Conduct
2
+
3
+ As contributors and maintainers of this project, and in the interest of
4
+ fostering an open and welcoming community, we pledge to respect all people who
5
+ contribute through reporting issues, posting feature requests, updating
6
+ documentation, submitting pull requests or patches, and other activities.
7
+
8
+ We are committed to making participation in this project a harassment-free
9
+ experience for everyone, regardless of level of experience, gender, gender
10
+ identity and expression, sexual orientation, disability, personal appearance,
11
+ body size, race, ethnicity, age, religion, or nationality.
12
+
13
+ Examples of unacceptable behavior by participants include:
14
+
15
+ * The use of sexualized language or imagery
16
+ * Personal attacks
17
+ * Trolling or insulting/derogatory comments
18
+ * Public or private harassment
19
+ * Publishing other's private information, such as physical or electronic
20
+ addresses, without explicit permission
21
+ * Other unethical or unprofessional conduct
22
+
23
+ Project maintainers have the right and responsibility to remove, edit, or
24
+ reject comments, commits, code, wiki edits, issues, and other contributions
25
+ that are not aligned to this Code of Conduct, or to ban temporarily or
26
+ permanently any contributor for other behaviors that they deem inappropriate,
27
+ threatening, offensive, or harmful.
28
+
29
+ By adopting this Code of Conduct, project maintainers commit themselves to
30
+ fairly and consistently applying these principles to every aspect of managing
31
+ this project. Project maintainers who do not follow or enforce the Code of
32
+ Conduct may be permanently removed from the project team.
33
+
34
+ This code of conduct applies both within project spaces and in public spaces
35
+ when an individual is representing the project or its community.
36
+
37
+ Instances of abusive, harassing, or otherwise unacceptable behavior may be
38
+ reported by contacting a project maintainer at robin@tenjin.ca. All
39
+ complaints will be reviewed and investigated and will result in a response that
40
+ is deemed necessary and appropriate to the circumstances. Maintainers are
41
+ obligated to maintain confidentiality with regard to the reporter of an
42
+ incident.
43
+
44
+ This Code of Conduct is adapted from the [Contributor Covenant][homepage],
45
+ version 1.3.0, available at
46
+ [http://contributor-covenant.org/version/1/3/0/][version]
47
+
48
+ [homepage]: http://contributor-covenant.org
49
+ [version]: http://contributor-covenant.org/version/1/3/0/
data/Gemfile CHANGED
@@ -1,4 +1,20 @@
1
+ # frozen_string_literal: true
2
+
1
3
  source 'https://rubygems.org'
2
4
 
3
- # Specify your gem's dependencies in dirt_core.gemspec
4
- gemspec
5
+ # Gem dependencies in dirt_core.gemspec
6
+ gemspec
7
+
8
+ group :development do
9
+ gem 'bundler', '~> 2.4'
10
+ gem 'rake', '~> 13.1'
11
+ gem 'rubocop', '~> 1.57'
12
+ gem 'rubocop-performance', '~> 1.19'
13
+ gem 'yard', '~> 0.9'
14
+ end
15
+
16
+ group :test do
17
+ gem 'rack-test', '~> 2.1'
18
+ gem 'rspec', '~> 3.12'
19
+ gem 'simplecov', '~> 0.22'
20
+ end
data/Gemfile.lock ADDED
@@ -0,0 +1,92 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ sinatra-bouncer (2.0.0)
5
+ sinatra (>= 2.2)
6
+
7
+ GEM
8
+ remote: https://rubygems.org/
9
+ specs:
10
+ ast (2.4.2)
11
+ diff-lcs (1.5.0)
12
+ docile (1.4.0)
13
+ json (2.6.3)
14
+ language_server-protocol (3.17.0.3)
15
+ mustermann (3.0.0)
16
+ ruby2_keywords (~> 0.0.1)
17
+ parallel (1.23.0)
18
+ parser (3.2.2.4)
19
+ ast (~> 2.4.1)
20
+ racc
21
+ racc (1.7.3)
22
+ rack (2.2.8)
23
+ rack-protection (3.1.0)
24
+ rack (~> 2.2, >= 2.2.4)
25
+ rack-test (2.1.0)
26
+ rack (>= 1.3)
27
+ rainbow (3.1.1)
28
+ rake (13.1.0)
29
+ regexp_parser (2.8.2)
30
+ rexml (3.2.6)
31
+ rspec (3.12.0)
32
+ rspec-core (~> 3.12.0)
33
+ rspec-expectations (~> 3.12.0)
34
+ rspec-mocks (~> 3.12.0)
35
+ rspec-core (3.12.2)
36
+ rspec-support (~> 3.12.0)
37
+ rspec-expectations (3.12.3)
38
+ diff-lcs (>= 1.2.0, < 2.0)
39
+ rspec-support (~> 3.12.0)
40
+ rspec-mocks (3.12.6)
41
+ diff-lcs (>= 1.2.0, < 2.0)
42
+ rspec-support (~> 3.12.0)
43
+ rspec-support (3.12.1)
44
+ rubocop (1.57.2)
45
+ json (~> 2.3)
46
+ language_server-protocol (>= 3.17.0)
47
+ parallel (~> 1.10)
48
+ parser (>= 3.2.2.4)
49
+ rainbow (>= 2.2.2, < 4.0)
50
+ regexp_parser (>= 1.8, < 3.0)
51
+ rexml (>= 3.2.5, < 4.0)
52
+ rubocop-ast (>= 1.28.1, < 2.0)
53
+ ruby-progressbar (~> 1.7)
54
+ unicode-display_width (>= 2.4.0, < 3.0)
55
+ rubocop-ast (1.30.0)
56
+ parser (>= 3.2.1.0)
57
+ rubocop-performance (1.19.1)
58
+ rubocop (>= 1.7.0, < 2.0)
59
+ rubocop-ast (>= 0.4.0)
60
+ ruby-progressbar (1.13.0)
61
+ ruby2_keywords (0.0.5)
62
+ simplecov (0.22.0)
63
+ docile (~> 1.1)
64
+ simplecov-html (~> 0.11)
65
+ simplecov_json_formatter (~> 0.1)
66
+ simplecov-html (0.12.3)
67
+ simplecov_json_formatter (0.1.4)
68
+ sinatra (3.1.0)
69
+ mustermann (~> 3.0)
70
+ rack (~> 2.2, >= 2.2.4)
71
+ rack-protection (= 3.1.0)
72
+ tilt (~> 2.0)
73
+ tilt (2.2.0)
74
+ unicode-display_width (2.5.0)
75
+ yard (0.9.34)
76
+
77
+ PLATFORMS
78
+ x86_64-linux
79
+
80
+ DEPENDENCIES
81
+ bundler (~> 2.4)
82
+ rack-test (~> 2.1)
83
+ rake (~> 13.1)
84
+ rspec (~> 3.12)
85
+ rubocop (~> 1.57)
86
+ rubocop-performance (~> 1.19)
87
+ simplecov (~> 0.22)
88
+ sinatra-bouncer!
89
+ yard (~> 0.9)
90
+
91
+ BUNDLED WITH
92
+ 2.4.19
data/README.md CHANGED
@@ -1,108 +1,268 @@
1
- #Sinatra-Bouncer
2
- Simple authorization permissions extension for [Sinatra](http://www.sinatrarb.com/). Require the gem, then declare which routes are permitted based on your own logic.
1
+ # Sinatra-Bouncer
2
+
3
+ Simple permissions extension for [Sinatra](http://www.sinatrarb.com/). Require the gem, then declare which
4
+ routes are permitted based on your own logic.
5
+
6
+ ## Big Picture
7
+
8
+ Bouncer rules look like:
3
9
 
4
- **Gemfile**
5
10
  ```ruby
6
- gem 'sinatra-bouncer'
7
- ```
11
+ rules do
12
+ # Routes match based on one or more strings.
13
+ can get: '/',
14
+ post: %w[/user/sign-in
15
+ /user/sign-out]
16
+
17
+ # Wildcards match anything directly under that path
18
+ can get: '/lib/js/*'
19
+
20
+ # Use a conditional rule block for additional logic
21
+ can_sometimes get: '/admin/*',
22
+ post: '/admin/actions/*' do
23
+ current_user.admin?
24
+ end
25
+
26
+ # ... etc ...
27
+ end
8
28
 
9
- **Terminal**
10
- ```sh
11
- gem install sinatra-bouncer
12
29
  ```
13
30
 
14
- ##Quickstart
15
- ###Step 1: Require/Register Bouncer
31
+ ## Features
16
32
 
17
- After registration, Bouncer will reject any request that either:
18
- * has no rule associated with it, or
19
- * has no associated rule that returns `true`
33
+ Here's what this Gem provides:
34
+
35
+ * **Block-by-default**
36
+ * Any route must be explicitly allowed
37
+ * **Declarative Syntax**
38
+ * Straightforward syntax reduces complexity of layered permissions
39
+ * Keeps permissions together for clarity
40
+ * **Conditional Logic Via Blocks**
41
+ * Often additional checks must be performed to know if a route is allowed.
42
+ * **Grouping**
43
+ * Routes can be matched by wildcard
44
+ * **Forced Boolean Affirmation**
45
+ * Condition blocks must explicitly return `true`, avoiding accidental truthy values
46
+
47
+ ## Anti-Features
48
+
49
+ Bouncer intentionally does not support some concepts.
50
+
51
+ * **No User Identification** *(aka Authentication)*
52
+ * Bouncer is not a user identification gem. It assumes you already have an existing solution
53
+ like [Warden](https://github.com/wardencommunity/warden). Knowing *who* someone is is already a complex enough
54
+ problem and should be separate from what they may do.
55
+ * **No Rails Integration**
56
+ * Bouncer is not intended to work with Rails. Use one of the Rails-based permissions gems for these situations.
57
+ * **No Negation**
58
+ * There is intentionally no negation (eg. `cannot`) to preserve the default-deny frame of mind
59
+
60
+ ## Installation
61
+
62
+ Add this to your gemfile:
20
63
 
21
- **Sinatra Classic**
22
64
  ```ruby
23
- require 'sinatra'
24
- require 'sinatra/bouncer'
65
+ gem 'sinatra-bouncer'
66
+ ```
25
67
 
26
- # ... routes and other config
68
+ Then run:
69
+
70
+ ```shell
71
+ bundle install
27
72
  ```
28
73
 
29
- **Modular**
74
+ **Sinatra Modular Style**
75
+
30
76
  ```ruby
31
77
  require 'sinatra/base'
32
78
  require 'sinatra/bouncer'
33
79
 
34
80
  class MyApp < Sinatra::Base
35
- register Sinatra::Bouncer
36
-
37
- # ... routes and other config
81
+ register Sinatra::Bouncer
82
+
83
+ rules do
84
+ # ... can statements ...
85
+ end
86
+
87
+ # ... routes and other config
38
88
  end
39
89
  ```
40
90
 
41
- ###Step 2: Declare Bouncer Rules
42
- Call `rules` with a block that uses `can` and `can_sometimes` to declare which paths are legalduring this request. The rules block is run in the context of the request, which means you will have access to sinatra helpers,
43
- the `request` object, and `params`.
91
+ **Sinatra Classic Style**
44
92
 
45
93
  ```ruby
46
94
  require 'sinatra'
47
95
  require 'sinatra/bouncer'
48
96
 
49
97
  rules do
50
- # example: allow any GET request
51
- can(:get, :all)
52
-
53
- # example: logged in users can edit their account
54
- if(current_user)
55
- can(:post, '/user_edits_account')
56
- end
98
+ # ... can statements ...
57
99
  end
58
100
 
59
- # ... route declarations as normal below
101
+ # ... routes and other config
60
102
  ```
61
103
 
62
- ## API
63
- #### can
64
- Any route declared with #can will be accepted without further challenge.
104
+ ## Usage
105
+
106
+ Call `rules` with a block that uses the `#can` and `#can_sometimes` DSL methods to declare rules for paths.
107
+
108
+ The rules block is run once as part of the configuration phase but the condition blocks are evaluated in the context of
109
+ the
110
+ request, which means you will have access to Sinatra helpers,
111
+ the `request` object, and `params`.
65
112
 
66
113
  ```ruby
114
+ require 'sinatra'
115
+ require 'sinatra/bouncer'
116
+
67
117
  rules do
68
- can(:post, '/user_posts_blog')
118
+ # example: always allow GET requests to root path or /sign-in
119
+ can get: %w[/
120
+ /sign-in]
121
+
122
+ # example: logged in users can view (GET) member restricted paths and edit their account (POST)
123
+ can_sometimes get: '/members/*',
124
+ post: '/members/edit-account' do
125
+ !current_user.nil?
126
+ end
127
+
128
+ # example: check an arbitrary request header is present
129
+ can_sometimes get: '/bots/*' do
130
+ !request.get_header('X-CUSTOM_PROP').nil?
131
+ end
69
132
  end
133
+
134
+ # ... Sinatra route declarations as normal ...
70
135
  ```
71
136
 
72
- ####can_sometimes
73
- `can_sometimes` is for occasions that you to check further, but want to defer that choice until the path is actually attempted.
74
- `can_sometimes` takes a block that will be run once the path is attempted. This block **must return an explicit boolean**
75
- (ie. `true` or `false`) to avoid any accidental truthy values creating unwanted access.
137
+ ### HTTP Method and Route Matching
138
+
139
+ Both `#can` and `#can_sometimes` accept multiple
140
+ [HTTP methods](https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods) as symbols
141
+ and each key is paired with one or more path strings.
142
+
143
+ ```ruby
144
+ # example: single method, single route
145
+ can get: '/'
146
+
147
+ # example: multiple methods, single route each
148
+ can get: '/',
149
+ post: '/blog/action/save'
150
+
151
+ # example: multiple methods, multiple routes (using string array syntax)
152
+ can get: %w[/
153
+ /sign-in
154
+ /blog/editor],
155
+ post: %w[/blog/action/save
156
+ /blog/action/delete]
157
+ ```
158
+
159
+ > **Note** Allowing GET implies allowing HEAD, since HEAD is by spec a GET without a response body. The reverse is not
160
+ > true, however; allowing HEAD will not also allow GET.
161
+
162
+ #### Wildcards and Special Symbols
163
+
164
+ > **Warning** Always be cautious when using wildcards and special symbols to not accidentally open up pathways that
165
+ > should remain private.
166
+
167
+ Provide a wildcard `*` to match any string excluding slash. There is intentionally no syntax for matching wildcards
168
+ recursively, so nested paths will also need to be declared.
169
+
170
+ ```ruby
171
+ # example: match anything directly under the /members/ path
172
+ can get: '/members/*'
173
+ ```
174
+
175
+ There are also 2 special symbols:
176
+
177
+ 1. `:any` will match any HTTP method.
178
+ 2. `:all` will match all paths.
179
+
180
+ ```ruby
181
+ # this allows any method type on the / path
182
+ can any: '/'
183
+
184
+ # this allows GET on all paths
185
+ can get: :all
186
+ ```
187
+
188
+ ### Always Allow: `can`
189
+
190
+ Any route declared with `#can` will be accepted without further challenge.
76
191
 
77
192
  ```ruby
78
193
  rules do
79
- can_sometimes('/login') # Anyone can access this path
194
+ # Anyone can access this path over GET
195
+ can get: '/login'
80
196
  end
81
197
  ```
82
198
 
83
- #### :any and :all special parameters
84
- Passing `can` or `can_sometimes`:
85
- * `:any` to the first parameter will match any HTTP method.
86
- * `:all` to the second parameter will match any path.
199
+ ### Conditionally Allow: `can_sometimes`
200
+
201
+ `can_sometimes` is for occasions that you to check further, but want to defer that choice until the path is actually
202
+ attempted.
203
+ `can_sometimes` takes a block that will be run once the path is attempted. This block **must return an explicit boolean
204
+ **
205
+ (ie. `true` or `false`) to avoid any accidental truthy values creating unwanted access.
87
206
 
88
207
  ```ruby
89
- # this allows get on all paths
90
- can(:get, :all)
208
+ rules do
209
+ can_sometimes get: '/login' # Anyone can access this path over GET
91
210
 
92
- # this allows any method type to run on the /login path
93
- can(:any, '/login')
211
+ can_sometimes post: '/user/blog/actions/save' do
212
+ !current_user.nil?
213
+ end
214
+ end
94
215
  ```
95
216
 
96
217
  ### Custom Bounce Behaviour
97
- The default bounce action is to `halt 403`. Call `bounce_with` with a block to specify your own behaviour. The block is also run in a sinatra request context, so you can use helpers here as well.
218
+
219
+ The default bounce action is to `halt 403`. Call `bounce_with` with a block to specify your own behaviour. The block is
220
+ also run in a sinatra request context, so you can use helpers here as well.
98
221
 
99
222
  ```ruby
100
223
  require 'sinatra'
101
224
  require 'sinatra/bouncer'
102
225
 
103
- bounce_with do
104
- redirect '/login'
226
+ bounce_with do
227
+ redirect '/login'
105
228
  end
106
229
 
107
230
  # bouncer rules, routes, etc...
108
231
  ```
232
+
233
+ ## Alternatives
234
+
235
+ The syntax for Bouncer is largely influenced by the now-deprecated [CanCan](https://github.com/ryanb/cancan) gem.
236
+
237
+ ## Contributing
238
+
239
+ Bug reports and pull requests are welcome on GitHub at https://github.com/TenjinInc/Sinatra-Bouncer.
240
+
241
+ Valued topics:
242
+
243
+ * Error messages (clarity, hinting)
244
+ * Documentation
245
+ * API
246
+ * Security correctness
247
+
248
+ This project is intended to be a friendly space for collaboration, and contributors are expected to adhere to the
249
+ [Contributor Covenant](https://www.contributor-covenant.org/) code of conduct. Play nice.
250
+
251
+ ### Core Developers
252
+
253
+ After checking out the repo, run `bundle install` to install dependencies. Then, run `rake spec` to run the tests. You
254
+ can also run `bin/console` for an interactive prompt that will allow you to experiment.
255
+
256
+ To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the
257
+ version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version,
258
+ push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
259
+
260
+ Documentation is produced by Yard. Run `bundle exec rake yard`. The goal is to have 100% documentation coverage and 100%
261
+ test coverage.
262
+
263
+ Release notes are provided in `RELEASE_NOTES.md`, and should vaguely
264
+ follow [Keep A Changelog](https://keepachangelog.com/en/1.0.0/) recommendations.
265
+
266
+ ## License
267
+
268
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/license/mit/).
data/RELEASE_NOTES.md ADDED
@@ -0,0 +1,153 @@
1
+ # Release Notes
2
+
3
+ All notable changes to this project will be documented below.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) and this project loosely follows
6
+ [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+
8
+ ## [Unreleased]
9
+
10
+ ### Major Changes
11
+
12
+ * none
13
+
14
+ ### Minor Changes
15
+
16
+ * none
17
+
18
+ ### Bugfixes
19
+
20
+ * none
21
+
22
+ ## [2.0.0] - 2023-11-13
23
+
24
+ ### Major Changes
25
+
26
+ * Converted to hash syntax in `can` and `can_sometimes` statements
27
+
28
+ ### Minor Changes
29
+
30
+ * Converted Cucumber tests to Rspec integration tests for consistency
31
+ * HEAD requests are evaluated like GET requests due to semantic equivalence
32
+ * Falsey values are now acceptable rule block results
33
+
34
+ ### Bugfixes
35
+
36
+ * none
37
+
38
+ ## [1.3.0] - 2023-09-13
39
+
40
+ ### Major Changes
41
+
42
+ * none
43
+
44
+ ### Minor Changes
45
+
46
+ * Increased minimum Ruby to 3.1
47
+ * Cleaned up development dependencies
48
+ * Rubocop cleanups
49
+ * Installed Simplecov
50
+ * Created Rakefile
51
+
52
+ ### Bugfixes
53
+
54
+ * none
55
+
56
+ ## [1.2.0] - 2016-10-02
57
+
58
+ ### Major Changes
59
+
60
+ * none
61
+
62
+ ### Minor Changes
63
+
64
+ * Supports wildcard matches in route strings
65
+
66
+ ### Bugfixes
67
+
68
+ * none
69
+
70
+ ## [1.1.1] - 2015-06-02
71
+
72
+ ### Major Changes
73
+
74
+ * none
75
+
76
+ ### Minor Changes
77
+
78
+ * none
79
+
80
+ ### Bugfixes
81
+
82
+ * Runs `bounce_with` in context of web request
83
+
84
+ ## [1.1.0] - 2015-05-28
85
+
86
+ ### Major Changes
87
+
88
+ * none
89
+
90
+ ### Minor Changes
91
+
92
+ * none
93
+
94
+ ### Bugfixes
95
+
96
+ * Correctly forgets rules between routes
97
+
98
+ ## [1.0.2] - 2015-05-24
99
+
100
+ ### Major Changes
101
+
102
+ * none
103
+
104
+ ### Minor Changes
105
+
106
+ * Changed default halt to 403
107
+ * Application available in rule block
108
+
109
+ ### Bugfixes
110
+
111
+ * Fixed sinatra module registration
112
+
113
+ ## [1.0.1] - 2015-05-21
114
+
115
+ ### Major Changes
116
+
117
+ * none
118
+
119
+ ### Minor Changes
120
+
121
+ * none
122
+
123
+ ### Bugfixes
124
+
125
+ * none
126
+
127
+ ## [1.0.0] - Unreleased Prototype
128
+
129
+ ### Major Changes
130
+
131
+ * none
132
+
133
+ ### Minor Changes
134
+
135
+ * none
136
+
137
+ ### Bugfixes
138
+
139
+ * none
140
+
141
+ ## [0.1.0] - Unreleased Prototype
142
+
143
+ ### Major Changes
144
+
145
+ * Initial prototype
146
+
147
+ ### Minor Changes
148
+
149
+ * none
150
+
151
+ ### Bugfixes
152
+
153
+ * none
data/Rakefile ADDED
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'bundler/gem_tasks'
4
+ require 'rspec/core/rake_task'
5
+ require 'yard'
6
+
7
+ RSpec::Core::RakeTask.new(:spec)
8
+
9
+ task default: :spec
10
+
11
+ YARD::Rake::YardocTask.new do |t|
12
+ t.files = %w[lib/**/*.rb]
13
+ # t.options = %w[--some-option]
14
+ t.stats_options = ['--list-undoc']
15
+ end
data/cucumber.yml ADDED
@@ -0,0 +1 @@
1
+ default: --publish-quiet
@@ -0,0 +1,90 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'rule'
4
+
5
+ module Sinatra
6
+ module Bouncer
7
+ # Core implementation of Bouncer logic
8
+ class BasicBouncer
9
+ attr_accessor :bounce_with, :rules_initializer
10
+
11
+ # Enumeration of HTTP method strings based on:
12
+ # https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods
13
+ # Ignoring CONNECT and TRACE due to rarity
14
+ HTTP_METHODS = %w[GET HEAD PUT POST DELETE OPTIONS PATCH].freeze
15
+
16
+ # Symbol versions of HTTP_METHODS constant
17
+ #
18
+ # @see HTTP_METHODS
19
+ HTTP_METHOD_SYMBOLS = HTTP_METHODS.collect do |http_method|
20
+ http_method.downcase.to_sym
21
+ end.freeze
22
+
23
+ # Method symbol used to match any method
24
+ WILDCARD_METHOD = :any
25
+
26
+ def initialize
27
+ @ruleset = Hash.new do
28
+ []
29
+ end
30
+
31
+ @rules_initializer = proc {}
32
+ end
33
+
34
+ def can(**method_routes)
35
+ if block_given?
36
+ hint = 'If you wish to conditionally allow, use #can_sometimes instead.'
37
+ raise BouncerError, "You cannot provide a block to #can. #{ hint }"
38
+ end
39
+
40
+ can_sometimes(**method_routes) do
41
+ true
42
+ end
43
+ end
44
+
45
+ def can_sometimes(**method_routes, &block)
46
+ unless block
47
+ hint = 'If you wish to always allow, use #can instead.'
48
+ raise BouncerError, "You must provide a block to #can_sometimes. #{ hint }"
49
+ end
50
+
51
+ method_routes.each do |method, paths|
52
+ unless HTTP_METHOD_SYMBOLS.include?(method) || method == WILDCARD_METHOD
53
+ hint = "Must be one of: #{ HTTP_METHOD_SYMBOLS } or :#{ WILDCARD_METHOD }"
54
+ raise BouncerError, "'#{ method }' is not a known HTTP method key. #{ hint }"
55
+ end
56
+
57
+ paths = [paths] unless paths.respond_to? :collect
58
+
59
+ @ruleset[method] += paths.collect { |path| Rule.new(path, &block) }
60
+ end
61
+ end
62
+
63
+ def can?(method, path, context)
64
+ rulesets = @ruleset[WILDCARD_METHOD] + @ruleset[method]
65
+
66
+ # HEAD requests are equivalent to GET requests without response
67
+ rulesets += @ruleset[:get] if method == :head
68
+
69
+ rules = rulesets.select { |rule| rule.match_path?(path) }
70
+
71
+ rules.any? do |rule_block|
72
+ ruling = rule_block.rule_passes? context
73
+
74
+ ruling
75
+ end
76
+ end
77
+
78
+ def bounce(instance)
79
+ if bounce_with
80
+ instance.instance_exec(&bounce_with)
81
+ else
82
+ instance.halt 403
83
+ end
84
+ end
85
+ end
86
+
87
+ class BouncerError < StandardError
88
+ end
89
+ end
90
+ end
@@ -0,0 +1,62 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Sinatra
4
+ module Bouncer
5
+ # Defines a Rule to be evaluated with each request
6
+ class Rule
7
+ def initialize(path, &rule_block)
8
+ if path == :all
9
+ @path = :all
10
+ else
11
+ path = "/#{ path }" unless path.start_with?('/')
12
+
13
+ @path = path.split('/')
14
+ end
15
+
16
+ @rule = rule_block
17
+ end
18
+
19
+ # Determines if the path matches the exact path or wildcard.
20
+ #
21
+ # @return `true` if the path matches
22
+ def match_path?(path)
23
+ return true if @path == :all
24
+
25
+ path = "/#{ path }" unless path.start_with?('/')
26
+
27
+ split_path = path.split('/')
28
+ matches = @path.length == split_path.length
29
+
30
+ @path.each_index do |i|
31
+ allowed_segment = @path[i]
32
+ given_segment = split_path[i]
33
+
34
+ matches &= given_segment == allowed_segment || allowed_segment == '*'
35
+ end
36
+
37
+ matches
38
+ end
39
+
40
+ # Evaluates the rule's block. Defensively prevents truthy values from the block from allowing a route.
41
+ #
42
+ # @raise BouncerError when the rule block is a truthy value but not exactly `true`
43
+ # @return Exactly `true` or `false`, depending on the result of the rule block
44
+ def rule_passes?(context)
45
+ ruling = context.instance_exec(&@rule)
46
+
47
+ unless !ruling || ruling.is_a?(TrueClass)
48
+ source = @rule.source_location.join(':')
49
+ msg = <<~ERR
50
+ Rule block at does not return explicit true/false.
51
+ Rules must return explicit true or false to prevent accidental truthy values.
52
+ Source: #{ source }
53
+ ERR
54
+
55
+ raise BouncerError, msg
56
+ end
57
+
58
+ !!ruling
59
+ end
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Sinatra
4
+ module Bouncer
5
+ # Current version of the gem
6
+ VERSION = '2.0.0'
7
+ end
8
+ end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  #--
2
4
  # Copyright (c) 2014 Tenjin Inc.
3
5
  #
@@ -21,55 +23,32 @@
21
23
  # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
22
24
  #++
23
25
 
24
- require_relative 'basic_bouncer'
26
+ require 'sinatra/base'
27
+ require_relative 'bouncer/basic_bouncer'
25
28
 
29
+ # Namespace module
26
30
  module Sinatra
31
+ # Namespace module
27
32
  module Bouncer
28
33
  def self.registered(base_class)
29
- base_class.helpers HelperMethods
30
-
31
- bouncer = BasicBouncer.new
32
-
33
- # TODO: can we instead store it somehow on the actual temp request object?
34
- base_class.set :bouncer, bouncer
34
+ base_class.set :bouncer, BasicBouncer.new
35
35
 
36
36
  base_class.before do
37
- bouncer.reset! # must clear all rules otherwise will leave doors open
38
-
39
- self.instance_exec &bouncer.rules_initializer
40
-
41
37
  http_method = request.request_method.downcase.to_sym
42
38
  path = request.path.downcase
43
39
 
44
- unless bouncer.can?(http_method, path)
45
- bouncer.bounce(self)
46
- end
40
+ settings.bouncer.bounce(self) unless settings.bouncer.can?(http_method, path, self)
47
41
  end
48
42
  end
49
43
 
50
- # Start ExtensionMethods
51
44
  def bounce_with(&block)
52
45
  bouncer.bounce_with = block
53
46
  end
54
47
 
55
48
  def rules(&block)
56
- bouncer.rules_initializer = block
57
- end
58
-
59
- # End ExtensionMethods
60
-
61
- module HelperMethods
62
- def can(*args)
63
- settings.bouncer.can(*args)
64
- end
65
-
66
- def can_sometimes(*args, &block)
67
- settings.bouncer.can_sometimes(*args, &block)
68
- end
49
+ settings.bouncer.instance_exec(&block)
69
50
  end
70
51
  end
71
52
 
72
- if defined? register
73
- register Bouncer
74
- end
75
- end
53
+ register Sinatra::Bouncer
54
+ end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ lib = File.expand_path('lib', __dir__)
4
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
5
+
6
+ require 'sinatra/bouncer/version'
7
+
8
+ Gem::Specification.new do |spec|
9
+ spec.name = 'sinatra-bouncer'
10
+ spec.version = Sinatra::Bouncer::VERSION
11
+ spec.authors = ['Tenjin Inc', 'Robin Miller']
12
+ spec.email = %w[contact@tenjin.ca robin@tenjin.ca]
13
+
14
+ spec.summary = 'Sinatra permissions plugin'
15
+ spec.description = 'Bouncer brings simple authorization to Sinatra.'
16
+ spec.homepage = 'https://github.com/TenjinInc/Sinatra-Bouncer'
17
+ spec.license = 'MIT'
18
+ spec.metadata = {
19
+ 'rubygems_mfa_required' => 'true'
20
+ }
21
+
22
+ spec.files = `git ls-files -z`.split("\x0").reject do |f|
23
+ f.match(%r{^(test|spec|features|integrations|benchmarks)/})
24
+ end
25
+
26
+ spec.require_paths = ['lib']
27
+
28
+ spec.required_ruby_version = '>= 3.1'
29
+
30
+ spec.add_dependency 'sinatra', '>= 2.2'
31
+ end
metadata CHANGED
@@ -1,15 +1,15 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: sinatra-bouncer
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.2.0
4
+ version: 2.0.0
5
5
  platform: ruby
6
6
  authors:
7
- - Tenjin
7
+ - Tenjin Inc
8
8
  - Robin Miller
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2016-10-02 00:00:00.000000000 Z
12
+ date: 2023-11-13 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: sinatra
@@ -17,113 +17,44 @@ dependencies:
17
17
  requirements:
18
18
  - - ">="
19
19
  - !ruby/object:Gem::Version
20
- version: '0'
20
+ version: '2.2'
21
21
  type: :runtime
22
22
  prerelease: false
23
23
  version_requirements: !ruby/object:Gem::Requirement
24
24
  requirements:
25
25
  - - ">="
26
26
  - !ruby/object:Gem::Version
27
- version: '0'
28
- - !ruby/object:Gem::Dependency
29
- name: simplecov
30
- requirement: !ruby/object:Gem::Requirement
31
- requirements:
32
- - - ">="
33
- - !ruby/object:Gem::Version
34
- version: '0'
35
- type: :development
36
- prerelease: false
37
- version_requirements: !ruby/object:Gem::Requirement
38
- requirements:
39
- - - ">="
40
- - !ruby/object:Gem::Version
41
- version: '0'
42
- - !ruby/object:Gem::Dependency
43
- name: rspec
44
- requirement: !ruby/object:Gem::Requirement
45
- requirements:
46
- - - "~>"
47
- - !ruby/object:Gem::Version
48
- version: 2.14.1
49
- type: :development
50
- prerelease: false
51
- version_requirements: !ruby/object:Gem::Requirement
52
- requirements:
53
- - - "~>"
54
- - !ruby/object:Gem::Version
55
- version: 2.14.1
56
- - !ruby/object:Gem::Dependency
57
- name: cucumber
58
- requirement: !ruby/object:Gem::Requirement
59
- requirements:
60
- - - "~>"
61
- - !ruby/object:Gem::Version
62
- version: 1.3.19
63
- type: :development
64
- prerelease: false
65
- version_requirements: !ruby/object:Gem::Requirement
66
- requirements:
67
- - - "~>"
68
- - !ruby/object:Gem::Version
69
- version: 1.3.19
70
- - !ruby/object:Gem::Dependency
71
- name: capybara
72
- requirement: !ruby/object:Gem::Requirement
73
- requirements:
74
- - - ">="
75
- - !ruby/object:Gem::Version
76
- version: '0'
77
- type: :development
78
- prerelease: false
79
- version_requirements: !ruby/object:Gem::Requirement
80
- requirements:
81
- - - ">="
82
- - !ruby/object:Gem::Version
83
- version: '0'
84
- - !ruby/object:Gem::Dependency
85
- name: launchy
86
- requirement: !ruby/object:Gem::Requirement
87
- requirements:
88
- - - ">="
89
- - !ruby/object:Gem::Version
90
- version: '0'
91
- type: :development
92
- prerelease: false
93
- version_requirements: !ruby/object:Gem::Requirement
94
- requirements:
95
- - - ">="
96
- - !ruby/object:Gem::Version
97
- version: '0'
98
- - !ruby/object:Gem::Dependency
99
- name: parallel_tests
100
- requirement: !ruby/object:Gem::Requirement
101
- requirements:
102
- - - ">="
103
- - !ruby/object:Gem::Version
104
- version: '0'
105
- type: :development
106
- prerelease: false
107
- version_requirements: !ruby/object:Gem::Requirement
108
- requirements:
109
- - - ">="
110
- - !ruby/object:Gem::Version
111
- version: '0'
27
+ version: '2.2'
112
28
  description: Bouncer brings simple authorization to Sinatra.
113
- email: contact@tenjin.ca
29
+ email:
30
+ - contact@tenjin.ca
31
+ - robin@tenjin.ca
114
32
  executables: []
115
33
  extensions: []
116
34
  extra_rdoc_files: []
117
35
  files:
36
+ - ".gitignore"
37
+ - ".rubocop.yml"
38
+ - ".ruby-version"
39
+ - ".simplecov"
40
+ - CODE_OF_CONDUCT.md
118
41
  - Gemfile
42
+ - Gemfile.lock
119
43
  - MIT-LICENSE
120
44
  - README.md
121
- - lib/sinatra/basic_bouncer.rb
45
+ - RELEASE_NOTES.md
46
+ - Rakefile
47
+ - cucumber.yml
122
48
  - lib/sinatra/bouncer.rb
123
- - lib/sinatra/rule.rb
124
- homepage: http://www.tenjin.ca
125
- licenses: []
126
- metadata: {}
49
+ - lib/sinatra/bouncer/basic_bouncer.rb
50
+ - lib/sinatra/bouncer/rule.rb
51
+ - lib/sinatra/bouncer/version.rb
52
+ - sinatra_bouncer.gemspec
53
+ homepage: https://github.com/TenjinInc/Sinatra-Bouncer
54
+ licenses:
55
+ - MIT
56
+ metadata:
57
+ rubygems_mfa_required: 'true'
127
58
  post_install_message:
128
59
  rdoc_options: []
129
60
  require_paths:
@@ -132,15 +63,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
132
63
  requirements:
133
64
  - - ">="
134
65
  - !ruby/object:Gem::Version
135
- version: 1.9.3
66
+ version: '3.1'
136
67
  required_rubygems_version: !ruby/object:Gem::Requirement
137
68
  requirements:
138
69
  - - ">="
139
70
  - !ruby/object:Gem::Version
140
71
  version: '0'
141
72
  requirements: []
142
- rubyforge_project:
143
- rubygems_version: 2.4.8
73
+ rubygems_version: 3.4.18
144
74
  signing_key:
145
75
  specification_version: 4
146
76
  summary: Sinatra permissions plugin
@@ -1,69 +0,0 @@
1
- require_relative 'rule'
2
-
3
- module Sinatra
4
- module Bouncer
5
- class BasicBouncer
6
- attr_accessor :bounce_with
7
- attr_accessor :rules_initializer
8
-
9
- def initialize
10
- # @rules = Hash.new do |method_to_paths, method|
11
- # method_to_paths[method] = Hash.new do |path_to_rules, path|
12
- # path_to_rules[path] = []
13
- # end
14
- # end
15
-
16
- @ruleset = Hash.new do
17
- []
18
- end
19
- @rules_initializer = Proc.new {}
20
- end
21
-
22
- def reset!
23
- @ruleset.clear
24
- end
25
-
26
- def can(method, *paths)
27
- if block_given?
28
- raise BouncerError.new('You cannot provide a block to #can. If you wish to conditionally allow, use #can_sometimes instead.')
29
- end
30
-
31
- can_sometimes(method, *paths) do
32
- true
33
- end
34
- end
35
-
36
- def can_sometimes(method, *paths, &block)
37
- unless block_given?
38
- raise BouncerError.new('You must provide a block to #can_sometimes. If you wish to always allow, use #can instead.')
39
- end
40
-
41
- paths.each do |path|
42
- @ruleset[method] += [Rule.new(path, &block)]
43
- end
44
- end
45
-
46
- def can?(method, path)
47
- rules = (@ruleset[:any_method] + @ruleset[method]).select { |rule| rule.match_path?(path) }
48
-
49
- rules.any? do |rule_block|
50
- ruling = rule_block.rule_passes?
51
-
52
- ruling
53
- end
54
- end
55
-
56
- def bounce(instance)
57
- if bounce_with
58
- instance.instance_exec &bounce_with
59
- else
60
- instance.halt 403
61
- end
62
- end
63
- end
64
-
65
- class BouncerError < StandardError
66
-
67
- end
68
- end
69
- end
data/lib/sinatra/rule.rb DELETED
@@ -1,48 +0,0 @@
1
- module Sinatra
2
- module Bouncer
3
- class Rule
4
- def initialize(path, &ruleblock)
5
- if path == :all
6
- @path = :all
7
- else
8
- path = '/' + path unless path.start_with?('/')
9
-
10
- @path = path.split('/')
11
- end
12
-
13
- @rule = ruleblock
14
- end
15
-
16
- def match_path?(path)
17
- return true if @path == :all
18
-
19
- path = '/' + path unless path.start_with?('/')
20
-
21
- split_path = path.split('/')
22
- matches = @path.length == split_path.length
23
-
24
- @path.each_index do |i|
25
- allowed_segment = @path[i]
26
- given_segment = split_path[i]
27
-
28
- matches &= given_segment == allowed_segment || allowed_segment == '*'
29
- end
30
-
31
- matches
32
- end
33
-
34
- def rule_passes?
35
- ruling = @rule.call
36
-
37
- unless ruling.is_a?(TrueClass)|| ruling.is_a?(FalseClass)
38
- source = @rule.source_location.join(':')
39
- raise BouncerError.new("Rule block at does not return explicit true/false.\n\n"+
40
- "Rules must return explicit true or false to prevent accidental truthy values.\n\n"+
41
- "Source: #{source}\n")
42
- end
43
-
44
- ruling
45
- end
46
- end
47
- end
48
- end