You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@cloudstack.apache.org by we...@apache.org on 2026/08/19 10:47:50 UTC

(cloudstack) branch 4.23.0-merge-4.22.1.1 updated (dc9438a0cee -> cd486acc939)

This is an automated email from the ASF dual-hosted git repository.

weizhouapache pushed a change to branch 4.23.0-merge-4.22.1.1
in repository https://gitbox.apache.org/repos/asf/cloudstack.git


 discard dc9438a0cee fixes after forward-merge
 discard 2d2a4c7d348 Merge remote-tracking branch 'origin/4.22'
     new efa58cc52fc Merge remote-tracking branch 'origin/4.22'
     new d7b4f35825a fixes after forward-merge
     new cd486acc939 kvm: execute "qemu-img convert" by Script.executeCommandForExitValue

This update added new revisions after undoing existing revisions.
That is to say, some revisions that were in the old version of the
branch are not in the new version.  This situation occurs
when a user --force pushes a change and generates a repository
containing something like this:

 * -- * -- B -- O -- O -- O   (dc9438a0cee)
            \
             N -- N -- N   refs/heads/4.23.0-merge-4.22.1.1 (cd486acc939)

You should already have received notification emails for all of the O
revisions, and so the following emails describe only the N revisions
from the common base, B.

Any revisions marked "omit" are not gone; other references still
refer to them.  Any revisions marked "discard" are gone forever.

The 3 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails.  The revisions
listed as "add" were already present in the repository and have only
been added to this reference.


Summary of changes:


(cloudstack) 03/03: kvm: execute "qemu-img convert" by Script.executeCommandForExitValue

Posted by we...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

weizhouapache pushed a commit to branch 4.23.0-merge-4.22.1.1
in repository https://gitbox.apache.org/repos/asf/cloudstack.git

commit cd486acc9393986c13497ff3c4504982a14c8fb2
Author: Wei Zhou <we...@apache.org>
AuthorDate: Wed Aug 19 12:46:06 2026 +0200

    kvm: execute "qemu-img convert" by Script.executeCommandForExitValue
    
    in additional to the change on LibvirtRestoreBackupCommandWrapper.java
    
    ```
        api,server,kvm: improve backup repository mount handling
    
        Improves handling of mount options for backup repository.
    ```
---
 .../kvm/resource/wrapper/LibvirtRestoreBackupCommandWrapper.java  | 8 ++------
 1 file changed, 2 insertions(+), 6 deletions(-)

diff --git a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtRestoreBackupCommandWrapper.java b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtRestoreBackupCommandWrapper.java
index b80f3d54eb4..3839d7f6bdb 100644
--- a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtRestoreBackupCommandWrapper.java
+++ b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtRestoreBackupCommandWrapper.java
@@ -56,10 +56,6 @@ import com.cloud.vm.VirtualMachine;
 public class LibvirtRestoreBackupCommandWrapper extends CommandWrapper<RestoreBackupCommand, Answer, LibvirtComputingResource> {
     private static final String BACKUP_TEMP_FILE_PREFIX = "csbackup";
     private static final String FILE_PATH_PLACEHOLDER = "%s/%s";
-    // Flattens the backing-file chain into a single self-contained qcow2 written to the
-    // destination volume path. Used when the source backup is an incremental whose qcow2
-    // has a backing reference to its parent (chain set up by nasbackup.sh's qemu-img rebase).
-    private static final String QEMU_IMG_FLATTEN_COMMAND = "qemu-img convert -O qcow2 %s %s";
     // Detects whether a qcow2 file references a parent in its backing-file metadata.
     // Returns 0 (true) when a backing file is present, 1 when not. Uses --output=json
     // so the test is robust to qemu-img version differences in human-readable output.
@@ -291,8 +287,8 @@ public class LibvirtRestoreBackupCommandWrapper extends CommandWrapper<RestoreBa
         // chain via qemu-img convert, which follows the backing-file links and
         // produces a single self-contained qcow2.
         if (hasBackingChain(backupPath)) {
-            int flattenExit = Script.runSimpleBashScriptForExitValue(
-                    String.format(QEMU_IMG_FLATTEN_COMMAND, backupPath, volumePath), timeout, false);
+            String[] qemuImgCmd = new String[] { Script.getExecutableAbsolutePath("qemu-img"), "convert", "-O", "qcow2", backupPath, volumePath };
+            int flattenExit = Script.executeCommandForExitValue(qemuImgCmd);
             return flattenExit == 0;
         }
 


(cloudstack) 02/03: fixes after forward-merge

Posted by we...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

weizhouapache pushed a commit to branch 4.23.0-merge-4.22.1.1
in repository https://gitbox.apache.org/repos/asf/cloudstack.git

commit d7b4f35825a13fd77369b58c842e9a7d5a681ce9
Author: Wei Zhou <we...@apache.org>
AuthorDate: Wed Aug 19 12:46:00 2026 +0200

    fixes after forward-merge
---
 .../acl/DynamicRoleBasedAPIAccessChecker.java        |  5 ++++-
 .../mom/webhook/WebhookServiceImplTest.java          | 20 ++++++++++++--------
 .../mom/webhook/dao/WebhookDeliveryDaoImplTest.java  |  6 +++---
 .../webhook/dao/WebhookDeliveryJoinDaoImplTest.java  |  6 +++---
 4 files changed, 22 insertions(+), 15 deletions(-)

diff --git a/plugins/acl/dynamic-role-based/src/main/java/org/apache/cloudstack/acl/DynamicRoleBasedAPIAccessChecker.java b/plugins/acl/dynamic-role-based/src/main/java/org/apache/cloudstack/acl/DynamicRoleBasedAPIAccessChecker.java
index b9b41722c32..1278adc0b88 100644
--- a/plugins/acl/dynamic-role-based/src/main/java/org/apache/cloudstack/acl/DynamicRoleBasedAPIAccessChecker.java
+++ b/plugins/acl/dynamic-role-based/src/main/java/org/apache/cloudstack/acl/DynamicRoleBasedAPIAccessChecker.java
@@ -101,9 +101,12 @@ public class DynamicRoleBasedAPIAccessChecker extends AdapterBase implements API
             return apiNames;
         }
         List<RolePermission> allPermissions = roleAndPermissions.second();
