fog-bouncer 0.0.6
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/.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
|