Merge ~ack/maas:dns-records-filter-owned into maas:master
- Git
- lp:~ack/maas
- dns-records-filter-owned
- Merge into master
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) |
Related bugs: |
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
Description of the change
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.
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.
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
1 | diff --git a/src/maasserver/models/dnsdata.py b/src/maasserver/models/dnsdata.py |
2 | index 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 | |
72 | diff --git a/src/maasserver/models/domain.py b/src/maasserver/models/domain.py |
73 | index 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, |
111 | diff --git a/src/maasserver/models/staticipaddress.py b/src/maasserver/models/staticipaddress.py |
112 | index 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): |
241 | diff --git a/src/maasserver/models/tests/test_dnsdata.py b/src/maasserver/models/tests/test_dnsdata.py |
242 | index 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") |
263 | diff --git a/src/maasserver/models/tests/test_domain.py b/src/maasserver/models/tests/test_domain.py |
264 | index 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) |
302 | diff --git a/src/maasserver/models/tests/test_staticipaddress.py b/src/maasserver/models/tests/test_staticipaddress.py |
303 | index 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 = {} |
329 | diff --git a/src/maasserver/websockets/handlers/domain.py b/src/maasserver/websockets/handlers/domain.py |
330 | index 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({ |
343 | diff --git a/src/maasserver/websockets/handlers/tests/test_domain.py b/src/maasserver/websockets/handlers/tests/test_domain.py |
344 | index 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, {}) |
UNIT TESTS filter- owned lp:~ack/maas/+git/maas into -b master lp:~maas-committers/maas
-b dns-records-
STATUS: SUCCESS 0dea1654582fb51 09e50a0bde
COMMIT: 4233e2c4f7780af