+        List<RolePermissionEntity> allPermissionEntities = allPermissions.stream().map(permission -> (RolePermissionEntity) permission)
+                .collect(Collectors.toList());
+
         List<String> allowedApis = new ArrayList<>();
         for (String api : apiNames) {
-            if (checkApiPermissionByRole(accountRole, api, allPermissions)) {
+            if (checkApiPermissionByRole(accountRole, api, allPermissionEntities, false)) {
                 allowedApis.add(api);
             }
         }
diff --git a/plugins/event-bus/webhook/src/test/java/org/apache/cloudstack/mom/webhook/WebhookServiceImplTest.java b/plugins/event-bus/webhook/src/test/java/org/apache/cloudstack/mom/webhook/WebhookServiceImplTest.java
index 90c0c323d0d..e945b990c80 100644
--- a/plugins/event-bus/webhook/src/test/java/org/apache/cloudstack/mom/webhook/WebhookServiceImplTest.java
+++ b/plugins/event-bus/webhook/src/test/java/org/apache/cloudstack/mom/webhook/WebhookServiceImplTest.java
@@ -17,6 +17,7 @@
 
 package org.apache.cloudstack.mom.webhook;
 
+import java.net.URI;
 import java.util.ArrayList;
 import java.util.Date;
 import java.util.List;
@@ -28,6 +29,7 @@ import org.apache.cloudstack.acl.ControlledEntity;
 import org.apache.cloudstack.framework.async.AsyncCallbackDispatcher;
 import org.apache.cloudstack.framework.events.Event;
 import org.apache.cloudstack.framework.events.EventBusException;
+import org.apache.cloudstack.mom.webhook.WebhookServiceImpl.DeliveryConfig;
 import org.apache.cloudstack.mom.webhook.dao.WebhookDao;
 import org.apache.cloudstack.mom.webhook.dao.WebhookDeliveryDao;
 import org.apache.cloudstack.mom.webhook.dao.WebhookFilterDao;
@@ -98,7 +100,7 @@ public class WebhookServiceImplTest {
     public void getDeliveryJobReturnsProperlyConfiguredJob() {
         Event event = Mockito.mock(Event.class);
         Webhook webhook = Mockito.mock(Webhook.class);
-        Pair<Integer, Integer> configs = new Pair<>(4, 5000);
+        DeliveryConfig configs = new DeliveryConfig(4, 5000, null, false, true, true);
 
         Mockito.when(event.getEventId()).thenReturn(123L);
         Mockito.when(webhook.getId()).thenReturn(456L);
@@ -114,7 +116,7 @@ public class WebhookServiceImplTest {
     public void getDeliveryJobInjectsDependencies() {
         Event event = Mockito.mock(Event.class);
         Webhook webhook = Mockito.mock(Webhook.class);
-        Pair<Integer, Integer> configs = new Pair<>(1, 1000);
+        DeliveryConfig configs = new DeliveryConfig(1, 1000, null, false, true, true);
 
         WebhookDeliveryThread job = webhookServiceImpl.getDeliveryJob(event, webhook, configs);
 
@@ -413,9 +415,11 @@ public class WebhookServiceImplTest {
         Account account = Mockito.mock(Account.class);
 
         Mockito.when(webhook.getAccountId()).thenReturn(2L);
+        Mockito.when(webhook.getPayloadUrl()).thenReturn("https://example.com");
         Mockito.when(accountManager.getAccount(Mockito.anyLong())).thenReturn(account);
 
-        Runnable job = webhookServiceImpl.getManualDeliveryJob(null, webhook, "   ", future);
+        URI uri = WebhookUrlValidator.validateWebhookDestinationUrl(webhook.getPayloadUrl(), false, null, true);
+        Runnable job = webhookServiceImpl.getManualDeliveryJob(null, webhook, "   ", uri, future);
 
         Assert.assertNotNull(job);
         Event event = (Event) ReflectionTestUtils.getField(job, "event");
@@ -438,7 +442,7 @@ public class WebhookServiceImplTest {
         Mockito.when(existingDelivery.getPayload()).thenReturn("test-payload");
         Mockito.when(eventJoinVO.getAccountUuid()).thenReturn("account-uuid");
 
-        Runnable job = webhookServiceImpl.getManualDeliveryJob(existingDelivery, webhook, null, future);
+        Runnable job = webhookServiceImpl.getManualDeliveryJob(existingDelivery, webhook, null, null, future);
 
         Assert.assertNotNull(job);
         Mockito.verify(eventJoinDao, Mockito.times(1)).findById(123L);
@@ -454,7 +458,7 @@ public class WebhookServiceImplTest {
         Mockito.when(accountManager.getAccount(1L)).thenReturn(account);
         Mockito.when(account.getUuid()).thenReturn("account-uuid");
 
-        Runnable job = webhookServiceImpl.getManualDeliveryJob(null, webhook, "test-payload", future);
+        Runnable job = webhookServiceImpl.getManualDeliveryJob(null, webhook, "test-payload", null, future);
 
         Assert.assertNotNull(job);
         Mockito.verify(accountManager, Mockito.times(1)).getAccount(1L);
@@ -470,7 +474,7 @@ public class WebhookServiceImplTest {
         Mockito.when(webhook.getAccountId()).thenReturn(2L);
         Mockito.when(accountManager.getAccount(Mockito.anyLong())).thenReturn(account);
 
-        WebhookDeliveryThread job = (WebhookDeliveryThread) webhookServiceImpl.getManualDeliveryJob(null, webhook, "test-payload", future);
+        WebhookDeliveryThread job = (WebhookDeliveryThread) webhookServiceImpl.getManualDeliveryJob(null, webhook, "test-payload", null, future);
 
         Assert.assertEquals(3, ReflectionTestUtils.getField(job, "deliveryTries"));
         Assert.assertEquals(10, ReflectionTestUtils.getField(job, "deliveryTimeout"));
@@ -607,7 +611,7 @@ public class WebhookServiceImplTest {
         WebhookDeliveryVO persistedDelivery = Mockito.mock(WebhookDeliveryVO.class);
         Mockito.when(webhookDeliveryDao.persist(Mockito.any(WebhookDeliveryVO.class))).thenReturn(persistedDelivery);
 
-        WebhookDelivery returnedDelivery = webhookServiceImpl.executeWebhookDelivery(delivery, webhook, "payload");
+        WebhookDelivery returnedDelivery = webhookServiceImpl.executeWebhookDelivery(delivery, webhook, "payload", null);
 
         Assert.assertEquals(persistedDelivery, returnedDelivery);
         Mockito.verify(webhookDeliveryDao, Mockito.times(1)).persist(Mockito.any(WebhookDeliveryVO.class));
@@ -638,7 +642,7 @@ public class WebhookServiceImplTest {
         });
         ReflectionTestUtils.setField(webhookServiceImpl, "webhookJobExecutor", executorService);
 
-        WebhookDelivery returnedDelivery = webhookServiceImpl.executeWebhookDelivery(null, webhook, "payload");
+        WebhookDelivery returnedDelivery = webhookServiceImpl.executeWebhookDelivery(null, webhook, "payload", null);
 
         Assert.assertNotNull(returnedDelivery);
         Assert.assertEquals("headers", returnedDelivery.getHeaders());
diff --git a/plugins/event-bus/webhook/src/test/java/org/apache/cloudstack/mom/webhook/dao/WebhookDeliveryDaoImplTest.java b/plugins/event-bus/webhook/src/test/java/org/apache/cloudstack/mom/webhook/dao/WebhookDeliveryDaoImplTest.java
index ff0f222cdcc..bc3fc034720 100644
--- a/plugins/event-bus/webhook/src/test/java/org/apache/cloudstack/mom/webhook/dao/WebhookDeliveryDaoImplTest.java
+++ b/plugins/event-bus/webhook/src/test/java/org/apache/cloudstack/mom/webhook/dao/WebhookDeliveryDaoImplTest.java
@@ -69,11 +69,11 @@ public class WebhookDeliveryDaoImplTest {
 
         doReturn(1).when(webhookDeliveryDao).remove(any(SearchCriteria.class));
 
-        int result = webhookDeliveryDao.deleteByDeleteApiParams(null, webhookId, null, startDate, null);
+        int result = webhookDeliveryDao.deleteByDeleteApiParams(null, List.of(webhookId), null, startDate, null, true);
 
         assertEquals(1, result);
         verify(webhookDeliveryDao).remove(any(SearchCriteria.class));
-        verify(mockSearchBuilder).and(eq("webhookId"), any(), eq(SearchCriteria.Op.EQ));
+        verify(mockSearchBuilder).and(eq("webhookId"), any(), eq(SearchCriteria.Op.IN));
         verify(mockSearchCriteria).setParameters("webhookId", webhookId);
     }
 
@@ -87,7 +87,7 @@ public class WebhookDeliveryDaoImplTest {
 
         doReturn(0).when(webhookDeliveryDao).remove(any(SearchCriteria.class));
 
-        int result = webhookDeliveryDao.deleteByDeleteApiParams(id, webhookId, managementServerId, startDate, endDate);
+        int result = webhookDeliveryDao.deleteByDeleteApiParams(id, List.of(webhookId), managementServerId, startDate, endDate, true);
 
         assertEquals(0, result);
     }
diff --git a/plugins/event-bus/webhook/src/test/java/org/apache/cloudstack/mom/webhook/dao/WebhookDeliveryJoinDaoImplTest.java b/plugins/event-bus/webhook/src/test/java/org/apache/cloudstack/mom/webhook/dao/WebhookDeliveryJoinDaoImplTest.java
index f7561536a3f..c475e1e20db 100644
--- a/plugins/event-bus/webhook/src/test/java/org/apache/cloudstack/mom/webhook/dao/WebhookDeliveryJoinDaoImplTest.java
+++ b/plugins/event-bus/webhook/src/test/java/org/apache/cloudstack/mom/webhook/dao/WebhookDeliveryJoinDaoImplTest.java
@@ -72,7 +72,7 @@ public class WebhookDeliveryJoinDaoImplTest {
 
         Pair<List<WebhookDeliveryJoinVO>, Integer> result =
                 webhookDeliveryJoinDao.searchAndCountByListApiParameters(id, null, null,
-                        null, null, null, null,null);
+                        null, null, null, null, null, true);
 
         assertNotNull(result);
         assertTrue(result.second() > 0);
@@ -90,7 +90,7 @@ public class WebhookDeliveryJoinDaoImplTest {
 
         Pair<List<WebhookDeliveryJoinVO>, Integer> result =
                 webhookDeliveryJoinDao.searchAndCountByListApiParameters(null, List.of(webhookId),
-                        null, null, null, null, null, null);
+                        null, null, null, null, null, null, true);
 
         assertNotNull(result);
         assertTrue(result.second() > 0);
@@ -112,7 +112,7 @@ public class WebhookDeliveryJoinDaoImplTest {
 
         Pair<List<WebhookDeliveryJoinVO>, Integer> result =
                 webhookDeliveryJoinDao.searchAndCountByListApiParameters(null, null,
-                        managementServerId, keyword, start, end, null, searchFilter);
+                        managementServerId, keyword, start, end, null, searchFilter, true);
 
         assertNotNull(result);
         assertTrue(result.second() > 0);


(cloudstack) 01/03: Merge remote-tracking branch 'origin/4.22'

Posted by we...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

weizhouapache pushed a commit to branch 4.23.0-merge-4.22.1.1
in repository https://gitbox.apache.org/repos/asf/cloudstack.git

commit efa58cc52fca2c58840b8c83b583e3e36f0f5577
Merge: 64178ead02d 85bcdb1cc48
Author: Wei Zhou <we...@apache.org>
AuthorDate: Wed Aug 19 12:44:34 2026 +0200

    Merge remote-tracking branch 'origin/4.22'

 .../java/org/apache/cloudstack/acl/APIChecker.java |  28 +++
 .../org/apache/cloudstack/api/ApiArgValidator.java |  73 +++++-
 .../admin/vm/ResetVMPasswordCmdByAdmin.java        |   2 +-
 .../autoscale/CreateAutoScaleVmProfileCmd.java     |   2 +
 .../autoscale/UpdateAutoScaleVmProfileCmd.java     |   1 +
 .../backup/repository/AddBackupRepositoryCmd.java  |   4 +-
 .../repository/UpdateBackupRepositoryCmd.java      |   8 +-
 .../command/user/userdata/DeleteUserDataCmd.java   |  17 +-
 .../user/userdata/LinkUserDataToTemplateCmd.java   |   3 +
 .../api/command/user/vm/BaseDeployVMCmd.java       |   1 +
 .../api/command/user/vm/ResetVMPasswordCmd.java    |   2 +-
 .../api/command/user/vm/ResetVMUserDataCmd.java    |   1 +
 .../api/command/user/vm/UpdateVMCmd.java           |   1 +
 .../cloudstack/api/response/DomainResponse.java    |   2 +-
 .../cloudstack/api/response/HostTagResponse.java   |   6 +-
 .../direct/download/DirectDownloadManager.java     |  10 +
 .../user/userdata/DeleteUserDataCmdTest.java       |  31 ++-
 .../template/MetalinkTemplateDownloader.java       |   7 +
 .../directdownload/DirectDownloadCommand.java      |  11 +-
 .../direct/download/DirectDownloadHelper.java      |   5 +-
 .../download/MetalinkDirectTemplateDownloader.java |  55 ++++-
 .../download/NfsDirectTemplateDownloader.java      |  79 ++++++-
 .../download/NfsDirectTemplateDownloaderTest.java  |  73 ++++++
 .../com/cloud/vm/VirtualMachineManagerImpl.java    |   6 +-
 .../java/com/cloud/host/dao/HostTagsDaoImpl.java   |   1 -
 .../framework/config/impl/ConfigDepotImpl.java     |  11 +-
 .../framework/jobs/AsyncJobDataHelper.java         | 150 ++++++++++++
 .../framework/jobs/impl/AsyncJobManagerImpl.java   |  50 ++--
 .../cloudstack/framework/jobs/impl/AsyncJobVO.java |  22 +-
 .../framework/jobs/AsyncJobDataHelperTest.java     | 249 ++++++++++++++++++++
 .../framework/jobs/AsyncJobManagerTest.java        |   2 -
 .../acl/DynamicRoleBasedAPIAccessChecker.java      |  34 +++
 .../acl/DynamicRoleBasedAPIAccessCheckerTest.java  | 132 +++++++++++
 .../acl/ProjectRoleBasedApiAccessChecker.java      |   5 +
 .../acl/StaticRoleBasedAPIAccessChecker.java       |  16 ++
 .../mom/webhook/WebhookApiServiceImpl.java         |  81 ++++---
 .../mom/webhook/WebhookDeliveryThread.java         | 116 ++++++++--
 .../cloudstack/mom/webhook/WebhookService.java     |  38 ++-
 .../cloudstack/mom/webhook/WebhookServiceImpl.java |  61 ++++-
 .../mom/webhook/WebhookUrlValidator.java           | 123 ++++++++++
 .../mom/webhook/dao/WebhookDeliveryDao.java        |   3 +-
 .../mom/webhook/dao/WebhookDeliveryDaoImpl.java    |  13 +-
 .../mom/webhook/dao/WebhookDeliveryJoinDao.java    |   6 +-
 .../webhook/dao/WebhookDeliveryJoinDaoImpl.java    |   9 +-
 .../mom/webhook/WebhookApiServiceImplTest.java     | 257 +++++++++++++++++----
 .../mom/webhook/WebhookDeliveryThreadTest.java     |   7 +
 .../mom/webhook/WebhookUrlValidatorTest.java       | 100 ++++++++
 .../LibvirtRestoreBackupCommandWrapper.java        | 127 +++++-----
 .../LibvirtRestoreBackupCommandWrapperTest.java    | 120 +++++-----
 .../cluster/KubernetesClusterManagerImpl.java      |   8 +-
 .../contrail/management/MockAccountManager.java    |   4 +
 .../api/response/LdapConfigurationResponse.java    |  17 +-
 .../apache/cloudstack/ldap/LdapManagerImpl.java    |  25 +-
 .../oauth2/api/command/ListOAuthProvidersCmd.java  |  43 +++-
 .../oauth2/google/GoogleOAuth2Provider.java        |  94 +++++---
 .../oauth2/google/GoogleOAuth2ProviderTest.java    | 176 ++++++++------
 .../api/command/SAML2LoginAPIAuthenticatorCmd.java |  78 ++++---
 scripts/vm/hypervisor/kvm/nasbackup.sh             |  41 +++-
 server/src/main/java/com/cloud/api/ApiServer.java  |   4 +-
 .../com/cloud/api/dispatch/ParamProcessWorker.java |   6 +
 .../java/com/cloud/api/query/QueryManagerImpl.java |  63 ++++-
 .../com/cloud/api/query/vo/AsyncJobJoinVO.java     |   3 +-
 .../com/cloud/dc/dao/DedicatedResourceDao.java     |   6 +
 .../com/cloud/dc/dao/DedicatedResourceDaoImpl.java |  12 +
 .../com/cloud/resource/ResourceManagerImpl.java    |  75 +++++-
 .../main/java/com/cloud/server/StatsCollector.java |  14 +-
 .../java/com/cloud/storage/StorageManagerImpl.java |   8 +-
 .../com/cloud/storage/VolumeApiServiceImpl.java    |  16 +-
 .../com/cloud/template/TemplateManagerImpl.java    |  25 +-
 .../main/java/com/cloud/user/AccountManager.java   |   3 +
 .../java/com/cloud/user/AccountManagerImpl.java    |  82 ++++---
 .../main/java/com/cloud/vm/UserVmManagerImpl.java  |  10 +-
 .../cloud/vm/snapshot/VMSnapshotManagerImpl.java   |  10 +-
 .../cloudstack/acl/ProjectRoleManagerImpl.java     |  17 +-
 .../org/apache/cloudstack/acl/RoleManagerImpl.java |   3 +
 .../annotation/AnnotationManagerImpl.java          |   9 +-
 .../diagnostics/DiagnosticsServiceImpl.java        |  37 ++-
 .../direct/download/DirectDownloadManagerImpl.java |  27 ++-
 .../cloud/api/dispatch/ParamProcessWorkerTest.java |  20 ++
 .../com/cloud/api/query/QueryManagerImplTest.java  | 230 +++++++++++++++++-
 .../cloud/resource/ResourceManagerImplTest.java    |  90 ++++++++
 .../java/com/cloud/server/StatsCollectorTest.java  |  43 ++++
 .../cloud/template/TemplateManagerImplTest.java    |  64 +++++
 .../com/cloud/user/AccountManagerImplTest.java     | 152 +++++++++++-
 .../java/com/cloud/vm/UserVmManagerImplTest.java   |   1 -
 .../cloudstack/acl/ProjectRoleManagerImplTest.java | 245 ++++++++++++++++++++
 .../diagnostics/DiagnosticsServiceImplTest.java    | 133 +++++++++++
 .../consoleproxy/ConsoleProxyNoVNCServer.java      |  10 +-
 systemvm/debian/opt/cloud/bin/cs/CsDhcp.py         |  67 +++++-
 test/integration/smoke/test_webhook_delivery.py    |  67 +++++-
 test/integration/smoke/test_webhook_lifecycle.py   |  83 ++++++-
 ui/package.json                                    |   2 +-
 ui/src/config/section/compute.js                   |   3 +-
 ui/src/config/section/infra/routers.js             |   3 +-
 ui/src/config/section/infra/systemVms.js           |   3 +-
 ui/src/config/section/network.js                   |   3 +-
 ui/src/config/section/user.js                      |   3 +-
 ui/src/utils/util.js                               |   9 +
 .../src/main/java/com/cloud/utils/StringUtils.java |  23 ++
 utils/src/main/java/com/cloud/utils/UriUtils.java  | 145 +++++++++++-
 100 files changed, 3893 insertions(+), 583 deletions(-)

diff --cc api/src/main/java/org/apache/cloudstack/acl/APIChecker.java
index 286a3598e4f,c9a8d384945..6defad5523c
--- a/api/src/main/java/org/apache/cloudstack/acl/APIChecker.java
+++ b/api/src/main/java/org/apache/cloudstack/acl/APIChecker.java
@@@ -20,10 -21,13 +21,14 @@@ import com.cloud.exception.RequestLimit
  import com.cloud.user.Account;
  import com.cloud.user.User;
  import com.cloud.utils.component.Adapter;
 +import org.apache.cloudstack.acl.apikeypair.ApiKeyPairPermission;
  
+ import java.util.ArrayList;
  import java.util.List;
  
+ import org.apache.logging.log4j.LogManager;
+ import org.apache.logging.log4j.Logger;
+ 
  /**
   * APICheckers is designed to verify the ownership of resources and to control the access to APIs.
   */
@@@ -43,6 -48,27 +49,28 @@@ public interface APIChecker extends Ada
       * @return the list of allowed apis for the given user
       */
      List<String> getApisAllowedToUser(Role role, User user, List<String> apiNames) throws PermissionDeniedException;
+ 
+     default List<String> getApisAllowedToAccount(Account account, List<String> apiNames) {
+         List<String> allowedApis = new ArrayList<>();
+         for (String apiName : apiNames) {
+             try {
+                 checkAccess(account, apiName);
+                 allowedApis.add(apiName);
+             } catch (RequestLimitException e) {
+                 // Non-ACL failure (e.g. rate limiting) should not be treated as simple "not allowed".
+                 // Propagate as unchecked so callers are aware of the failure.
+                 throw new RuntimeException("Failed to check access for API [" + apiName + "] due to request limits", e);
+             } catch (PermissionDeniedException e) {
+                 LOGGER.trace("Account [" + account + "] is not allowed to access API [" + apiName + "]");
+             }
+         }
+         return allowedApis;
+     }
+ 
      boolean isEnabled();
 +    List<RolePermissionEntity> getImplicitRolePermissions(RoleType roleType);
+ 
+     default void refreshRoleCacheOnPermissionsChange(Role role) {
+         // Only applicable for dynamic role based checkers
+     }
  }
diff --cc api/src/main/java/org/apache/cloudstack/api/command/user/vm/BaseDeployVMCmd.java
index 38e9921853a,50129f26d70..8d79ac4af45
--- a/api/src/main/java/org/apache/cloudstack/api/command/user/vm/BaseDeployVMCmd.java
+++ b/api/src/main/java/org/apache/cloudstack/api/command/user/vm/BaseDeployVMCmd.java
@@@ -157,10 -147,11 +157,11 @@@ public abstract class BaseDeployVMCmd e
                      "Using HTTP POST (via POST body), you can send up to 1MB of data after base64 encoding. " +
                      "You also need to change vm.userdata.max.length value",
              length = 1048576)
 -    private String userData;
 +    protected String userData;
  
+     @ACL
      @Parameter(name = ApiConstants.USER_DATA_ID, type = CommandType.UUID, entityType = UserDataResponse.class, description = "the ID of the Userdata", since = "4.18")
 -    private Long userdataId;
 +    protected Long userdataId;
  
      @Parameter(name = ApiConstants.USER_DATA_DETAILS, type = CommandType.MAP, description = "used to specify the parameters values for the variables in userdata.", since = "4.18")
      private Map userdataDetails;
diff --cc plugins/acl/dynamic-role-based/src/test/java/org/apache/cloudstack/acl/DynamicRoleBasedAPIAccessCheckerTest.java
index 71fbbb4a365,c89000e7ed1..84fd5d15446
--- a/plugins/acl/dynamic-role-based/src/test/java/org/apache/cloudstack/acl/DynamicRoleBasedAPIAccessCheckerTest.java
+++ b/plugins/acl/dynamic-role-based/src/test/java/org/apache/cloudstack/acl/DynamicRoleBasedAPIAccessCheckerTest.java
@@@ -198,67 -198,133 +200,197 @@@ public class DynamicRoleBasedAPIAccessC
          Assert.assertEquals(0, apisReceived.size());
      }
  
 +    @Test(expected = UnavailableCommandException.class)
 +    public void checkAccessTestInvalidApiKeyPairPermission() {
 +        final String api = "someDeniedApi";
 +        final ApiKeyPairPermission permission = new ApiKeyPairPermissionVO(1L, api, Permission.DENY, null);
 +        assertFalse(apiAccessCheckerSpy.checkAccess(getTestUser(), api, permission));
 +    }
 +
 +    @Test(expected = UnavailableCommandException.class)
 +    public void checkAccessTestUnrelatedApiKeyPairPermission() {
 +        final String api = "someDeniedApi";
 +        final ApiKeyPairPermission permission = new ApiKeyPairPermissionVO(1L, "apiName", Permission.ALLOW, null);
 +        assertFalse(apiAccessCheckerSpy.checkAccess(getTestUser(), api, permission));
 +    }
 +
 +    @Test
 +    public void checkAccessTestValidApiKeyPairPermission() {
 +        final String api = "someAllowedApi";
 +        final ApiKeyPairPermission permission = new ApiKeyPairPermissionVO(1L, api, Permission.ALLOW, null);
 +        assertTrue(apiAccessCheckerSpy.checkAccess(getTestUser(), api, permission));
 +    }
 +
 +    @Test
 +    public void checkAccessTestValidMultipleApiKeyPermissions() {
 +        final String api = "someAllowedApi";
 +        final ApiKeyPairPermission[] permissions = new ApiKeyPairPermission[]{
 +            new ApiKeyPairPermissionVO(1L, "someDeniedApi", Permission.DENY, null),
 +            new ApiKeyPairPermissionVO(1L, api, Permission.ALLOW, null)
 +        };
 +        assertTrue(apiAccessCheckerSpy.checkAccess(getTestUser(), api, permissions));
 +    }
 +
 +    @Test(expected = UnavailableCommandException.class)
 +    public void checkAccessTestInvalidMultipleApiKeyPermissions() {
 +        final String api = "someDeniedApi";
 +        final ApiKeyPairPermission[] permissions = new ApiKeyPairPermission[]{
 +                new ApiKeyPairPermissionVO(1L, "someAllowedApi", Permission.ALLOW, null),
 +                new ApiKeyPairPermissionVO(1L, api, Permission.DENY, null)
 +        };
 +        assertFalse(apiAccessCheckerSpy.checkAccess(getTestUser(), api, permissions));
 +    }
 +
 +
 +    @Test
 +    public void checkAccessTestValidApiKeyPairPermissionWithNullOverride() {
 +        final String api = "someAllowedApi";
 +        final ApiKeyPairPermission[] emptyPermissionArray = List.of().toArray(new ApiKeyPairPermission[0]);
 +        final RolePermission permission = new RolePermissionVO(1L, api, Permission.ALLOW, null);
 +        Mockito.doReturn(Collections.singletonList(permission)).when(roleServiceMock).findAllPermissionsBy(Mockito.anyLong());
 +
 +        assertTrue(apiAccessCheckerSpy.checkAccess(getTestUser(), api, emptyPermissionArray));
 +        Mockito.verify(roleServiceMock).findAllPermissionsBy(Mockito.anyLong());
 +    }
 +
 +    @Test(expected = UnavailableCommandException.class)
 +    public void checkAccessTestInvalidApiKeyPairPermissionWithNullOverride() {
 +        final String api = "someDeniedApi";
 +        final ApiKeyPairPermission[] emptyPermissionArray = List.of().toArray(new ApiKeyPairPermission[0]);
 +        final RolePermission permission = new RolePermissionVO(1L, api, Permission.DENY, null);
 +        Mockito.doReturn(Collections.singletonList(permission)).when(roleServiceMock).findAllPermissionsBy(Mockito.anyLong());
 +
 +        assertTrue(apiAccessCheckerSpy.checkAccess(getTestUser(), api, emptyPermissionArray));
 +        Mockito.verify(roleServiceMock, Mockito.times(1)).findAllPermissionsBy(Mockito.anyLong());
 +    }
++
+     // --- Tests for checkAccess(Account, String) ---
+ 
+     @Test(expected = PermissionDeniedException.class)
+     public void testCheckAccessAccountNullRoleShouldThrow() {
+         Mockito.when(roleServiceMock.findRole(Mockito.anyLong())).thenReturn(null);
+         apiAccessCheckerSpy.checkAccess(getTestAccount(), "someApi");
+     }
+ 
+     @Test
+     public void testCheckAccessAccountAdminShouldAllow() {
+         Account adminAccount = new AccountVO("root admin", 1L, null, Account.Type.ADMIN, "admin-uuid");
+         Mockito.when(roleServiceMock.findRole(Mockito.anyLong())).thenReturn(new RoleVO(1L, "Admin", RoleType.Admin, "default admin role"));
+         assertTrue(apiAccessCheckerSpy.checkAccess(adminAccount, "anyApi"));
+     }
+ 
+     @Test
+     public void testCheckAccessAccountAllowedApi() {
+         final String allowedApiName = "someAllowedApi";
+         final RolePermission permission = new RolePermissionVO(1L, allowedApiName, Permission.ALLOW, null);
+         Mockito.when(roleServiceMock.findAllPermissionsBy(Mockito.anyLong())).thenReturn(Collections.singletonList(permission));
+         assertTrue(apiAccessCheckerSpy.checkAccess(getTestAccount(), allowedApiName));
+     }
+ 
+     @Test(expected = PermissionDeniedException.class)
+     public void testCheckAccessAccountDeniedApi() {
+         final String deniedApiName = "someDeniedApi";
+         final RolePermission permission = new RolePermissionVO(1L, deniedApiName, Permission.DENY, null);
+         Mockito.when(roleServiceMock.findAllPermissionsBy(Mockito.anyLong())).thenReturn(Collections.singletonList(permission));
+         apiAccessCheckerSpy.checkAccess(getTestAccount(), deniedApiName);
+     }
+ 
+     @Test
+     public void testCheckAccessAccountUsesCachedPermissions() throws Exception {
+         // Enable caching by setting a positive cachePeriod
+         Field cachePeriodField = DynamicRoleBasedAPIAccessChecker.class.getDeclaredField("cachePeriod");
+         cachePeriodField.setAccessible(true);
+         cachePeriodField.set(apiAccessCheckerSpy, 1);
+ 
+         Field rpCacheField = DynamicRoleBasedAPIAccessChecker.class.getDeclaredField("rolePermissionsCache");
+         rpCacheField.setAccessible(true);
+         rpCacheField.set(apiAccessCheckerSpy, new LazyCache<Long, Pair<Role, List<RolePermission>>>(32, 1, apiAccessCheckerSpy::getRolePermissions));
+ 
+         final String allowedApiName = "someAllowedApi";
+         final RolePermission permission = new RolePermissionVO(1L, allowedApiName, Permission.ALLOW, null);
+         Mockito.when(roleServiceMock.findAllPermissionsBy(Mockito.anyLong())).thenReturn(Collections.singletonList(permission));
+ 
+         // First call should populate the cache
+         apiAccessCheckerSpy.checkAccess(getTestAccount(), allowedApiName);
+         // Second call should use cached permissions and not hit the DAO again
+         apiAccessCheckerSpy.checkAccess(getTestAccount(), allowedApiName);
+ 
+         Mockito.verify(roleServiceMock, Mockito.times(1)).findAllPermissionsBy(Mockito.anyLong());
+     }
+ 
+     // --- Tests for getApisAllowedToAccount ---
+ 
+     @Test
+     public void testGetApisAllowedToAccountDisabledShouldReturnAll() {
+         Mockito.doReturn(false).when(apiAccessCheckerSpy).isEnabled();
+         List<String> input = new ArrayList<>(Arrays.asList("api1", "api2", "api3"));
+         List<String> result = apiAccessCheckerSpy.getApisAllowedToAccount(getTestAccount(), input);
+         Assert.assertEquals(3, result.size());
+     }
+ 
+     @Test(expected = PermissionDeniedException.class)
+     public void testGetApisAllowedToAccountNullRoleShouldThrow() {
+         Mockito.when(roleServiceMock.findRole(Mockito.anyLong())).thenReturn(null);
+         apiAccessCheckerSpy.getApisAllowedToAccount(getTestAccount(), new ArrayList<>(Arrays.asList("api1")));
+     }
+ 
+     @Test
+     public void testGetApisAllowedToAccountAdminShouldReturnAll() {
+         Account adminAccount = new AccountVO("root admin", 1L, null, Account.Type.ADMIN, "admin-uuid");
+         Mockito.when(roleServiceMock.findRole(Mockito.anyLong())).thenReturn(new RoleVO(1L, "Admin", RoleType.Admin, "default admin role"));
+         List<String> input = new ArrayList<>(Arrays.asList("api1", "api2", "api3"));
+         List<String> result = apiAccessCheckerSpy.getApisAllowedToAccount(adminAccount, input);
+         Assert.assertEquals(3, result.size());
+         Assert.assertEquals(input, result);
+     }
+ 
+     @Test
+     public void testGetApisAllowedToAccountFiltersCorrectly() {
+         final RolePermission allowPermission = new RolePermissionVO(1L, "allowedApi", Permission.ALLOW, null);
+         final RolePermission denyPermission = new RolePermissionVO(1L, "deniedApi", Permission.DENY, null);
+         Mockito.when(roleServiceMock.findAllPermissionsBy(Mockito.anyLong())).thenReturn(Arrays.asList(allowPermission, denyPermission));
+         List<String> input = new ArrayList<>(Arrays.asList("allowedApi", "deniedApi", "unknownApi"));
+         List<String> result = apiAccessCheckerSpy.getApisAllowedToAccount(getTestAccount(), input);
+         Assert.assertEquals(1, result.size());
+         Assert.assertEquals("allowedApi", result.get(0));
+     }
+ 
+     @Test
+     public void testGetApisAllowedToAccountAnnotationFallback() {
+         Mockito.when(roleServiceMock.findAllPermissionsBy(Mockito.anyLong())).thenReturn(Collections.emptyList());
+         apiAccessCheckerSpy.addApiToRoleBasedAnnotationsMap(RoleType.User, "annotatedApi");
+         List<String> input = new ArrayList<>(Arrays.asList("annotatedApi", "unknownApi"));
+         List<String> result = apiAccessCheckerSpy.getApisAllowedToAccount(getTestAccount(), input);
+         Assert.assertEquals(1, result.size());
+         Assert.assertEquals("annotatedApi", result.get(0));
+     }
+ 
+     @Test
+     public void testGetApisAllowedToAccountUsesCachedPermissions() {
+         try {
+             // Ensure caching is enabled by setting a positive cachePeriod
+             Field cachePeriodField = DynamicRoleBasedAPIAccessChecker.class.getDeclaredField("cachePeriod");
+             cachePeriodField.setAccessible(true);
+             cachePeriodField.set(apiAccessCheckerSpy, 1);
+ 
+             Field rpCacheField = DynamicRoleBasedAPIAccessChecker.class.getDeclaredField("rolePermissionsCache");
+             rpCacheField.setAccessible(true);
+             rpCacheField.set(apiAccessCheckerSpy, new LazyCache<Long, Pair<Role, List<RolePermission>>>(32, 1, apiAccessCheckerSpy::getRolePermissions));
+ 
+             final RolePermission permission = new RolePermissionVO(1L, "api1", Permission.ALLOW, null);
+             Mockito.when(roleServiceMock.findAllPermissionsBy(Mockito.anyLong())).thenReturn(Collections.singletonList(permission));
+ 
+             Account account = getTestAccount();
+             List<String> apis = new ArrayList<>(Arrays.asList("api1"));
+ 
+             // First call should load permissions from the DAO and populate the cache
+             apiAccessCheckerSpy.getApisAllowedToAccount(account, apis);
+             // Second call should use cached permissions and not hit the DAO again
+             apiAccessCheckerSpy.getApisAllowedToAccount(account, apis);
+ 
+             Mockito.verify(roleServiceMock, Mockito.times(1)).findAllPermissionsBy(Mockito.anyLong());
+         } catch (NoSuchFieldException | IllegalAccessException e) {
+             Assert.fail("Failed to set cachePeriod for test: " + e.getMessage());
+         }
+     }
  }
diff --cc plugins/event-bus/webhook/src/main/java/org/apache/cloudstack/mom/webhook/WebhookApiServiceImpl.java
index a484f29e8d2,37f4b3d0a92..9c2ffefdaf4
--- a/plugins/event-bus/webhook/src/main/java/org/apache/cloudstack/mom/webhook/WebhookApiServiceImpl.java
+++ b/plugins/event-bus/webhook/src/main/java/org/apache/cloudstack/mom/webhook/WebhookApiServiceImpl.java
@@@ -66,10 -59,8 +66,9 @@@ import com.cloud.exception.PermissionDe
  import com.cloud.projects.Project;
  import com.cloud.user.Account;
  import com.cloud.user.AccountManager;
 +import com.cloud.utils.EnumUtils;
  import com.cloud.utils.Pair;
  import com.cloud.utils.Ternary;
- import com.cloud.utils.UriUtils;
  import com.cloud.utils.component.ManagerBase;
  import com.cloud.utils.db.Filter;
  import com.cloud.utils.db.SearchBuilder;
@@@ -235,25 -224,13 +234,32 @@@ public class WebhookApiServiceImpl exte
          throw new InvalidParameterValueException(error);
      }
  
 +    protected WebhookFilterResponse createWebhookFilterResponse(WebhookFilter webhookFilter, WebhookVO webhookVO) {
 +        WebhookFilterResponse response = new WebhookFilterResponse();
 +        response.setObjectName("webhookfilter");
 +        response.setId(webhookFilter.getUuid());
 +        if (webhookVO == null) {
 +            webhookVO = webhookDao.findById(webhookFilter.getWebhookId());
 +        }
 +        if (webhookVO != null) {
 +            response.setWebhookId(webhookVO.getUuid());
 +            response.setWebhookName(webhookVO.getName());
 +        }
 +        response.setType(webhookFilter.getType().toString());
 +        response.setMode(webhookFilter.getMode().toString());
 +        response.setMatchType(webhookFilter.getMatchType().toString());
 +        response.setValue(webhookFilter.getValue());
 +        response.setCreated(webhookFilter.getCreated());
 +        return response;
 +    }
 +
+     protected URI validatePayloadUrlByDeliverySecurityPolicy(String payloadUrl, long domainId) {
+         return WebhookUrlValidator.validateWebhookDestinationUrl(payloadUrl,
+                 WebhookService.WebhookDeliveryAllowHttp.valueIn(domainId),
+                 WebhookService.WebhookDeliveryBlocklist.valueIn(domainId),
+                 WebhookService.WebhookDeliveryBlockLocalAddresses.value());
+     }
+ 
      @Override
      public ListResponse<WebhookResponse> listWebhooks(ListWebhooksCmd cmd) {
          final CallContext ctx = CallContext.current();
@@@ -263,25 -240,8 +269,27 @@@
          final String name = cmd.getName();
          final String keyword = cmd.getKeyword();
          final String scopeStr = cmd.getScope();
+         final boolean isCallerRootAdmin = accountManager.isRootAdmin(caller.getId());
+         final boolean isCallerAdmin = isCallerRootAdmin || accountManager.isAdmin(caller.getId());
++
 +        Webhook.Scope scope = null;
 +        if (StringUtils.isNotEmpty(scopeStr)) {
 +            scope = EnumUtils.getEnumIgnoreCase(Webhook.Scope.class, scopeStr);
 +            if (scope == null) {
 +                throw new InvalidParameterValueException("Invalid scope specified");
 +            }
 +        }
-         if ((Webhook.Scope.Global.equals(scope) && !Account.Type.ADMIN.equals(caller.getType())) ||
-                 (Webhook.Scope.Domain.equals(scope) &&
-                         !List.of(Account.Type.ADMIN, Account.Type.DOMAIN_ADMIN).contains(caller.getType()))) {
++        if ((Webhook.Scope.Global.equals(scope) && !isCallerRootAdmin) ||
++                (Webhook.Scope.Domain.equals(scope) && !isCallerAdmin)) {
 +            throw new InvalidParameterValueException(String.format("Scope %s can not be specified", scope));
 +        }
 +        Webhook.State state = null;
 +        if (StringUtils.isNotEmpty(stateStr)) {
 +            state = EnumUtils.getEnumIgnoreCase(Webhook.State.class, stateStr);
 +            if (state == null) {
 +                throw new InvalidParameterValueException("Invalid state specified");
 +            }
 +        }
          List<WebhookResponse> responsesList = new ArrayList<>();
          List<Long> permittedAccounts = new ArrayList<>();
          Ternary<Long, Boolean, Project.ListProjectResourcesCriteria> domainIdRecursiveListProject =
@@@ -342,9 -322,12 +350,11 @@@
          final String scopeStr = cmd.getScope();
          final String stateStr = cmd.getState();
          Webhook.Scope scope = Webhook.Scope.Local;
+         final boolean isOwnerRootAdmin = accountManager.isRootAdmin(owner.getId());
+         final boolean isOwnerAdmin = isOwnerRootAdmin || accountManager.isAdmin(owner.getId());
          if (StringUtils.isNotEmpty(scopeStr)) {
 -            try {
 -                scope = Webhook.Scope.valueOf(scopeStr);
 -            } catch (IllegalArgumentException iae) {
 +            scope = EnumUtils.getEnumIgnoreCase(Webhook.Scope.class, scopeStr);
 +            if (scope == null) {
                  throw new InvalidParameterValueException("Invalid scope specified");
              }
          }
@@@ -424,27 -400,30 +431,28 @@@
              updateNeeded = true;
          }
          if (StringUtils.isNotEmpty(stateStr)) {
 -            try {
 -                Webhook.State state = Webhook.State.valueOf(stateStr);
 -                webhook.setState(state);
 -                updateNeeded = true;
 -            } catch (IllegalArgumentException iae) {
 +            Webhook.State state = EnumUtils.getEnumIgnoreCase(Webhook.State.class, stateStr);
 +            if (state == null) {
                  throw new InvalidParameterValueException("Invalid state specified");
              }
 +            webhook.setState(state);
 +            updateNeeded = true;
          }
          Account owner = accountManager.getAccount(webhook.getAccountId());
+         final boolean isOwnerRootAdmin = accountManager.isRootAdmin(owner.getId());
+         final boolean isOwnerAdmin = isOwnerRootAdmin || accountManager.isAdmin(owner.getId());
          if (StringUtils.isNotEmpty(scopeStr)) {
 -            try {
 -                Webhook.Scope scope = Webhook.Scope.valueOf(scopeStr);
 -                if ((Webhook.Scope.Global.equals(scope) && !isOwnerRootAdmin) ||
 -                        (Webhook.Scope.Domain.equals(scope) && !isOwnerAdmin)) {
 -                    throw new InvalidParameterValueException(
 -                            String.format("Scope %s can not be specified for owner %s", scope, owner.getName()));
 -                }
 -                webhook.setScope(scope);
 -                updateNeeded = true;
 -            } catch (IllegalArgumentException iae) {
 +            Webhook.Scope scope = EnumUtils.getEnumIgnoreCase(Webhook.Scope.class, scopeStr);
 +            if (scope == null) {
                  throw new InvalidParameterValueException("Invalid scope specified");
              }
-             if ((Webhook.Scope.Global.equals(scope) && !Account.Type.ADMIN.equals(owner.getType())) ||
-                     (Webhook.Scope.Domain.equals(scope) &&
-                             !List.of(Account.Type.ADMIN, Account.Type.DOMAIN_ADMIN).contains(owner.getType()))) {
++            if ((Webhook.Scope.Global.equals(scope) && !isOwnerRootAdmin) ||
++                    (Webhook.Scope.Domain.equals(scope) && !isOwnerAdmin)) {
 +                throw new InvalidParameterValueException(
 +                        String.format("Scope %s can not be specified for owner %s", scope, owner.getName()));
 +            }
 +            webhook.setScope(scope);
 +            updateNeeded = true;
          }
          URI uri = URI.create(webhook.getPayloadUrl());
          if (StringUtils.isNotEmpty(payloadUrl)) {
@@@ -576,17 -566,14 +599,17 @@@
                  webhook.setSecretKey(secretKey);
              }
              if (sslVerification != null) {
 -                webhook.setSslVerification(Boolean.TRUE.equals(sslVerification));
 +                webhook.setSslVerification(sslVerification);
              }
          }
 +        if (webhook != null) {
 +            accountManager.checkAccess(caller, SecurityChecker.AccessType.OperateEntry, false, webhook);
 +        }
          if (ObjectUtils.allNull(deliveryId, webhookId)) {
 -            webhook = new WebhookVO(owner.getDomainId(), owner.getId(), payloadUrl, secretKey,
 +            webhook = new WebhookVO(caller.getDomainId(), caller.getId(), payloadUrl, secretKey,
                      Boolean.TRUE.equals(sslVerification));
          }
-         WebhookDelivery webhookDelivery = webhookService.executeWebhookDelivery(existingDelivery, webhook, payload);
+         WebhookDelivery webhookDelivery = webhookService.executeWebhookDelivery(existingDelivery, webhook, payload, uri);
          if (webhookDelivery.getId() != WebhookDelivery.ID_DUMMY) {
              return createWebhookDeliveryResponse(webhookDeliveryJoinDao.findById(webhookDelivery.getId()));
          }
diff --cc plugins/event-bus/webhook/src/main/java/org/apache/cloudstack/mom/webhook/WebhookDeliveryThread.java
index 3f2d85458d3,e68fcc2f30c..9b67ba8eb03
--- a/plugins/event-bus/webhook/src/main/java/org/apache/cloudstack/mom/webhook/WebhookDeliveryThread.java
+++ b/plugins/event-bus/webhook/src/main/java/org/apache/cloudstack/mom/webhook/WebhookDeliveryThread.java
@@@ -99,9 -104,7 +106,10 @@@ public class WebhookDeliveryThread impl
  
      protected void setHttpClient() throws NoSuchAlgorithmException, KeyStoreException, KeyManagementException {
          if (webhook.isSslVerification()) {
 -            httpClient = HttpClients.custom().disableRedirectHandling().build();
 +            httpClient = HttpClients.custom()
 +                    .setSSLContext(SSLContext.getDefault())
++                    .disableRedirectHandling()
 +                    .build();
              return;
          }
          httpClient = HttpClients
diff --cc plugins/event-bus/webhook/src/main/java/org/apache/cloudstack/mom/webhook/WebhookService.java
index 7d8f9a0a744,ed8defd27c0..446a87335a4
--- a/plugins/event-bus/webhook/src/main/java/org/apache/cloudstack/mom/webhook/WebhookService.java
+++ b/plugins/event-bus/webhook/src/main/java/org/apache/cloudstack/mom/webhook/WebhookService.java
@@@ -57,9 -91,9 +91,11 @@@ public interface WebhookService extend
              "Interval (in seconds) for cleaning up webhook deliveries",
              false, ConfigKey.Scope.Global);
  
+ 
+ 
      void handleEvent(Event event) throws EventBusException;
-     WebhookDelivery executeWebhookDelivery(WebhookDelivery delivery, Webhook webhook, String payload)
+     WebhookDelivery executeWebhookDelivery(WebhookDelivery delivery, Webhook webhook, String payload, URI uri)
              throws CloudRuntimeException;
 +    void invalidateWebhooksCache();
 +    void invalidateWebhookFiltersCache(long webhookId);
  }
diff --cc plugins/event-bus/webhook/src/main/java/org/apache/cloudstack/mom/webhook/WebhookServiceImpl.java
index 624de54d41b,002916fef9d..85befa13a92
--- a/plugins/event-bus/webhook/src/main/java/org/apache/cloudstack/mom/webhook/WebhookServiceImpl.java
+++ b/plugins/event-bus/webhook/src/main/java/org/apache/cloudstack/mom/webhook/WebhookServiceImpl.java
@@@ -89,10 -84,26 +90,29 @@@ public class WebhookServiceImpl extend
      @Inject
      AccountManager accountManager;
  
 +    protected LazyCache<org.apache.commons.lang3.tuple.Pair<Long, List<Long>>, List<WebhookVO>> webhooksCache;
 +    protected LazyCache<Long, List<WebhookFilterVO>> webhookFiltersCache;
 +
-     protected WebhookDeliveryThread getDeliveryJob(Event event, Webhook webhook, Pair<Integer, Integer> configs) {
+     static class DeliveryConfig {
+         final int tries;
+         final int timeout;
+         final String blocklist;
+         final boolean blockLocalAddresses;
+         final boolean allowRedirects;
+         final boolean allowHttp;
+ 
+         DeliveryConfig(int tries, int timeout, String blocklist, boolean blockLocalAddresses,
+                 boolean allowRedirects, boolean allowHttp) {
+             this.tries = tries;
+             this.timeout = timeout;
+             this.blocklist = blocklist;
+             this.blockLocalAddresses = blockLocalAddresses;
+             this.allowRedirects = allowRedirects;
+             this.allowHttp = allowHttp;
+         }
+     }
+ 
+     protected WebhookDeliveryThread getDeliveryJob(Event event, Webhook webhook, DeliveryConfig config) {
          WebhookDeliveryThread.WebhookDeliveryContext<WebhookDeliveryThread.WebhookDeliveryResult> context =
                  new WebhookDeliveryThread.WebhookDeliveryContext<>(null, event.getEventId(), webhook.getId());
          AsyncCallbackDispatcher<WebhookServiceImpl, WebhookDeliveryThread.WebhookDeliveryResult> caller =
@@@ -182,28 -136,28 +206,33 @@@
              domainIds.addAll(domainDao.getDomainParentIds(event.getResourceDomainId()));
          }
          List<WebhookVO> webhooks =
 -                webhookDao.listByEnabledForDelivery(event.getResourceAccountId(), domainIds);
 +                webhooksCache.get(org.apache.commons.lang3.tuple.Pair.of(event.getResourceAccountId(), domainIds));
-         Map<Long, Pair<Integer, Integer>> domainConfigs = new HashMap<>();
+         Map<Long, DeliveryConfig> domainConfigs = new HashMap<>();
          for (WebhookVO webhook : webhooks) {
 +            List<? extends WebhookFilter> filters = webhookFiltersCache.get(webhook.getId());
 +            if (!isEventMatchingFilters(event, filters)) {
 +                logger.debug("Skipping delivering {} to {} as it doesn't match filters", event, webhook);
 +                continue;
 +            }
              if (!domainConfigs.containsKey(webhook.getDomainId())) {
                  domainConfigs.put(webhook.getDomainId(),
-                         new Pair<>(WebhookDeliveryTries.valueIn(webhook.getDomainId()),
-                         WebhookDeliveryTimeout.valueIn(webhook.getDomainId())));
+                         new DeliveryConfig(
+                                 WebhookDeliveryTries.valueIn(webhook.getDomainId()),
+                                 WebhookDeliveryTimeout.valueIn(webhook.getDomainId()),
+                                 WebhookDeliveryBlocklist.valueIn(webhook.getDomainId()),
+                                 WebhookDeliveryBlockLocalAddresses.value(),
+                                 WebhookDeliveryAllowRedirects.valueIn(webhook.getDomainId()),
+                                 WebhookDeliveryAllowHttp.valueIn(webhook.getDomainId())));
              }
-             Pair<Integer, Integer> configs = domainConfigs.get(webhook.getDomainId());
-             WebhookDeliveryThread job = getDeliveryJob(event, webhook, configs);
+             DeliveryConfig config = domainConfigs.get(webhook.getDomainId());
+             WebhookDeliveryThread job = getDeliveryJob(event, webhook, config);
              jobs.add(job);
          }
          return jobs;
      }
  
-     protected Runnable getManualDeliveryJob(WebhookDelivery existingDelivery, Webhook webhook, String payload,
+     protected Runnable getManualDeliveryJob(WebhookDelivery existingDelivery, Webhook webhook, String payload, URI uri,
 -                AsyncCallFuture<WebhookDeliveryThread.WebhookDeliveryResult> future) {
 +                        CompletableFuture<WebhookDeliveryThread.WebhookDeliveryResult> future) {
          if (StringUtils.isBlank(payload)) {
              payload = "{ \"CloudStack\": \"works!\" }";
          }
@@@ -358,10 -308,10 +395,10 @@@
      }
  
      @Override
-     public WebhookDelivery executeWebhookDelivery(WebhookDelivery delivery, Webhook webhook, String payload)
+     public WebhookDelivery executeWebhookDelivery(WebhookDelivery delivery, Webhook webhook, String payload, URI uri)
              throws CloudRuntimeException {
 -        AsyncCallFuture<WebhookDeliveryThread.WebhookDeliveryResult> future = new AsyncCallFuture<>();
 +        CompletableFuture<WebhookDeliveryThread.WebhookDeliveryResult> future = new CompletableFuture<>();
-         Runnable job = getManualDeliveryJob(delivery, webhook, payload, future);
+         Runnable job = getManualDeliveryJob(delivery, webhook, payload, uri, future);
          webhookJobExecutor.submit(job);
          WebhookDeliveryThread.WebhookDeliveryResult result = null;
          WebhookDeliveryVO webhookDeliveryVO;
diff --cc plugins/event-bus/webhook/src/test/java/org/apache/cloudstack/mom/webhook/WebhookApiServiceImplTest.java
index eb0da8e75c6,e623b758cbb..0e536c0bb97
--- a/plugins/event-bus/webhook/src/test/java/org/apache/cloudstack/mom/webhook/WebhookApiServiceImplTest.java
+++ b/plugins/event-bus/webhook/src/test/java/org/apache/cloudstack/mom/webhook/WebhookApiServiceImplTest.java
@@@ -16,6 -16,6 +16,7 @@@
  // under the License.
  package org.apache.cloudstack.mom.webhook;
  
++import java.net.URI;
  import java.util.Date;
  import java.util.List;
  
@@@ -83,22 -60,13 +84,23 @@@ public class WebhookApiServiceImplTest 
      @Mock
      WebhookDeliveryDao webhookDeliveryDao;
      @Mock
 +    WebhookDeliveryJoinDao webhookDeliveryJoinDao;
 +    @Mock
 +    WebhookFilterDao webhookFilterDao;
 +    @Mock
+     ManagementServerHostDao managementServerHostDao;
+     @Mock
      AccountManager accountManager;
+ 
      @Mock
      DomainDao domainDao;
 +    @Mock
 +    WebhookService webhookService;
-     @Mock
-     ManagementServerHostDao managementServerHostDao;
  
 +    @Mock
 +    Account caller;
 +
 +    @Spy
      @InjectMocks
      WebhookApiServiceImpl webhookApiServiceImpl = new WebhookApiServiceImpl();
  
@@@ -292,1026 -259,181 +293,1202 @@@
                  webhookApiServiceImpl.getNormalizedPayloadUrl("https://abc.com"));
      }
  
 +    @Test(expected = InvalidParameterValueException.class)
 +    public void basicWebhookDeliveryApiCheckThrowsExceptionForInvalidDeliveryId() {
 +        Mockito.when(webhookDeliveryDao.findById(1L)).thenReturn(null);
-         webhookApiServiceImpl.basicWebhookDeliveryApiCheck(Mockito.mock(Account.class), 1L, null, null, null, null);
++        webhookApiServiceImpl.basicWebhookDeliveryApiCheck(Mockito.mock(Account.class), 1L, null, null, null, null, false);
 +    }
 +
 +    @Test(expected = InvalidParameterValueException.class)
 +    public void basicWebhookDeliveryApiCheckThrowsExceptionForInvalidWebhookId() {
 +        Mockito.when(webhookDao.findById(1L)).thenReturn(null);
-         webhookApiServiceImpl.basicWebhookDeliveryApiCheck(Mockito.mock(Account.class), null, 1L, null, null, null);
++        webhookApiServiceImpl.basicWebhookDeliveryApiCheck(Mockito.mock(Account.class), null, 1L, null, null, null, false);
 +    }
 +
 +    @Test(expected = InvalidParameterValueException.class)
 +    public void basicWebhookDeliveryApiCheckThrowsExceptionForEndDateBeforeStartDate() {
-         webhookApiServiceImpl.basicWebhookDeliveryApiCheck(Mockito.mock(Account.class), null, null, null, new Date(), new Date(System.currentTimeMillis() - 1000));
++        webhookApiServiceImpl.basicWebhookDeliveryApiCheck(Mockito.mock(Account.class), null, null, null, new Date(), new Date(System.currentTimeMillis() - 1000), false);
 +    }
 +
 +    @Test(expected = PermissionDeniedException.class)
 +    public void basicWebhookDeliveryApiCheckThrowsExceptionForNonAdminAccessToManagementServer() {
 +        Account caller = Mockito.mock(Account.class);
-         Mockito.when(caller.getType()).thenReturn(Account.Type.NORMAL);
-         webhookApiServiceImpl.basicWebhookDeliveryApiCheck(caller, null, null, 1L, null, null);
++        webhookApiServiceImpl.basicWebhookDeliveryApiCheck(caller, null, null, 1L, null, null, false);
 +    }
 +
 +    @Test(expected = InvalidParameterValueException.class)
 +    public void basicWebhookDeliveryApiCheckThrowsExceptionForInvalidManagementServerId() {
 +        Account caller = Mockito.mock(Account.class);
-         Mockito.when(caller.getType()).thenReturn(Account.Type.ADMIN);
 +        Mockito.when(managementServerHostDao.findById(1L)).thenReturn(null);
-         webhookApiServiceImpl.basicWebhookDeliveryApiCheck(caller, null, null, 1L, null, null);
++        webhookApiServiceImpl.basicWebhookDeliveryApiCheck(caller, null, null, 1L, null, null, true);
 +    }
 +
 +    @Test
 +    public void basicWebhookDeliveryApiCheckReturnsManagementServerHostVOForValidInput() {
 +        Account caller = Mockito.mock(Account.class);
-         Mockito.when(caller.getType()).thenReturn(Account.Type.ADMIN);
 +        ManagementServerHostVO managementServerHostVO = Mockito.mock(ManagementServerHostVO.class);
 +        Mockito.when(managementServerHostDao.findById(1L)).thenReturn(managementServerHostVO);
-         ManagementServerHostVO result = webhookApiServiceImpl.basicWebhookDeliveryApiCheck(caller, null, null, 1L, null, null);
++        ManagementServerHostVO result = webhookApiServiceImpl.basicWebhookDeliveryApiCheck(caller, null, null, 1L, null, null, true);
++    }
++
+     @Test
+     public void shouldReturnNullWhenAllParametersAreNull() {
+         Account caller = Mockito.mock(Account.class);
+         ManagementServerHostVO result = webhookApiServiceImpl.basicWebhookDeliveryApiCheck(caller, null, null, null, null, null, false);
+         Assert.assertNull(result);
+     }
+ 
+     @Test
+     public void shouldValidateDeliveryIdAndReturnNull() {
+         Account caller = Mockito.mock(Account.class);
+         Long deliveryId = 1L;
+         WebhookDeliveryVO deliveryVO = Mockito.mock(WebhookDeliveryVO.class);
+         Mockito.when(deliveryVO.getWebhookId()).thenReturn(1L);
+         WebhookVO webhookVO = Mockito.mock(WebhookVO.class);
+         Mockito.when(webhookDeliveryDao.findById(deliveryId)).thenReturn(deliveryVO);
+         Mockito.when(webhookDao.findById(1L)).thenReturn(webhookVO);
+         Mockito.doNothing().when(accountManager).checkAccess(caller, SecurityChecker.AccessType.OperateEntry, false, webhookVO);
+         ManagementServerHostVO result = webhookApiServiceImpl.basicWebhookDeliveryApiCheck(caller, deliveryId, null, null, null, null, false);
+         Assert.assertNull(result);
+         Mockito.verify(webhookDeliveryDao).findById(deliveryId);
+     }
+ 
+     @Test(expected = InvalidParameterValueException.class)
+     public void shouldThrowExceptionForInvalidDeliveryId() {
+         Account caller = Mockito.mock(Account.class);
+         Mockito.when(webhookDeliveryDao.findById(1L)).thenReturn(null);
+         webhookApiServiceImpl.basicWebhookDeliveryApiCheck(caller, 1L, null, null, null, null, false);
+     }
+ 
+     @Test
+     public void shouldSkipWebhookAccessCheckWhenWebhookNotFound() {
+         Account caller = Mockito.mock(Account.class);
+         Long deliveryId = 1L;
+         WebhookDeliveryVO deliveryVO = Mockito.mock(WebhookDeliveryVO.class);
+         Mockito.when(deliveryVO.getWebhookId()).thenReturn(1L);
+         Mockito.when(webhookDeliveryDao.findById(deliveryId)).thenReturn(deliveryVO);
+         Mockito.when(webhookDao.findById(1L)).thenReturn(null);
+         ManagementServerHostVO result = webhookApiServiceImpl.basicWebhookDeliveryApiCheck(caller, deliveryId, null, null, null, null, false);
+         Assert.assertNull(result);
+         Mockito.verify(accountManager, Mockito.never()).checkAccess(Mockito.any(), Mockito.any(), Mockito.anyBoolean(), Mockito.any());
+     }
+ 
+     @Test
+     public void shouldValidateWebhookIdAndReturnNull() {
+         Account caller = Mockito.mock(Account.class);
+         Long webhookId = 1L;
+         WebhookVO webhookVO = Mockito.mock(WebhookVO.class);
+         Mockito.when(webhookDao.findById(webhookId)).thenReturn(webhookVO);
+         Mockito.doNothing().when(accountManager).checkAccess(caller, SecurityChecker.AccessType.OperateEntry, false, webhookVO);
+         ManagementServerHostVO result = webhookApiServiceImpl.basicWebhookDeliveryApiCheck(caller, null, webhookId, null, null, null, false);
+         Assert.assertNull(result);
+         Mockito.verify(webhookDao).findById(webhookId);
+     }
+ 
+     @Test(expected = InvalidParameterValueException.class)
+     public void shouldThrowExceptionForInvalidWebhookId() {
+         Account caller = Mockito.mock(Account.class);
+         Mockito.when(webhookDao.findById(1L)).thenReturn(null);
+         webhookApiServiceImpl.basicWebhookDeliveryApiCheck(caller, null, 1L, null, null, null, false);
+     }
+ 
+     @Test(expected = PermissionDeniedException.class)
+     public void shouldThrowExceptionWhenDeliveryAccessDenied() {
+         Account caller = Mockito.mock(Account.class);
+         Long deliveryId = 1L;
+         WebhookDeliveryVO deliveryVO = Mockito.mock(WebhookDeliveryVO.class);
+         Mockito.when(deliveryVO.getWebhookId()).thenReturn(1L);
+         WebhookVO webhookVO = Mockito.mock(WebhookVO.class);
+         Mockito.when(webhookDeliveryDao.findById(deliveryId)).thenReturn(deliveryVO);
+         Mockito.when(webhookDao.findById(1L)).thenReturn(webhookVO);
+         Mockito.doThrow(PermissionDeniedException.class).when(accountManager).checkAccess(caller, SecurityChecker.AccessType.OperateEntry, false, webhookVO);
+         webhookApiServiceImpl.basicWebhookDeliveryApiCheck(caller, deliveryId, null, null, null, null, false);
+     }
+ 
+     @Test(expected = PermissionDeniedException.class)
+     public void shouldThrowExceptionWhenWebhookAccessDenied() {
+         Account caller = Mockito.mock(Account.class);
+         WebhookVO webhookVO = Mockito.mock(WebhookVO.class);
+         Mockito.when(webhookDao.findById(1L)).thenReturn(webhookVO);
+         Mockito.doThrow(PermissionDeniedException.class).when(accountManager).checkAccess(caller, SecurityChecker.AccessType.OperateEntry, false, webhookVO);
+         webhookApiServiceImpl.basicWebhookDeliveryApiCheck(caller, null, 1L, null, null, null, false);
+     }
+ 
+     @Test(expected = InvalidParameterValueException.class)
+     public void shouldThrowExceptionWhenEndDateBeforeStartDate() {
+         Account caller = Mockito.mock(Account.class);
+         Date endDate = new Date(100);
+         Date startDate = new Date(200);
+         webhookApiServiceImpl.basicWebhookDeliveryApiCheck(caller, null, null, null, startDate, endDate, false);
+     }
+ 
+     @Test
+     public void shouldValidateDateRangeWhenDatesAreValid() {
+         Account caller = Mockito.mock(Account.class);
+         Date startDate = new Date(100);
+         Date endDate = new Date(200);
+         ManagementServerHostVO result = webhookApiServiceImpl.basicWebhookDeliveryApiCheck(caller, null, null, null, startDate, endDate, false);
+         Assert.assertNull(result);
+     }
+ 
+     @Test
+     public void shouldIgnoreDateValidationWhenOnlyStartDateProvided() {
+         Account caller = Mockito.mock(Account.class);
+         Date startDate = new Date(200);
+         ManagementServerHostVO result = webhookApiServiceImpl.basicWebhookDeliveryApiCheck(caller, null, null, null, startDate, null, false);
+         Assert.assertNull(result);
+     }
+ 
+     @Test
+     public void shouldIgnoreDateValidationWhenOnlyEndDateProvided() {
+         Account caller = Mockito.mock(Account.class);
+         Date endDate = new Date(100);
+         ManagementServerHostVO result = webhookApiServiceImpl.basicWebhookDeliveryApiCheck(caller, null, null, null, null, endDate, false);
+         Assert.assertNull(result);
+     }
+ 
+     @Test(expected = PermissionDeniedException.class)
+     public void shouldThrowExceptionWhenNonAdminAccessesManagementServer() {
+         Account caller = Mockito.mock(Account.class);
+         webhookApiServiceImpl.basicWebhookDeliveryApiCheck(caller, null, null, 1L, null, null, false);
+     }
+ 
+     @Test
+     public void shouldReturnManagementServerWhenAdminAccessesValidServer() {
+         Account caller = Mockito.mock(Account.class);
+         Long managementServerId = 1L;
+         ManagementServerHostVO managementServerHostVO = Mockito.mock(ManagementServerHostVO.class);
+         Mockito.when(managementServerHostDao.findById(managementServerId)).thenReturn(managementServerHostVO);
+         ManagementServerHostVO result = webhookApiServiceImpl.basicWebhookDeliveryApiCheck(caller, null, null, managementServerId, null, null, true);
+         Assert.assertNotNull(result);
+         Assert.assertEquals(managementServerHostVO, result);
+     }
+ 
+     @Test(expected = InvalidParameterValueException.class)
+     public void shouldThrowExceptionForInvalidManagementServerId() {
+         Account caller = Mockito.mock(Account.class);
+         Mockito.when(managementServerHostDao.findById(1L)).thenReturn(null);
+         webhookApiServiceImpl.basicWebhookDeliveryApiCheck(caller, null, null, 1L, null, null, true);
+     }
+ 
+     @Test
+     public void shouldValidateDeliveryAndManagementServer() {
+         Account caller = Mockito.mock(Account.class);
+         Long deliveryId = 1L;
+         Long managementServerId = 1L;
+         WebhookDeliveryVO deliveryVO = Mockito.mock(WebhookDeliveryVO.class);
+         Mockito.when(deliveryVO.getWebhookId()).thenReturn(1L);
+         WebhookVO webhookVO = Mockito.mock(WebhookVO.class);
+         ManagementServerHostVO managementServerHostVO = Mockito.mock(ManagementServerHostVO.class);
+         Mockito.when(webhookDeliveryDao.findById(deliveryId)).thenReturn(deliveryVO);
+         Mockito.when(webhookDao.findById(1L)).thenReturn(webhookVO);
+         Mockito.doNothing().when(accountManager).checkAccess(caller, SecurityChecker.AccessType.OperateEntry, false, webhookVO);
+         Mockito.when(managementServerHostDao.findById(managementServerId)).thenReturn(managementServerHostVO);
+         ManagementServerHostVO result = webhookApiServiceImpl.basicWebhookDeliveryApiCheck(caller, deliveryId, null, managementServerId, null, null, true);
+         Assert.assertNotNull(result);
          Assert.assertEquals(managementServerHostVO, result);
      }
  
      @Test
 +    public void basicWebhookDeliveryApiCheckValidatesWebhookDeliveryAccess() {
 +        Account caller = Mockito.mock(Account.class);
 +        WebhookDeliveryVO webhookDeliveryVO = Mockito.mock(WebhookDeliveryVO.class);
 +        WebhookVO webhookVO = Mockito.mock(WebhookVO.class);
 +        Mockito.when(webhookDeliveryDao.findById(1L)).thenReturn(webhookDeliveryVO);
 +        Mockito.when(webhookDeliveryVO.getWebhookId()).thenReturn(1L);
 +        Mockito.when(webhookDao.findById(1L)).thenReturn(webhookVO);
 +        Mockito.doNothing().when(accountManager).checkAccess(caller, SecurityChecker.AccessType.OperateEntry, false, webhookVO);
-         webhookApiServiceImpl.basicWebhookDeliveryApiCheck(caller, 1L, null, null, null, null);
++        webhookApiServiceImpl.basicWebhookDeliveryApiCheck(caller, 1L, null, null, null, null, false);
 +        Mockito.verify(accountManager).checkAccess(caller, SecurityChecker.AccessType.OperateEntry, false, webhookVO);
 +    }
 +
 +    @Test
 +    public void basicWebhookDeliveryApiCheckValidatesWebhookAccess() {
 +        Account caller = Mockito.mock(Account.class);
 +        WebhookVO webhookVO = Mockito.mock(WebhookVO.class);
 +        Mockito.when(webhookDao.findById(1L)).thenReturn(webhookVO);
 +        Mockito.doNothing().when(accountManager).checkAccess(caller, SecurityChecker.AccessType.OperateEntry, false, webhookVO);
-         webhookApiServiceImpl.basicWebhookDeliveryApiCheck(caller, null, 1L, null, null, null);
++        webhookApiServiceImpl.basicWebhookDeliveryApiCheck(caller, null, 1L, null, null, null, false);
 +        Mockito.verify(accountManager).checkAccess(caller, SecurityChecker.AccessType.OperateEntry, false, webhookVO);
 +    }
 +
 +    @Test
 +    public void createWebhookDeliveryResponsePopulatesAllFieldsCorrectly() {
 +        WebhookDeliveryJoinVO webhookDeliveryVO = Mockito.mock(WebhookDeliveryJoinVO.class);
 +        Mockito.when(webhookDeliveryVO.getUuid()).thenReturn("uuid");
 +        Mockito.when(webhookDeliveryVO.getEventUuid()).thenReturn("eventUuid");
 +        Mockito.when(webhookDeliveryVO.getEventType()).thenReturn("eventType");
 +        Mockito.when(webhookDeliveryVO.getWebhookUuId()).thenReturn("webhookUuid");
 +        Mockito.when(webhookDeliveryVO.getWebhookName()).thenReturn("webhookName");
 +        Mockito.when(webhookDeliveryVO.getManagementServerUuId()).thenReturn("managementServerUuid");
 +        Mockito.when(webhookDeliveryVO.getManagementServerName()).thenReturn("managementServerName");
 +        Mockito.when(webhookDeliveryVO.getHeaders()).thenReturn("headers");
 +        Mockito.when(webhookDeliveryVO.getPayload()).thenReturn("payload");
 +        Mockito.when(webhookDeliveryVO.isSuccess()).thenReturn(true);
 +        Mockito.when(webhookDeliveryVO.getResponse()).thenReturn("response");
 +        Mockito.when(webhookDeliveryVO.getStartTime()).thenReturn(new Date(1000));
 +        Mockito.when(webhookDeliveryVO.getEndTime()).thenReturn(new Date(2000));
 +
 +        WebhookDeliveryResponse response = webhookApiServiceImpl.createWebhookDeliveryResponse(webhookDeliveryVO);
 +
 +        Assert.assertEquals("webhookdelivery", ReflectionTestUtils.getField(response, "objectName"));
 +        Assert.assertEquals("uuid", ReflectionTestUtils.getField(response, "id"));
 +        Assert.assertEquals("eventUuid", ReflectionTestUtils.getField(response, "eventId"));
 +        Assert.assertEquals("eventType", ReflectionTestUtils.getField(response, "eventType"));
 +        Assert.assertEquals("webhookUuid", ReflectionTestUtils.getField(response, "webhookId"));
 +        Assert.assertEquals("webhookName", ReflectionTestUtils.getField(response, "webhookName"));
 +        Assert.assertEquals("managementServerUuid", ReflectionTestUtils.getField(response, "managementServerId"));
 +        Assert.assertEquals("managementServerName", ReflectionTestUtils.getField(response, "managementServerName"));
 +        Assert.assertEquals("headers", ReflectionTestUtils.getField(response, "headers"));
 +        Assert.assertEquals("payload", ReflectionTestUtils.getField(response, "payload"));
 +        Assert.assertTrue((Boolean) ReflectionTestUtils.getField(response, "success"));
 +        Assert.assertEquals("response", ReflectionTestUtils.getField(response, "response"));
 +        Assert.assertEquals(new Date(1000), ReflectionTestUtils.getField(response, "startTime"));
 +        Assert.assertEquals(new Date(2000), ReflectionTestUtils.getField(response, "endTime"));
 +    }
 +
 +    @Test(expected = NullPointerException.class)
 +    public void createWebhookDeliveryResponseThrowsExceptionForNullInput() {
 +        webhookApiServiceImpl.createWebhookDeliveryResponse(null);
 +    }
 +
 +    @Test
 +    public void createTestWebhookDeliveryResponsePopulatesAllFieldsCorrectly() {
 +        WebhookDelivery webhookDelivery = Mockito.mock(WebhookDelivery.class);
 +        Mockito.when(webhookDelivery.getUuid()).thenReturn("uuid");
 +        Mockito.when(webhookDelivery.getManagementServerId()).thenReturn(1L);
 +        Mockito.when(webhookDelivery.getHeaders()).thenReturn("headers");
 +        Mockito.when(webhookDelivery.getPayload()).thenReturn("payload");
 +        Mockito.when(webhookDelivery.isSuccess()).thenReturn(true);
 +        Mockito.when(webhookDelivery.getResponse()).thenReturn("response");
 +        Mockito.when(webhookDelivery.getStartTime()).thenReturn(new Date(1000));
 +        Mockito.when(webhookDelivery.getEndTime()).thenReturn(new Date(2000));
 +
 +        Webhook webhook = Mockito.mock(Webhook.class);
 +        Mockito.when(webhook.getUuid()).thenReturn("webhookUuid");
 +        Mockito.when(webhook.getName()).thenReturn("webhookName");
 +
 +        ManagementServerHostVO msHost = Mockito.mock(ManagementServerHostVO.class);
 +        Mockito.when(msHost.getUuid()).thenReturn("managementServerUuid");
 +        Mockito.when(msHost.getName()).thenReturn("managementServerName");
 +        Mockito.when(managementServerHostDao.findByMsid(1L)).thenReturn(msHost);
 +
 +        WebhookDeliveryResponse response = webhookApiServiceImpl.createTestWebhookDeliveryResponse(webhookDelivery, webhook);
 +
 +        Assert.assertEquals("webhookdelivery", ReflectionTestUtils.getField(response, "objectName"));
 +        Assert.assertEquals("uuid", ReflectionTestUtils.getField(response, "id"));
 +        Assert.assertEquals(WebhookDelivery.TEST_EVENT_TYPE, ReflectionTestUtils.getField(response, "eventType"));
 +        Assert.assertEquals("webhookUuid", ReflectionTestUtils.getField(response, "webhookId"));
 +        Assert.assertEquals("webhookName", ReflectionTestUtils.getField(response, "webhookName"));
 +        Assert.assertEquals("managementServerUuid", ReflectionTestUtils.getField(response, "managementServerId"));
 +        Assert.assertEquals("managementServerName", ReflectionTestUtils.getField(response, "managementServerName"));
 +        Assert.assertEquals("headers", ReflectionTestUtils.getField(response, "headers"));
 +        Assert.assertEquals("payload", ReflectionTestUtils.getField(response, "payload"));
 +        Assert.assertTrue((Boolean) ReflectionTestUtils.getField(response, "success"));
 +        Assert.assertEquals("response", ReflectionTestUtils.getField(response, "response"));
 +        Assert.assertEquals(new Date(1000), ReflectionTestUtils.getField(response, "startTime"));
 +        Assert.assertEquals(new Date(2000), ReflectionTestUtils.getField(response, "endTime"));
 +    }
 +
 +    @Test
 +    public void createTestWebhookDeliveryResponseHandlesNullWebhook() {
 +        WebhookDelivery webhookDelivery = Mockito.mock(WebhookDelivery.class);
 +        Mockito.when(webhookDelivery.getUuid()).thenReturn("uuid");
 +        Mockito.when(webhookDelivery.getManagementServerId()).thenReturn(1L);
 +        Mockito.when(webhookDelivery.getHeaders()).thenReturn("headers");
 +        Mockito.when(webhookDelivery.getPayload()).thenReturn("payload");
 +        Mockito.when(webhookDelivery.isSuccess()).thenReturn(true);
 +        Mockito.when(webhookDelivery.getResponse()).thenReturn("response");
 +        Mockito.when(webhookDelivery.getStartTime()).thenReturn(new Date(1000));
 +        Mockito.when(webhookDelivery.getEndTime()).thenReturn(new Date(2000));
 +
 +        ManagementServerHostVO msHost = Mockito.mock(ManagementServerHostVO.class);
 +        Mockito.when(msHost.getUuid()).thenReturn("managementServerUuid");
 +        Mockito.when(msHost.getName()).thenReturn("managementServerName");
 +        Mockito.when(managementServerHostDao.findByMsid(1L)).thenReturn(msHost);
 +
 +        WebhookDeliveryResponse response = webhookApiServiceImpl.createTestWebhookDeliveryResponse(webhookDelivery, null);
 +
 +        Assert.assertEquals("webhookdelivery", ReflectionTestUtils.getField(response, "objectName"));
 +        Assert.assertEquals("uuid", ReflectionTestUtils.getField(response, "id"));
 +        Assert.assertEquals(WebhookDelivery.TEST_EVENT_TYPE, ReflectionTestUtils.getField(response, "eventType"));
 +        Assert.assertNull(ReflectionTestUtils.getField(response, "webhookId"));
 +        Assert.assertNull(ReflectionTestUtils.getField(response, "webhookName"));
 +        Assert.assertEquals("managementServerUuid", ReflectionTestUtils.getField(response, "managementServerId"));
 +        Assert.assertEquals("managementServerName", ReflectionTestUtils.getField(response, "managementServerName"));
 +        Assert.assertEquals("headers", ReflectionTestUtils.getField(response, "headers"));
 +        Assert.assertEquals("payload", ReflectionTestUtils.getField(response, "payload"));
 +        Assert.assertTrue((Boolean) ReflectionTestUtils.getField(response, "success"));
 +        Assert.assertEquals("response", ReflectionTestUtils.getField(response, "response"));
 +        Assert.assertEquals(new Date(1000), ReflectionTestUtils.getField(response, "startTime"));
 +        Assert.assertEquals(new Date(2000), ReflectionTestUtils.getField(response, "endTime"));
 +    }
 +
 +    @Test
 +    public void createTestWebhookDeliveryResponseHandlesNullManagementServer() {
 +        WebhookDelivery webhookDelivery = Mockito.mock(WebhookDelivery.class);
 +        Mockito.when(webhookDelivery.getUuid()).thenReturn("uuid");
 +        Mockito.when(webhookDelivery.getManagementServerId()).thenReturn(1L);
 +        Mockito.when(webhookDelivery.getHeaders()).thenReturn("headers");
 +        Mockito.when(webhookDelivery.getPayload()).thenReturn("payload");
 +        Mockito.when(webhookDelivery.isSuccess()).thenReturn(true);
 +        Mockito.when(webhookDelivery.getResponse()).thenReturn("response");
 +        Mockito.when(webhookDelivery.getStartTime()).thenReturn(new Date(1000));
 +        Mockito.when(webhookDelivery.getEndTime()).thenReturn(new Date(2000));
 +
 +        Webhook webhook = Mockito.mock(Webhook.class);
 +        Mockito.when(webhook.getUuid()).thenReturn("webhookUuid");
 +        Mockito.when(webhook.getName()).thenReturn("webhookName");
 +
 +        Mockito.when(managementServerHostDao.findByMsid(1L)).thenReturn(null);
 +
 +        WebhookDeliveryResponse response = webhookApiServiceImpl.createTestWebhookDeliveryResponse(webhookDelivery, webhook);
 +
 +        Assert.assertEquals("webhookdelivery", ReflectionTestUtils.getField(response, "objectName"));
 +        Assert.assertEquals("uuid", ReflectionTestUtils.getField(response, "id"));
 +        Assert.assertEquals(WebhookDelivery.TEST_EVENT_TYPE, ReflectionTestUtils.getField(response, "eventType"));
 +        Assert.assertEquals("webhookUuid", ReflectionTestUtils.getField(response, "webhookId"));
 +        Assert.assertEquals("webhookName", ReflectionTestUtils.getField(response, "webhookName"));
 +        Assert.assertNull(ReflectionTestUtils.getField(response, "managementServerId"));
 +        Assert.assertNull(ReflectionTestUtils.getField(response, "managementServerName"));
 +        Assert.assertEquals("headers", ReflectionTestUtils.getField(response, "headers"));
 +        Assert.assertEquals("payload", ReflectionTestUtils.getField(response, "payload"));
 +        Assert.assertTrue((Boolean) ReflectionTestUtils.getField(response, "success"));
 +        Assert.assertEquals("response", ReflectionTestUtils.getField(response, "response"));
 +        Assert.assertEquals(new Date(1000), ReflectionTestUtils.getField(response, "startTime"));
 +        Assert.assertEquals(new Date(2000), ReflectionTestUtils.getField(response, "endTime"));
 +    }
 +
 +    @Test
 +    public void getOwnerReturnsFinalizedOwnerForValidInput() {
 +        CreateWebhookCmd cmd = Mockito.mock(CreateWebhookCmd.class);
 +        Account finalizedOwner = Mockito.mock(Account.class);
 +
 +        Mockito.when(cmd.getAccountName()).thenReturn("accountName");
 +        Mockito.when(cmd.getDomainId()).thenReturn(1L);
 +        Mockito.when(cmd.getProjectId()).thenReturn(2L);
 +        Mockito.when(accountManager.finalizeOwner(caller, "accountName", 1L, 2L)).thenReturn(finalizedOwner);
 +
 +        Account result = webhookApiServiceImpl.getOwner(cmd);
 +
 +        Assert.assertEquals(finalizedOwner, result);
 +    }
 +
 +    @Test(expected = InvalidParameterValueException.class)
 +    public void getOwnerThrowsExceptionForInvalidAccount() {
 +        CreateWebhookCmd cmd = Mockito.mock(CreateWebhookCmd.class);
 +
 +        Mockito.when(cmd.getAccountName()).thenReturn("invalidAccount");
 +        Mockito.when(cmd.getDomainId()).thenReturn(1L);
 +        Mockito.when(cmd.getProjectId()).thenReturn(null);
 +        Mockito.when(accountManager.finalizeOwner(caller, "invalidAccount", 1L, null))
 +                .thenThrow(new InvalidParameterValueException("Invalid account"));
 +
 +        webhookApiServiceImpl.getOwner(cmd);
 +    }
 +
 +    @Test
 +    public void createWebhookFilterResponsePopulatesAllFieldsCorrectly() {
 +        WebhookFilter webhookFilter = Mockito.mock(WebhookFilter.class);
 +        Mockito.when(webhookFilter.getUuid()).thenReturn("filterUuid");
 +        Mockito.when(webhookFilter.getType()).thenReturn(WebhookFilter.Type.EventType);
 +        Mockito.when(webhookFilter.getMode()).thenReturn(WebhookFilter.Mode.Include);
 +        Mockito.when(webhookFilter.getMatchType()).thenReturn(WebhookFilter.MatchType.Exact);
 +        Mockito.when(webhookFilter.getValue()).thenReturn("value");
 +        Mockito.when(webhookFilter.getCreated()).thenReturn(new Date(1000));
 +
 +        WebhookVO webhookVO = Mockito.mock(WebhookVO.class);
 +        Mockito.when(webhookVO.getUuid()).thenReturn("webhookUuid");
 +        Mockito.when(webhookVO.getName()).thenReturn("webhookName");
 +
 +        WebhookFilterResponse response = webhookApiServiceImpl.createWebhookFilterResponse(webhookFilter, webhookVO);
 +
 +        Assert.assertEquals("webhookfilter", ReflectionTestUtils.getField(response, "objectName"));
 +        Assert.assertEquals("filterUuid", ReflectionTestUtils.getField(response, "id"));
 +        Assert.assertEquals("webhookUuid", ReflectionTestUtils.getField(response, "webhookId"));
 +        Assert.assertEquals("webhookName", ReflectionTestUtils.getField(response, "webhookName"));
 +        Assert.assertEquals("EventType", ReflectionTestUtils.getField(response, "type"));
 +        Assert.assertEquals("Include", ReflectionTestUtils.getField(response, "mode"));
 +        Assert.assertEquals("Exact", ReflectionTestUtils.getField(response, "matchType"));
 +        Assert.assertEquals("value", ReflectionTestUtils.getField(response, "value"));
 +        Assert.assertEquals(new Date(1000), ReflectionTestUtils.getField(response, "created"));
 +    }
 +
 +    @Test
 +    public void createWebhookFilterResponseHandlesNullWebhookVO() {
 +        WebhookFilter webhookFilter = Mockito.mock(WebhookFilter.class);
 +        Mockito.when(webhookFilter.getUuid()).thenReturn("filterUuid");
 +        Mockito.when(webhookFilter.getWebhookId()).thenReturn(1L);
 +        Mockito.when(webhookFilter.getType()).thenReturn(WebhookFilter.Type.EventType);
 +        Mockito.when(webhookFilter.getMode()).thenReturn(WebhookFilter.Mode.Include);
 +        Mockito.when(webhookFilter.getMatchType()).thenReturn(WebhookFilter.MatchType.Exact);
 +        Mockito.when(webhookFilter.getValue()).thenReturn("value");
 +        Mockito.when(webhookFilter.getCreated()).thenReturn(new Date(1000));
 +
 +        WebhookVO webhookVO = Mockito.mock(WebhookVO.class);
 +        Mockito.when(webhookVO.getUuid()).thenReturn("webhookUuid");
 +        Mockito.when(webhookVO.getName()).thenReturn("webhookName");
 +        Mockito.when(webhookDao.findById(1L)).thenReturn(webhookVO);
 +
 +        WebhookFilterResponse response = webhookApiServiceImpl.createWebhookFilterResponse(webhookFilter, null);
 +
 +        Assert.assertEquals("webhookfilter", ReflectionTestUtils.getField(response, "objectName"));
 +        Assert.assertEquals("filterUuid", ReflectionTestUtils.getField(response, "id"));
 +        Assert.assertEquals("webhookUuid", ReflectionTestUtils.getField(response, "webhookId"));
 +        Assert.assertEquals("webhookName", ReflectionTestUtils.getField(response, "webhookName"));
 +        Assert.assertEquals("EventType", ReflectionTestUtils.getField(response, "type"));
 +        Assert.assertEquals("Include", ReflectionTestUtils.getField(response, "mode"));
 +        Assert.assertEquals("Exact", ReflectionTestUtils.getField(response, "matchType"));
 +        Assert.assertEquals("value", ReflectionTestUtils.getField(response, "value"));
 +        Assert.assertEquals(new Date(1000), ReflectionTestUtils.getField(response, "created"));
 +    }
 +
 +    @Test
 +    public void createWebhookFilterResponseHandlesNullWebhookFromDao() {
 +        WebhookFilter webhookFilter = Mockito.mock(WebhookFilter.class);
 +        Mockito.when(webhookFilter.getUuid()).thenReturn("filterUuid");
 +        Mockito.when(webhookFilter.getWebhookId()).thenReturn(1L);
 +        Mockito.when(webhookFilter.getType()).thenReturn(WebhookFilter.Type.EventType);
 +        Mockito.when(webhookFilter.getMode()).thenReturn(WebhookFilter.Mode.Include);
 +        Mockito.when(webhookFilter.getMatchType()).thenReturn(WebhookFilter.MatchType.Exact);
 +        Mockito.when(webhookFilter.getValue()).thenReturn("value");
 +        Mockito.when(webhookFilter.getCreated()).thenReturn(new Date(1000));
 +
 +        Mockito.when(webhookDao.findById(1L)).thenReturn(null);
 +
 +        WebhookFilterResponse response = webhookApiServiceImpl.createWebhookFilterResponse(webhookFilter, null);
 +
 +        Assert.assertEquals("webhookfilter", ReflectionTestUtils.getField(response, "objectName"));
 +        Assert.assertEquals("filterUuid", ReflectionTestUtils.getField(response, "id"));
 +        Assert.assertNull(ReflectionTestUtils.getField(response, "webhookId"));
 +        Assert.assertNull(ReflectionTestUtils.getField(response, "webhookName"));
 +        Assert.assertEquals("EventType", ReflectionTestUtils.getField(response, "type"));
 +        Assert.assertEquals("Include", ReflectionTestUtils.getField(response, "mode"));
 +        Assert.assertEquals("Exact", ReflectionTestUtils.getField(response, "matchType"));
 +        Assert.assertEquals("value", ReflectionTestUtils.getField(response, "value"));
 +        Assert.assertEquals(new Date(1000), ReflectionTestUtils.getField(response, "created"));
 +    }
 +
 +    @Test
 +    public void listWebhooksReturnsEmptyResponseForNoWebhooks() {
 +        ListWebhooksCmd cmd = Mockito.mock(ListWebhooksCmd.class);
 +
 +        Mockito.when(cmd.getId()).thenReturn(null);
 +        Mockito.when(cmd.getState()).thenReturn(null);
 +        Mockito.when(cmd.getName()).thenReturn(null);
 +        Mockito.when(cmd.getKeyword()).thenReturn(null);
 +        Mockito.when(cmd.getScope()).thenReturn(null);
 +        Mockito.when(cmd.getStartIndex()).thenReturn(0L);
 +        Mockito.when(cmd.getPageSizeVal()).thenReturn(10L);
 +        SearchBuilder<WebhookJoinVO> sb = Mockito.mock(SearchBuilder.class);
 +        Mockito.when(sb.entity()).thenReturn(Mockito.mock(WebhookJoinVO.class));
 +        SearchCriteria<WebhookJoinVO> sc = Mockito.mock(SearchCriteria.class);
 +        Mockito.when(sb.create()).thenReturn(sc);
 +        Mockito.when(webhookJoinDao.createSearchBuilder()).thenReturn(sb);
 +        Mockito.when(webhookJoinDao.searchAndCount(Mockito.any(), Mockito.any())).thenReturn(new Pair<>(List.of(), 0));
 +
 +        ListResponse<WebhookResponse> response = webhookApiServiceImpl.listWebhooks(cmd);
 +
 +        Assert.assertTrue(response.getResponses().isEmpty());
 +        Assert.assertEquals(0, (int) response.getCount());
 +    }
 +
 +    @Test(expected = InvalidParameterValueException.class)
 +    public void listWebhooksThrowsExceptionForInvalidScope() {
 +        ListWebhooksCmd cmd = Mockito.mock(ListWebhooksCmd.class);
 +
 +        Mockito.when(cmd.getScope()).thenReturn("InvalidScope");
 +
 +        webhookApiServiceImpl.listWebhooks(cmd);
 +    }
 +
 +    @Test(expected = InvalidParameterValueException.class)
 +    public void listWebhooksThrowsExceptionForInvalidState() {
 +        ListWebhooksCmd cmd = Mockito.mock(ListWebhooksCmd.class);
 +
 +        Mockito.when(cmd.getState()).thenReturn("InvalidState");
 +
 +        webhookApiServiceImpl.listWebhooks(cmd);
 +    }
 +
 +    @Test
 +    public void listWebhooksFiltersByValidScopeAndState() {
 +        ListWebhooksCmd cmd = Mockito.mock(ListWebhooksCmd.class);
-         Mockito.when(caller.getType()).thenReturn(Account.Type.DOMAIN_ADMIN);
++        Mockito.when(accountManager.isAdmin(Mockito.anyLong())).thenReturn(true);
 +        WebhookJoinVO webhook = Mockito.mock(WebhookJoinVO.class);
 +        Mockito.when(webhook.getState()).thenReturn(Webhook.State.Enabled);
 +        Mockito.when(webhook.getScope()).thenReturn(Webhook.Scope.Domain);
 +
 +        Mockito.when(cmd.getScope()).thenReturn("Domain");
 +        Mockito.when(cmd.getState()).thenReturn("Enabled");
 +        Mockito.when(cmd.getStartIndex()).thenReturn(0L);
 +        Mockito.when(cmd.getPageSizeVal()).thenReturn(10L);
 +        SearchBuilder<WebhookJoinVO> sb = Mockito.mock(SearchBuilder.class);
 +        Mockito.when(sb.entity()).thenReturn(Mockito.mock(WebhookJoinVO.class));
 +        SearchCriteria<WebhookJoinVO> sc = Mockito.mock(SearchCriteria.class);
 +        Mockito.when(sb.create()).thenReturn(sc);
 +        Mockito.when(webhookJoinDao.createSearchBuilder()).thenReturn(sb);
 +        Mockito.when(webhookJoinDao.searchAndCount(Mockito.any(), Mockito.any())).thenReturn(new Pair<>(List.of(webhook), 1));
 +
 +        ListResponse<WebhookResponse> response = webhookApiServiceImpl.listWebhooks(cmd);
 +
 +        Assert.assertEquals(1, (int) response.getCount());
 +        Assert.assertEquals(1, response.getResponses().size());
 +    }
 +
 +    @Test(expected = InvalidParameterValueException.class)
 +    public void listWebhooksThrowsExceptionForUnauthorizedScope() {
 +        ListWebhooksCmd cmd = Mockito.mock(ListWebhooksCmd.class);
 +
 +        Mockito.when(cmd.getScope()).thenReturn("Global");
-         Mockito.when(caller.getType()).thenReturn(Account.Type.NORMAL);
 +
 +        webhookApiServiceImpl.listWebhooks(cmd);
 +    }
 +
 +    @Test
 +    public void createWebhookCreatesWebhookSuccessfully() {
 +        CreateWebhookCmd cmd = Mockito.mock(CreateWebhookCmd.class);
 +        WebhookVO webhook = Mockito.mock(WebhookVO.class);
 +        WebhookJoinVO webhookJoinVO = Mockito.mock(WebhookJoinVO.class);
 +        Mockito.when(webhookJoinVO.getState()).thenReturn(Webhook.State.Enabled);
 +        Mockito.when(webhookJoinVO.getScope()).thenReturn(Webhook.Scope.Local);
 +
 +        Mockito.when(cmd.getName()).thenReturn("webhookName");
 +        Mockito.when(cmd.getDescription()).thenReturn("webhookDescription");
 +        Mockito.when(cmd.getPayloadUrl()).thenReturn("https://example.com");
 +        Mockito.when(cmd.getSecretKey()).thenReturn("secretKey");
 +        Mockito.when(cmd.isSslVerification()).thenReturn(true);
 +        Mockito.when(cmd.getScope()).thenReturn("Local");
 +        Mockito.when(cmd.getState()).thenReturn("Enabled");
-         Mockito.when(caller.getType()).thenReturn(Account.Type.ADMIN);
 +        Mockito.when(caller.getDomainId()).thenReturn(1L);
 +        Mockito.when(webhookDao.persist(Mockito.any(WebhookVO.class))).thenReturn(webhook);
 +        Mockito.when(webhook.getId()).thenReturn(1L);
 +        Mockito.when(webhookJoinDao.findById(1L)).thenReturn(webhookJoinVO);
 +        Mockito.doReturn(caller).when(webhookApiServiceImpl).getOwner(cmd);
 +
 +        WebhookResponse response = webhookApiServiceImpl.createWebhook(cmd);
 +
 +        Assert.assertNotNull(response);
 +        Assert.assertEquals("webhook", ReflectionTestUtils.getField(response, "objectName"));
 +    }
 +
 +    @Test(expected = InvalidParameterValueException.class)
 +    public void createWebhookThrowsExceptionForInvalidScope() {
 +        CreateWebhookCmd cmd = Mockito.mock(CreateWebhookCmd.class);
 +
 +        Mockito.when(cmd.getScope()).thenReturn("InvalidScope");
++        Mockito.doReturn(caller).when(webhookApiServiceImpl).getOwner(cmd);
 +
 +        webhookApiServiceImpl.createWebhook(cmd);
 +    }
 +
 +    @Test(expected = InvalidParameterValueException.class)
 +    public void createWebhookThrowsExceptionForInvalidState() {
 +        CreateWebhookCmd cmd = Mockito.mock(CreateWebhookCmd.class);
 +
 +        Mockito.when(cmd.getState()).thenReturn("InvalidState");
++        Mockito.doReturn(caller).when(webhookApiServiceImpl).getOwner(cmd);
 +
 +        webhookApiServiceImpl.createWebhook(cmd);
 +    }
 +
-     @Test(expected = IllegalArgumentException.class)
++    @Test(expected = InvalidParameterValueException.class)
 +    public void createWebhookThrowsExceptionForInvalidUrl() {
 +        CreateWebhookCmd cmd = Mockito.mock(CreateWebhookCmd.class);
 +
 +        Mockito.when(cmd.getPayloadUrl()).thenReturn("invalid-url");
 +        Mockito.doReturn(caller).when(webhookApiServiceImpl).getOwner(cmd);
 +
 +        webhookApiServiceImpl.createWebhook(cmd);
 +    }
 +
 +    @Test(expected = InvalidParameterValueException.class)
 +    public void createWebhookThrowsExceptionForNonHttpsWithSslVerification() {
 +        CreateWebhookCmd cmd = Mockito.mock(CreateWebhookCmd.class);
 +
 +        Mockito.when(cmd.getPayloadUrl()).thenReturn("http://example.com");
 +        Mockito.when(cmd.isSslVerification()).thenReturn(true);
 +        Mockito.doReturn(caller).when(webhookApiServiceImpl).getOwner(cmd);
-         Mockito.doNothing().when(webhookApiServiceImpl)
-                 .validateWebhookOwnerPayloadUrl(caller, "http://example.com", null);
 +
 +        webhookApiServiceImpl.createWebhook(cmd);
 +    }
 +
 +    @Test(expected = InvalidParameterValueException.class)
 +    public void createWebhookThrowsExceptionForDuplicatePayloadUrl() {
 +        CreateWebhookCmd cmd = Mockito.mock(CreateWebhookCmd.class);
 +        WebhookVO existingWebhook = Mockito.mock(WebhookVO.class);
 +
 +        Mockito.when(cmd.getPayloadUrl()).thenReturn("https://example.com");
 +        Mockito.when(caller.getId()).thenReturn(1L);
 +        Mockito.doReturn(caller).when(webhookApiServiceImpl).getOwner(cmd);
 +        Mockito.when(webhookDao.findByAccountAndPayloadUrl(1L, "https://example.com")).thenReturn(existingWebhook);
 +
 +        webhookApiServiceImpl.createWebhook(cmd);
 +    }
 +
 +    @Test
 +    public void updateWebhookUpdatesAllFieldsSuccessfully() {
 +        UpdateWebhookCmd cmd = Mockito.mock(UpdateWebhookCmd.class);
 +        WebhookVO webhook = Mockito.mock(WebhookVO.class);
 +        Mockito.when(webhook.getId()).thenReturn(1L);
 +        Mockito.when(webhook.getPayloadUrl()).thenReturn("http://abc.xyz");
 +        String updatedUrl = "https://cloudstack.apache.org/";
 +
 +        Mockito.when(cmd.getId()).thenReturn(1L);
 +        Mockito.when(cmd.getName()).thenReturn("Updated Name");
 +        Mockito.when(cmd.getDescription()).thenReturn("Updated Description");
 +        Mockito.when(cmd.getPayloadUrl()).thenReturn(updatedUrl);
 +        Mockito.when(cmd.getSecretKey()).thenReturn("UpdatedSecretKey");
 +        Mockito.when(cmd.isSslVerification()).thenReturn(true);
 +        Mockito.when(cmd.getScope()).thenReturn("Local");
 +        Mockito.when(cmd.getState()).thenReturn("Enabled");
 +        Mockito.when(webhookDao.findById(1L)).thenReturn(webhook);
 +        Mockito.when(webhook.getAccountId()).thenReturn(1L);
 +        Mockito.when(accountManager.getAccount(1L)).thenReturn(caller);
 +        Mockito.when(webhookDao.update(1L, webhook)).thenReturn(true);
 +        Mockito.doReturn(Mockito.mock(WebhookResponse.class)).when(webhookApiServiceImpl)
 +                .createWebhookResponse(1L);
 +
 +        WebhookResponse response = webhookApiServiceImpl.updateWebhook(cmd);
 +
 +
 +        Assert.assertNotNull(response);
 +        Mockito.verify(webhook).setName("Updated Name");
 +        Mockito.verify(webhook).setDescription("Updated Description");
 +        Mockito.verify(webhook).setPayloadUrl(updatedUrl);
 +        Mockito.verify(webhook).setSecretKey("UpdatedSecretKey");
 +        Mockito.verify(webhook).setSslVerification(true);
 +        Mockito.verify(webhook).setScope(Webhook.Scope.Local);
 +        Mockito.verify(webhook).setState(Webhook.State.Enabled);
 +    }
 +
 +    @Test(expected = InvalidParameterValueException.class)
 +    public void updateWebhookThrowsExceptionForInvalidState() {
 +        UpdateWebhookCmd cmd = Mockito.mock(UpdateWebhookCmd.class);
 +        WebhookVO webhook = Mockito.mock(WebhookVO.class);
 +
 +        Mockito.when(cmd.getId()).thenReturn(1L);
 +        Mockito.when(cmd.getState()).thenReturn("InvalidState");
 +        Mockito.when(webhookDao.findById(1L)).thenReturn(webhook);
 +
 +        webhookApiServiceImpl.updateWebhook(cmd);
 +    }
 +
 +    @Test(expected = InvalidParameterValueException.class)
 +    public void updateWebhookThrowsExceptionForInvalidScope() {
 +        UpdateWebhookCmd cmd = Mockito.mock(UpdateWebhookCmd.class);
 +        WebhookVO webhook = Mockito.mock(WebhookVO.class);
 +
 +        Mockito.when(cmd.getId()).thenReturn(1L);
 +        Mockito.when(cmd.getScope()).thenReturn("InvalidScope");
 +        Mockito.when(webhookDao.findById(1L)).thenReturn(webhook);
 +        Mockito.when(webhook.getAccountId()).thenReturn(1L);
 +        Mockito.when(accountManager.getAccount(1L)).thenReturn(caller);
 +
 +        webhookApiServiceImpl.updateWebhook(cmd);
 +    }
 +
 +    @Test(expected = InvalidParameterValueException.class)
 +    public void updateWebhookThrowsExceptionForNonHttpsWithSslVerification() {
 +        UpdateWebhookCmd cmd = Mockito.mock(UpdateWebhookCmd.class);
 +        WebhookVO webhook = Mockito.mock(WebhookVO.class);
 +        Mockito.when(webhook.getPayloadUrl()).thenReturn("http://abc.xyz");
 +
 +        Mockito.when(cmd.getId()).thenReturn(1L);
 +        Mockito.when(cmd.getPayloadUrl()).thenReturn("http://cloudstack.apache.org/");
 +        Mockito.when(cmd.isSslVerification()).thenReturn(true);
 +        Mockito.when(webhook.getAccountId()).thenReturn(1L);
 +        Mockito.when(accountManager.getAccount(1L)).thenReturn(caller);
-         Mockito.doNothing().when(webhookApiServiceImpl)
-                 .validateWebhookOwnerPayloadUrl(caller, "http://cloudstack.apache.org/", webhook);
 +        Mockito.when(webhookDao.findById(1L)).thenReturn(webhook);
 +
 +        webhookApiServiceImpl.updateWebhook(cmd);
 +    }
 +
 +    @Test
 +    public void updateWebhookDoesNotUpdateUnchangedFields() {
 +        UpdateWebhookCmd cmd = Mockito.mock(UpdateWebhookCmd.class);
 +        WebhookVO webhook = Mockito.mock(WebhookVO.class);
 +        Mockito.when(webhook.getId()).thenReturn(1L);
 +        Mockito.when(webhook.getPayloadUrl()).thenReturn("http://abc.xyz");
 +
 +        Mockito.when(cmd.getId()).thenReturn(1L);
 +        Mockito.when(cmd.getName()).thenReturn(null);
 +        Mockito.when(cmd.getDescription()).thenReturn(null);
 +        Mockito.when(cmd.getPayloadUrl()).thenReturn(null);
 +        Mockito.when(cmd.getSecretKey()).thenReturn(null);
 +        Mockito.when(cmd.isSslVerification()).thenReturn(null);
 +        Mockito.when(cmd.getScope()).thenReturn(null);
 +        Mockito.when(cmd.getState()).thenReturn(null);
 +        Mockito.when(webhookDao.findById(1L)).thenReturn(webhook);
++        Mockito.when(accountManager.getAccount(Mockito.anyLong())).thenReturn(caller);
 +        Mockito.doReturn(Mockito.mock(WebhookResponse.class)).when(webhookApiServiceImpl)
 +                .createWebhookResponse(1L);
 +
 +        WebhookResponse response = webhookApiServiceImpl.updateWebhook(cmd);
 +
 +        Assert.assertNotNull(response);
 +        Mockito.verify(webhookDao, Mockito.never()).update(Mockito.anyLong(), Mockito.any());
 +    }
 +
 +    @Test
 +    public void listWebhookDeliveriesReturnsEmptyResponseForNoDeliveries() {
 +        ListWebhookDeliveriesCmd cmd = Mockito.mock(ListWebhookDeliveriesCmd.class);
 +
 +        Mockito.when(cmd.getId()).thenReturn(null);
 +        Mockito.when(cmd.getWebhookId()).thenReturn(null);
 +        Mockito.when(cmd.getManagementServerId()).thenReturn(null);
 +        Mockito.when(cmd.getKeyword()).thenReturn(null);
 +        Mockito.when(cmd.getStartDate()).thenReturn(null);
 +        Mockito.when(cmd.getEndDate()).thenReturn(null);
 +        Mockito.when(cmd.getEventType()).thenReturn(null);
 +        Mockito.when(cmd.getStartIndex()).thenReturn(0L);
 +        Mockito.when(cmd.getPageSizeVal()).thenReturn(10L);
 +        Mockito.when(webhookDeliveryJoinDao.searchAndCountByListApiParameters(Mockito.any(), Mockito.anyList(),
-                         Mockito.any(), Mockito.any(), Mockito.any(), Mockito.any(), Mockito.any(), Mockito.any()))
++                        Mockito.any(), Mockito.any(), Mockito.any(), Mockito.any(), Mockito.any(), Mockito.any(),
++                        Mockito.anyBoolean()))
 +                .thenReturn(new Pair<>(List.of(), 0));
 +
 +        ListResponse<WebhookDeliveryResponse> response = webhookApiServiceImpl.listWebhookDeliveries(cmd);
 +
 +        Assert.assertNotNull(response);
 +        Assert.assertTrue(response.getResponses().isEmpty());
 +        Assert.assertEquals(0, (int) response.getCount());
 +    }
 +
 +    @Test
 +    public void listWebhookDeliveriesFiltersByWebhookId() {
 +        ListWebhookDeliveriesCmd cmd = Mockito.mock(ListWebhookDeliveriesCmd.class);
 +        WebhookDeliveryJoinVO delivery = Mockito.mock(WebhookDeliveryJoinVO.class);
 +        WebhookDeliveryResponse deliveryResponse = Mockito.mock(WebhookDeliveryResponse.class);
 +
 +        Mockito.when(cmd.getId()).thenReturn(null);
 +        Mockito.when(cmd.getWebhookId()).thenReturn(1L);
 +        Mockito.when(cmd.getManagementServerId()).thenReturn(null);
 +        Mockito.when(cmd.getStartIndex()).thenReturn(0L);
 +        Mockito.when(cmd.getPageSizeVal()).thenReturn(10L);
 +        Mockito.when(webhookDeliveryJoinDao.searchAndCountByListApiParameters(Mockito.any(), Mockito.anyList(),
-                         Mockito.any(), Mockito.any(), Mockito.any(), Mockito.any(), Mockito.any(), Mockito.any()))
++                        Mockito.any(), Mockito.any(), Mockito.any(), Mockito.any(), Mockito.any(), Mockito.any(),
++                        Mockito.anyBoolean()))
 +                .thenReturn(new Pair<>(List.of(delivery), 1));
 +        Mockito.doReturn(deliveryResponse).when(webhookApiServiceImpl).createWebhookDeliveryResponse(delivery);
 +        Mockito.doReturn(null).when(webhookApiServiceImpl)
-                 .basicWebhookDeliveryApiCheck(caller, null, 1L, null, null, null);
++                .basicWebhookDeliveryApiCheck(caller, null, 1L, null, null, null, false);
 +
 +        ListResponse<WebhookDeliveryResponse> response = webhookApiServiceImpl.listWebhookDeliveries(cmd);
 +
 +        Assert.assertNotNull(response);
 +        Assert.assertEquals(1, response.getResponses().size());
 +        Assert.assertEquals(deliveryResponse, response.getResponses().get(0));
 +        Assert.assertEquals(1, (int) response.getCount());
 +    }
 +
 +    @Test(expected = InvalidParameterValueException.class)
 +    public void listWebhookDeliveriesThrowsExceptionForEndDateBeforeStartDate() {
 +        ListWebhookDeliveriesCmd cmd = Mockito.mock(ListWebhookDeliveriesCmd.class);
 +
 +        Mockito.when(cmd.getStartDate()).thenReturn(new Date(System.currentTimeMillis()));
 +        Mockito.when(cmd.getEndDate()).thenReturn(new Date(System.currentTimeMillis() - 1000));
 +
 +        webhookApiServiceImpl.listWebhookDeliveries(cmd);
 +    }
 +
 +    @Test
 +    public void listWebhookDeliveriesFiltersByEventType() {
 +        ListWebhookDeliveriesCmd cmd = Mockito.mock(ListWebhookDeliveriesCmd.class);
 +        WebhookDeliveryJoinVO delivery = Mockito.mock(WebhookDeliveryJoinVO.class);
 +        WebhookDeliveryResponse deliveryResponse = Mockito.mock(WebhookDeliveryResponse.class);
 +
 +        Mockito.when(cmd.getId()).thenReturn(null);
 +        Mockito.when(cmd.getWebhookId()).thenReturn(null);
 +        Mockito.when(cmd.getManagementServerId()).thenReturn(null);
 +        Mockito.when(cmd.getEventType()).thenReturn("EventType");
 +        Mockito.when(cmd.getStartIndex()).thenReturn(0L);
 +        Mockito.when(cmd.getPageSizeVal()).thenReturn(10L);
 +        Mockito.when(webhookDeliveryJoinDao.searchAndCountByListApiParameters(Mockito.any(), Mockito.anyList(),
-                         Mockito.any(), Mockito.any(), Mockito.any(), Mockito.any(), Mockito.any(), Mockito.any()))
++                        Mockito.any(), Mockito.any(), Mockito.any(), Mockito.any(), Mockito.any(), Mockito.any(),
++                        Mockito.anyBoolean()))
 +                .thenReturn(new Pair<>(List.of(delivery), 1));
 +        Mockito.doReturn(deliveryResponse).when(webhookApiServiceImpl).createWebhookDeliveryResponse(delivery);
 +
 +        ListResponse<WebhookDeliveryResponse> response = webhookApiServiceImpl.listWebhookDeliveries(cmd);
 +
 +        Assert.assertNotNull(response);
 +        Assert.assertEquals(1, response.getResponses().size());
 +        Assert.assertEquals(deliveryResponse, response.getResponses().get(0));
 +        Assert.assertEquals(1, (int) response.getCount());
 +    }
 +
 +    @Test
 +    public void deleteWebhookDeliveryRemovesDeliveriesSuccessfully() {
 +        DeleteWebhookDeliveryCmd cmd = Mockito.mock(DeleteWebhookDeliveryCmd.class);
 +
 +        Mockito.when(cmd.getId()).thenReturn(null);
 +        Mockito.when(cmd.getWebhookId()).thenReturn(1L);
 +        Mockito.when(cmd.getManagementServerId()).thenReturn(null);
 +        Mockito.when(cmd.getStartDate()).thenReturn(null);
 +        Mockito.when(cmd.getEndDate()).thenReturn(null);
 +        Mockito.doReturn(null).when(webhookApiServiceImpl)
-                 .basicWebhookDeliveryApiCheck(caller, null, 1L, null, null, null);
-         Mockito.when(webhookDeliveryDao.deleteByDeleteApiParams(Mockito.any(), Mockito.any(), Mockito.any(), Mockito.any(), Mockito.any())).thenReturn(5);
++                .basicWebhookDeliveryApiCheck(caller, null, 1L, null, null, null, false);
++        Mockito.when(webhookDeliveryDao.deleteByDeleteApiParams(Mockito.any(), Mockito.any(), Mockito.any(),
++                Mockito.any(), Mockito.any(), Mockito.anyBoolean())).thenReturn(5);
 +
 +        int removed = webhookApiServiceImpl.deleteWebhookDelivery(cmd);
 +
 +        Assert.assertEquals(5, removed);
-         Mockito.verify(webhookDeliveryDao).deleteByDeleteApiParams(null, 1L, null, null, null);
++        Mockito.verify(webhookDeliveryDao).deleteByDeleteApiParams(null, List.of(1L), null, null, null, false);
 +    }
 +
 +    @Test(expected = InvalidParameterValueException.class)
 +    public void deleteWebhookDeliveryThrowsExceptionForInvalidDates() {
 +        DeleteWebhookDeliveryCmd cmd = Mockito.mock(DeleteWebhookDeliveryCmd.class);
 +        Mockito.when(cmd.getStartDate()).thenReturn(new Date(System.currentTimeMillis()));
 +        Mockito.when(cmd.getEndDate()).thenReturn(new Date(System.currentTimeMillis() - 1000));
 +
 +        webhookApiServiceImpl.deleteWebhookDelivery(cmd);
 +    }
 +
 +    @Test
 +    public void deleteWebhookDeliveryHandlesNoDeliveriesToRemove() {
 +        DeleteWebhookDeliveryCmd cmd = Mockito.mock(DeleteWebhookDeliveryCmd.class);
 +
 +        Mockito.when(cmd.getId()).thenReturn(null);
 +        Mockito.when(cmd.getWebhookId()).thenReturn(1L);
 +        Mockito.when(cmd.getManagementServerId()).thenReturn(null);
 +        Mockito.when(cmd.getStartDate()).thenReturn(null);
 +        Mockito.when(cmd.getEndDate()).thenReturn(null);
 +        Mockito.when(webhookDao.findById(1L)).thenReturn(Mockito.mock(WebhookVO.class));
-         Mockito.when(webhookDeliveryDao.deleteByDeleteApiParams(Mockito.any(), Mockito.any(), Mockito.any(), Mockito.any(), Mockito.any())).thenReturn(0);
++        Mockito.when(webhookDeliveryDao.deleteByDeleteApiParams(Mockito.any(), Mockito.any(), Mockito.any(),
++                Mockito.any(), Mockito.any(), Mockito.anyBoolean())).thenReturn(0);
 +
 +        int removed = webhookApiServiceImpl.deleteWebhookDelivery(cmd);
 +
 +        Assert.assertEquals(0, removed);
-         Mockito.verify(webhookDeliveryDao).deleteByDeleteApiParams(null, 1L, null, null, null);
++        Mockito.verify(webhookDeliveryDao).deleteByDeleteApiParams(null, List.of(1L), null, null, null, false);
 +    }
 +
 +    @Test(expected = PermissionDeniedException.class)
 +    public void deleteWebhookDeliveryThrowsExceptionForUnauthorizedAccess() {
 +        DeleteWebhookDeliveryCmd cmd = Mockito.mock(DeleteWebhookDeliveryCmd.class);
 +
 +        Mockito.when(cmd.getId()).thenReturn(null);
 +        Mockito.when(cmd.getWebhookId()).thenReturn(1L);
-         Mockito.when(caller.getType()).thenReturn(Account.Type.NORMAL);
 +        Mockito.when(webhookDao.findById(1L)).thenReturn(Mockito.mock(WebhookVO.class));
 +
 +        webhookApiServiceImpl.deleteWebhookDelivery(cmd);
 +    }
 +
 +    @Test(expected = InvalidParameterValueException.class)
 +    public void executeWebhookDeliveryThrowsExceptionWhenNoIdentifiersProvided() {
 +        ExecuteWebhookDeliveryCmd cmd = Mockito.mock(ExecuteWebhookDeliveryCmd.class);
 +        Mockito.when(cmd.getId()).thenReturn(null);
 +        Mockito.when(cmd.getWebhookId()).thenReturn(null);
 +        Mockito.when(cmd.getPayloadUrl()).thenReturn(null);
 +
 +        webhookApiServiceImpl.executeWebhookDelivery(cmd);
 +    }
 +
 +    @Test(expected = InvalidParameterValueException.class)
 +    public void executeWebhookDeliveryThrowsExceptionWhenBothDeliveryIdAndWebhookIdProvided() {
 +        ExecuteWebhookDeliveryCmd cmd = Mockito.mock(ExecuteWebhookDeliveryCmd.class);
 +        Mockito.when(cmd.getId()).thenReturn(1L);
 +        Mockito.when(cmd.getWebhookId()).thenReturn(2L);
 +
 +        webhookApiServiceImpl.executeWebhookDelivery(cmd);
 +    }
 +
 +    @Test(expected = InvalidParameterValueException.class)
 +    public void executeWebhookDeliveryThrowsExceptionForInvalidDeliveryId() {
 +        ExecuteWebhookDeliveryCmd cmd = Mockito.mock(ExecuteWebhookDeliveryCmd.class);
 +        Mockito.when(cmd.getId()).thenReturn(1L);
 +
 +        webhookApiServiceImpl.executeWebhookDelivery(cmd);
 +    }
 +
 +    @Test(expected = InvalidParameterValueException.class)
 +    public void executeWebhookDeliveryThrowsExceptionForInvalidWebhookId() {
 +        ExecuteWebhookDeliveryCmd cmd = Mockito.mock(ExecuteWebhookDeliveryCmd.class);
 +        Mockito.when(cmd.getWebhookId()).thenReturn(1L);
 +
 +        webhookApiServiceImpl.executeWebhookDelivery(cmd);
 +    }
 +
 +    @Test
 +    public void executeWebhookDeliveryExecutesSuccessfullyForValidDeliveryId() {
 +        ExecuteWebhookDeliveryCmd cmd = Mockito.mock(ExecuteWebhookDeliveryCmd.class);
 +        WebhookDeliveryVO delivery = Mockito.mock(WebhookDeliveryVO.class);
 +        WebhookVO webhook = Mockito.mock(WebhookVO.class);
 +        WebhookDelivery webhookDelivery = Mockito.mock(WebhookDelivery.class);
 +        WebhookDeliveryResponse response = Mockito.mock(WebhookDeliveryResponse.class);
 +
 +        Mockito.when(cmd.getId()).thenReturn(1L);
 +        Mockito.when(cmd.getWebhookId()).thenReturn(null);
 +        Mockito.when(webhookDeliveryDao.findById(1L)).thenReturn(delivery);
 +        Mockito.when(delivery.getWebhookId()).thenReturn(2L);
 +        Mockito.when(webhookDao.findById(2L)).thenReturn(webhook);
-         Mockito.when(webhookService.executeWebhookDelivery(delivery, webhook, null)).thenReturn(webhookDelivery);
++        Mockito.when(webhookService.executeWebhookDelivery(delivery, webhook, null, null)).thenReturn(webhookDelivery);
 +        Mockito.when(webhookDelivery.getId()).thenReturn(3L);
 +        Mockito.when(webhookDeliveryJoinDao.findById(3L)).thenReturn(Mockito.mock(WebhookDeliveryJoinVO.class));
 +        Mockito.doReturn(response).when(webhookApiServiceImpl).createWebhookDeliveryResponse(Mockito.any());
 +
 +        WebhookDeliveryResponse result = webhookApiServiceImpl.executeWebhookDelivery(cmd);
 +
 +        Assert.assertNotNull(result);
 +        Assert.assertEquals(response, result);
 +    }
 +
 +    @Test
 +    public void executeWebhookDeliveryExecutesSuccessfullyForValidWebhookId() {
 +        ExecuteWebhookDeliveryCmd cmd = Mockito.mock(ExecuteWebhookDeliveryCmd.class);
 +        WebhookVO webhook = Mockito.mock(WebhookVO.class);
 +        WebhookDelivery webhookDelivery = Mockito.mock(WebhookDelivery.class);
 +        WebhookDeliveryResponse response = Mockito.mock(WebhookDeliveryResponse.class);
 +
 +        Mockito.when(cmd.getId()).thenReturn(null);
 +        Mockito.when(cmd.getWebhookId()).thenReturn(1L);
 +        Mockito.when(webhookDao.findById(1L)).thenReturn(webhook);
-         Mockito.when(webhookService.executeWebhookDelivery(null, webhook, null)).thenReturn(webhookDelivery);
++        Mockito.when(webhookService.executeWebhookDelivery(null, webhook, null, null)).thenReturn(webhookDelivery);
 +        Mockito.when(webhookDelivery.getId()).thenReturn(WebhookDelivery.ID_DUMMY);
 +        Mockito.doReturn(response).when(webhookApiServiceImpl).createTestWebhookDeliveryResponse(webhookDelivery, webhook);
 +
 +        WebhookDeliveryResponse result = webhookApiServiceImpl.executeWebhookDelivery(cmd);
 +
 +        Assert.assertNotNull(result);
 +        Assert.assertEquals(response, result);
 +    }
 +
 +    @Test(expected = InvalidParameterValueException.class)
 +    public void executeWebhookDeliveryThrowsExceptionForInvalidPayloadUrl() {
 +        ExecuteWebhookDeliveryCmd cmd = Mockito.mock(ExecuteWebhookDeliveryCmd.class);
 +        Mockito.when(cmd.getPayloadUrl()).thenReturn("invalid-url");
 +
 +        webhookApiServiceImpl.executeWebhookDelivery(cmd);
 +    }
 +
 +    @Test
 +    public void executeWebhookDeliveryExecutesSuccessfullyForValidPayloadUrl() {
 +        ExecuteWebhookDeliveryCmd cmd = Mockito.mock(ExecuteWebhookDeliveryCmd.class);
 +        WebhookDelivery webhookDelivery = Mockito.mock(WebhookDelivery.class);
 +        WebhookDeliveryResponse response = Mockito.mock(WebhookDeliveryResponse.class);
 +
 +        Mockito.when(cmd.getId()).thenReturn(null);
 +        Mockito.when(cmd.getWebhookId()).thenReturn(null);
 +        Mockito.when(cmd.getPayloadUrl()).thenReturn("https://example.com");
++        Mockito.when(accountManager.finalizeOwner(Mockito.any(), Mockito.any(), Mockito.any(), Mockito.any()))
++                .thenReturn(caller);
 +        Mockito.when(webhookService.executeWebhookDelivery(Mockito.eq(null), Mockito.any(Webhook.class),
-                 Mockito.eq(null))).thenReturn(webhookDelivery);
++                Mockito.eq(null), Mockito.any(URI.class))).thenReturn(webhookDelivery);
 +        Mockito.when(webhookDelivery.getId()).thenReturn(WebhookDelivery.ID_DUMMY);
 +        Mockito.doReturn(response).when(webhookApiServiceImpl).createTestWebhookDeliveryResponse(
 +                Mockito.eq(webhookDelivery), Mockito.any(Webhook.class));
 +
 +        WebhookDeliveryResponse result = webhookApiServiceImpl.executeWebhookDelivery(cmd);
 +
 +        Assert.assertNotNull(result);
 +        Assert.assertEquals(response, result);
 +    }
 +
 +    @Test
 +    public void listWebhookFiltersReturnsEmptyResponseForNoFilters() {
 +        ListWebhookFiltersCmd cmd = Mockito.mock(ListWebhookFiltersCmd.class);
 +        Mockito.when(cmd.getId()).thenReturn(null);
 +        Mockito.when(cmd.getWebhookId()).thenReturn(null);
 +        Mockito.when(cmd.getStartIndex()).thenReturn(0L);
 +        Mockito.when(cmd.getPageSizeVal()).thenReturn(10L);
 +        Mockito.when(webhookFilterDao.searchBy(Mockito.any(), Mockito.any(), Mockito.anyLong(), Mockito.anyLong()))
 +                .thenReturn(new Pair<>(List.of(), 0));
 +
 +        ListResponse<WebhookFilterResponse> response = webhookApiServiceImpl.listWebhookFilters(cmd);
 +
 +        Assert.assertNotNull(response);
 +        Assert.assertTrue(response.getResponses().isEmpty());
 +    }
 +
 +    @Test
 +    public void listWebhookFiltersReturnsFiltersSuccessfully() {
 +        ListWebhookFiltersCmd cmd = Mockito.mock(ListWebhookFiltersCmd.class);
 +        WebhookFilterVO filter = Mockito.mock(WebhookFilterVO.class);
 +        Mockito.when(filter.getWebhookId()).thenReturn(1L);
 +        WebhookVO webhook = Mockito.mock(WebhookVO.class);
 +        WebhookFilterResponse filterResponse = Mockito.mock(WebhookFilterResponse.class);
 +
 +        Mockito.when(cmd.getId()).thenReturn(null);
 +        Mockito.when(cmd.getWebhookId()).thenReturn(1L);
 +        Mockito.when(cmd.getStartIndex()).thenReturn(0L);
 +        Mockito.when(cmd.getPageSizeVal()).thenReturn(10L);
 +        Mockito.when(webhookFilterDao.searchBy(Mockito.any(), Mockito.any(), Mockito.anyLong(), Mockito.anyLong()))
 +                .thenReturn(new Pair<>(List.of(filter), 1));
 +        Mockito.when(webhookDao.findById(1L)).thenReturn(webhook);
 +        Mockito.doReturn(filterResponse).when(webhookApiServiceImpl).createWebhookFilterResponse(filter, webhook);
 +
 +        ListResponse<WebhookFilterResponse> response = webhookApiServiceImpl.listWebhookFilters(cmd);
 +
 +        Assert.assertNotNull(response);
 +        Assert.assertEquals(1, response.getResponses().size());
 +        Assert.assertEquals(filterResponse, response.getResponses().get(0));
 +    }
 +
 +    @Test(expected = InvalidParameterValueException.class)
 +    public void addWebhookFilterThrowsExceptionForInvalidWebhookId() {
 +        AddWebhookFilterCmd cmd = Mockito.mock(AddWebhookFilterCmd.class);
 +        Mockito.when(cmd.getId()).thenReturn(1L);
 +        Mockito.when(webhookDao.findById(1L)).thenReturn(null);
 +
 +        webhookApiServiceImpl.addWebhookFilter(cmd);
 +    }
 +
 +    @Test(expected = InvalidParameterValueException.class)
 +    public void addWebhookFilterThrowsExceptionForInvalidMode() {
 +        AddWebhookFilterCmd cmd = Mockito.mock(AddWebhookFilterCmd.class);
 +        WebhookVO webhook = Mockito.mock(WebhookVO.class);
 +
 +        Mockito.when(cmd.getId()).thenReturn(1L);
 +        Mockito.when(cmd.getMode()).thenReturn("InvalidMode");
 +        Mockito.when(webhookDao.findById(1L)).thenReturn(webhook);
 +
 +        webhookApiServiceImpl.addWebhookFilter(cmd);
 +    }
 +
 +    @Test
 +    public void addWebhookFilterAddsFilterSuccessfully() {
 +        AddWebhookFilterCmd cmd = Mockito.mock(AddWebhookFilterCmd.class);
 +        WebhookVO webhook = Mockito.mock(WebhookVO.class);
 +        WebhookFilterVO filter = Mockito.mock(WebhookFilterVO.class);
 +        WebhookFilterResponse filterResponse = Mockito.mock(WebhookFilterResponse.class);
 +
 +        Mockito.when(cmd.getId()).thenReturn(1L);
 +        Mockito.when(cmd.getType()).thenReturn("EventType");
 +        Mockito.when(cmd.getMode()).thenReturn("Include");
 +        Mockito.when(cmd.getMatchType()).thenReturn("Exact");
 +        Mockito.when(cmd.getValue()).thenReturn("value");
 +        Mockito.when(webhookDao.findById(1L)).thenReturn(webhook);
 +        Mockito.when(webhookFilterDao.persist(Mockito.any(WebhookFilterVO.class))).thenReturn(filter);
 +        Mockito.doReturn(filterResponse).when(webhookApiServiceImpl).createWebhookFilterResponse(filter, webhook);
 +
 +        WebhookFilterResponse response = webhookApiServiceImpl.addWebhookFilter(cmd);
 +
 +        Assert.assertNotNull(response);
 +        Assert.assertEquals(filterResponse, response);
 +    }
 +
 +    @Test
 +    public void addWebhookFilterAddsFilterSuccessfullyEvenWithExisting() {
 +        AddWebhookFilterCmd cmd = Mockito.mock(AddWebhookFilterCmd.class);
 +        WebhookVO webhook = Mockito.mock(WebhookVO.class);
 +        WebhookFilterVO filter = Mockito.mock(WebhookFilterVO.class);
 +        WebhookFilterVO newFilter = Mockito.mock(WebhookFilterVO.class);
 +        WebhookFilterResponse filterResponse = Mockito.mock(WebhookFilterResponse.class);
 +
 +        Mockito.when(cmd.getId()).thenReturn(1L);
 +        Mockito.when(webhook.getId()).thenReturn(1L);
 +        Mockito.when(filter.getType()).thenReturn(WebhookFilter.Type.EventType);
 +        Mockito.when(filter.getMode()).thenReturn(WebhookFilter.Mode.Exclude);
 +        Mockito.when(filter.getMatchType()).thenReturn(WebhookFilter.MatchType.Prefix);
 +        Mockito.when(filter.getValue()).thenReturn("value.old");
 +        Mockito.when(cmd.getType()).thenReturn("EventType");
 +        Mockito.when(cmd.getMode()).thenReturn("Include");
 +        Mockito.when(cmd.getMatchType()).thenReturn("Exact");
 +        Mockito.when(cmd.getValue()).thenReturn("value");
 +        Mockito.when(webhookDao.findById(1L)).thenReturn(webhook);
 +        Mockito.when(webhookFilterDao.listByWebhook(1L)).thenReturn(List.of(filter));
 +        Mockito.when(webhookFilterDao.persist(Mockito.any(WebhookFilterVO.class))).thenReturn(newFilter);
 +        Mockito.doReturn(filterResponse).when(webhookApiServiceImpl).createWebhookFilterResponse(newFilter, webhook);
 +
 +        WebhookFilterResponse response = webhookApiServiceImpl.addWebhookFilter(cmd);
 +
 +        Assert.assertNotNull(response);
 +        Assert.assertEquals(filterResponse, response);
 +    }
 +
 +    @Test(expected = InvalidParameterValueException.class)
 +    public void addWebhookFilterConflictsWithExisting() {
 +        AddWebhookFilterCmd cmd = Mockito.mock(AddWebhookFilterCmd.class);
 +        WebhookVO webhook = Mockito.mock(WebhookVO.class);
 +        WebhookFilterVO filter = Mockito.mock(WebhookFilterVO.class);
 +
 +        Mockito.when(webhook.getId()).thenReturn(1L);
 +        Mockito.when(filter.getType()).thenReturn(WebhookFilter.Type.EventType);
 +        Mockito.when(filter.getMode()).thenReturn(WebhookFilter.Mode.Exclude);
 +        Mockito.when(filter.getMatchType()).thenReturn(WebhookFilter.MatchType.Prefix);
 +        Mockito.when(filter.getValue()).thenReturn("value");
 +        Mockito.when(cmd.getId()).thenReturn(1L);
 +        Mockito.when(cmd.getType()).thenReturn("EventType");
 +        Mockito.when(cmd.getMode()).thenReturn("Include");
 +        Mockito.when(cmd.getMatchType()).thenReturn("Exact");
 +        Mockito.when(cmd.getValue()).thenReturn("value.extra");
 +        Mockito.when(webhookDao.findById(1L)).thenReturn(webhook);
 +        Mockito.when(webhookFilterDao.listByWebhook(1L)).thenReturn(List.of(filter));
 +
 +        webhookApiServiceImpl.addWebhookFilter(cmd);
 +    }
 +
 +    @Test(expected = InvalidParameterValueException.class)
 +    public void addWebhookFilterSameEventConflictsWithExisting() {
 +        AddWebhookFilterCmd cmd = Mockito.mock(AddWebhookFilterCmd.class);
 +        WebhookVO webhook = Mockito.mock(WebhookVO.class);
 +        WebhookFilterVO filter = Mockito.mock(WebhookFilterVO.class);
 +
 +        Mockito.when(webhook.getId()).thenReturn(1L);
 +        Mockito.when(filter.getType()).thenReturn(WebhookFilter.Type.EventType);
 +        Mockito.when(filter.getMode()).thenReturn(WebhookFilter.Mode.Include);
 +        Mockito.when(filter.getMatchType()).thenReturn(WebhookFilter.MatchType.Exact);
 +        Mockito.when(filter.getValue()).thenReturn(EventTypes.EVENT_VM_CREATE);
 +        Mockito.when(cmd.getId()).thenReturn(1L);
 +        Mockito.when(cmd.getType()).thenReturn(WebhookFilter.Type.EventType.name());
 +        Mockito.when(cmd.getMode()).thenReturn(WebhookFilter.Mode.Exclude.name());
 +        Mockito.when(cmd.getMatchType()).thenReturn(WebhookFilter.MatchType.Exact.name());
 +        Mockito.when(cmd.getValue()).thenReturn(EventTypes.EVENT_VM_CREATE);
 +        Mockito.when(webhookDao.findById(1L)).thenReturn(webhook);
 +        Mockito.when(webhookFilterDao.listByWebhook(1L)).thenReturn(List.of(filter));
 +
 +        webhookApiServiceImpl.addWebhookFilter(cmd);
 +    }
 +
 +    @Test
 +    public void deleteWebhookFilterDeletesFilterSuccessfully() {
 +        DeleteWebhookFilterCmd cmd = Mockito.mock(DeleteWebhookFilterCmd.class);
 +        WebhookFilterVO filter = Mockito.mock(WebhookFilterVO.class);
 +        WebhookVO webhook = Mockito.mock(WebhookVO.class);
 +
 +        Mockito.when(cmd.getId()).thenReturn(1L);
 +        Mockito.when(webhookFilterDao.searchBy(Mockito.any(), Mockito.any(), Mockito.anyLong(), Mockito.anyLong()))
 +                .thenReturn(new Pair<>(List.of(filter), 1));
 +        Mockito.when(webhookDao.findById(Mockito.anyLong())).thenReturn(webhook);
 +        Mockito.when(webhookFilterDao.delete(Mockito.anyLong(), Mockito.anyLong())).thenReturn(1);
 +
 +        int result = webhookApiServiceImpl.deleteWebhookFilter(cmd);
 +
 +        Assert.assertEquals(1, result);
 +    }
 +
 +    @Test
 +    public void deleteWebhookFilterHandlesNoFiltersToDelete() {
 +        DeleteWebhookFilterCmd cmd = Mockito.mock(DeleteWebhookFilterCmd.class);
 +
 +        Mockito.when(cmd.getId()).thenReturn(1L);
 +        Mockito.when(webhookFilterDao.searchBy(Mockito.any(), Mockito.any(), Mockito.anyLong(), Mockito.anyLong()))
 +                .thenReturn(new Pair<>(List.of(), 0));
 +
 +        int result = webhookApiServiceImpl.deleteWebhookFilter(cmd);
 +
 +        Assert.assertEquals(0, result);
 +    }
