Skip to content

Commit 0bc4e71

Browse files
authored
Merge pull request #223 from andrewsomething/firewall-improvements
Firewall improvements
2 parents 2ca07d9 + 8dbc2f4 commit 0bc4e71

7 files changed

Lines changed: 257 additions & 9 deletions

File tree

README.md

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,36 @@ droplet = digitalocean.Droplet(token="secretspecialuniquesnowflake",
131131
droplet.create()
132132
```
133133

134+
### Creating a Firewall
135+
136+
This example creates a firewall that only accepts inbound tcp traffic on port 80 from a specific load balancer and allows outbout tcp traffic on all ports to all addresses.
137+
138+
```python
139+
from digitalocean import Firewall, InboundRule, OutboundRule, Destinations, Sources
140+
141+
inbound_rule = InboundRule(protocol="tcp", ports="80",
142+
sources=Sources(
143+
load_balancer_uids=[
144+
"4de7ac8b-495b-4884-9a69-1050c6793cd6"]
145+
)
146+
)
147+
148+
outbound_rule = OutboundRule(protocol="tcp", ports="all",
149+
destinations=Destinations(
150+
addresses=[
151+
"0.0.0.0/0",
152+
"::/0"]
153+
)
154+
)
155+
156+
firewall = Firewall(token="secretspecialuniquesnowflake",
157+
name="new-firewall",
158+
inbound_rules=[inbound_rule],
159+
outbound_rules=[outbound_rule],
160+
droplet_ids=[8043964, 8043972])
161+
firewall.create()
162+
```
163+
134164
## Getting account requests/hour limits status:
135165
Each request will also include the rate limit information:
136166

digitalocean/Firewall.py

Lines changed: 166 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,144 @@
11
# -*- coding: utf-8 -*-
22
from .baseapi import BaseAPI, POST, DELETE, PUT
3+
import jsonpickle
4+
5+
6+
class _targets(object):
7+
"""
8+
An internal object that both `Sources` and `Destinations` derive from.
9+
10+
Not for direct use by end users.
11+
"""
12+
def __init__(self, **kwargs):
13+
self.addresses = []
14+
self.droplet_ids = []
15+
self.load_balancer_uids = []
16+
self.tags = []
17+
18+
for attr in kwargs.keys():
19+
setattr(self, attr, kwargs[attr])
20+
21+
22+
class Sources(_targets):
23+
"""
24+
An object holding information about an InboundRule's sources.
25+
26+
Args:
27+
addresses (obj:`list`): An array of strings containing the IPv4
28+
addresses, IPv6 addresses, IPv4 CIDRs, and/or IPv6 CIDRs to which
29+
the Firewall will allow traffic.
30+
droplet_ids (obj:`list`): An array containing the IDs of the Droplets
31+
to which the Firewall will allow traffic.
32+
load_balancer_uids (obj:`list`): An array containing the IDs of the
33+
Load Balancers to which the Firewall will allow traffic.
34+
tags (obj:`list`): An array containing the names of Tags corresponding
35+
to groups of Droplets to which the Firewall will allow traffic.
36+
"""
37+
pass
38+
39+
40+
class Destinations(_targets):
41+
"""
42+
An object holding information about an OutboundRule's destinations.
43+
44+
Args:
45+
addresses (obj:`list`): An array of strings containing the IPv4
46+
addresses, IPv6 addresses, IPv4 CIDRs, and/or IPv6 CIDRs to which
47+
the Firewall will allow traffic.
48+
droplet_ids (obj:`list`): An array containing the IDs of the Droplets
49+
to which the Firewall will allow traffic.
50+
load_balancer_uids (obj:`list`): An array containing the IDs of the
51+
Load Balancers to which the Firewall will allow traffic.
52+
tags (obj:`list`): An array containing the names of Tags corresponding
53+
to groups of Droplets to which the Firewall will allow traffic.
54+
"""
55+
pass
56+
57+
58+
class InboundRule(object):
59+
"""
60+
An object holding information about a Firewall's inbound rule.
61+
62+
Args:
63+
protocol (str): The type of traffic to be allowed. This may be one
64+
of "tcp", "udp", or "icmp".
65+
port (str): The ports on which traffic will be allowed specified as a
66+
string containing a single port, a range (e.g. "8000-9000"), or
67+
"all" to open all ports for a protocol.
68+
sources (obj): A `Sources` object.
69+
"""
70+
def __init__(self, protocol="", ports="", sources=""):
71+
self.protocol = protocol
72+
self.ports = ports
73+
74+
if isinstance(sources, Sources):
75+
self.sources = sources
76+
else:
77+
for source in sources:
78+
self.sources = Sources(**sources)
79+
80+
81+
class OutboundRule(object):
82+
"""
83+
An object holding information about a Firewall's outbound rule.
84+
85+
Args:
86+
protocol (str): The type of traffic to be allowed. This may be one
87+
of "tcp", "udp", or "icmp".
88+
port (str): The ports on which traffic will be allowed specified as a
89+
string containing a single port, a range (e.g. "8000-9000"), or
90+
"all" to open all ports for a protocol.
91+
destinations (obj): A `Destinations` object.
92+
"""
93+
def __init__(self, protocol="", ports="", destinations=""):
94+
self.protocol = protocol
95+
self.ports = ports
96+
97+
if isinstance(destinations, Destinations):
98+
self.destinations = destinations
99+
else:
100+
for destination in destinations:
101+
self.destinations = Destinations(**destinations)
3102

4103

5104
class Firewall(BaseAPI):
105+
"""
106+
An object representing an DigitalOcean Firewall.
107+
108+
Attributes accepted at creation time:
109+
110+
Args:
111+
name (str): The Firewall's name.
112+
droplet_ids (obj:`list` of `int`): A list of Droplet IDs to be assigned
113+
to the Firewall.
114+
tags (obj:`list` of `str`): A list Tag names to be assigned to the
115+
Firewall.
116+
inbound_rules (obj:`list`): A list of `InboundRules` objects
117+
outbound_rules (obj:`list`): A list of `OutboundRules` objects
118+
119+
Attributes returned by API:
120+
id (str): A UUID to identify and reference a Firewall.
121+
status (str): A status string indicating the current state of the
122+
Firewall. This can be "waiting", "succeeded", or "failed".
123+
created_at (str): The time at which the Firewall was created.
124+
name (str): The Firewall's name.
125+
pending_changes (obj:`list`): Details exactly which Droplets are having
126+
their security policies updated.
127+
droplet_ids (obj:`list` of `int`): A list of Droplet IDs to be assigned
128+
to the Firewall.
129+
tags (obj:`list` of `str`): A list Tag names to be assigned to the
130+
Firewall.
131+
inbound_rules (obj:`list`): A list of `InboundRules` objects
132+
outbound_rules (obj:`list`): A list of `OutboundRules` objects
133+
"""
6134
def __init__(self, *args, **kwargs):
7135
self.id = None
8136
self.status = None
9137
self.created_at = None
10138
self.pending_changes = []
11139
self.name = None
12-
self.inbound_rules = None
13-
self.outbound_rules = None
140+
self.inbound_rules = []
141+
self.outbound_rules = []
14142
self.droplet_ids = None
15143
self.tags = None
16144

@@ -25,13 +153,45 @@ def get_object(cls, api_token, firewall_id):
25153
firewall.load()
26154
return firewall
27155

156+
def _set_firewall_attributes(self, data):
157+
self.id = data['firewall']['id']
158+
self.name = data['firewall']['name']
159+
self.status = data['firewall']['status']
160+
self.created_at = data['firewall']['created_at']
161+
self.pending_changes = data['firewall']['pending_changes']
162+
self.droplet_ids = data['firewall']['droplet_ids']
163+
self.tags = data['firewall']['tags']
164+
165+
in_rules = list()
166+
for rule in data['firewall']['inbound_rules']:
167+
in_rules.append(InboundRule(**rule))
168+
self.inbound_rules = in_rules
169+
170+
out_rules = list()
171+
for rule in data['firewall']['outbound_rules']:
172+
out_rules.append(OutboundRule(**rule))
173+
self.outbound_rules = out_rules
174+
28175
def load(self):
29176
data = self.get_data("firewalls/%s" % self.id)
30-
firewall_dict = data['firewall']
177+
if data:
178+
self._set_firewall_attributes(data)
179+
180+
return self
181+
182+
def create(self, *args, **kwargs):
183+
inbound = jsonpickle.encode(self.inbound_rules, unpicklable=False)
184+
outbound = jsonpickle.encode(self.outbound_rules, unpicklable=False)
185+
params = {'name': self.name,
186+
'droplet_ids': self.droplet_ids,
187+
'inbound_rules': jsonpickle.decode(inbound),
188+
'outbound_rules': jsonpickle.decode(outbound),
189+
'tags': self.tags}
190+
191+
data = self.get_data('firewalls/', type=POST, params=params)
31192

32-
# Setting the attribute values
33-
for attr in firewall_dict.keys():
34-
setattr(self, attr, firewall_dict[attr])
193+
if data:
194+
self._set_firewall_attributes(data)
35195

36196
return self
37197

digitalocean/Manager.py

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
from .Domain import Domain
1212
from .Droplet import Droplet
1313
from .FloatingIP import FloatingIP
14-
from .Firewall import Firewall
14+
from .Firewall import Firewall, InboundRule, OutboundRule
1515
from .Image import Image
1616
from .LoadBalancer import LoadBalancer
1717
from .LoadBalancer import StickySesions, HealthCheck, ForwardingRule
@@ -359,6 +359,14 @@ def get_all_firewalls(self):
359359
for jsoned in data['firewalls']:
360360
firewall = Firewall(**jsoned)
361361
firewall.token = self.token
362+
in_rules = list()
363+
for rule in jsoned['inbound_rules']:
364+
in_rules.append(InboundRule(**rule))
365+
firewall.inbound_rules = in_rules
366+
out_rules = list()
367+
for rule in jsoned['outbound_rules']:
368+
out_rules.append(OutboundRule(**rule))
369+
firewall.outbound_rules = out_rules
362370
firewalls.append(firewall)
363371
return firewalls
364372

digitalocean/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,4 +26,4 @@
2626
from .LoadBalancer import StickySesions, ForwardingRule, HealthCheck
2727
from .Certificate import Certificate
2828
from .Snapshot import Snapshot
29-
from .Firewall import Firewall
29+
from .Firewall import Firewall, InboundRule, OutboundRule, Destinations, Sources

digitalocean/tests/test_firewall.py

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,19 @@ def test_load(self):
4141
self.assertEqual(f.id, 12345)
4242
self.assertEqual(f.name, "firewall")
4343
self.assertEqual(f.status, "succeeded")
44-
# TODO: Assert the inbound and outbound rules
44+
self.assertEqual(f.inbound_rules[0].ports, "80")
45+
self.assertEqual(f.inbound_rules[0].protocol, "tcp")
46+
self.assertEqual(f.inbound_rules[0].sources.load_balancer_uids,
47+
["12345"])
48+
self.assertEqual(f.inbound_rules[0].sources.addresses, [])
49+
self.assertEqual(f.inbound_rules[0].sources.tags, [])
50+
self.assertEqual(f.outbound_rules[0].ports, "80")
51+
self.assertEqual(f.outbound_rules[0].protocol, "tcp")
52+
self.assertEqual(
53+
f.outbound_rules[0].destinations.load_balancer_uids, [])
54+
self.assertEqual(f.outbound_rules[0].destinations.addresses,
55+
["0.0.0.0/0", "::/0"])
56+
self.assertEqual(f.outbound_rules[0].destinations.tags, [])
4557
self.assertEqual(f.created_at, "2017-05-23T21:24:00Z")
4658
self.assertEqual(f.droplet_ids, [12345])
4759
self.assertEqual(f.tags, [])

digitalocean/tests/test_manager.py

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -562,5 +562,42 @@ def test_get_volume_snapshots(self):
562562
self.assertEqual(volume_snapshots[0].resource_type, 'volume')
563563
self.assertEqual(len(volume_snapshots[0].regions), 1)
564564

565+
@responses.activate
566+
def test_get_firewalls(self):
567+
data = self.load_from_file('firewalls/all.json')
568+
569+
url = self.base_url + "firewalls"
570+
responses.add(responses.GET,
571+
url,
572+
body=data,
573+
status=200,
574+
content_type='application/json')
575+
576+
firewalls = self.manager.get_all_firewalls()
577+
f = firewalls[0]
578+
579+
self.assert_get_url_equal(responses.calls[0].request.url, url)
580+
self.assertEqual(f.id, "12345")
581+
self.assertEqual(f.name, "firewall")
582+
self.assertEqual(f.status, "succeeded")
583+
self.assertEqual(f.inbound_rules[0].ports, "80")
584+
self.assertEqual(f.inbound_rules[0].protocol, "tcp")
585+
self.assertEqual(f.inbound_rules[0].sources.load_balancer_uids,
586+
["12345"])
587+
self.assertEqual(f.inbound_rules[0].sources.addresses, [])
588+
self.assertEqual(f.inbound_rules[0].sources.tags, [])
589+
self.assertEqual(f.outbound_rules[0].ports, "80")
590+
self.assertEqual(f.outbound_rules[0].protocol, "tcp")
591+
self.assertEqual(
592+
f.outbound_rules[0].destinations.load_balancer_uids, [])
593+
self.assertEqual(f.outbound_rules[0].destinations.addresses,
594+
["0.0.0.0/0", "::/0"])
595+
self.assertEqual(f.outbound_rules[0].destinations.tags, [])
596+
self.assertEqual(f.created_at, "2017-05-23T21:24:00Z")
597+
self.assertEqual(f.droplet_ids, [12345])
598+
self.assertEqual(f.tags, [])
599+
self.assertEqual(f.pending_changes, [])
600+
601+
565602
if __name__ == '__main__':
566603
unittest.main()

requirements.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
requests>=2.2.1
2+
jsonpickle
23

34
# Testing requirements
45
pytest

0 commit comments

Comments
 (0)