fog-bouncer 0.0.6
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +19 -0
- data/.simplecov +3 -0
- data/Gemfile +6 -0
- data/README.md +71 -0
- data/Rakefile +11 -0
- data/bin/fog-bouncer +10 -0
- data/bouncer.jpg +0 -0
- data/fog-bouncer.gemspec +25 -0
- data/lib/fog/bouncer.rb +95 -0
- data/lib/fog/bouncer/cli.rb +23 -0
- data/lib/fog/bouncer/group.rb +140 -0
- data/lib/fog/bouncer/group_manager.rb +75 -0
- data/lib/fog/bouncer/ip_permissions.rb +51 -0
- data/lib/fog/bouncer/protocols.rb +115 -0
- data/lib/fog/bouncer/security.rb +88 -0
- data/lib/fog/bouncer/source.rb +87 -0
- data/lib/fog/bouncer/source_manager.rb +59 -0
- data/lib/fog/bouncer/sources.rb +61 -0
- data/lib/fog/bouncer/version.rb +5 -0
- data/spec/fog/bouncer/group_spec.rb +61 -0
- data/spec/fog/bouncer/protocols_spec.rb +25 -0
- data/spec/fog/bouncer/security_spec.rb +85 -0
- data/spec/fog/bouncer/source_spec.rb +49 -0
- data/spec/fog/bouncer/sources/cidr_spec.rb +9 -0
- data/spec/fog/bouncer_spec.rb +45 -0
- data/spec/helper.rb +28 -0
- data/spec/support/security/private.rb +46 -0
- metadata +179 -0
@@ -0,0 +1,51 @@
|
|
1
|
+
module Fog
|
2
|
+
module Bouncer
|
3
|
+
module IPPermissions
|
4
|
+
def self.from(protocols, options = {})
|
5
|
+
permissions = []
|
6
|
+
|
7
|
+
protocols.each do |protocol|
|
8
|
+
next if (options[:remote_only] && protocol.local?) ||
|
9
|
+
(options[:local_only] && protocol.remote?)
|
10
|
+
|
11
|
+
source = protocol.source
|
12
|
+
permission = permissions.find { |permission| permission["IpProtocol"] == protocol.type && permission["FromPort"] == protocol.from && permission["ToPort"] == protocol.to }
|
13
|
+
|
14
|
+
if permission.nil?
|
15
|
+
permission = { "Groups" => [], "IpRanges" => [], "IpProtocol" => protocol.type, "FromPort" => protocol.from, "ToPort" => protocol.to }
|
16
|
+
permissions << permission
|
17
|
+
end
|
18
|
+
|
19
|
+
if source.is_a?(Fog::Bouncer::Sources::CIDR)
|
20
|
+
permission["IpRanges"] << { "CidrIp" => source.range }
|
21
|
+
else
|
22
|
+
permission["Groups"] << { "UserId" => source.user_id, "GroupName" => source.name }
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
permissions
|
27
|
+
end
|
28
|
+
|
29
|
+
def self.to(group, permissions)
|
30
|
+
permissions.each do |permission|
|
31
|
+
remote_sources = []
|
32
|
+
remote_sources = remote_sources | permission["groups"].collect { |group| "#{group["groupName"]}@#{group["userId"]}" }
|
33
|
+
remote_sources = remote_sources | permission["ipRanges"].collect { |range| range["cidrIp"] }
|
34
|
+
remote_sources.each do |remote_source|
|
35
|
+
source = group.sources.find { |s| s.match(remote_source) }
|
36
|
+
|
37
|
+
if source.nil?
|
38
|
+
source = Sources.for(remote_source, group)
|
39
|
+
group.sources << source
|
40
|
+
end
|
41
|
+
|
42
|
+
source.remote = true
|
43
|
+
|
44
|
+
protocol = source.add_protocol(permission["ipProtocol"], Range.new(permission["fromPort"], permission["toPort"]))
|
45
|
+
protocol.remote = true
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
@@ -0,0 +1,115 @@
|
|
1
|
+
module Fog
|
2
|
+
module Bouncer
|
3
|
+
class Protocol
|
4
|
+
attr_reader :from, :local, :source, :to
|
5
|
+
attr_writer :local, :remote
|
6
|
+
|
7
|
+
def self.range(port)
|
8
|
+
if port.is_a?(Range)
|
9
|
+
[port.begin, port.end]
|
10
|
+
else
|
11
|
+
[port, port]
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
def initialize(port, source)
|
16
|
+
@from, @to = Protocol.range(port)
|
17
|
+
@source = source
|
18
|
+
validate
|
19
|
+
end
|
20
|
+
|
21
|
+
def local
|
22
|
+
@local ||= false
|
23
|
+
end
|
24
|
+
|
25
|
+
def local?
|
26
|
+
!!local
|
27
|
+
end
|
28
|
+
|
29
|
+
def match(type, port)
|
30
|
+
type.to_s == self.type && Protocol.range(port) == [from, to]
|
31
|
+
end
|
32
|
+
|
33
|
+
def remote
|
34
|
+
@remote ||= false
|
35
|
+
end
|
36
|
+
|
37
|
+
def remote?
|
38
|
+
!!remote
|
39
|
+
end
|
40
|
+
|
41
|
+
def type
|
42
|
+
@type ||= self.class.to_s.gsub("Fog::Bouncer::Protocols::", "").downcase
|
43
|
+
end
|
44
|
+
|
45
|
+
def ==(other)
|
46
|
+
type == other.type &&
|
47
|
+
from == other.from &&
|
48
|
+
to == other.to
|
49
|
+
end
|
50
|
+
|
51
|
+
def <=>(other)
|
52
|
+
[from, to] <=> [other.from, other.to]
|
53
|
+
end
|
54
|
+
|
55
|
+
def inspect
|
56
|
+
"<#{self.class.name} @from=#{from.inspect} @to=#{to.inspect} @local=#{local} @remote=#{remote}>"
|
57
|
+
end
|
58
|
+
|
59
|
+
def to_log
|
60
|
+
{ source: source.source, protocol: type, from: from, to: to }
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
module Protocols
|
65
|
+
class InvalidICMPType < StandardError; end
|
66
|
+
class InvalidPort < StandardError; end
|
67
|
+
|
68
|
+
class ICMP < Protocol
|
69
|
+
ICMP_MAPPING = {
|
70
|
+
all: -1,
|
71
|
+
ping: 8..0
|
72
|
+
}
|
73
|
+
|
74
|
+
ICMP_TYPE_RANGE = (-1..255)
|
75
|
+
|
76
|
+
def initialize(port, source)
|
77
|
+
if port.is_a?(Symbol) && range = ICMP_MAPPING[port]
|
78
|
+
port = range
|
79
|
+
end
|
80
|
+
super
|
81
|
+
end
|
82
|
+
|
83
|
+
def match(type, port)
|
84
|
+
if port.is_a?(Symbol) && range = ICMP_MAPPING[port]
|
85
|
+
type.to_s == self.type && Protocol.range(range) == [from, to]
|
86
|
+
else
|
87
|
+
super
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
private
|
92
|
+
|
93
|
+
def validate
|
94
|
+
raise InvalidICMPType.new("Must be between and including -1 and 255.") unless ICMP_TYPE_RANGE.include?(from)
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
class TCP < Protocol
|
99
|
+
private
|
100
|
+
|
101
|
+
def validate
|
102
|
+
raise InvalidPort.new("Invalid port #{from}. Must be between and including 0 and 65535.") unless (0..65535).include?(from)
|
103
|
+
end
|
104
|
+
end
|
105
|
+
|
106
|
+
class UDP < Protocol
|
107
|
+
private
|
108
|
+
|
109
|
+
def validate
|
110
|
+
raise InvalidPort.new("Invalid port #{from}. Must be between and including 0 and 65535.") unless (0..65535).include?(from)
|
111
|
+
end
|
112
|
+
end
|
113
|
+
end
|
114
|
+
end
|
115
|
+
end
|
@@ -0,0 +1,88 @@
|
|
1
|
+
module Fog
|
2
|
+
module Bouncer
|
3
|
+
class DefinitionNotFound < StandardError; end
|
4
|
+
class SourceBlockRequired < StandardError; end
|
5
|
+
class Security
|
6
|
+
attr_reader :name, :description
|
7
|
+
|
8
|
+
def initialize(name, &block)
|
9
|
+
@name = name
|
10
|
+
@definitions = {}
|
11
|
+
@using = []
|
12
|
+
instance_eval(&block)
|
13
|
+
apply_definitions
|
14
|
+
end
|
15
|
+
|
16
|
+
def accounts
|
17
|
+
@accounts ||= { 'amazon-elb' => 'amazon-elb', 'self' => Fog::Bouncer.aws_account_id }
|
18
|
+
end
|
19
|
+
|
20
|
+
def define(name, source, &block)
|
21
|
+
raise SourceBlockRequired unless block_given?
|
22
|
+
@definitions[name] = { source: source, block: block }
|
23
|
+
end
|
24
|
+
|
25
|
+
def definitions(name)
|
26
|
+
@definitions[name] || raise(DefinitionNotFound.new("No definition found for #{name}."))
|
27
|
+
end
|
28
|
+
|
29
|
+
def extra_remote_groups
|
30
|
+
groups.select { |group| !group.local? && group.remote? }
|
31
|
+
end
|
32
|
+
|
33
|
+
def groups
|
34
|
+
@groups ||= []
|
35
|
+
end
|
36
|
+
|
37
|
+
def import_remote_groups
|
38
|
+
Fog::Bouncer.fog.security_groups.each do |remote_group|
|
39
|
+
group = group(remote_group.name, remote_group.description)
|
40
|
+
group.remote = remote_group
|
41
|
+
IPPermissions.to(group, remote_group.ip_permissions) if remote_group.ip_permissions
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
def missing_remote_groups
|
46
|
+
groups.select { |group| group.local? && !group.remote? }
|
47
|
+
end
|
48
|
+
|
49
|
+
def sync
|
50
|
+
GroupManager.new(self).synchronize
|
51
|
+
end
|
52
|
+
|
53
|
+
def use(name)
|
54
|
+
@using << definitions(name)
|
55
|
+
end
|
56
|
+
|
57
|
+
def clear_remote
|
58
|
+
GroupManager.new(self).clear
|
59
|
+
end
|
60
|
+
|
61
|
+
private
|
62
|
+
|
63
|
+
def account(name, account_id)
|
64
|
+
accounts[name] = account_id
|
65
|
+
end
|
66
|
+
|
67
|
+
def apply_definitions
|
68
|
+
return if @using.empty?
|
69
|
+
|
70
|
+
@using.each do |definition|
|
71
|
+
@groups.each do |group|
|
72
|
+
group.add_source(definition[:source], &definition[:block])
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
def group(name, description, &block)
|
78
|
+
group = groups.find { |group| group.name == name }
|
79
|
+
if group.nil?
|
80
|
+
group = Group.new(name, description, self, &block)
|
81
|
+
groups << group
|
82
|
+
end
|
83
|
+
|
84
|
+
group
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
@@ -0,0 +1,87 @@
|
|
1
|
+
module Fog
|
2
|
+
module Bouncer
|
3
|
+
class Source
|
4
|
+
attr_reader :group, :source
|
5
|
+
attr_writer :local, :remote
|
6
|
+
|
7
|
+
def initialize(source, group, &block)
|
8
|
+
@source = source
|
9
|
+
@group = group
|
10
|
+
if block_given?
|
11
|
+
@local = true
|
12
|
+
instance_eval(&block)
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
def extras
|
17
|
+
protocols.select { |protocol| !protocol.local? }
|
18
|
+
end
|
19
|
+
|
20
|
+
def add_protocol(type, port)
|
21
|
+
protocol = protocols.find { |p| p.match(type, port) }
|
22
|
+
if protocol.nil?
|
23
|
+
protocol = case type.to_sym
|
24
|
+
when :icmp
|
25
|
+
Fog::Bouncer::Protocols::ICMP.new(port, self)
|
26
|
+
when :tcp
|
27
|
+
Fog::Bouncer::Protocols::TCP.new(port, self)
|
28
|
+
when :udp
|
29
|
+
Fog::Bouncer::Protocols::UDP.new(port, self)
|
30
|
+
end
|
31
|
+
|
32
|
+
protocols << protocol
|
33
|
+
end
|
34
|
+
|
35
|
+
protocol
|
36
|
+
end
|
37
|
+
|
38
|
+
def local
|
39
|
+
@local ||= false
|
40
|
+
end
|
41
|
+
|
42
|
+
def local?
|
43
|
+
!!local
|
44
|
+
end
|
45
|
+
|
46
|
+
def missing
|
47
|
+
protocols.select { |protocol| protocol.local? && !protocol.remote? }
|
48
|
+
end
|
49
|
+
|
50
|
+
def protocols
|
51
|
+
@protocols ||= []
|
52
|
+
end
|
53
|
+
|
54
|
+
def remote
|
55
|
+
@remote ||= false
|
56
|
+
end
|
57
|
+
|
58
|
+
def remote?
|
59
|
+
!!remote
|
60
|
+
end
|
61
|
+
|
62
|
+
def ==(other)
|
63
|
+
source == other.source &&
|
64
|
+
group == other.group &&
|
65
|
+
protocols.sort! == other.protocols.sort!
|
66
|
+
end
|
67
|
+
|
68
|
+
def inspect
|
69
|
+
"<#{self.class.name} @source=#{source.inspect} @local=#{local} @remote=#{remote} @protocols=#{protocols.inspect}>"
|
70
|
+
end
|
71
|
+
|
72
|
+
private
|
73
|
+
|
74
|
+
def icmp(*ports)
|
75
|
+
ports.each { |port| p = add_protocol(:icmp, port); p.local = true }
|
76
|
+
end
|
77
|
+
|
78
|
+
def tcp(*ports)
|
79
|
+
ports.each { |port| p = add_protocol(:tcp, port); p.local = true }
|
80
|
+
end
|
81
|
+
|
82
|
+
def udp(*ports)
|
83
|
+
ports.each { |port| p = add_protocol(:udp, port); p.local = true }
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
@@ -0,0 +1,59 @@
|
|
1
|
+
module Fog
|
2
|
+
module Bouncer
|
3
|
+
class SourceManager
|
4
|
+
def self.log(data, &block)
|
5
|
+
Fog::Bouncer.log({source_manager: true}.merge(data), &block)
|
6
|
+
end
|
7
|
+
|
8
|
+
def log(data, &block)
|
9
|
+
self.class.log({group_name: @group.name}.merge(data), &block)
|
10
|
+
end
|
11
|
+
|
12
|
+
def initialize(group)
|
13
|
+
@group = group
|
14
|
+
end
|
15
|
+
|
16
|
+
def synchronize
|
17
|
+
log(synchronize: true) do
|
18
|
+
create_missing_source_permissions
|
19
|
+
remove_extra_source_permissions
|
20
|
+
@group.sources.each { |s| s.remote = true } unless Fog::Bouncer.pretending?
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
private
|
25
|
+
|
26
|
+
def create_missing_source_permissions
|
27
|
+
if missing_source_permissions.any?
|
28
|
+
@group.remote.connection.authorize_security_group_ingress(@group.name, "IpPermissions" => IPPermissions.from(missing_source_permissions, :local_only => true)) unless Fog::Bouncer.pretending?
|
29
|
+
missing_source_permissions.each do |protocol|
|
30
|
+
log({authorized: true}.merge(protocol.to_log))
|
31
|
+
protocol.remote = true unless Fog::Bouncer.pretending?
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
def missing_source_permissions
|
37
|
+
@group.sources.map do |source|
|
38
|
+
source.protocols.select { |p| p.local? && !p.remote? }
|
39
|
+
end.flatten.compact
|
40
|
+
end
|
41
|
+
|
42
|
+
def remove_extra_source_permissions
|
43
|
+
if extra_source_permissions.any?
|
44
|
+
@group.remote.connection.revoke_security_group_ingress(@group.name, "IpPermissions" => IPPermissions.from(extra_source_permissions, :remote_only => true)) unless Fog::Bouncer.pretending?
|
45
|
+
extra_source_permissions.each do |protocol|
|
46
|
+
log({revoked: true}.merge(protocol.to_log))
|
47
|
+
protocol.source.protocols.delete_if { |p| p == protocol } unless Fog::Bouncer.pretending?
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
def extra_source_permissions
|
53
|
+
@group.sources.map do |source|
|
54
|
+
source.protocols.select { |p| !p.local? && p.remote? }
|
55
|
+
end.flatten.compact
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
@@ -0,0 +1,61 @@
|
|
1
|
+
require "fog/bouncer/source"
|
2
|
+
require "ipaddress"
|
3
|
+
|
4
|
+
module Fog
|
5
|
+
module Bouncer
|
6
|
+
module Sources
|
7
|
+
def self.for(source, group, &block)
|
8
|
+
if source =~ /^\d+\.\d+\.\d+.\d+\/\d+$/
|
9
|
+
CIDR.new(source, group, &block)
|
10
|
+
else
|
11
|
+
Group.new(source, group, &block)
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
class CIDR < Fog::Bouncer::Source
|
16
|
+
def initialize(source, group, &block)
|
17
|
+
source = IPAddress::IPv4.new(source).to_string
|
18
|
+
super
|
19
|
+
end
|
20
|
+
|
21
|
+
def match(source)
|
22
|
+
range == source
|
23
|
+
end
|
24
|
+
|
25
|
+
def range
|
26
|
+
@source
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
class Group < Fog::Bouncer::Source
|
31
|
+
attr_reader :name, :user_alias, :user_id
|
32
|
+
|
33
|
+
def initialize(source, group, &block)
|
34
|
+
super
|
35
|
+
case source
|
36
|
+
when /^(.+)@(.+)$/
|
37
|
+
@name = $1
|
38
|
+
@user_alias = $2
|
39
|
+
if @user_alias[/^\d+$/]
|
40
|
+
@user_id = @user_alias
|
41
|
+
if account = group.security.accounts.find { |key, id| id == @user_id }
|
42
|
+
@user_alias = account[0]
|
43
|
+
end
|
44
|
+
end
|
45
|
+
else
|
46
|
+
@name = source
|
47
|
+
@user_alias = 'self'
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
def match(source)
|
52
|
+
"#{name}@#{user_id}" == source || name == source
|
53
|
+
end
|
54
|
+
|
55
|
+
def user_id
|
56
|
+
@user_id ||= group.security.accounts[user_alias]
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|