alpaca 0.9.0 → 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
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