++
+     public void shouldValidateAllParametersSuccessfully() {
+         Account caller = Mockito.mock(Account.class);
+         Long deliveryId = 1L;
+         Long managementServerId = 1L;
+         Date startDate = new Date(100);
+         Date endDate = new Date(200);
+         WebhookDeliveryVO deliveryVO = Mockito.mock(WebhookDeliveryVO.class);
+         Mockito.when(deliveryVO.getWebhookId()).thenReturn(1L);
+         WebhookVO webhookVO = Mockito.mock(WebhookVO.class);
+         ManagementServerHostVO managementServerHostVO = Mockito.mock(ManagementServerHostVO.class);
+         Mockito.when(webhookDeliveryDao.findById(deliveryId)).thenReturn(deliveryVO);
+         Mockito.when(webhookDao.findById(1L)).thenReturn(webhookVO);
+         Mockito.doNothing().when(accountManager).checkAccess(caller, SecurityChecker.AccessType.OperateEntry, false, webhookVO);
+         Mockito.when(managementServerHostDao.findById(managementServerId)).thenReturn(managementServerHostVO);
+         ManagementServerHostVO result = webhookApiServiceImpl.basicWebhookDeliveryApiCheck(caller, deliveryId, null, managementServerId, startDate, endDate, true);
+         Assert.assertNotNull(result);
+         Assert.assertEquals(managementServerHostVO, result);
+     }
  }
