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:28:23 UTC

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

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 2d2a4c7d3484b6cad168eb837a42d10f264ff8de
Merge: 64178ead02d 85bcdb1cc48
Author: Wei Zhou <we...@apache.org>
AuthorDate: Wed Aug 19 11:31:38 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        | 135 ++++++-----
 .../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, 3895 insertions(+), 589 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..3839d7f6bdb
--- 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,12 @@@ 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 +280,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);
++            String[] qemuImgCmd = new String[] { Script.getExecutableAbsolutePath("qemu-img"), "convert", "-O", "qcow2", backupPath, volumePath };
++            int flattenExit = Script.executeCommandForExitValue(qemuImgCmd);
 +            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',