alpaca 0.9.0 → 1.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.
data/README.md CHANGED
@@ -1,16 +1,21 @@
1
1
  ## Alpaca (outta nowhere)
2
2
 
3
- ![Alpaca](https://raw.github.com/jeffchao/alpaca/master/alpaca.jpeg)
3
+ [![Gem Version](https://badge.fury.io/rb/alpaca.png)](http://badge.fury.io/rb/alpaca)
4
+ [![Build Status](https://travis-ci.org/jeffchao/alpaca.png?branch=master)](https://travis-ci.org/jeffchao/alpaca)
5
+ [![Dependency Status](https://gemnasium.com/jeffchao/alpaca.png)](https://gemnasium.com/jeffchao/alpaca.png)
6
+ [![Coverage Status](https://coveralls.io/repos/jeffchao/alpaca/badge.png)](https://coveralls.io/r/jeffchao/alpaca)
4
7
 
5
8
  Alpaca (outta nowhere) is a rack middleware that allows developers to quickly and easily configure and manage a whitelist and/or blacklist. The motivation for Alpaca is to address use cases around security concerns such as malicious clients, denial of service, or adding an extra layer of security to an API or a subset of API endpoints.
6
9
 
7
- In progress
10
+ ![Alpaca](https://raw.github.com/jeffchao/alpaca/master/alpaca.jpeg)
11
+
12
+ Features
8
13
  ----------
9
14
 
10
- ~~Global-level whitelist and blacklist~~
11
- ~~Configuration and management via YAML~~
12
- ~~Whitelist-by-default, blacklist-by-default~~
13
- Controller-level whitelist and blacklist via `before_filter`
15
+ - Global-level whitelisting and blacklisting at the rack layer
16
+ - Controller-level whitelisting and blacklisting via `before_filter`
17
+ - Per-action whitelisting and blacklisting at the controller level
18
+ - Configuration management via YAML
14
19
 
15
20
  Getting started
16
21
  ----------
@@ -44,11 +49,69 @@ use Rack::Alpaca
44
49
  Usage
45
50
  ----------
46
51
 
47
- Alpaca supports whitelisting and blacklisting single IP addresses (e.g., `0.0.0.1`), hostnames (e.g., `localhost`), and range of IP addresses with subnet masks (e.g., `198.18.0.0/15`, `2001:db8::/32`). You may use IPv4 or IPv6. You can make changes in `config/alpaca.yml` to either list.
52
+ Alpaca supports:
53
+
54
+ - whitelisting and blacklisting single IP addresses (e.g., `0.0.0.1`)
55
+ - hostnames (e.g., `127.0.0.1` also affects `localhost`)
56
+ - range of IP addresses via subnet masks (e.g., `198.18.0.0/15`, `2001:db8::/32`).
57
+
58
+ ### Global-level whitelisting and blacklisting
59
+
60
+ You may use IPv4 or IPv6. Make changes in `config/alpaca.yml` by adding or removing IPs to and from either list. Your file should resemble the following:
61
+
62
+ ```yaml
63
+ whitelist:
64
+ - 0.0.0.1
65
+ - 198.18.0.0/15
66
+ - "::/128"
67
+ blacklist:
68
+ - 0.0.0.1
69
+ - 0.0.0.2
70
+ - "2001:db8::/32"
71
+ default: allow
72
+ ```
73
+
74
+ Depending on your strategy, you may choose to enforce an allow-by-default or deny-by-default approach. You can use the `default` key in the configuration file with either `allow` or `deny` as its value.
75
+
76
+ **A note about precedence**: If an IP exists in both the whitelist and blacklist, then whitelist will take precedence and allow the IP.
77
+
78
+ ### Controller-level whitelisting and blacklisting
79
+
80
+ There exists two methods for handling IPs at the controller level. You must have your global-level default set to `allow` for it to be useful. This is because a global-level `deny` would have already blocked all IPs at the rack layer.
81
+
82
+ ```ruby
83
+ before_filter :enable_whitelist_and_deny_by_default
84
+
85
+ # or
86
+
87
+ before_filter :enable_blacklist_and_allow_by_default
88
+ ```
89
+
90
+ You may optionally attach this filter to specific method(s):
91
+
92
+ ```ruby
93
+ before_filter :enable_whitlist_and_deny_by_default, only: [:create, :update]
94
+ ```
95
+
96
+ Lastly, you may add additional IPs that were not previously defined in your alpaca.yml`:
97
+
98
+ ```ruby
99
+ before_filter only: [:create, :update] { |f| f.enable_whitelist_and_deny_by_default(['0.0.0.1']) }
100
+ ```
101
+
102
+ ### Example setups
103
+
104
+ Given that some configuration permuations may be unecessary or illogical, the following is a table of typical use cases. The cells represent the resulting behavior:
48
105
 
49
- Depending on your strategy, you may choose to enforce a whitelist-by-default or blacklist-by-default approach. You can use the `default` key in the configuration file with either `whitelist` or `blacklist` as its value.
106
+ | | global allow-by-default | global deny-by-default |
107
+ | --- | ----------------------- | ---------------------- |
108
+ | **no controller filter** | all IPs allowed | all IPs denied |
109
+ | **controller filter whitelist, no added IPs** | IPs in whitelist from `alpaca.yml` allowed for controller. All other IPs denied for controller. All IPs allowed everywhere else | all IPs denied |
110
+ | **controller filter whitelist, added IPs** | IPs in whitelist from `alpaca.yml` and arguments to filter allowed for controller. All other IPs denied for controller. All IPs allowed everywhere else | all IPs denied |
111
+ | **controller filter blacklist, no added IPs** | IPs in blacklist from `alpaca.yml` denied for controller. All other IPs allowed for controller. All IPs allowed everywhere else | all IPs denied |
112
+ | **controller filter blacklist, added IPs** | IPs in blacklist from `alpaca.yml` and arguments denied for controller. All other IPs allowed for controller. All IPs allowed everywhere else | all IPs denied |
50
113
 
51
- Performance (WIP)
114
+ Performance
52
115
  ----------
53
116
 
54
117
  Through initial testing, Alpaca does not appear to cause noticeable overhead. Future tests under different types of load will be documented here.
@@ -1,54 +1,5 @@
1
1
  require 'rack'
2
2
  require 'yaml'
3
3
  require 'ipaddr'
4
-
5
- module Rack
6
- module Alpaca
7
- class << self
8
- attr_reader :whitelist, :blacklist, :default
9
-
10
- def new (app)
11
- @app = app
12
- config = YAML.load_file('config/alpaca.yml')
13
- @whitelist ||= config['whitelist'].map { |ip| IPAddr.new(ip) }.freeze
14
- @blacklist ||= config['blacklist'].map { |ip| IPAddr.new(ip) }.freeze
15
- @default = config['default']
16
-
17
- self
18
- end
19
-
20
- def call (env)
21
- req = Rack::Request.new(env)
22
-
23
- if whitelisted?('whitelist', req)
24
- @app.call(env)
25
- elsif blacklisted?('blacklist', req)
26
- [503, {}, ["Request blocked\n"]]
27
- else
28
- default_strategy(env)
29
- end
30
- end
31
-
32
- private
33
-
34
- def default_strategy (env)
35
- if @default == 'whitelist'
36
- @app.call(env)
37
- elsif @default == 'blacklist'
38
- [503, {}, ["Request blocked\n"]]
39
- else
40
- raise 'Unknown default strategy'
41
- end
42
- end
43
-
44
- def check (type, req)
45
- instance_variable_get("@#{type}").any? do |ip|
46
- ip.include?(req.ip)
47
- end
48
- end
49
-
50
- alias_method :whitelisted?, :check
51
- alias_method :blacklisted?, :check
52
- end
53
- end
54
- end
4
+ require 'alpaca/controller_additions'
5
+ require 'rack/alpaca'
@@ -0,0 +1,40 @@
1
+ module Alpaca
2
+ # These are additional helper methods that are added to
3
+ # ApplicationController. In particular, this module contains
4
+ # filters that should be used as before_filters.
5
+ module ControllerAdditions
6
+ def enable_whitelist_and_deny_by_default (additional_ips = [])
7
+ render_blocked_request unless whitelisted?(additional_ips)
8
+ end
9
+
10
+ def enable_blacklist_and_allow_by_default (additional_ips = [])
11
+ if blacklisted?(additional_ips)
12
+ render_blocked_request
13
+ end
14
+ end
15
+
16
+ private
17
+
18
+ def whitelisted? (additional_ips)
19
+ additional_ips.any? { |ip| IPAddr.new(ip).include?(request.ip) } ||
20
+ Rack::Alpaca.whitelist.any? { |ip| ip.include?(request.ip) }
21
+ end
22
+
23
+ def blacklisted? (additional_ips)
24
+ additional_ips.any? { |ip| IPAddr.new(ip).include?(request.ip) } ||
25
+ Rack::Alpaca.blacklist.any? { |ip| ip.include?(request.ip) }
26
+ end
27
+
28
+ def render_blocked_request
29
+ render text: "Request blocked\n",
30
+ status: :service_unavailable,
31
+ content_type: 'text/plain'
32
+ end
33
+ end
34
+ end
35
+
36
+ if defined?(ActionController::Base)
37
+ ActionController::Base.instance_eval do
38
+ include Alpaca::ControllerAdditions
39
+ end
40
+ end
@@ -0,0 +1,51 @@
1
+ module Rack
2
+ module Alpaca
3
+ class << self
4
+ attr_reader :whitelist, :blacklist
5
+ attr_accessor :default
6
+
7
+ def new (app)
8
+ @app = app
9
+ config = YAML.load_file('config/alpaca.yml')
10
+ @whitelist ||= config['whitelist'].map { |ip| IPAddr.new(ip) }.freeze
11
+ @blacklist ||= config['blacklist'].map { |ip| IPAddr.new(ip) }.freeze
12
+ @default = config['default']
13
+
14
+ self
15
+ end
16
+
17
+ def call (env)
18
+ req = Rack::Request.new(env)
19
+
20
+ if whitelisted?('whitelist', req)
21
+ @app.call(env)
22
+ elsif blacklisted?('blacklist', req)
23
+ [503, {}, ["Request blocked\n"]]
24
+ else
25
+ default_strategy(env)
26
+ end
27
+ end
28
+
29
+ private
30
+
31
+ def default_strategy (env)
32
+ if @default == 'allow'
33
+ @app.call(env)
34
+ elsif @default == 'deny'
35
+ [503, {}, ["Request blocked\n"]]
36
+ else
37
+ raise 'Unknown default strategy'
38
+ end
39
+ end
40
+
41
+ def check (type, req)
42
+ instance_variable_get("@#{type}").any? do |ip|
43
+ ip.include?(req.ip)
44
+ end
45
+ end
46
+
47
+ alias_method :whitelisted?, :check
48
+ alias_method :blacklisted?, :check
49
+ end
50
+ end
51
+ end
@@ -1,5 +1,5 @@
1
1
  module Rack
2
2
  module Alpaca
3
- VERSION = '0.9.0'.freeze
3
+ VERSION = '1.0.0'
4
4
  end
5
5
  end
@@ -0,0 +1,54 @@
1
+ require_relative 'spec_helper'
2
+
3
+ describe 'Alpaca::ControllerAdditions' do
4
+ before do
5
+ @ips = ["0.0.0.1", "198.18.0.0/15", "0.0.0.3"].map { |ip| IPAddr.new(ip) }
6
+ end
7
+
8
+ it 'should initialize with controller additions' do
9
+ controller = FooController.new
10
+ controller.methods.must_include(:enable_whitelist_and_deny_by_default)
11
+ controller.methods.must_include(:enable_blacklist_and_allow_by_default)
12
+ end
13
+
14
+ describe 'with global allow-by-default' do
15
+ describe '#enable_whitelist_and_deny_by_default' do
16
+ it 'should only permit IPs on the whitelist' do
17
+ @ips[0...-1].each do |ip|
18
+ get '/', {}, 'REMOTE_ADDR' => ip.to_s
19
+ last_response.status.must_equal 200
20
+ last_response.body.must_equal 'foo'
21
+ end
22
+
23
+ get '/foo', {}, 'REMOTE_ADDR' => @ips.last.to_s
24
+ last_response.status.must_equal 503
25
+ last_response.body.must_equal "Request blocked\n"
26
+ end
27
+
28
+ describe 'with additional IPs' do
29
+ it 'should permit IPs on the whitelist and arguments' do
30
+ @ips = @ips[0...-1].push('0.0.0.4')
31
+ @ips[0..-1].each do |ip|
32
+ get '/baz', {}, 'REMOTE_ADDR' => ip.to_s
33
+ last_response.status.must_equal 200
34
+ last_response.body.must_equal 'baz'
35
+ end
36
+ end
37
+ end
38
+ end
39
+
40
+ describe '#enable_blacklist_and_allow_by_default' do
41
+ it 'should allow all IPs except ones on the blacklist' do
42
+ @ips[1..-1].each do |ip|
43
+ get '/', {}, 'REMOTE_ADDR' => ip.to_s
44
+ last_response.status.must_equal 200
45
+ last_response.body.must_equal 'foo'
46
+ end
47
+
48
+ get '/bar', {}, 'REMOTE_ADDR' => @ips.first.to_s
49
+ last_response.status.must_equal 503
50
+ last_response.body.must_equal "Request blocked\n"
51
+ end
52
+ end
53
+ end
54
+ end
@@ -64,4 +64,18 @@ describe 'Rack::Alpaca' do
64
64
  end
65
65
  end
66
66
  end
67
+
68
+ describe 'allow-by-default' do
69
+ before do
70
+ @ips = ["0.0.0.1", "198.18.0.0/15", "::/128"].map { |ip| IPAddr.new(ip) }
71
+ end
72
+
73
+ it 'should permit any IP address' do
74
+ @ips.each do |ip|
75
+ get '/', {}, 'REMOTE_ADDR' => ip.to_s
76
+ last_response.status.must_equal 200
77
+ last_response.body.must_equal 'foo'
78
+ end
79
+ end
80
+ end
67
81
  end
@@ -1,3 +1,12 @@
1
+ require 'simplecov'
2
+ require 'coveralls'
3
+
4
+ SimpleCov.formatter = SimpleCov::Formatter::MultiFormatter[
5
+ SimpleCov::Formatter::HTMLFormatter,
6
+ Coveralls::SimpleCov::Formatter
7
+ ]
8
+ SimpleCov.start
9
+
1
10
  require "rubygems"
2
11
  require "bundler/setup"
3
12
 
@@ -8,12 +17,51 @@ require "rack/test"
8
17
  require 'active_support'
9
18
  require "alpaca"
10
19
 
20
+ require_relative 'support/action_controller'
21
+ require_relative 'support/foo_controller'
22
+ require_relative 'support/bar_controller'
23
+ require_relative 'support/baz_controller'
24
+
11
25
  class Minitest::Spec
12
26
  include Rack::Test::Methods
13
27
 
14
28
  def app
15
29
  Rack::Builder.new {
16
30
  use Rack::Alpaca
31
+
32
+ map '/foo' do
33
+ run lambda { |env|
34
+ controller = ::FooController.new
35
+ controller.request = Rack::Request.new(env)
36
+ controller.response = Rack::Response.new
37
+ controller.process('foo')
38
+
39
+ controller.response.finish
40
+ }
41
+ end
42
+
43
+ map '/bar' do
44
+ run lambda { |env|
45
+ controller = ::BarController.new
46
+ controller.request = Rack::Request.new(env)
47
+ controller.response = Rack::Response.new
48
+ controller.process('bar')
49
+
50
+ controller.response.finish
51
+ }
52
+ end
53
+
54
+ map '/baz' do
55
+ run lambda { |env|
56
+ controller = ::BazController.new
57
+ controller.request = Rack::Request.new(env)
58
+ controller.response = Rack::Response.new
59
+ controller.process('baz')
60
+
61
+ controller.response.finish
62
+ }
63
+ end
64
+
17
65
  run lambda { |env| [200, {}, ['foo']] }
18
66
  }.to_app
19
67
  end
@@ -0,0 +1,27 @@
1
+ require_relative 'filters'
2
+
3
+ module ActionController
4
+ class Metal
5
+ attr_accessor :request, :response
6
+
7
+ def process (action)
8
+ send(action)
9
+ end
10
+
11
+ def render (*args)
12
+ response.status = Rack::Utils.status_code(args.first[:status])
13
+ response.body = [args.first[:text]]
14
+ response
15
+ end
16
+ end
17
+
18
+ class Base < Metal
19
+ include Filters
20
+ end
21
+ end
22
+
23
+ if defined?(ActionController::Base)
24
+ ActionController::Base.instance_eval do
25
+ include Alpaca::ControllerAdditions
26
+ end
27
+ end
@@ -0,0 +1,6 @@
1
+ class BarController < ActionController::Base
2
+ before_filter :enable_blacklist_and_allow_by_default
3
+
4
+ def bar
5
+ end
6
+ end
@@ -0,0 +1,7 @@
1
+ class BazController < ActionController::Base
2
+ before_filter :enable_whitelist_and_deny_by_default, ['0.0.0.4']
3
+
4
+ def baz
5
+ render({ status: :ok, text: "baz", content_type: 'text/plain' })
6
+ end
7
+ end
@@ -0,0 +1,22 @@
1
+ module Filters
2
+ def self.included (base)
3
+ base.extend ClassMethods
4
+ end
5
+
6
+ module ClassMethods
7
+ def before_filters
8
+ @before_filters ||= []
9
+ end
10
+
11
+ def before_filter (*args)
12
+ method = args.shift
13
+ ips = args.shift
14
+ before_filters << (ips.nil? ? [method] : [method, ips])
15
+ end
16
+ end
17
+
18
+ def process (action)
19
+ self.class.before_filters.each { |method, ips| ips.nil? ? send(method) : send(method, ips) }
20
+ super
21
+ end
22
+ end
@@ -0,0 +1,6 @@
1
+ class FooController < ActionController::Base
2
+ before_filter :enable_whitelist_and_deny_by_default
3
+
4
+ def foo
5
+ end
6
+ end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: alpaca
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.9.0
4
+ version: 1.0.0
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2013-05-21 00:00:00.000000000 Z
12
+ date: 2013-06-04 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: rack
@@ -75,6 +75,22 @@ dependencies:
75
75
  - - ! '>='
76
76
  - !ruby/object:Gem::Version
77
77
  version: '0'
78
+ - !ruby/object:Gem::Dependency
79
+ name: simplecov
80
+ requirement: !ruby/object:Gem::Requirement
81
+ none: false
82
+ requirements:
83
+ - - ! '>='
84
+ - !ruby/object:Gem::Version
85
+ version: '0'
86
+ type: :development
87
+ prerelease: false
88
+ version_requirements: !ruby/object:Gem::Requirement
89
+ none: false
90
+ requirements:
91
+ - - ! '>='
92
+ - !ruby/object:Gem::Version
93
+ version: '0'
78
94
  - !ruby/object:Gem::Dependency
79
95
  name: rack-test
80
96
  requirement: !ruby/object:Gem::Requirement
@@ -97,13 +113,21 @@ executables: []
97
113
  extensions: []
98
114
  extra_rdoc_files: []
99
115
  files:
116
+ - lib/alpaca/controller_additions.rb
100
117
  - lib/alpaca.rb
101
118
  - lib/rack/alpaca/version.rb
119
+ - lib/rack/alpaca.rb
102
120
  - lib/rails/generators/alpaca/install/install_generator.rb
103
121
  - Rakefile
104
122
  - README.md
123
+ - spec/alpaca_controller_additions_spec.rb
105
124
  - spec/rack_alpaca_spec.rb
106
125
  - spec/spec_helper.rb
126
+ - spec/support/action_controller.rb
127
+ - spec/support/bar_controller.rb
128
+ - spec/support/baz_controller.rb
129
+ - spec/support/filters.rb
130
+ - spec/support/foo_controller.rb
107
131
  homepage: http://github.com/jeffchao/alpaca
108
132
  licenses: []
109
133
  post_install_message:
@@ -130,5 +154,11 @@ signing_key:
130
154
  specification_version: 3
131
155
  summary: Whitelist and blacklist IPs
132
156
  test_files:
157
+ - spec/alpaca_controller_additions_spec.rb
133
158
  - spec/rack_alpaca_spec.rb
134
159
  - spec/spec_helper.rb
160
+ - spec/support/action_controller.rb
161
+ - spec/support/bar_controller.rb
162
+ - spec/support/baz_controller.rb
163
+ - spec/support/filters.rb
164
+ - spec/support/foo_controller.rb