diff --cc plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtRestoreBackupCommandWrapper.java
index cc2a0868fe1,9ebb180b055..b80f3d54eb4
--- a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtRestoreBackupCommandWrapper.java
+++ b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtRestoreBackupCommandWrapper.java
@@@ -52,23 -55,7 +55,16 @@@ import com.cloud.vm.VirtualMachine
  @ResourceWrapper(handles = RestoreBackupCommand.class)
  public class LibvirtRestoreBackupCommandWrapper extends CommandWrapper<RestoreBackupCommand, Answer, LibvirtComputingResource> {
      private static final String BACKUP_TEMP_FILE_PREFIX = "csbackup";
-     private static final String MOUNT_COMMAND = "sudo mount -t %s %s %s";
-     private static final String UMOUNT_COMMAND = "sudo umount %s";
      private static final String FILE_PATH_PLACEHOLDER = "%s/%s";
-     private static final String ATTACH_QCOW2_DISK_COMMAND = " virsh attach-disk %s %s %s --driver qemu --subdriver qcow2 --cache none";
-     private static final String ATTACH_RAW_DISK_COMMAND = " virsh attach-disk %s %s %s --driver qemu --cache none";
-     private static final String ATTACH_RBD_DISK_XML_COMMAND = " virsh attach-device %s /dev/stdin <<EOF%sEOF";
-     private static final String CURRRENT_DEVICE = "virsh domblklist --domain %s | tail -n 3 | head -n 1 | awk '{print $1}'";
-     private static final String RSYNC_COMMAND = "rsync -az %s %s";
 +    // Flattens the backing-file chain into a single self-contained qcow2 written to the
 +    // destination volume path. Used when the source backup is an incremental whose qcow2
 +    // has a backing reference to its parent (chain set up by nasbackup.sh's qemu-img rebase).
 +    private static final String QEMU_IMG_FLATTEN_COMMAND = "qemu-img convert -O qcow2 %s %s";
 +    // Detects whether a qcow2 file references a parent in its backing-file metadata.
 +    // Returns 0 (true) when a backing file is present, 1 when not. Uses --output=json
 +    // so the test is robust to qemu-img version differences in human-readable output.
 +    private static final String QEMU_IMG_HAS_BACKING_COMMAND =
 +            "qemu-img info --output=json %s 2>/dev/null | grep -q '\"backing-filename\"'";
  
      private String getVolumeUuidFromPath(String volumePath, PrimaryDataStoreTO volumePool) {
          if (Storage.StoragePoolType.Linstor.equals(volumePool.getPoolType())) {
@@@ -279,19 -275,8 +284,20 @@@
              return replaceBlockDeviceWithBackup(storagePoolMgr, volumePool, volumePath, backupPath, timeout, createTargetVolume, size);
          }
  
 +        // For NAS-backed incremental backups, the source qcow2 has a backing-file
 +        // reference to its parent (set by nasbackup.sh's qemu-img rebase). A plain
 +        // rsync would copy only the differential blocks, leaving a volume that
 +        // depends on a backing file the primary storage doesn't have. Flatten the
 +        // chain via qemu-img convert, which follows the backing-file links and
 +        // produces a single self-contained qcow2.
 +        if (hasBackingChain(backupPath)) {
 +            int flattenExit = Script.runSimpleBashScriptForExitValue(
 +                    String.format(QEMU_IMG_FLATTEN_COMMAND, backupPath, volumePath), timeout, false);
 +            return flattenExit == 0;
 +        }
 +
-         int exitValue = Script.runSimpleBashScriptForExitValue(String.format(RSYNC_COMMAND, backupPath, volumePath), timeout, false);
+         String[] rsyncCmd = new String[] { Script.getExecutableAbsolutePath("rsync"), "-az", backupPath, volumePath };
+         int exitValue = Script.executeCommandForExitValue(rsyncCmd);
          return exitValue == 0;
      }
  
diff --cc plugins/network-elements/juniper-contrail/src/test/java/org/apache/cloudstack/network/contrail/management/MockAccountManager.java
index a0b1973fd8e,860674b329f..0f794f081a3
--- a/plugins/network-elements/juniper-contrail/src/test/java/org/apache/cloudstack/network/contrail/management/MockAccountManager.java
+++ b/plugins/network-elements/juniper-contrail/src/test/java/org/apache/cloudstack/network/contrail/management/MockAccountManager.java
@@@ -24,32 -24,24 +24,33 @@@ import java.util.Map
  import javax.inject.Inject;
  import javax.naming.ConfigurationException;
  
 -import com.cloud.api.auth.SetupUserTwoFactorAuthenticationCmd;
 -import org.apache.cloudstack.acl.Role;
 -import org.apache.cloudstack.api.command.admin.account.CreateAccountCmd;
 -import org.apache.cloudstack.api.command.admin.user.GetUserKeysCmd;
 -import org.apache.cloudstack.api.command.admin.user.MoveUserCmd;
 -import org.apache.cloudstack.api.response.UserTwoFactorAuthenticationSetupResponse;
 -import org.apache.cloudstack.auth.UserTwoFactorAuthenticator;
 -import org.apache.cloudstack.framework.config.ConfigKey;
 -
  import org.apache.cloudstack.acl.ControlledEntity;
++import org.apache.cloudstack.acl.Role;
 +import org.apache.cloudstack.acl.RolePermissionEntity;
  import org.apache.cloudstack.acl.RoleType;
  import org.apache.cloudstack.acl.SecurityChecker.AccessType;
 +import org.apache.cloudstack.acl.apikeypair.ApiKeyPair;
 +import org.apache.cloudstack.acl.apikeypair.ApiKeyPairPermission;
 +import org.apache.cloudstack.api.BaseCmd;
 +import org.apache.cloudstack.api.command.admin.account.CreateAccountCmd;
  import org.apache.cloudstack.api.command.admin.account.UpdateAccountCmd;
  import org.apache.cloudstack.api.command.admin.user.DeleteUserCmd;
 -import org.apache.cloudstack.api.command.admin.user.RegisterUserKeyCmd;
 +import org.apache.cloudstack.api.command.admin.user.DeleteUserKeysCmd;
 +import org.apache.cloudstack.api.command.admin.user.GetUserKeysCmd;
 +import org.apache.cloudstack.api.command.admin.user.ListUserKeyRulesCmd;
 +import org.apache.cloudstack.api.command.admin.user.ListUserKeysCmd;
 +import org.apache.cloudstack.api.command.admin.user.MoveUserCmd;
 +import org.apache.cloudstack.api.command.admin.user.RegisterUserKeysCmd;
  import org.apache.cloudstack.api.command.admin.user.UpdateUserCmd;
 +import org.apache.cloudstack.api.response.ApiKeyPairResponse;
 +import org.apache.cloudstack.api.response.ListResponse;
 +import org.apache.cloudstack.api.response.UserTwoFactorAuthenticationSetupResponse;
 +import org.apache.cloudstack.auth.UserTwoFactorAuthenticator;
 +import org.apache.cloudstack.backup.BackupOffering;
  import org.apache.cloudstack.context.CallContext;
 +import org.apache.cloudstack.framework.config.ConfigKey;
  
 +import com.cloud.api.auth.SetupUserTwoFactorAuthenticationCmd;
  import com.cloud.api.query.vo.ControlledViewEntity;
  import com.cloud.configuration.ResourceLimit;
  import com.cloud.configuration.dao.ResourceCountDao;
@@@ -611,25 -546,6 +612,28 @@@ public class MockAccountManager extend
      }
  
      @Override
 +    public void verifyCallerPrivilegeForUserOrAccountOperations(User user) {
 +    }
 +
 +    @Override
 +    public void checkCallerRoleTypeAllowedForUserOrAccountOperations(Account userAccount, User user) {
 +    }
 +
 +    @Override
 +    public Account getActiveAccountByUuid(String accountUuid) {
 +        return null;
 +    }
 +
 +    @Override
 +    public User getOneActiveUserForAccount(Account account) {
 +        return null;
 +    }
 +
 +    @Override
 +    public Account getAccountByUuid(String accountUuid) {
 +        return null;
 +    }
