Merge ~ack/maas:dns-records-filter-owned into maas:master

Proposed by Alberto Donato
Status: Merged
Approved by: Alberto Donato
Approved revision: 4233e2c4f7780af0dea1654582fb5109e50a0bde
Merge reported by: MAAS Lander
Merged at revision: not available
Proposed branch: ~ack/maas:dns-records-filter-owned
Merge into: maas:master
Diff against target: 398 lines (+126/-36)
8 files modified
src/maasserver/models/dnsdata.py (+14/-8)
src/maasserver/models/domain.py (+10/-10)
src/maasserver/models/staticipaddress.py (+32/-17)
src/maasserver/models/tests/test_dnsdata.py (+11/-0)
src/maasserver/models/tests/test_domain.py (+17/-0)
src/maasserver/models/tests/test_staticipaddress.py (+16/-0)
src/maasserver/websockets/handlers/domain.py (+3/-0)
src/maasserver/websockets/handlers/tests/test_domain.py (+23/-1)
Reviewer Review Type Date Requested Status
Björn Tillenius Approve
MAAS Lander Approve
Review via email: mp+345405@code.launchpad.net

Commit message

LP: #1767033 - filter DNS entries only showing those related to owned resources for limited users

To post a comment you must log in.
Revision history for this message
MAAS Lander (maas-lander) wrote :

UNIT TESTS
-b dns-records-filter-owned lp:~ack/maas/+git/maas into -b master lp:~maas-committers/maas

STATUS: SUCCESS
COMMIT: 4233e2c4f7780af0dea1654582fb5109e50a0bde

review: Approve
Revision history for this message
Björn Tillenius (bjornt) wrote :

Looks good, assuming that it's ok not to show all the entries on the page any more. Not sure what the purpose of showing the domain for non-admin users, though. Was there any discussion about that?

Assuming that was discussed, +1. Of not, it would be good to bring it up first.

review: Approve
Revision history for this message
Alberto Donato (ack) wrote :

> Looks good, assuming that it's ok not to show all the entries on the page any
> more. Not sure what the purpose of showing the domain for non-admin users,
> though. Was there any discussion about that?
>
> Assuming that was discussed, +1. Of not, it would be good to bring it up
> first.

There's probably still some use in showing entries for the nodes owned by non-admin.
Technically, it's also possible that a user that was previously an admin could still have static IPs or dns entries created, so it's somewhat useful to see that.

Revision history for this message
Andres Rodriguez (andreserl) wrote :

Machines deployed by a normal user have a hostname on a specific domain, so i think it is a good observability.

That also raises a good question though, are normal users allowed to see the DNS records via the API ? I would imagine they are? What about the domains ?

