Skip to content

Commit 7c2aabf

Browse files
author
Marcus Sorensen
committed
Don't allow inadvertent deletion of hidden details via API
1 parent f7345e8 commit 7c2aabf

File tree

2 files changed

+130
-19
lines changed

2 files changed

+130
-19
lines changed

server/src/main/java/com/cloud/vm/UserVmManagerImpl.java

Lines changed: 20 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2756,13 +2756,18 @@ public UserVm updateVirtualMachine(UpdateVMCmd cmd) throws ResourceUnavailableEx
27562756
final List<String> userReadOnlySettings = Stream.of(QueryService.UserVMReadOnlyDetails.value().split(","))
27572757
.map(item -> (item).trim())
27582758
.collect(Collectors.toList());
2759+
List<UserVmDetailVO> existingDetails = userVmDetailsDao.listDetails(id);
27592760
if (cleanupDetails){
27602761
if (caller != null && caller.getType() == Account.Type.ADMIN) {
2761-
userVmDetailsDao.removeDetails(id);
2762+
for (final UserVmDetailVO detail : existingDetails) {
2763+
if (detail != null && detail.isDisplay()) {
2764+
userVmDetailsDao.removeDetail(id, detail.getName());
2765+
}
2766+
}
27622767
} else {
2763-
for (final UserVmDetailVO detail : userVmDetailsDao.listDetails(id)) {
2768+
for (final UserVmDetailVO detail : existingDetails) {
27642769
if (detail != null && !userDenyListedSettings.contains(detail.getName())
2765-
&& !userReadOnlySettings.contains(detail.getName())) {
2770+
&& !userReadOnlySettings.contains(detail.getName()) && detail.isDisplay()) {
27662771
userVmDetailsDao.removeDetail(id, detail.getName());
27672772
}
27682773
}
@@ -2782,15 +2787,25 @@ public UserVm updateVirtualMachine(UpdateVMCmd cmd) throws ResourceUnavailableEx
27822787
if (userReadOnlySettings.contains(detailName)) {
27832788
throw new InvalidParameterValueException("You're not allowed to add or edit the read-only setting: " + detailName);
27842789
}
2790+
if (existingDetails.stream().anyMatch(d -> Objects.equals(d.getName(), detailName) && !d.isDisplay())){
2791+
throw new InvalidParameterValueException("You're not allowed to add or edit the non-displayable setting: " + detailName);
2792+
}
27852793
}
2786-
// Add any hidden/denied or read-only detail
2787-
for (final UserVmDetailVO detail : userVmDetailsDao.listDetails(id)) {
2794+
// Add any existing user denied or read-only details. We do it here because admins would already provide these (or can delete them).
2795+
for (final UserVmDetailVO detail : existingDetails) {
27882796
if (userDenyListedSettings.contains(detail.getName()) || userReadOnlySettings.contains(detail.getName())) {
27892797
details.put(detail.getName(), detail.getValue());
27902798
}
27912799
}
27922800
}
27932801

2802+
// ensure details marked as non-displayable are maintained, regardless of admin or not
2803+
for (final UserVmDetailVO existingDetail : existingDetails) {
2804+
if (!existingDetail.isDisplay()) {
2805+
details.put(existingDetail.getName(), existingDetail.getValue());
2806+
}
2807+
}
2808+
27942809
verifyVmLimits(vmInstance, details);
27952810
vmInstance.setDetails(details);
27962811
_vmDao.saveDetails(vmInstance);

test/integration/component/test_update_vm.py

Lines changed: 110 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -14,12 +14,13 @@
1414
# KIND, either express or implied. See the License for the
1515
# specific language governing permissions and limitations
1616
# under the License.
17-
17+
import json
1818

1919
from marvin.cloudstackTestCase import cloudstackTestCase
2020
from marvin.lib.base import Account, VirtualMachine, ServiceOffering
21-
from marvin.lib.utils import cleanup_resources
21+
from marvin.lib.utils import (validateList, cleanup_resources)
2222
from marvin.lib.common import get_zone, get_domain, get_template
23+
from marvin.codes import PASS
2324
from nose.plugins.attrib import attr
2425

2526
class TestData(object):
@@ -59,6 +60,7 @@ class TestUpdateVirtualMachine(cloudstackTestCase):
5960
def setUp(self):
6061
self.testdata = TestData().testdata
6162
self.apiclient = self.testClient.getApiClient()
63+
self.dbclient = self.testClient.getDbConnection()
6264

6365
# Get Zone, Domain and Default Built-in template
6466
self.domain = get_domain(self.apiclient)
@@ -106,18 +108,7 @@ def test_update_vm_name(self):
106108
templateid=self.template.id
107109
)
108110

109-
list_vms = VirtualMachine.list(self.apiclient, id=self.virtual_machine.id)
110-
self.assertEqual(
111-
isinstance(list_vms, list),
112-
True,
113-
"List VM response was not a valid list"
114-
)
115-
self.assertNotEqual(
116-
len(list_vms),
117-
0,
118-
"List VM response was empty"
119-
)
120-
vm = list_vms[0]
111+
vm = self.listVmById(self.virtual_machine.id)
121112

122113
self.debug(
123114
"VirtualMachine launched with id, name, displayname: %s %s %s"\
@@ -152,6 +143,111 @@ def test_update_vm_name(self):
152143
self.assertEqual(vmnew.displayname, vmnewstarted.displayname,
153144
msg="display name changed on start, displayname is %s" % vmnewstarted.displayname)
154145

146+
@attr(tags=['advanced', 'simulator', 'basic', 'sg', 'details'], required_hardware="false")
147+
def test_update_vm_details_admin(self):
148+
"""Test Update VirtualMachine Details
149+
150+
# Set up a VM
151+
# Set up hidden detail in DB for VM
152+
153+
# Validate the following:
154+
# 1. Can add two details (detail1, detail2)
155+
# 2. Can fetch new details on VM
156+
# 3. Can delete detail1
157+
# 4. Hidden detail not removed
158+
# 6. The detail2 remains
159+
# 7. Ensure cleanup parameter doesn't remove hidden details
160+
"""
161+
hidden_detail_name = "configDriveLocation"
162+
detail1 = "detail1"
163+
detail2 = "detail2"
164+
165+
# set up a VM
166+
self.virtual_machine = VirtualMachine.create(
167+
self.apiclient,
168+
self.testdata["virtual_machine"],
169+
accountid=self.account.name,
170+
zoneid=self.zone.id,
171+
domainid=self.account.domainid,
172+
serviceofferingid=self.service_offering.id,
173+
templateid=self.template.id
174+
)
175+
176+
vm = self.listVmById(self.virtual_machine.id)
177+
178+
self.debug(
179+
"VirtualMachine launched with id, name, displayname: %s %s %s" \
180+
% (self.virtual_machine.id, vm.name, vm.displayname)
181+
)
182+
183+
# set up a hidden detail
184+
dbresult = self.dbclient.execute("select id from vm_instance where uuid='%s'" % vm.id)
185+
self.assertEqual(validateList(dbresult)[0], PASS, "sql query returned invalid response")
186+
vm_db_id = dbresult[0][0]
187+
self.debug("VM has database id %d" % vm_db_id)
188+
189+
self.dbclient.execute("insert into user_vm_details (vm_id, name, value, display) values (%d,'%s','HOST', 0)" % (vm_db_id, hidden_detail_name))
190+
191+
vm = self.listVmById(self.virtual_machine.id)
192+
self.debug("VirtualMachine fetched with details: %s of type %s" % (vm.details, type(vm.details)))
193+
194+
self.assertIsNone(vm.details[hidden_detail_name], "hidden detail should be hidden")
195+
196+
# add two details by appending to what was returned via API
197+
updating_vm_details = vm.details.__dict__
198+
updating_vm_details[detail1] = "foo"
199+
updating_vm_details[detail2] = "bar"
200+
201+
self.debug("Updating VM to new details: %s" % updating_vm_details)
202+
vm = self.virtual_machine.update(self.apiclient, details=[updating_vm_details])
203+
204+
self.assertIsNotNone(vm.details[detail1], "Expect " + detail1)
205+
self.assertIsNotNone(vm.details[detail2], "Expect " + detail2)
206+
self.assertIsNone(vm.details[hidden_detail_name], "hidden detail should be hidden")
207+
self.assertTrue(self.detailInDatabase(vm_db_id, hidden_detail_name), "hidden detail should still exist in db")
208+
209+
# delete one detail
210+
updating_vm_details = vm.details.__dict__
211+
del updating_vm_details["detail1"]
212+
213+
self.debug("Deleting one detail by updating details: %s" % updating_vm_details)
214+
vm = self.virtual_machine.update(self.apiclient, details=[updating_vm_details])
215+
216+
self.assertIsNone(vm.details[detail1], "Do not expect " + detail1)
217+
self.assertIsNotNone(vm.details[detail2], "Expect " + detail2)
218+
self.assertIsNone(vm.details[hidden_detail_name], "hidden detail should be hidden")
219+
self.assertTrue(self.detailInDatabase(vm_db_id, hidden_detail_name), "hidden detail should still exist in db")
220+
221+
# cleanup, ensure hidden detail is not deleted
222+
vm = self.virtual_machine.update(self.apiclient, cleanupdetails="true")
223+
self.assertIsNone(vm.details[detail1], "Do not expect " + detail1)
224+
self.assertIsNone(vm.details[detail2], "Do not expect " + detail2)
225+
self.assertIsNone(vm.details[hidden_detail_name], "hidden detail should be hidden")
226+
self.assertTrue(self.detailInDatabase(vm_db_id, hidden_detail_name), "hidden detail should still exist in db")
227+
228+
229+
def detailInDatabase(self, vm_id, detail_name):
230+
dbresult = self.dbclient.execute("select id from user_vm_details where vm_id=%s and name='%s'" % (vm_id, detail_name))
231+
self.debug("Detail %s for VM %s: %s" % (detail_name, vm_id, dbresult))
232+
if validateList(dbresult)[0] == PASS:
233+
return True
234+
return False
235+
236+
237+
def listVmById(self, id):
238+
list_vms = VirtualMachine.list(self.apiclient, id=id)
239+
self.assertEqual(
240+
isinstance(list_vms, list),
241+
True,
242+
"List VM response was not a valid list"
243+
)
244+
self.assertNotEqual(
245+
len(list_vms),
246+
0,
247+
"List VM response was empty"
248+
)
249+
return list_vms[0]
250+
155251
def tearDown(self):
156252
try:
157253
cleanup_resources(self.apiclient, self.cleanup)

0 commit comments

Comments
 (0)