++
+     public void refreshRoleCheckersCacheOnPermissionsChange(Role role) {
+     }
  }
diff --cc plugins/user-authenticators/ldap/src/main/java/org/apache/cloudstack/ldap/LdapManagerImpl.java
index 8327072639c,7ee02b511b3..a93b7a9e133
--- a/plugins/user-authenticators/ldap/src/main/java/org/apache/cloudstack/ldap/LdapManagerImpl.java
+++ b/plugins/user-authenticators/ldap/src/main/java/org/apache/cloudstack/ldap/LdapManagerImpl.java
@@@ -48,7 -43,9 +44,8 @@@ import org.apache.cloudstack.api.respon
  import org.apache.cloudstack.api.response.LdapUserResponse;
  import org.apache.cloudstack.api.response.LinkAccountToLdapResponse;
  import org.apache.cloudstack.api.response.LinkDomainToLdapResponse;
+ import org.apache.cloudstack.context.CallContext;
  import org.apache.cloudstack.framework.messagebus.MessageBus;
 -import org.apache.cloudstack.framework.messagebus.MessageSubscriber;
  import org.apache.cloudstack.ldap.dao.LdapConfigurationDao;
  import org.apache.cloudstack.ldap.dao.LdapTrustMapDao;
  import org.apache.commons.lang.Validate;