There was an error fetching revisions from git servers. Please try again in a few minutes. If the problem persists, contact Launchpad support.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1diff --git a/src/maasserver/models/dnsdata.py b/src/maasserver/models/dnsdata.py
2index 8a9f412..028b7ac 100644
3--- a/src/maasserver/models/dnsdata.py
4+++ b/src/maasserver/models/dnsdata.py
5@@ -69,15 +69,17 @@ class HostnameRRsetMapping:
6
7 def __init__(
8 self, system_id=None, rrset: set=None, node_type=None,
9- dnsresource_id=None):
10+ dnsresource_id=None, user_id=None):
11 self.system_id = system_id
12 self.node_type = node_type
13 self.dnsresource_id = dnsresource_id
14+ self.user_id = user_id
15 self.rrset = set() if rrset is None else rrset.copy()
16
17 def __repr__(self):
18- return "HostnameRRSetMapping(%r, %r, %r, %r)" % (
19- self.system_id, self.rrset, self.node_type, self.dnsresource_id)
20+ return "HostnameRRSetMapping(%r, %r, %r, %r, %r)" % (
21+ self.system_id, self.rrset, self.node_type, self.dnsresource_id,
22+ self.user_id)
23
24 def __eq__(self, other):
25 return self.__dict__ == other.__dict__
26@@ -153,6 +155,7 @@ class DNSDataManager(Manager, DNSDataQueriesMixin):
27 domain.name,
28 node.system_id,
29 node.node_type,
30+ node.user_id,
31 dnsdata.id,
32 """ + ttl_clause + """ AS ttl,
33 dnsdata.rrtype,
34@@ -173,6 +176,7 @@ class DNSDataManager(Manager, DNSDataQueriesMixin):
35 nd.hostname AS hostname,
36 nd.system_id AS system_id,
37 nd.node_type AS node_type,
38+ nd.owner_id AS user_id ,
39 nd.domain_id AS domain_id,
40 CONCAT(nd.hostname, '.', dom.name) AS fqdn
41 FROM maasserver_node AS nd
42@@ -220,7 +224,7 @@ class DNSDataManager(Manager, DNSDataQueriesMixin):
43 mapping = defaultdict(HostnameRRsetMapping)
44 cursor.execute(sql_query, (domain.id,))
45 for (dnsresource_id, name, d_name, system_id, node_type,
46- dnsdata_id, ttl, rrtype, rrdata) in cursor.fetchall():
47+ user_id, dnsdata_id, ttl, rrtype, rrdata) in cursor.fetchall():
48 if name == '@' and d_name != domain.name:
49 name, d_name = d_name.split('.', 1)
50 # Since we don't allow more than one label in dnsresource
51@@ -228,14 +232,16 @@ class DNSDataManager(Manager, DNSDataQueriesMixin):
52 assert d_name == domain.name, (
53 "Invalid domain; expected '%s' == '%s'" % (
54 d_name, domain.name))
55- mapping[name].node_type = node_type
56- mapping[name].system_id = system_id
57+ entry = mapping[name]
58+ entry.node_type = node_type
59+ entry.system_id = system_id
60+ entry.user_id = user_id
61 if with_ids:
62- mapping[name].dnsresource_id = dnsresource_id
63+ entry.dnsresource_id = dnsresource_id
64 rrtuple = (ttl, rrtype, rrdata, dnsdata_id)
65 else:
66 rrtuple = (ttl, rrtype, rrdata)
67- mapping[name].rrset.add(rrtuple)
68+ entry.rrset.add(rrtuple)
69 return mapping
70
71
72diff --git a/src/maasserver/models/domain.py b/src/maasserver/models/domain.py
73index 4735698..3751a23 100644
74--- a/src/maasserver/models/domain.py
75+++ b/src/maasserver/models/domain.py
76@@ -376,24 +376,24 @@ class Domain(CleanSave, TimestampedModel):
77 ip_mapping = StaticIPAddress.objects.get_hostname_ip_mapping(
78 self, raw_ttl=True)
79 for hostname, info in ip_mapping.items():
80- hostname = hostname[:-len(self.name) - 1]
81- rr_mapping[hostname].dnsresource_id = info.dnsresource_id
82+ entry = rr_mapping[hostname[:-len(self.name) - 1]]
83+ entry.dnsresource_id = info.dnsresource_id
84 if info.system_id is not None:
85- rr_mapping[hostname].system_id = info.system_id
86- rr_mapping[hostname].node_type = info.node_type
87+ entry.system_id = info.system_id
88+ entry.node_type = info.node_type
89+ if info.user_id is not None:
90+ entry.user_id = info.user_id
91 for ip in info.ips:
92- if IPAddress(ip).version == 6:
93- rr_mapping[hostname].rrset.add(
94- (info.ttl, 'AAAA', ip, None))
95- else:
96- rr_mapping[hostname].rrset.add(
97- (info.ttl, 'A', ip, None))
98+ record_type = 'AAAA' if IPAddress(ip).version == 6 else 'A'
99+ entry.rrset.add((info.ttl, record_type, ip, None))
100+
101 data = []
102 for hostname, info in rr_mapping.items():
103 data += [{
104 'name': hostname,
105 'system_id': info.system_id,
106 'node_type': info.node_type,
107+ 'user_id': info.user_id,
108 'dnsresource_id': info.dnsresource_id,
109 'ttl': ttl,
110 'rrtype': rrtype,
111diff --git a/src/maasserver/models/staticipaddress.py b/src/maasserver/models/staticipaddress.py
112index 837045d..bfc9d61 100644
113--- a/src/maasserver/models/staticipaddress.py
114+++ b/src/maasserver/models/staticipaddress.py
115@@ -64,6 +64,7 @@ _mapping_base_fields = (
116 'fqdn',
117 'system_id',
118 'node_type',
119+ 'user_id',
120 'ttl',
121 'ip',
122 )
123@@ -96,17 +97,18 @@ class HostnameIPMapping:
124
125 def __init__(
126 self, system_id=None, ttl=None, ips: set=None, node_type=None,
127- dnsresource_id=None):
128+ dnsresource_id=None, user_id=None):
129 self.system_id = system_id
130 self.node_type = node_type
131 self.ttl = ttl
132 self.ips = set() if ips is None else ips.copy()
133 self.dnsresource_id = dnsresource_id
134+ self.user_id = user_id
135
136 def __repr__(self):
137- return "HostnameIPMapping(%r, %r, %r, %r, %r)" % (
138+ return "HostnameIPMapping(%r, %r, %r, %r, %r, %r)" % (
139 self.system_id, self.ttl, self.ips, self.node_type,
140- self.dnsresource_id)
141+ self.dnsresource_id, self.user_id)
142
143 def __eq__(self, other):
144 return self.__dict__ == other.__dict__
145@@ -335,6 +337,7 @@ class StaticIPAddressManager(Manager):
146 COALESCE(dnsrr.fqdn, node.fqdn) AS fqdn,
147 node.system_id,
148 node.node_type,
149+ staticip.user_id,
150 """ + ttl_clause + """ AS ttl,
151 staticip.ip,
152 dnsrr.id AS dnsresource_id
153@@ -374,6 +377,7 @@ class StaticIPAddressManager(Manager):
154 dom.name as dom_name,
155 nd.system_id,
156 nd.node_type,
157+ nd.owner_id AS user_id,
158 nd.domain_id,
159 nd.address_ttl,
160 dom.ttl,
161@@ -461,13 +465,16 @@ class StaticIPAddressManager(Manager):
162 # TTL. It is left as an exercise for the admin to make sure that
163 # the any non-default TTL applied to the Node and DNSResource are
164 # equal.
165+ entry = mapping[fqdn]
166 if result.system_id is not None:
167- mapping[fqdn].node_type = result.node_type
168- mapping[fqdn].system_id = result.system_id
169+ entry.node_type = result.node_type
170+ entry.system_id = result.system_id
171 if result.ttl is not None:
172- mapping[fqdn].ttl = result.ttl
173- mapping[fqdn].ips.add(result.ip)
174- mapping[fqdn].dnsresource_id = result.dnsresource_id
175+ entry.ttl = result.ttl
176+ if result.user_id is not None:
177+ entry.user_id = result.user_id
178+ entry.ips.add(result.ip)
179+ entry.dnsresource_id = result.dnsresource_id
180 return mapping
181
182 def get_hostname_ip_mapping(self, domain_or_subnet, raw_ttl=False):
183@@ -501,6 +508,7 @@ class StaticIPAddressManager(Manager):
184 CONCAT(node.hostname, '.', domain.name) AS fqdn,
185 node.system_id,
186 node.node_type,
187+ staticip.user_id,
188 """ + ttl_clause + """ AS ttl,
189 staticip.ip,
190 COALESCE(
191@@ -594,6 +602,7 @@ class StaticIPAddressManager(Manager):
192 CONCAT(node.hostname, '.', domain.name) AS fqdn,
193 node.system_id,
194 node.node_type,
195+ node.owner_id AS user_id,
196 """ + ttl_clause + """ AS ttl,
197 staticip.ip,
198 interface.name,
199@@ -655,14 +664,17 @@ class StaticIPAddressManager(Manager):
200 # interface IPs. See Bug#1584850
201 for result in cursor.fetchall():
202 result = MappingQueryResult(*result)
203- mapping[result.fqdn].node_type = result.node_type
204- mapping[result.fqdn].system_id = result.system_id
205- mapping[result.fqdn].ttl = result.ttl
206+ entry = mapping[result.fqdn]
207+ entry.node_type = result.node_type
208+ entry.system_id = result.system_id
209+ if result.user_id is not None:
210+ entry.user_id = result.user_id
211+ entry.ttl = result.ttl
212 if result.is_boot:
213 iface_is_boot[result.fqdn] = True
214 # If we have an IP on the right interface type, save it.
215 if result.is_boot == iface_is_boot[result.fqdn]:
216- mapping[result.fqdn].ips.add(result.ip)
217+ entry.ips.add(result.ip)
218 # Next, get all the addresses, on all the interfaces, and add the ones
219 # that are not already present on the FQDN as $IFACE.$FQDN. Exclude
220 # any discovered addresses once there are any non-discovered addresses.
221@@ -675,11 +687,14 @@ class StaticIPAddressManager(Manager):
222 # node, then consider adding the IP.
223 if result.assigned or not assigned_ips[result.fqdn]:
224 if result.ip not in mapping[result.fqdn].ips:
225- name = "%s.%s" % (result.iface_name, result.fqdn)
226- mapping[name].node_type = result.node_type
227- mapping[name].system_id = result.system_id
228- mapping[name].ttl = result.ttl
229- mapping[name].ips.add(result.ip)
230+ entry = mapping[
231+ "%s.%s" % (result.iface_name, result.fqdn)]
232+ entry.node_type = result.node_type
233+ entry.system_id = result.system_id
234+ if result.user_id is not None:
235+ entry.user_id = result.user_id
236+ entry.ttl = result.ttl
237+ entry.ips.add(result.ip)
238 return mapping
239
240 def filter_by_ip_family(self, family):
241diff --git a/src/maasserver/models/tests/test_dnsdata.py b/src/maasserver/models/tests/test_dnsdata.py
242index fa4923e..3b769be 100644
243--- a/src/maasserver/models/tests/test_dnsdata.py
244+++ b/src/maasserver/models/tests/test_dnsdata.py
245@@ -287,6 +287,17 @@ class TestDNSDataMapping(MAASServerTestCase):
246 actual = DNSData.objects.get_hostname_dnsdata_mapping(domain)
247 self.assertEqual(expected_mapping, actual)
248
249+ def test_get_hostname_dnsdata_mapping_includes_node_owner_id(self):
250+ domain = Domain.objects.get_default_domain()
251+ user = factory.make_User()
252+ node_name = factory.make_name("node")
253+ factory.make_Node_with_Interface_on_Subnet(
254+ hostname=node_name, domain=domain, owner=user)
255+ dnsrr = factory.make_DNSResource(domain=domain, name=node_name)
256+ factory.make_DNSData(dnsresource=dnsrr, ip_addresses=True)
257+ mapping = DNSData.objects.get_hostname_dnsdata_mapping(domain)
258+ self.assertEqual(mapping[node_name].user_id, user.id)
259+
260 def test_get_hostname_dnsdata_mapping_returns_mapping_at_domain(self):
261 parent = Domain.objects.get_default_domain()
262 name = factory.make_name("node")
263diff --git a/src/maasserver/models/tests/test_domain.py b/src/maasserver/models/tests/test_domain.py
264index c47ab37..f2a7f77 100644
265--- a/src/maasserver/models/tests/test_domain.py
266+++ b/src/maasserver/models/tests/test_domain.py
267@@ -381,6 +381,8 @@ class DomainTest(MAASServerTestCase):
268 hostname = hostname[:-len(domain.name) - 1]
269 if info.system_id is not None:
270 rr_map[hostname].system_id = info.system_id
271+ if info.user_id is not None:
272+ rr_map[hostname].user_id = info.user_id
273 for ip in info.ips:
274 if IPAddress(ip).version == 4:
275 rr_map[hostname].rrset.add((info.ttl, 'A', ip, None))
276@@ -392,6 +394,7 @@ class DomainTest(MAASServerTestCase):
277 'name': name,
278 'system_id': info.system_id,
279 'node_type': info.node_type,
280+ 'user_id': info.user_id,
281 'dnsresource_id': info.dnsresource_id,
282 'ttl': ttl,
283 'rrtype': rrtype,
284@@ -417,3 +420,17 @@ class DomainTest(MAASServerTestCase):
285 expected = self.render_rrdata(domain, for_list=False)
286 actual = domain.render_json_for_related_rrdata(for_list=False)
287 self.assertItemsEqual(expected, actual)
288+
289+ def test_render_json_for_related_rrdata_includes_user_id(self):
290+ domain = factory.make_Domain()
291+ node_name = factory.make_name('node')
292+ user = factory.make_User()
293+ factory.make_Node_with_Interface_on_Subnet(
294+ hostname=node_name, domain=domain, owner=user)
295+ dnsrr = factory.make_DNSResource(domain=domain, name=node_name)
296+ factory.make_DNSData(dnsresource=dnsrr, ip_addresses=True)
297+ expected = self.render_rrdata(domain, for_list=False)
298+ actual = domain.render_json_for_related_rrdata(for_list=True)
299+ self.assertEqual(actual, expected)
300+ for record in actual:
301+ self.assertEqual(record['user_id'], user.id)
302diff --git a/src/maasserver/models/tests/test_staticipaddress.py b/src/maasserver/models/tests/test_staticipaddress.py
303index f0fcda1..7b72861 100644
304--- a/src/maasserver/models/tests/test_staticipaddress.py
305+++ b/src/maasserver/models/tests/test_staticipaddress.py
306@@ -382,6 +382,22 @@ class TestStaticIPAddressManagerMapping(MAASServerTestCase):
307 mapping = StaticIPAddress.objects.get_hostname_ip_mapping(domain)
308 self.assertEqual(expected_mapping, mapping)
309
310+ def test_get_hostname_ip_mapping_includes_user_id(self):
311+ user = factory.make_User()
312+ domain = Domain.objects.get_default_domain()
313+ node_name = factory.make_name("node")
314+ node = factory.make_Node(
315+ hostname=node_name, domain=domain, owner=user, interface=True)
316+ boot_interface = node.get_boot_interface()
317+ subnet = factory.make_Subnet()
318+ factory.make_StaticIPAddress(
319+ alloc_type=IPADDRESS_TYPE.STICKY,
320+ ip=factory.pick_ip_in_Subnet(subnet),
321+ subnet=subnet, interface=boot_interface, user=user)
322+ mapping = StaticIPAddress.objects.get_hostname_ip_mapping(domain)
323+ fqdn = '{}.{}'.format(node.hostname, domain.name)
324+ self.assertEqual(mapping[fqdn].user_id, user.id)
325+
326 def test_get_hostname_ip_mapping_returns_all_mappings_for_subnet(self):
327 domain = Domain.objects.get_default_domain()
328 expected_mapping = {}
329diff --git a/src/maasserver/websockets/handlers/domain.py b/src/maasserver/websockets/handlers/domain.py
330index 35b50ab..50c435c 100644
331--- a/src/maasserver/websockets/handlers/domain.py
332+++ b/src/maasserver/websockets/handlers/domain.py
333@@ -57,6 +57,9 @@ class DomainHandler(TimestampedModelHandler, AdminOnlyMixin):
334
335 def dehydrate(self, domain, data, for_list=False):
336 rrsets = domain.render_json_for_related_rrdata(for_list=for_list)
337+ if not self.user.is_superuser:
338+ rrsets = [
339+ rrset for rrset in rrsets if rrset['user_id'] == self.user.id]
340 if not for_list:
341 data["rrsets"] = rrsets
342 data["hosts"] = len({
343diff --git a/src/maasserver/websockets/handlers/tests/test_domain.py b/src/maasserver/websockets/handlers/tests/test_domain.py
344index fe83a7d..f04f2db 100644
345--- a/src/maasserver/websockets/handlers/tests/test_domain.py
346+++ b/src/maasserver/websockets/handlers/tests/test_domain.py
347@@ -61,6 +61,8 @@ class TestDomainHandler(MAASServerTestCase):
348 name = name[:-domainname_len - 1]
349 if info.system_id is not None:
350 rr_map[name].system_id = info.system_id
351+ if info.user_id is not None:
352+ rr_map[name].user_id = info.user_id
353 for ip in info.ips:
354 if IPAddress(ip).version == 4:
355 rr_map[name].rrset.add((info.ttl, 'A', ip, None))
356@@ -72,6 +74,7 @@ class TestDomainHandler(MAASServerTestCase):
357 'name': hostname,
358 'system_id': info.system_id,
359 'node_type': info.node_type,
360+ 'user_id': info.user_id,
361 'dnsresource_id': info.dnsresource_id,
362 'ttl': ttl,
363 'rrtype': rrtype,
364@@ -94,7 +97,7 @@ class TestDomainHandler(MAASServerTestCase):
365 return data
366
367 def test_get(self):
368- user = factory.make_User()
369+ user = factory.make_admin()
370 handler = DomainHandler(user, {})
371 domain = factory.make_Domain()
372 factory.make_DNSData(domain=domain)
373@@ -104,6 +107,25 @@ class TestDomainHandler(MAASServerTestCase):
374 self.dehydrate_domain(domain),
375 handler.get({"id": domain.id}))
376
377+ def test_get_normal_user_only_owned_entries(self):
378+ user = factory.make_User()
379+ other_user = factory.make_User()
380+ handler = DomainHandler(user, {})
381+ domain = factory.make_Domain()
382+ hostname1 = factory.make_name("node")
383+ factory.make_Node_with_Interface_on_Subnet(
384+ hostname=hostname1, domain=domain, owner=user)
385+ dnsrr1 = factory.make_DNSResource(domain=domain, name=hostname1)
386+ factory.make_DNSData(dnsresource=dnsrr1, ip_addresses=True)
387+ hostname2 = factory.make_name("node")
388+ factory.make_Node_with_Interface_on_Subnet(
389+ hostname=hostname2, domain=domain, owner=other_user)
390+ dnsrr2 = factory.make_DNSResource(domain=domain, name=hostname2)
391+ factory.make_DNSData(dnsresource=dnsrr2, ip_addresses=True)
392+ details = handler.get({"id": domain.id})
393+ for entry in details['rrsets']:
394+ self.assertEqual(entry['user_id'], user.id)
395+
396 def test_list(self):
397 user = factory.make_User()
398 handler = DomainHandler(user, {})

Subscribers

People subscribed via source and target branches