This is an automated email from the ASF dual-hosted git repository.
lizhimins pushed a commit to branch rocketmq-studio
in repository https://gitbox.apache.org/repos/asf/rocketmq-dashboard.git
The following commit(s) were added to refs/heads/rocketmq-studio by this push:
new c9cafb11 fix: validate resource write requests (#981)
c9cafb11 is described below
commit c9cafb1120b60043eb28e98cfba71a520c0b9a5f
Author: aias00 <[email protected]>
AuthorDate: Wed Aug 5 02:39:21 2026 -0700
fix: validate resource write requests (#981)
---
.../studio/instance/acl/AclController.java | 20 ++++---
.../studio/instance/acl/CreateAclRuleDTO.java | 49 +++++++++++++++++
.../studio/instance/acl/UpdateAclRuleDTO.java | 52 ++++++++++++++++++
.../studio/instance/acl/UpdateAclUserDTO.java | 40 ++++++++++++++
.../studio/instance/topic/TopicController.java | 6 +--
.../studio/instance/topic/UpdateTopicDTO.java | 57 ++++++++++++++++++++
.../studio/ops/alert/AlertRuleController.java | 14 +++--
.../studio/ops/alert/AlertRuleRequestDTO.java | 63 ++++++++++++++++++++++
.../studio/instance/acl/AclControllerTest.java | 36 +++++++++++++
.../studio/instance/topic/TopicControllerTest.java | 12 +++++
.../studio/ops/alert/AlertRuleControllerTest.java | 48 +++++++++++++++++
11 files changed, 383 insertions(+), 14 deletions(-)
diff --git
a/server/src/main/java/org/apache/rocketmq/studio/instance/acl/AclController.java
b/server/src/main/java/org/apache/rocketmq/studio/instance/acl/AclController.java
index 29671efe..39d3894f 100644
---
a/server/src/main/java/org/apache/rocketmq/studio/instance/acl/AclController.java
+++
b/server/src/main/java/org/apache/rocketmq/studio/instance/acl/AclController.java
@@ -17,6 +17,7 @@
package org.apache.rocketmq.studio.instance.acl;
import org.apache.rocketmq.studio.common.domain.Result;
+import org.apache.rocketmq.studio.common.exception.BusinessException;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.GetMapping;
@@ -44,13 +45,13 @@ public class AclController {
}
@PostMapping("/rules/create")
- public Result<AclRuleVO> createRule(@RequestBody AclRuleVO rule) {
- return Result.ok(aclService.createRule(rule));
+ public Result<AclRuleVO> createRule(@Valid @RequestBody(required = false)
CreateAclRuleDTO rule) {
+ return Result.ok(aclService.createRule(requireRequest(rule, "ACL rule
request is required").toAclRuleVO()));
}
@PostMapping("/rules/update")
- public Result<AclRuleVO> updateRule(@RequestBody AclRuleVO rule) {
- return Result.ok(aclService.updateRule(rule));
+ public Result<AclRuleVO> updateRule(@Valid @RequestBody(required = false)
UpdateAclRuleDTO rule) {
+ return Result.ok(aclService.updateRule(requireRequest(rule, "ACL rule
request is required").toAclRuleVO()));
}
@PostMapping("/rules/delete")
@@ -75,8 +76,8 @@ public class AclController {
}
@PostMapping("/users/update")
- public Result<AclUserVO> updateUser(@RequestBody AclUserVO user) {
- return Result.ok(aclService.updateUser(user));
+ public Result<AclUserVO> updateUser(@Valid @RequestBody(required = false)
UpdateAclUserDTO user) {
+ return Result.ok(aclService.updateUser(requireRequest(user, "ACL user
request is required").toAclUserVO()));
}
@PostMapping("/users/delete")
@@ -84,4 +85,11 @@ public class AclController {
aclService.deleteUser(request.getId());
return Result.ok();
}
+
+ private <T> T requireRequest(T request, String message) {
+ if (request == null) {
+ throw new BusinessException(400, message);
+ }
+ return request;
+ }
}
diff --git
a/server/src/main/java/org/apache/rocketmq/studio/instance/acl/CreateAclRuleDTO.java
b/server/src/main/java/org/apache/rocketmq/studio/instance/acl/CreateAclRuleDTO.java
new file mode 100644
index 00000000..9cac8dd8
--- /dev/null
+++
b/server/src/main/java/org/apache/rocketmq/studio/instance/acl/CreateAclRuleDTO.java
@@ -0,0 +1,49 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.rocketmq.studio.instance.acl;
+
+import jakarta.validation.constraints.NotBlank;
+import lombok.Data;
+
+import java.util.List;
+
+@Data
+public class CreateAclRuleDTO {
+ @NotBlank(message = "principal is required")
+ private String principal;
+ @NotBlank(message = "resource is required")
+ private String resource;
+ private String resourceType;
+ private String resourcePattern;
+ private List<String> actions;
+ private String decision;
+ private String scope;
+ private String aclVersion;
+
+ public AclRuleVO toAclRuleVO() {
+ return AclRuleVO.builder()
+ .principal(principal)
+ .resource(resource)
+ .resourceType(resourceType)
+ .resourcePattern(resourcePattern)
+ .actions(actions)
+ .decision(decision)
+ .scope(scope)
+ .aclVersion(aclVersion)
+ .build();
+ }
+}
diff --git
a/server/src/main/java/org/apache/rocketmq/studio/instance/acl/UpdateAclRuleDTO.java
b/server/src/main/java/org/apache/rocketmq/studio/instance/acl/UpdateAclRuleDTO.java
new file mode 100644
index 00000000..a7c4862d
--- /dev/null
+++
b/server/src/main/java/org/apache/rocketmq/studio/instance/acl/UpdateAclRuleDTO.java
@@ -0,0 +1,52 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.rocketmq.studio.instance.acl;
+
+import jakarta.validation.constraints.NotBlank;
+import lombok.Data;
+
+import java.util.List;
+
+@Data
+public class UpdateAclRuleDTO {
+ @NotBlank(message = "id is required")
+ private String id;
+ @NotBlank(message = "principal is required")
+ private String principal;
+ @NotBlank(message = "resource is required")
+ private String resource;
+ private String resourceType;
+ private String resourcePattern;
+ private List<String> actions;
+ private String decision;
+ private String scope;
+ private String aclVersion;
+
+ public AclRuleVO toAclRuleVO() {
+ return AclRuleVO.builder()
+ .id(id)
+ .principal(principal)
+ .resource(resource)
+ .resourceType(resourceType)
+ .resourcePattern(resourcePattern)
+ .actions(actions)
+ .decision(decision)
+ .scope(scope)
+ .aclVersion(aclVersion)
+ .build();
+ }
+}
diff --git
a/server/src/main/java/org/apache/rocketmq/studio/instance/acl/UpdateAclUserDTO.java
b/server/src/main/java/org/apache/rocketmq/studio/instance/acl/UpdateAclUserDTO.java
new file mode 100644
index 00000000..19d19e00
--- /dev/null
+++
b/server/src/main/java/org/apache/rocketmq/studio/instance/acl/UpdateAclUserDTO.java
@@ -0,0 +1,40 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.rocketmq.studio.instance.acl;
+
+import jakarta.validation.constraints.NotBlank;
+import lombok.Data;
+
+import java.util.List;
+
+@Data
+public class UpdateAclUserDTO {
+ @NotBlank(message = "id is required")
+ private String id;
+ private String username;
+ private boolean admin;
+ private List<String> clusters;
+
+ public AclUserVO toAclUserVO() {
+ return AclUserVO.builder()
+ .id(id)
+ .username(username)
+ .admin(admin)
+ .clusters(clusters)
+ .build();
+ }
+}
diff --git
a/server/src/main/java/org/apache/rocketmq/studio/instance/topic/TopicController.java
b/server/src/main/java/org/apache/rocketmq/studio/instance/topic/TopicController.java
index 706e095b..51fba64c 100644
---
a/server/src/main/java/org/apache/rocketmq/studio/instance/topic/TopicController.java
+++
b/server/src/main/java/org/apache/rocketmq/studio/instance/topic/TopicController.java
@@ -52,9 +52,9 @@ public class TopicController {
}
@PostMapping("/update")
- public Result<TopicVO> updateTopic(@RequestBody(required = false) TopicVO
topic) {
+ public Result<TopicVO> updateTopic(@Valid @RequestBody(required = false)
UpdateTopicDTO topic) {
requireTopicRequest(topic);
- return Result.ok(metadataService.updateTopic(topic));
+ return Result.ok(metadataService.updateTopic(topic.toTopicVO()));
}
@PostMapping("/delete")
@@ -80,7 +80,7 @@ public class TopicController {
return Result.ok(metadataService.sendMessage(request));
}
- private void requireTopicRequest(TopicVO topic) {
+ private void requireTopicRequest(UpdateTopicDTO topic) {
if (topic == null) {
throw new BusinessException(400, "Topic request is required");
}
diff --git
a/server/src/main/java/org/apache/rocketmq/studio/instance/topic/UpdateTopicDTO.java
b/server/src/main/java/org/apache/rocketmq/studio/instance/topic/UpdateTopicDTO.java
new file mode 100644
index 00000000..e33dbf2a
--- /dev/null
+++
b/server/src/main/java/org/apache/rocketmq/studio/instance/topic/UpdateTopicDTO.java
@@ -0,0 +1,57 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.rocketmq.studio.instance.topic;
+
+import jakarta.validation.constraints.NotBlank;
+import jakarta.validation.constraints.PositiveOrZero;
+import lombok.Data;
+import org.apache.rocketmq.studio.common.domain.enums.TopicPerm;
+import org.apache.rocketmq.studio.common.domain.enums.TopicType;
+
+@Data
+public class UpdateTopicDTO {
+ @NotBlank(message = "name is required")
+ private String name;
+ private String namespace;
+ private String clusterId;
+ private String instanceId;
+ private TopicType type;
+ @PositiveOrZero(message = "writeQueues must be zero or positive")
+ private Integer writeQueues;
+ @PositiveOrZero(message = "readQueues must be zero or positive")
+ private Integer readQueues;
+ private TopicPerm perm;
+ private String remark;
+
+ public TopicVO toTopicVO() {
+ TopicVO topic = new TopicVO();
+ topic.setName(name);
+ topic.setNamespace(namespace);
+ topic.setClusterId(clusterId);
+ topic.setInstanceId(instanceId);
+ topic.setType(type);
+ if (writeQueues != null) {
+ topic.setWriteQueues(writeQueues);
+ }
+ if (readQueues != null) {
+ topic.setReadQueues(readQueues);
+ }
+ topic.setPerm(perm);
+ topic.setRemark(remark);
+ return topic;
+ }
+}
diff --git
a/server/src/main/java/org/apache/rocketmq/studio/ops/alert/AlertRuleController.java
b/server/src/main/java/org/apache/rocketmq/studio/ops/alert/AlertRuleController.java
index d7613a5f..9e7e63a1 100644
---
a/server/src/main/java/org/apache/rocketmq/studio/ops/alert/AlertRuleController.java
+++
b/server/src/main/java/org/apache/rocketmq/studio/ops/alert/AlertRuleController.java
@@ -41,13 +41,17 @@ public class AlertRuleController {
}
@PostMapping("/create")
- public Result<AlertRuleVO> createRule(@RequestBody(required = false)
AlertRuleVO rule) {
- return Result.ok(alertService.createRule(requireAlertRule(rule)));
+ public Result<AlertRuleVO> createRule(@Valid @RequestBody(required =
false) AlertRuleRequestDTO rule) {
+ return
Result.ok(alertService.createRule(requireAlertRule(rule).toAlertRuleVO()));
}
@PostMapping("/update")
- public Result<AlertRuleVO> updateRule(@RequestBody(required = false)
AlertRuleVO rule) {
- return Result.ok(alertService.updateRule(requireAlertRule(rule)));
+ public Result<AlertRuleVO> updateRule(@Valid @RequestBody(required =
false) AlertRuleRequestDTO rule) {
+ AlertRuleRequestDTO request = requireAlertRule(rule);
+ if (request.getId() == null || request.getId().isBlank()) {
+ throw new BusinessException(400, "id is required");
+ }
+ return Result.ok(alertService.updateRule(request.toAlertRuleVO()));
}
@PostMapping("/toggle")
@@ -61,7 +65,7 @@ public class AlertRuleController {
return Result.ok();
}
- private AlertRuleVO requireAlertRule(AlertRuleVO rule) {
+ private AlertRuleRequestDTO requireAlertRule(AlertRuleRequestDTO rule) {
if (rule == null) {
throw new BusinessException(400, "Alert rule request is required");
}
diff --git
a/server/src/main/java/org/apache/rocketmq/studio/ops/alert/AlertRuleRequestDTO.java
b/server/src/main/java/org/apache/rocketmq/studio/ops/alert/AlertRuleRequestDTO.java
new file mode 100644
index 00000000..57c66b1f
--- /dev/null
+++
b/server/src/main/java/org/apache/rocketmq/studio/ops/alert/AlertRuleRequestDTO.java
@@ -0,0 +1,63 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.rocketmq.studio.ops.alert;
+
+import jakarta.validation.constraints.NotBlank;
+import jakarta.validation.constraints.Pattern;
+import lombok.Data;
+
+import java.util.List;
+
+@Data
+public class AlertRuleRequestDTO {
+ private String id;
+ @NotBlank(message = "name is required")
+ private String name;
+ private String metric;
+ @Pattern(regexp = ">|>=|<|<=|==|!=", message = "operator is invalid")
+ private String operator;
+ private double threshold;
+ private String thresholdUnit;
+ @Pattern(regexp = "(?:[0-9]+(?:ms|s|m|h|d|w|y))+", message = "duration is
invalid")
+ private String duration;
+ private List<String> channels;
+ private boolean enabled;
+ private String description;
+ private String brokerName;
+ private String clusterName;
+ @Pattern(regexp = "critical|warning|info", flags =
Pattern.Flag.CASE_INSENSITIVE,
+ message = "severity is invalid")
+ private String severity;
+
+ public AlertRuleVO toAlertRuleVO() {
+ return AlertRuleVO.builder()
+ .id(id)
+ .name(name)
+ .metric(metric)
+ .operator(operator)
+ .threshold(threshold)
+ .thresholdUnit(thresholdUnit)
+ .duration(duration)
+ .channels(channels)
+ .enabled(enabled)
+ .description(description)
+ .brokerName(brokerName)
+ .clusterName(clusterName)
+ .severity(severity)
+ .build();
+ }
+}
diff --git
a/server/src/test/java/org/apache/rocketmq/studio/instance/acl/AclControllerTest.java
b/server/src/test/java/org/apache/rocketmq/studio/instance/acl/AclControllerTest.java
index 6b8baf1b..8e66a1a2 100644
---
a/server/src/test/java/org/apache/rocketmq/studio/instance/acl/AclControllerTest.java
+++
b/server/src/test/java/org/apache/rocketmq/studio/instance/acl/AclControllerTest.java
@@ -121,6 +121,18 @@ class AclControllerTest {
.andExpect(jsonPath("$.data.principal").value("user1"));
}
+ @Test
+ void createRuleShouldRejectMissingPrincipal() throws Exception {
+ mockMvc.perform(post("/api/acl/rules/create")
+ .contentType(MediaType.APPLICATION_JSON)
+ .content("{\"resource\":\"topic-1\"}"))
+ .andExpect(status().isBadRequest())
+ .andExpect(jsonPath("$.code").value(400))
+ .andExpect(jsonPath("$.message").value("principal is
required"));
+
+ verifyNoInteractions(aclService);
+ }
+
@Test
void updateRuleShouldReturnUpdatedRule() throws Exception {
AclRuleVO input = AclRuleVO.builder()
@@ -161,6 +173,18 @@ class AclControllerTest {
.andExpect(jsonPath("$.message").value("ACL rule not found:
missing-rule"));
}
+ @Test
+ void updateRuleShouldRejectMissingId() throws Exception {
+ mockMvc.perform(post("/api/acl/rules/update")
+ .contentType(MediaType.APPLICATION_JSON)
+
.content("{\"principal\":\"user1\",\"resource\":\"topic-1\"}"))
+ .andExpect(status().isBadRequest())
+ .andExpect(jsonPath("$.code").value(400))
+ .andExpect(jsonPath("$.message").value("id is required"));
+
+ verifyNoInteractions(aclService);
+ }
+
@Test
void deleteRuleShouldPassValidatedRequest() throws Exception {
mockMvc.perform(post("/api/acl/rules/delete")
@@ -286,6 +310,18 @@ class AclControllerTest {
.andExpect(jsonPath("$.data.admin").value(false));
}
+ @Test
+ void updateUserShouldRejectMissingId() throws Exception {
+ mockMvc.perform(post("/api/acl/users/update")
+ .contentType(MediaType.APPLICATION_JSON)
+ .content("{\"username\":\"admin\"}"))
+ .andExpect(status().isBadRequest())
+ .andExpect(jsonPath("$.code").value(400))
+ .andExpect(jsonPath("$.message").value("id is required"));
+
+ verifyNoInteractions(aclService);
+ }
+
@Test
void deleteUserShouldPassValidatedRequest() throws Exception {
mockMvc.perform(post("/api/acl/users/delete")
diff --git
a/server/src/test/java/org/apache/rocketmq/studio/instance/topic/TopicControllerTest.java
b/server/src/test/java/org/apache/rocketmq/studio/instance/topic/TopicControllerTest.java
index d39af4cc..118cfc7f 100644
---
a/server/src/test/java/org/apache/rocketmq/studio/instance/topic/TopicControllerTest.java
+++
b/server/src/test/java/org/apache/rocketmq/studio/instance/topic/TopicControllerTest.java
@@ -184,6 +184,18 @@ class TopicControllerTest {
verifyNoInteractions(metadataService);
}
+ @Test
+ void updateTopicShouldRejectBlankName() throws Exception {
+ mockMvc.perform(post("/api/topics/update")
+ .contentType(MediaType.APPLICATION_JSON)
+ .content("{\"name\":\" \"}"))
+ .andExpect(status().isBadRequest())
+ .andExpect(jsonPath("$.code").value(400))
+ .andExpect(jsonPath("$.message").value("name is required"));
+
+ verifyNoInteractions(metadataService);
+ }
+
@Test
void sendMessageShouldReturnResult() throws Exception {
SendMessageDTO request = SendMessageDTO.builder()
diff --git
a/server/src/test/java/org/apache/rocketmq/studio/ops/alert/AlertRuleControllerTest.java
b/server/src/test/java/org/apache/rocketmq/studio/ops/alert/AlertRuleControllerTest.java
index 057b04ba..8fac72fe 100644
---
a/server/src/test/java/org/apache/rocketmq/studio/ops/alert/AlertRuleControllerTest.java
+++
b/server/src/test/java/org/apache/rocketmq/studio/ops/alert/AlertRuleControllerTest.java
@@ -103,6 +103,54 @@ class AlertRuleControllerTest {
verifyNoInteractions(alertService);
}
+ @Test
+ void createRuleShouldRejectInvalidRuleFields() throws Exception {
+ mockMvc.perform(post("/api/alert-rules/create")
+ .contentType(MediaType.APPLICATION_JSON)
+ .content("{\"name\":\"High
Lag\",\"operator\":\"invalid\"}"))
+ .andExpect(status().isBadRequest())
+ .andExpect(jsonPath("$.code").value(400))
+ .andExpect(jsonPath("$.message").value("operator is invalid"));
+
+ verifyNoInteractions(alertService);
+ }
+
+ @Test
+ void createRuleShouldRejectInvalidDuration() throws Exception {
+ mockMvc.perform(post("/api/alert-rules/create")
+ .contentType(MediaType.APPLICATION_JSON)
+ .content("{\"name\":\"High
Lag\",\"duration\":\"later\"}"))
+ .andExpect(status().isBadRequest())
+ .andExpect(jsonPath("$.code").value(400))
+ .andExpect(jsonPath("$.message").value("duration is invalid"));
+
+ verifyNoInteractions(alertService);
+ }
+
+ @Test
+ void createRuleShouldRejectInvalidSeverity() throws Exception {
+ mockMvc.perform(post("/api/alert-rules/create")
+ .contentType(MediaType.APPLICATION_JSON)
+ .content("{\"name\":\"High
Lag\",\"severity\":\"urgent\"}"))
+ .andExpect(status().isBadRequest())
+ .andExpect(jsonPath("$.code").value(400))
+ .andExpect(jsonPath("$.message").value("severity is invalid"));
+
+ verifyNoInteractions(alertService);
+ }
+
+ @Test
+ void updateRuleShouldRejectMissingId() throws Exception {
+ mockMvc.perform(post("/api/alert-rules/update")
+ .contentType(MediaType.APPLICATION_JSON)
+ .content("{\"name\":\"High Lag\"}"))
+ .andExpect(status().isBadRequest())
+ .andExpect(jsonPath("$.code").value(400))
+ .andExpect(jsonPath("$.message").value("id is required"));
+
+ verifyNoInteractions(alertService);
+ }
+
@Test
void updateRuleShouldRejectNullRequestBody() throws Exception {
mockMvc.perform(post("/api/alert-rules/update")