diff --cc plugins/user-authenticators/oauth2/src/main/java/org/apache/cloudstack/oauth2/api/command/ListOAuthProvidersCmd.java
index 2d0a2e2a741,60e1037f9ff..9c72e47e559
--- a/plugins/user-authenticators/oauth2/src/main/java/org/apache/cloudstack/oauth2/api/command/ListOAuthProvidersCmd.java
+++ b/plugins/user-authenticators/oauth2/src/main/java/org/apache/cloudstack/oauth2/api/command/ListOAuthProvidersCmd.java
@@@ -41,12 -42,14 +43,16 @@@ import org.apache.cloudstack.auth.UserO
  import org.apache.cloudstack.oauth2.OAuth2AuthManager;
  import org.apache.cloudstack.oauth2.api.response.OauthProviderResponse;
  import org.apache.cloudstack.oauth2.vo.OauthProviderVO;
- import org.apache.commons.lang3.ArrayUtils;
+ import org.apache.commons.lang.ArrayUtils;
+ import org.apache.commons.lang3.EnumUtils;
+ import org.apache.commons.lang3.ObjectUtils;
  
 +import com.cloud.api.ApiDBUtils;
+ import com.cloud.api.ApiServer;
  import com.cloud.api.response.ApiResponseSerializer;
 +import com.cloud.domain.Domain;
  import com.cloud.user.Account;
+ import com.cloud.utils.HttpUtils;
  
  @APICommand(name = "listOauthProvider", description = "List OAuth providers registered", responseObject = OauthProviderResponse.class, entityType = {},
          requestHasSensitiveInfo = false, responseHasSensitiveInfo = false,
@@@ -113,27 -135,9 +150,28 @@@ public class ListOAuthProvidersCmd exte
          if (ArrayUtils.isNotEmpty(providerArray)) {
              provider = providerArray[0];
          }
+         boolean secretKeyAllowed = isSecretKeyAllowedForAuthenticatedCaller(params, session, remoteAddress, req);
  
 -        List<OauthProviderVO> resultList = _oauth2mgr.listOauthProviders(provider, id);
 +        boolean domainRequested = ArrayUtils.isNotEmpty((String[])params.get(ApiConstants.DOMAIN_ID))
 +                || ArrayUtils.isNotEmpty((String[])params.get(ApiConstants.DOMAIN));
 +        domainId = _oauth2mgr.resolveDomainId(params);
 +
 +        if (domainRequested && domainId == null) {
 +            ListResponse<OauthProviderResponse> response = new ListResponse<>();
 +            response.setResponses(new ArrayList<>(), 0);
 +            response.setResponseName(getCommandName());
 +            setResponseObject(response);
 +            return ApiResponseSerializer.toSerializedString(response, responseType);
 +        }
 +
 +        List<OauthProviderVO> resultList = _oauth2mgr.listOauthProviders(provider, id, domainId);
 +        boolean isAuthenticated = session != null && session.getAttribute(ApiConstants.USER_ID) != null;
 +        if (domainRequested && domainId != null && domainId > 0) {
 +            resultList.removeIf(p -> p.getDomainId() == null);
 +        } else if (!domainRequested && !isAuthenticated) {
 +            resultList.removeIf(p -> p.getDomainId() != null);
 +        }
 +
          List<UserOAuth2Authenticator> userOAuth2AuthenticatorPlugins = _oauth2mgr.listUserOAuth2AuthenticationProviders();
          List<String> authenticatorPluginNames = new ArrayList<>();
          for (UserOAuth2Authenticator authenticator : userOAuth2AuthenticatorPlugins) {
@@@ -142,11 -146,10 +180,12 @@@
          }
          List<OauthProviderResponse> responses = new ArrayList<>();
          for (OauthProviderVO result : resultList) {
 +            Domain domain = result.getDomainId() != null ? ApiDBUtils.findDomainById(result.getDomainId()) : null;
              OauthProviderResponse r = new OauthProviderResponse(result.getUuid(), result.getProvider(),
-                     result.getDescription(), result.getClientId(), result.getSecretKey(), result.getRedirectUri(), result.getAuthorizeUrl(), result.getTokenUrl(), domain);
+                     result.getDescription(), result.getClientId(), secretKeyAllowed ? result.getSecretKey() : null,
 -                    result.getRedirectUri());
 -            if (OAuth2AuthManager.OAuth2IsPluginEnabled.value() && authenticatorPluginNames.contains(result.getProvider()) && result.isEnabled()) {
++                    result.getRedirectUri(), result.getAuthorizeUrl(), result.getTokenUrl(), domain);
 +            boolean oauthEnabled = OAuth2AuthManager.isPluginEnabledForDomain(result.getDomainId());
 +            if (oauthEnabled && authenticatorPluginNames.contains(result.getProvider()) && result.isEnabled()) {
                  r.setEnabled(true);
              } else {
                  r.setEnabled(false);
diff --cc plugins/user-authenticators/oauth2/src/main/java/org/apache/cloudstack/oauth2/google/GoogleOAuth2Provider.java
index 3b37f0f6e0b,e383b41999e..6b99432e4f6
--- a/plugins/user-authenticators/oauth2/src/main/java/org/apache/cloudstack/oauth2/google/GoogleOAuth2Provider.java
+++ b/plugins/user-authenticators/oauth2/src/main/java/org/apache/cloudstack/oauth2/google/GoogleOAuth2Provider.java
@@@ -60,26 -88,6 +91,31 @@@ public class GoogleOAuth2Provider exten
  
      @Override
      public boolean verifyUser(String email, String secretCode) {
 +        return verifyUser(email, secretCode, null);
 +    }
 +
 +    @Override
 +    public String verifySecretCodeAndFetchEmail(String secretCode) {
 +        return verifySecretCodeAndFetchEmail(secretCode, null);
 +    }
 +
++    @Override
++    public String verifySecretCodeAndFetchEmail(String secretCode, Long domainId) {
++        return verifyCodeAndFetchEmailInternal(secretCode, domainId, null);
++    }
++
 +    protected void clearAccessAndRefreshTokens() {
 +        accessToken = null;
 +        refreshToken = null;
 +    }
 +
 +    @Override
 +    public String getUserEmailAddress() throws CloudRuntimeException {
 +        return null;
 +    }
 +
 +    @Override
 +    public boolean verifyUser(String email, String secretCode, Long domainId) {
          if (StringUtils.isAnyEmpty(email, secretCode)) {
              throw new CloudAuthenticationException("Either email or secret code should not be null/empty");
          }
@@@ -89,21 -97,24 +125,24 @@@
              throw new CloudAuthenticationException("Google provider is not registered, so user cannot be verified");
          }
  
-         String verifiedEmail = verifySecretCodeAndFetchEmail(secretCode, domainId);
+         String verifiedEmail = consumeValidatedEmailFromCache(secretCode);
+         if (StringUtils.isEmpty(verifiedEmail)) {
 -            verifiedEmail = verifyCodeAndFetchEmailInternal(secretCode, providerVO);
++            verifiedEmail = verifyCodeAndFetchEmailInternal(secretCode, domainId, providerVO);
+         }
 -        if (!email.equals(verifiedEmail)) {
 +        if (verifiedEmail == null || !email.equals(verifiedEmail)) {
              throw new CloudRuntimeException("Unable to verify the email address with the provided secret");
          }
-         clearAccessAndRefreshTokens();
  
          return true;
      }
  
-     @Override
-     public String verifySecretCodeAndFetchEmail(String secretCode, Long domainId) {
-         OauthProviderVO provider = _oauthProviderDao.findByProviderAndDomainWithGlobalFallback(getName(), domainId);
-         String clientId = provider.getClientId();
-         String secret = provider.getSecretKey();
-         String redirectURI = provider.getRedirectUri();
 -    protected String verifyCodeAndFetchEmailInternal(String secretCode, OauthProviderVO googleProvider) {
++    protected String verifyCodeAndFetchEmailInternal(String secretCode, Long domainId, OauthProviderVO googleProvider) {
+         if (googleProvider == null) {
 -            googleProvider = _oauthProviderDao.findByProvider(getName());
++            googleProvider = _oauthProviderDao.findByProviderAndDomainWithGlobalFallback(getName(), domainId);
+         }
+         String clientId = googleProvider.getClientId();
+         String secret = googleProvider.getSecretKey();
+         String redirectURI = googleProvider.getRedirectUri();
          GoogleClientSecrets clientSecrets = new GoogleClientSecrets()
                  .setWeb(new GoogleClientSecrets.Details()
                          .setClientId(clientId)
@@@ -146,7 -157,18 +185,8 @@@
          } catch (IOException e) {
              throw new CloudRuntimeException(String.format("Failed to fetch the email address with the provided secret: %s", e.getMessage()));
          }
-         return userinfo.getEmail();
+         String verifiedEmail = userinfo.getEmail();
+         addValidatedEmailToCache(secretCode, verifiedEmail);
+         return verifiedEmail;
      }
 -
 -    @Override
 -    public String verifyCodeAndFetchEmail(String secretCode) {
 -        return verifyCodeAndFetchEmailInternal(secretCode, null);
 -    }
--
 -    @Override
 -    public String getUserEmailAddress() throws CloudRuntimeException {
 -        return null;
 -    }
  }
diff --cc plugins/user-authenticators/oauth2/src/test/java/org/apache/cloudstack/oauth2/google/GoogleOAuth2ProviderTest.java
index 1f4a4eb363c,a6c628182b4..a419cecc6b4
--- a/plugins/user-authenticators/oauth2/src/test/java/org/apache/cloudstack/oauth2/google/GoogleOAuth2ProviderTest.java
+++ b/plugins/user-authenticators/oauth2/src/test/java/org/apache/cloudstack/oauth2/google/GoogleOAuth2ProviderTest.java
@@@ -28,8 -33,6 +33,7 @@@ import org.junit.Before
  import org.junit.Test;
  import org.mockito.InjectMocks;
  import org.mockito.Mock;
- import org.mockito.MockedConstruction;
 +import org.mockito.Mockito;
  import org.mockito.MockitoAnnotations;
  import org.mockito.Spy;
  
@@@ -79,78 -83,95 +84,95 @@@ public class GoogleOAuth2ProviderTest 
      }
  
      @Test(expected = CloudRuntimeException.class)
-     public void testVerifyUserWithInvalidSecretCode() throws IOException {
-         OauthProviderVO providerVO = mock(OauthProviderVO.class);
-         when(_oauthProviderDao.findByProviderAndDomainWithGlobalFallback(anyString(), Mockito.isNull())).thenReturn(providerVO);
-         when(providerVO.getProvider()).thenReturn("testProvider");
-         when(providerVO.getSecretKey()).thenReturn("testSecret");
-         when(providerVO.getClientId()).thenReturn("testClientid");
-         _googleOAuth2Provider.accessToken = "testAccessToken";
-         _googleOAuth2Provider.refreshToken = "testRefreshToken";
-         Oauth2 oauth2 = mock(Oauth2.class);
-         try (MockedConstruction<Oauth2.Builder> ignored = Mockito.mockConstruction(Oauth2.Builder.class,
-                 (mock, context) -> when(mock.build()).thenReturn(oauth2))) {
-             Userinfo userinfo = mock(Userinfo.class);
-             Oauth2.Userinfo userinfo1 = mock(Oauth2.Userinfo.class);
-             when(oauth2.userinfo()).thenReturn(userinfo1);
-             Oauth2.Userinfo.Get userinfoGet = mock(Oauth2.Userinfo.Get.class);
-             when(userinfo1.get()).thenReturn(userinfoGet);
-             when(userinfoGet.execute()).thenReturn(userinfo);
-             when(userinfo.getEmail()).thenReturn(null);
- 
-             _googleOAuth2Provider.verifyUser("email@example.com", "secretCode");
-         }
+     public void testVerifyUserWithInvalidSecretCode() {
 -        when(_oauthProviderDao.findByProvider(anyString())).thenReturn(mockProvider);
++        when(_oauthProviderDao.findByProviderAndDomainWithGlobalFallback(anyString(), Mockito.isNull())).thenReturn(mockProvider);
+         doReturn(null).when(_googleOAuth2Provider).verifyCodeAndFetchEmailInternal(
 -                "secretCode", mockProvider);
++                "secretCode", null, mockProvider);
+ 
+         _googleOAuth2Provider.verifyUser("email@example.com", "secretCode");
      }
  
      @Test(expected = CloudRuntimeException.class)
-     public void testVerifyUserWithMismatchedEmail() throws IOException {
-         OauthProviderVO providerVO = mock(OauthProviderVO.class);
-         when(_oauthProviderDao.findByProviderAndDomainWithGlobalFallback(anyString(), Mockito.isNull())).thenReturn(providerVO);
-         when(providerVO.getProvider()).thenReturn("testProvider");
-         when(providerVO.getSecretKey()).thenReturn("testSecret");
-         when(providerVO.getClientId()).thenReturn("testClientid");
-         _googleOAuth2Provider.accessToken = "testAccessToken";
-         _googleOAuth2Provider.refreshToken = "testRefreshToken";
-         Oauth2 oauth2 = mock(Oauth2.class);
-         try (MockedConstruction<Oauth2.Builder> ignored = Mockito.mockConstruction(Oauth2.Builder.class,
-                 (mock, context) -> when(mock.build()).thenReturn(oauth2))) {
-             Userinfo userinfo = mock(Userinfo.class);
-             Oauth2.Userinfo userinfo1 = mock(Oauth2.Userinfo.class);
-             when(oauth2.userinfo()).thenReturn(userinfo1);
-             Oauth2.Userinfo.Get userinfoGet = mock(Oauth2.Userinfo.Get.class);
-             when(userinfo1.get()).thenReturn(userinfoGet);
-             when(userinfoGet.execute()).thenReturn(userinfo);
-             when(userinfo.getEmail()).thenReturn("otheremail@example.com");
- 
-             _googleOAuth2Provider.verifyUser("email@example.com", "secretCode");
+     public void testVerifyUserWithMismatchedEmail() {
 -        when(_oauthProviderDao.findByProvider(anyString())).thenReturn(mockProvider);
++        when(_oauthProviderDao.findByProviderAndDomainWithGlobalFallback(anyString(), Mockito.isNull())).thenReturn(mockProvider);
+         doReturn("otheremail@example.com").when(_googleOAuth2Provider).verifyCodeAndFetchEmailInternal(
 -                "secretCode", mockProvider);
++                "secretCode", null, mockProvider);
+ 
+         _googleOAuth2Provider.verifyUser("email@example.com", "secretCode");
+     }
+ 
+     @Test
+     public void testVerifyUserEmail() {
 -        when(_oauthProviderDao.findByProvider(anyString())).thenReturn(mockProvider);
++        when(_oauthProviderDao.findByProviderAndDomainWithGlobalFallback(anyString(), Mockito.isNull())).thenReturn(mockProvider);
+         doReturn("email@example.com").when(_googleOAuth2Provider).verifyCodeAndFetchEmailInternal(
 -                "secretCode", mockProvider);
++                "secretCode", null, mockProvider);
+ 
+         boolean result = _googleOAuth2Provider.verifyUser("email@example.com", "secretCode");
+ 
+         assertTrue(result);
+     }
+ 
+     @Test
+     public void testCacheInitializationAndCleanupResources() {
+         // Verifies that cache cleanup executor is properly initialized
+         GoogleOAuth2Provider provider = new GoogleOAuth2Provider();
+         assertNotNull("GoogleOAuth2Provider should initialize", provider);
+     }
+ 
+     @Test
+     public void testNoSensitiveDataInErrorMessages() {
+         // Verifies that error messages don't expose sensitive information
 -        when(_oauthProviderDao.findByProvider(anyString())).thenReturn(mockProvider);
++        when(_oauthProviderDao.findByProviderAndDomainWithGlobalFallback(anyString(), Mockito.isNull())).thenReturn(mockProvider);
+         String testSecret = "secret_key";
+         String testEmail = "email@example.com";
+ 
+         try {
+             _googleOAuth2Provider.verifyUser(testEmail, testSecret);
+             fail("Expected exception");
+         } catch (Exception e) {
+             String errorMsg = e.getMessage();
+             // Verify sensitive terms are not in error messages
+             assertFalse("Error should not contain secret", errorMsg.toLowerCase().contains(testSecret));
+             assertFalse("Error should not contain email", errorMsg.toLowerCase().contains(testEmail));
+             assertFalse("Error should not contain token", errorMsg.toLowerCase().contains("access_token"));
+         }
+     }
+ 
+     @Test
+     public void testVerifyUserErrorHandlingAndCleanup() {
+         // Tests that any authentication error properly cleans up
 -        when(_oauthProviderDao.findByProvider(anyString())).thenReturn(mockProvider);
++        when(_oauthProviderDao.findByProviderAndDomainWithGlobalFallback(anyString(), Mockito.isNull())).thenReturn(mockProvider);
+ 
+         doReturn("error@example.com").when(_googleOAuth2Provider).verifyCodeAndFetchEmailInternal(
 -                "bad_code", mockProvider);
++                "bad_code", null, mockProvider);
+ 
+         try {
+             _googleOAuth2Provider.verifyUser("expected@example.com", "bad_code");
+             fail("Should throw exception for email mismatch");
+         } catch (CloudRuntimeException e) {
+             assertEquals("Should have proper error message",
+                 "Unable to verify the email address with the provided secret",
+                 e.getMessage());
          }
      }
  
      @Test
-     public void testVerifyUserEmail() throws IOException {
-         OauthProviderVO providerVO = mock(OauthProviderVO.class);
-         when(_oauthProviderDao.findByProviderAndDomainWithGlobalFallback(anyString(), Mockito.isNull())).thenReturn(providerVO);
-         when(providerVO.getProvider()).thenReturn("testProvider");
-         when(providerVO.getSecretKey()).thenReturn("testSecret");
-         when(providerVO.getClientId()).thenReturn("testClientid");
-         _googleOAuth2Provider.accessToken = "testAccessToken";
-         _googleOAuth2Provider.refreshToken = "testRefreshToken";
-         Oauth2 oauth2 = mock(Oauth2.class);
-         try (MockedConstruction<Oauth2.Builder> ignored = Mockito.mockConstruction(Oauth2.Builder.class,
-                 (mock, context) -> when(mock.build()).thenReturn(oauth2))) {
-             Userinfo userinfo = mock(Userinfo.class);
-             Oauth2.Userinfo userinfo1 = mock(Oauth2.Userinfo.class);
-             when(oauth2.userinfo()).thenReturn(userinfo1);
-             Oauth2.Userinfo.Get userinfoGet = mock(Oauth2.Userinfo.Get.class);
-             when(userinfo1.get()).thenReturn(userinfoGet);
-             when(userinfoGet.execute()).thenReturn(userinfo);
-             when(userinfo.getEmail()).thenReturn("email@example.com");
- 
-             boolean result = _googleOAuth2Provider.verifyUser("email@example.com", "secretCode");
- 
-             assertTrue(result);
-             assertNull(_googleOAuth2Provider.accessToken);
-             assertNull(_googleOAuth2Provider.refreshToken);
+     public void testMultipleFailedVerificationAttempts() {
+         // Tests that multiple failures are handled gracefully without cache pollution
 -        when(_oauthProviderDao.findByProvider(anyString())).thenReturn(mockProvider);
++        when(_oauthProviderDao.findByProviderAndDomainWithGlobalFallback(anyString(), Mockito.isNull())).thenReturn(mockProvider);
+ 
+         // Multiple failed attempts
+         for (int i = 0; i < 5; i++) {
+             try {
+                 doReturn("wrong@example.com").when(_googleOAuth2Provider)
 -                    .verifyCodeAndFetchEmailInternal("code_" + i, mockProvider);
++                    .verifyCodeAndFetchEmailInternal("code_" + i, null, mockProvider);
+                 _googleOAuth2Provider.verifyUser("correct@example.com", "code_" + i);
+                 fail("Should fail on attempt " + i);
+             } catch (CloudRuntimeException e) {
+                 // Expected - cache should be cleaned for each failure
+                 assertTrue("Should report email verification failure",
+                     e.getMessage().contains("email"));
+             }
          }
      }
  }
diff --cc scripts/vm/hypervisor/kvm/nasbackup.sh
index 5b3bed7c875,656ff5ac28d..216f5b5a4de
--- a/scripts/vm/hypervisor/kvm/nasbackup.sh
+++ b/scripts/vm/hypervisor/kvm/nasbackup.sh
@@@ -287,51 -194,21 +310,59 @@@ print(len(files)
      sleep 5
    done
  
 -  # Use qemu-img convert to sparsify linstor backups which get bloated due to virsh backup-begin.
 +  # Sparsify behavior:
 +  # - For LINSTOR backups (existing): qemu-img convert sparsifies the bloated output.
 +  # - For INCREMENTAL: rebase the resulting thin qcow2 onto its parent so the chain is self-describing
 +  #   (so a future restore can flatten without external chain metadata).
    name="root"
 +  # PARENT_PATHS arrives as a comma-separated list, one entry per VM volume in the same
 +  # order as DISK_PATHS. Split into a bash array so we can index by disk position.
 +  local -a parent_paths_arr=()
 +  if [[ "$effective_mode" == "incremental" && -n "$PARENT_PATHS" ]]; then
 +    IFS=',' read -ra parent_paths_arr <<< "$PARENT_PATHS"
 +  fi
 +  local disk_idx=0
    while read -r disk fullpath; do
 +    if [[ "$effective_mode" == "incremental" ]]; then
 +      volUuid="${fullpath##*/}"
 +      # Pick this disk's specific parent file. Each volume's backup is named after its
 +      # own UUID, so a single PARENT_PATH would wrongly rebase data disks onto the root
 +      # parent.
 +      if [[ $disk_idx -ge ${#parent_paths_arr[@]} ]]; then
 +        echo "PARENT_PATHS list shorter than DISK_PATHS — missing parent for disk index $disk_idx"
 +        cleanup
 +        exit 1
 +      fi
 +      local this_parent_rel="${parent_paths_arr[$disk_idx]}"
 +      local parent_abs="$mount_point/$this_parent_rel"
 +      if [[ ! -f "$parent_abs" ]]; then
 +        echo "Parent backup file does not exist on NAS: $parent_abs"
 +        cleanup
 +        exit 1
 +      fi
 +      local parent_rel
 +      parent_rel=$(realpath --relative-to="$dest" "$parent_abs")
 +      if ! qemu-img rebase -u -b "$parent_rel" -F qcow2 "$dest/$name.$volUuid.qcow2" >> "$logFile" 2> >(cat >&2); then
 +        echo "qemu-img rebase failed for $dest/$name.$volUuid.qcow2 onto $parent_rel"
 +        cleanup
 +        exit 1
 +      fi
 +      name="datadisk"
 +      disk_idx=$((disk_idx + 1))
 +      continue
 +    fi
-     if [[ "$fullpath" != /dev/drbd/by-res/* ]]; then
+     if [[ "$fullpath" == /dev/drbd/by-res/* ]]; then
+       volUuid=$(get_linstor_uuid_from_path "$fullpath")
+     elif [[ "$fullpath" == /dev/drbd[0-9]* ]]; then
+       if ! volUuid=$(get_linstor_uuid_from_device "$fullpath"); then
+         echo "Failed to resolve LINSTOR volume UUID for $fullpath"
+         cleanup
+         exit 1
+       fi
+     else
+       name="datadisk"
        continue
      fi
-     volUuid=$(get_linstor_uuid_from_path "$fullpath")
      if ! qemu-img convert -O qcow2 "$dest/$name.$volUuid.qcow2" "$dest/$name.$volUuid.qcow2.tmp" >> "$logFile" 2> >(cat >&2); then
        echo "qemu-img convert failed for $dest/$name.$volUuid.qcow2"
        cleanup
diff --cc server/src/main/java/com/cloud/user/AccountManager.java
index e3840e29725,f12a1b68b03..054880d45b4
--- a/server/src/main/java/com/cloud/user/AccountManager.java
+++ b/server/src/main/java/com/cloud/user/AccountManager.java
@@@ -20,9 -20,8 +20,10 @@@ import java.net.InetAddress
  import java.util.List;
  import java.util.Map;
  
 +import com.cloud.exception.ResourceAllocationException;
  import org.apache.cloudstack.acl.ControlledEntity;
+ import org.apache.cloudstack.acl.Role;
 +import org.apache.cloudstack.acl.apikeypair.ApiKeyPair;
  import org.apache.cloudstack.api.command.admin.account.UpdateAccountCmd;
  import org.apache.cloudstack.api.command.admin.user.DeleteUserCmd;
  import org.apache.cloudstack.api.command.admin.user.MoveUserCmd;
@@@ -211,7 -209,5 +212,9 @@@ public interface AccountManager extend
  
      void verifyCallerPrivilegeForUserOrAccountOperations(Account userAccount);
  
 +    void verifyCallerPrivilegeForUserOrAccountOperations(User user);
 +
 +    void checkCallerRoleTypeAllowedForUserOrAccountOperations(Account userAccount, User user);
++
+     void refreshRoleCheckersCacheOnPermissionsChange(Role role);
  }
diff --cc server/src/main/java/com/cloud/user/AccountManagerImpl.java
index e4d835f74b1,b4c23beaef2..7c1d1e9af8b
--- a/server/src/main/java/com/cloud/user/AccountManagerImpl.java
+++ b/server/src/main/java/com/cloud/user/AccountManagerImpl.java
@@@ -45,17 -42,8 +45,19 @@@ import javax.crypto.spec.SecretKeySpec
  import javax.inject.Inject;
  import javax.naming.ConfigurationException;
  
 +import com.cloud.exception.ResourceAllocationException;
 +import com.cloud.projects.dao.ProjectInvitationDao;
 +import com.cloud.user.dao.AccountDao;
 +import com.cloud.user.dao.SSHKeyPairDao;
 +import com.cloud.user.dao.UserAccountDao;
 +import com.cloud.user.dao.UserDao;
 +import com.cloud.utils.db.TransactionCallbackWithException;
++
+ import org.apache.cloudstack.acl.APIAclChecker;
  import org.apache.cloudstack.acl.APIChecker;
 +import org.apache.cloudstack.acl.ApiKeyPairManagerImpl;
 +import org.apache.cloudstack.acl.ApiKeyPairPermissionVO;
 +import org.apache.cloudstack.acl.ApiKeyPairVO;
  import org.apache.cloudstack.acl.ControlledEntity;
  import org.apache.cloudstack.acl.InfrastructureEntity;
  import org.apache.cloudstack.acl.QuerySelector;
@@@ -1722,9 -1600,16 +1736,17 @@@ public class AccountManagerImpl extend
          }
      }
  
+     @Override
+     public void refreshRoleCheckersCacheOnPermissionsChange(Role role) {
+         List<APIAclChecker> aclCheckers = getApiACLCheckers();
+         for (final APIAclChecker aclChecker : aclCheckers) {
+             aclChecker.refreshRoleCacheOnPermissionsChange(role);
+         }
+     }
+ 
 -    protected void verifyCallerPrivilegeForUserOrAccountOperations(User user) {
 -        logger.debug(String.format("Verifying whether the caller has the correct privileges based on the user's role type and API permissions: %s", user));
 +    @Override
 +    public void verifyCallerPrivilegeForUserOrAccountOperations(User user) {
 +        logger.debug("Verifying whether the caller has the correct privileges based on the user's role type and API permissions: {}", user);
  
          Account userAccount = getAccount(user.getAccountId());
          if (!Account.Type.PROJECT.equals(userAccount.getType())) {
diff --cc server/src/test/java/com/cloud/template/TemplateManagerImplTest.java
index 6d8003914d3,af5bdbf9fd1..8c7994793e4
--- a/server/src/test/java/com/cloud/template/TemplateManagerImplTest.java
+++ b/server/src/test/java/com/cloud/template/TemplateManagerImplTest.java
@@@ -19,70 -19,28 +19,74 @@@
  package com.cloud.template;
  
  
 -import static org.mockito.ArgumentMatchers.any;
 -import static org.mockito.ArgumentMatchers.anyBoolean;
 -import static org.mockito.ArgumentMatchers.anyLong;
 -import static org.mockito.ArgumentMatchers.nullable;
 -import static org.mockito.Mockito.doNothing;
 -import static org.mockito.Mockito.eq;
 -import static org.mockito.Mockito.mock;
 -import static org.mockito.Mockito.when;
 +import com.cloud.agent.AgentManager;
 +import com.cloud.api.query.dao.SnapshotJoinDao;
 +import com.cloud.api.query.dao.UserVmJoinDao;
 +import com.cloud.api.query.vo.UserVmJoinVO;
 +import com.cloud.dc.dao.DataCenterDao;
 +import com.cloud.deployasis.dao.TemplateDeployAsIsDetailsDao;
 +import com.cloud.domain.dao.DomainDao;
 +import com.cloud.event.dao.UsageEventDao;
 +import com.cloud.exception.InvalidParameterValueException;
 +import com.cloud.exception.ResourceAllocationException;
 +import com.cloud.host.Status;
 +import com.cloud.host.DetailVO;
 +import com.cloud.host.Host;
 +import com.cloud.host.HostVO;
 +import com.cloud.host.dao.HostDao;
 +import com.cloud.host.dao.HostDetailsDao;
 +import com.cloud.hypervisor.Hypervisor;
 +import com.cloud.hypervisor.HypervisorGuruManager;
 +import com.cloud.projects.ProjectManager;
 +import com.cloud.resourcelimit.CheckedReservation;
 +import com.cloud.storage.DataStoreRole;
 +import com.cloud.storage.GuestOSVO;
 +import com.cloud.storage.Snapshot;
 +import com.cloud.storage.SnapshotVO;
 +import com.cloud.storage.Storage;
 +import com.cloud.storage.StorageManager;
 +import com.cloud.storage.StoragePool;
 +import com.cloud.storage.StoragePoolStatus;
 +import com.cloud.storage.TemplateProfile;
 +import com.cloud.storage.VMTemplateStoragePoolVO;
 +import com.cloud.storage.VMTemplateStorageResourceAssoc;
 +import com.cloud.storage.VMTemplateVO;
 +import com.cloud.storage.VolumeVO;
 +import com.cloud.storage.dao.GuestOSDao;
 +import com.cloud.storage.dao.LaunchPermissionDao;
 +import com.cloud.storage.dao.SnapshotDao;
 +import com.cloud.storage.dao.StoragePoolHostDao;
 +import com.cloud.storage.dao.VMTemplateDao;
 +import com.cloud.storage.dao.VMTemplateDetailsDao;
 +import com.cloud.storage.dao.VMTemplatePoolDao;
 +import com.cloud.storage.dao.VMTemplateZoneDao;
 +import com.cloud.storage.dao.VolumeDao;
 +import com.cloud.user.Account;
 +import com.cloud.user.AccountManager;
 +import com.cloud.user.AccountVO;
 +import com.cloud.user.ResourceLimitService;
 +import com.cloud.user.User;
 +import com.cloud.user.UserData;
++import com.cloud.user.UserDataVO;
 +import com.cloud.user.UserVO;
 +import com.cloud.user.dao.AccountDao;
++import com.cloud.user.dao.UserDataDao;
++import com.cloud.utils.UriUtils;
 +import com.cloud.utils.concurrency.NamedThreadFactory;
 +import com.cloud.utils.exception.CloudRuntimeException;
 +import com.cloud.uservm.UserVm;
 +import com.cloud.vm.UserVmVO;
 +import com.cloud.vm.VMInstanceVO;
 +import com.cloud.vm.VirtualMachine;
 +import com.cloud.vm.VirtualMachine.State;
 +import com.cloud.vm.VmIsoMapVO;
 +import com.cloud.vm.dao.UserVmDao;
 +import com.cloud.vm.dao.VMInstanceDao;
 +import com.cloud.vm.dao.VmIsoMapDao;
  
 -import java.io.IOException;
 -import java.util.ArrayList;
 -import java.util.List;
 -import java.util.UUID;
 -import java.util.concurrent.BlockingQueue;
 -import java.util.concurrent.ExecutorService;
 -import java.util.concurrent.LinkedBlockingQueue;
 -import java.util.concurrent.ThreadFactory;
 -import java.util.concurrent.ThreadPoolExecutor;
 -import java.util.concurrent.TimeUnit;
 -import java.util.concurrent.atomic.AtomicInteger;
 +import junit.framework.TestCase;
  
+ import org.apache.cloudstack.api.command.user.iso.RegisterIsoCmd;
  import org.apache.cloudstack.api.command.user.template.CreateTemplateCmd;
  import org.apache.cloudstack.api.command.user.template.DeleteTemplateCmd;
  import org.apache.cloudstack.api.command.user.template.RegisterTemplateCmd;
@@@ -233,21 -223,12 +238,27 @@@ public class TemplateManagerImplTest ex
      @Mock
      HeuristicRuleHelper heuristicRuleHelperMock;
  
 +    @Mock
 +    UserVmDao _userVmDao;
 +
 +    @Mock
 +    VmIsoMapDao _vmIsoMapDao;
 +
 +    @Mock
 +    HostDao _hostDao;
 +
 +    @Mock
 +    HostDetailsDao _hostDetailsDao;
 +
 +    @Mock
 +    UserVmJoinDao _userVmJoinDao;
 +
+     @Mock
+     private UserDataDao userDataDaoMock;
+ 
+     @Mock
+     private UserDataVO userDataMock;
+ 
      public class CustomThreadPoolExecutor extends ThreadPoolExecutor {
          AtomicInteger ai = new AtomicInteger(0);
          public CustomThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit,
diff --cc server/src/test/java/com/cloud/user/AccountManagerImplTest.java
index 379ae76d119,119704cc397..955c939f4fa
--- a/server/src/test/java/com/cloud/user/AccountManagerImplTest.java
+++ b/server/src/test/java/com/cloud/user/AccountManagerImplTest.java
@@@ -19,15 -19,12 +19,15 @@@ package com.cloud.user
  import static org.mockito.ArgumentMatchers.any;
  import static org.mockito.ArgumentMatchers.nullable;
  
 +import com.cloud.utils.Ternary;
  import java.net.InetAddress;
  import java.net.UnknownHostException;
 +import java.time.Instant;
  import java.util.ArrayList;
  import java.util.Arrays;
 -import java.util.HashMap;
  import java.util.Collections;
- import java.util.HashMap;
 +import java.util.Date;
++import java.util.HashMap;
  import java.util.List;
  import java.util.Map;
  
@@@ -2032,115 -1617,118 +2063,230 @@@ public class AccountManagerImplTest ext
          accountManagerImpl.checkCallerApiPermissionsForUserOrAccountOperations(accountMock);
      }
  
 +    @Test(expected = InvalidParameterValueException.class)
 +    public void testPasswordChangeRequiredWithSamlThrowsException() {
 +        accountManagerImpl.createUser(
 +                "user", "pass", "fn", "ln", "e@mail.com",
 +                "UTC", "acct", 1L, null,
 +                User.Source.SAML2, true
 +        );
 +    }
 +
 +    @Test(expected = InvalidParameterValueException.class)
 +    public void testPasswordChangeRequiredWithLdapSourceThrows() {
 +        accountManagerImpl.createUser(
 +                "user", "pass", "fn", "ln", "e@mail.com",
 +                "UTC", "acct", 1L, null,
 +                User.Source.LDAP, true);
 +    }
 +
 +    @Test(expected = CloudRuntimeException.class)
 +    public void testDomainNotFound() {
 +        Mockito.when(_domainMgr.getDomain(1L)).thenReturn(null);
 +        accountManagerImpl.createUser(
 +                "user", "pass", "fn", "ln", "e@mail.com",
 +                "UTC", "acct", 1L, null,
 +                User.Source.UNKNOWN, false);
 +    }
 +
 +    @Test(expected = CloudRuntimeException.class)
 +    public void testCreateUserInactiveDomain() {
 +        Mockito.when(domainVoMock.getState()).thenReturn(Domain.State.Inactive);
 +        Mockito.when(_domainMgr.getDomain(Mockito.anyLong())).thenReturn(domainVoMock);
 +        accountManagerImpl.createUser(
 +                "user", "pass", "fn", "ln", "e@mail.com",
 +                "UTC", "acct", 1L, null,
 +                User.Source.NATIVE, false);
 +    }
 +
 +    @Test(expected = InvalidParameterValueException.class)
 +    public void testCreateUserCheckAccess() {
 +        Mockito.when(domainVoMock.getState()).thenReturn(Domain.State.Active);
 +        Mockito.when(_domainMgr.getDomain(Mockito.anyLong())).thenReturn(domainVoMock);
 +        Mockito.doNothing().when(accountManagerImpl).checkAccess(Mockito.any(Account.class), Mockito.any(Domain.class));
 +        accountManagerImpl.createUser(
 +                "user", "pass", "fn", "ln", "e@mail.com",
 +                "UTC", "acct", 1L, null,
 +                User.Source.NATIVE, false);
 +    }
 +
 +    @Test(expected = InvalidParameterValueException.class)
 +    public void testCreateUserMissingOrProjectAccount() {
 +        Mockito.when(domainVoMock.getState()).thenReturn(Domain.State.Active);
 +        Mockito.when(_accountDao.findEnabledAccount(Mockito.anyString(), Mockito.anyLong())).thenReturn(accountMock);
 +        Mockito.when(accountMock.getType()).thenReturn(Account.Type.PROJECT);
 +        Mockito.when(_domainMgr.getDomain(Mockito.anyLong())).thenReturn(domainVoMock);
 +        Mockito.doNothing().when(accountManagerImpl).checkAccess(Mockito.any(Account.class), Mockito.any(Domain.class));
 +        accountManagerImpl.createUser(
 +                "user", "pass", "fn", "ln", "e@mail.com",
 +                "UTC", "acct", 1L, null,
 +                User.Source.NATIVE, false);
 +    }
 +
 +    @Test
 +    public void testCreateUserSuccess() {
 +        Account rootAdminAccount = Mockito.mock(Account.class);
 +        Mockito.when(rootAdminAccount.getId()).thenReturn(1L);
 +        Mockito.when(accountManagerImpl.isRootAdmin(1L)).thenReturn(true);
 +        User callingUser = Mockito.mock(User.class);
 +        CallContext.register(callingUser, rootAdminAccount);
 +
 +        String newPassword = "newPassword";
 +        configureUserMockAuthenticators(newPassword);
 +        Mockito.doNothing().when(accountManagerImpl).checkAccess(any(Account.class), any(Domain.class));
 +        Mockito.doReturn(accountMock).when(accountManagerImpl).getAccount(Mockito.anyLong());
 +        Mockito.doNothing().when(passwordPolicyMock).verifyIfPasswordCompliesWithPasswordPolicies(Mockito.anyString(), Mockito.anyString(), Mockito.anyLong());
 +        Mockito.when(_domainMgr.getDomain(Mockito.anyLong())).thenReturn(domainVoMock);
 +        Mockito.when(domainVoMock.getState()).thenReturn(Domain.State.Active);
 +
 +        Mockito.when(_accountDao.findEnabledAccount(Mockito.anyString(), Mockito.anyLong())).thenReturn(accountMock);
 +        Mockito.when(accountMock.getId()).thenReturn(10L);
 +        Mockito.when(accountMock.getType()).thenReturn(Account.Type.NORMAL);
 +
 +        Mockito.when(userAccountDao.validateUsernameInDomain(Mockito.anyString(), Mockito.anyLong())).thenReturn(true);
 +        Mockito.when(userDaoMock.findUsersByName(Mockito.anyString())).thenReturn(Collections.emptyList());
 +        UserVO createdUser = new UserVO();
 +        String userMockUUID = "userMockUUID";
 +        createdUser.setUuid(userMockUUID);
 +        Mockito.when(userDaoMock.persist(Mockito.any(UserVO.class))).thenReturn(createdUser);
 +        UserVO userResultVO = accountManagerImpl.createUser(
 +                "user", newPassword, "fn", "ln", "e@mail.com",
 +                "UTC", "acct", 1L, null,
 +                User.Source.NATIVE, false
 +        );
 +        Assert.assertNotNull(userResultVO);
 +        UserVO userResultPasswordChangeVO = accountManagerImpl.createUser(
 +                "user", newPassword, "fn", "ln", "e@mail.com",
 +                "UTC", "acct", 1L, null,
 +                User.Source.NATIVE, true
 +        );
 +        Assert.assertNotNull(userResultVO);
 +    }
 +
 +    @Test
 +    public void deleteAndCleanupUserTestUserCleanup() {
 +        long userId = userVoMock.getId();
 +        Mockito.doNothing().when(accountManagerImpl).removeUserApiKeys(userId);
 +        Mockito.doReturn(true).when(_projectMgr).cleanupProjectsForUser(null, userVoMock);
 +
 +        accountManagerImpl.deleteAndCleanupUser(userVoMock);
 +
 +        Mockito.verify(accountManagerImpl).removeUserApiKeys(userId);
 +        Mockito.verify(_projectMgr).cleanupProjectsForUser(null, userVoMock);
 +    }
++
+     // --- Tests for checkRoleEscalation ---
+ 
+     private void setPrivateField(Object target, String fieldName, Object value) throws Exception {
+         Class<?> clazz = target.getClass();
+         while (clazz != null) {
+             try {
+                 java.lang.reflect.Field field = clazz.getDeclaredField(fieldName);
+                 field.setAccessible(true);
+                 field.set(target, value);
+                 return;
+             } catch (NoSuchFieldException e) {
+                 clazz = clazz.getSuperclass();
+             }
+         }
+         throw new NoSuchFieldException(fieldName);
+     }
+ 
+     @Test
+     public void testCheckRoleEscalationSamePermissionsShouldPass() throws Exception {
+         APIChecker checker = Mockito.mock(APIChecker.class);
+         List<String> apis = Arrays.asList("api1", "api2", "api3");
+         Mockito.when(checker.isEnabled()).thenReturn(true);
+ 
+         Account caller = Mockito.mock(Account.class);
+         Account requested = Mockito.mock(Account.class);
+ 
+         accountManagerImpl.setApiAccessCheckers(Arrays.asList(checker));
+         setPrivateField(accountManagerImpl, "apiNameList", new ArrayList<>(apis));
+ 
+         accountManagerImpl.checkRoleEscalation(caller, requested);
+     }
+ 
+     @Test
+     public void testCheckRoleEscalationCallerHasMorePermissionsShouldPass() throws Exception {
+         List<String> allApis = Arrays.asList("api1", "api2", "api3");
+         List<String> requestedApis = Arrays.asList("api1", "api2");
+ 
+         APIAclChecker checker = Mockito.mock(APIAclChecker.class);
+         Mockito.when(checker.isEnabled()).thenReturn(true);
+ 
+         Account caller = Mockito.mock(Account.class);
+         Account requested = Mockito.mock(Account.class);
+ 
+         Mockito.when(checker.getApisAllowedToAccount(Mockito.eq(requested), Mockito.anyList())).thenReturn(requestedApis);
+         Mockito.when(checker.getApisAllowedToAccount(Mockito.eq(caller), Mockito.anyList())).thenReturn(allApis);
+ 
+         accountManagerImpl.setApiAccessCheckers(Arrays.asList(checker));
+         setPrivateField(accountManagerImpl, "apiNameList", new ArrayList<>(allApis));
+ 
+         accountManagerImpl.checkRoleEscalation(caller, requested);
+     }
+ 
+     @Test(expected = PermissionDeniedException.class)
+     public void testCheckRoleEscalationRequestedHasMorePermissionsShouldThrow() throws Exception {
+         List<String> allApis = Arrays.asList("api1", "api2", "api3");
+         List<String> requestedApis = Arrays.asList("api1", "api2", "api3");
+         List<String> callerApis = Arrays.asList("api1");
+ 
+         APIAclChecker checker = Mockito.mock(APIAclChecker.class);
+         Mockito.when(checker.isEnabled()).thenReturn(true);
+ 
+         Account caller = Mockito.mock(Account.class);
+         Account requested = Mockito.mock(Account.class);
+ 
+         Mockito.when(checker.getApisAllowedToAccount(Mockito.eq(requested), Mockito.anyList())).thenReturn(requestedApis);
+         Mockito.when(checker.getApisAllowedToAccount(Mockito.eq(caller), Mockito.anyList())).thenReturn(callerApis);
+ 
+         accountManagerImpl.setApiAccessCheckers(Arrays.asList(checker));
+         setPrivateField(accountManagerImpl, "apiNameList", new ArrayList<>(allApis));
+ 
+         accountManagerImpl.checkRoleEscalation(caller, requested);
+     }
+ 
+     @Test
+     public void testCheckRoleEscalationEmptyApiListShouldPass() throws Exception {
+         APIAclChecker checker = Mockito.mock(APIAclChecker.class);
+         Mockito.when(checker.isEnabled()).thenReturn(true);
+         Mockito.when(checker.getApisAllowedToAccount(Mockito.any(Account.class), Mockito.anyList())).thenReturn(Collections.emptyList());
+ 
+         Account caller = Mockito.mock(Account.class);
+         Account requested = Mockito.mock(Account.class);
+ 
+         accountManagerImpl.setApiAccessCheckers(Arrays.asList(checker));
+         setPrivateField(accountManagerImpl, "apiNameList", new ArrayList<>());
+ 
+         accountManagerImpl.checkRoleEscalation(caller, requested);
+     }
+ 
+     @Test
+     public void testCheckRoleEscalationMultipleCheckersAppliedSequentially() throws Exception {
+         List<String> allApis = Arrays.asList("api1", "api2", "api3");
+         List<String> afterChecker1 = Arrays.asList("api1", "api2");
+         List<String> afterChecker2 = Arrays.asList("api1");
+ 
+         APIAclChecker checker1 = Mockito.mock(APIAclChecker.class);
+         Mockito.when(checker1.isEnabled()).thenReturn(true);
+         APIAclChecker checker2 = Mockito.mock(APIAclChecker.class);
+         Mockito.when(checker2.isEnabled()).thenReturn(true);
+ 
+         Account caller = Mockito.mock(Account.class);
+         Account requested = Mockito.mock(Account.class);
+ 
+         // requested: checker1 filters to [api1, api2], checker2 further filters to [api1]
+         Mockito.when(checker1.getApisAllowedToAccount(Mockito.eq(requested), Mockito.eq(allApis))).thenReturn(afterChecker1);
+         Mockito.when(checker2.getApisAllowedToAccount(Mockito.eq(requested), Mockito.eq(afterChecker1))).thenReturn(afterChecker2);
+         // caller: same filtering, so no escalation
+         Mockito.when(checker1.getApisAllowedToAccount(Mockito.eq(caller), Mockito.eq(afterChecker2))).thenReturn(afterChecker2);
+         Mockito.when(checker2.getApisAllowedToAccount(Mockito.eq(caller), Mockito.eq(afterChecker2))).thenReturn(afterChecker2);
+ 
+         accountManagerImpl.setApiAccessCheckers(Arrays.asList(checker1, checker2));
+         setPrivateField(accountManagerImpl, "apiNameList", new ArrayList<>(allApis));
+ 
+         accountManagerImpl.checkRoleEscalation(caller, requested);
+     }
  }
diff --cc ui/src/config/section/compute.js
index b6e83b5eb43,10bf0a3973a..598c80bc480
--- a/ui/src/config/section/compute.js
+++ b/ui/src/config/section/compute.js
@@@ -18,19 -18,8 +18,20 @@@
  import { shallowRef, defineAsyncComponent } from 'vue'
  import store from '@/store'
  import { isZoneCreated } from '@/utils/zone'
 +import { getAPI, postAPI, getBaseUrl } from '@/api'
 +import { getLatestKubernetesIsoParams } from '@/utils/acsrepo'
 +import kubernetesIcon from '@/assets/icons/kubernetes.svg?inline'
+ import { escapeHtml } from '@/utils/util'
  
 +const attachedIsoCount = (record) => (record.isos && record.isos.length) || (record.isoid ? 1 : 0)
 +// Server pre-computes the effective cap (cluster-scoped vm.iso.max.count clamped to the
 +// hypervisor's own limit). Fall back to the hypervisor floor for older servers.
 +const isoMaxCount = (record) => record.isomaxcount != null
 +  ? record.isomaxcount
 +  : (record.hypervisor === 'KVM' ? 2 : 1)
 +const isoActionAvailable = (record) =>
 +  record.hypervisor !== 'External' && ['Running', 'Stopped'].includes(record.state) && record.vmtype !== 'sharedfsvm'
 +
  export default {
    name: 'compute',
    title: 'label.compute',
diff --cc ui/src/config/section/network.js
index 30608a82a98,f59f6e6d15b..3be69ab1061
--- a/ui/src/config/section/network.js
+++ b/ui/src/config/section/network.js
@@@ -18,9 -18,9 +18,10 @@@
  import { shallowRef, defineAsyncComponent } from 'vue'
  import store from '@/store'
  import tungsten from '@/assets/icons/tungsten.svg?inline'
 -import { isAdmin } from '@/role'
 +import { isAdmin, isAdminOrDomainAdmin } from '@/role'
  import { isZoneCreated } from '@/utils/zone'
 +import { vueProps } from '@/vue-app'
+ import { escapeHtml } from '@/utils/util'
  
  export default {
    name: 'network',