Repository: AthenZ/athenz

Issue:
Secure assertion deletion using Athenz Templates
Using the Athenz Template allows for more secure application operations because it enables managing domain information without granting applications the authority to update policies or assertions.

However, there is a bit of a challenge when we want to delete certain assertions after applying a Template. Even if we remove the assertions we want to delete from the Template and provide the Template again, `addSolutionTemplate()` executes `processPolicy()` with `ignoreDelete = true`, so the existing assertions are not deleted.

https://github.com/AthenZ/athenz/blob/e3c60afb076adb37445c19efddfae0de3090fedc/servers/zms/src/main/java/com/yahoo/athenz/zms/DBService.java#L5140-L5141
https://github.com/AthenZ/athenz/blob/e3c60afb076adb37445c19efddfae0de3090fedc/servers/zms/src/main/java/com/yahoo/athenz/zms/DBService.java#L567-L585

As a result, we have to grant the application the authority to update assertions.

Is there a way to delete certain assertions without granting the application the authority to update assertions? Alternatively, is there a method using the Athenz Template to add a new version of the policy and activate it?

Hints from issue comments:
@havetisyan: It's kind of dangerous for templates to remove assertions since this could lead to unintended deletion of assertions resulting in outages. For example, you might have a user who has applied a template and then went ahead and added more assertions to that policy. If you apply an update, that will delete all those assertions that were not part of the template, we'll be deleting those assertions.

For this kind of scenario to work, the server needs to keep track of internally what assertions it created as part of the template (something that the user cannot change or set) and then it can delete those assertions only. It would be something similar to the resource ownership idea but not allow the user to set or change it and it has to be set the assertion level which we currently do not support.

We already have a separate delete assertion api - so we can easily update the authorization requirements for this api to have a unique authz check (e.g. similar to membership changes). So you can grant someone to have the capability to delete assertion but not update any. This would be pretty straightforward to introduce without breaking anyone.

As far multiple policy version - currently the templates do not have support for it. That seems somewhat advance use case for templates to start creating multiple policy versions.
@ysknkd: @havetisyan Thank you for your response.

I have understood that due to the risks associated with deleting assertions via templates, there is currently no feature for assertion deletion through templates.

> We already have a separate delete assertion api - so we can easily update the authorization requirements for this api to have a unique authz check (e.g. similar to membership changes). So you can grant someone to have the capability to delete assertion but not update any. This would be pretty straightforward to introduce without breaking anyone.

Thank you for your suggestion. However, there are some points I don't quite understand. I believe the API for deleting assertions is deleteAssertion(). Are you suggesting modifying this API to allow for a unique authorization check?
https://github.com/AthenZ/athenz/blob/cbbb2b0faa8e86f440b33fffa11066e24f43086a/core/zms/src/main/rdl/Policy.rdli#L153-L162

The reason I'm asking is that currently, deleteAssertion() requires policy update permissions, which means it allows not only deletion but also changes. I'm concerned that updating the current authorization requirements might affect existing users. I would appreciate it if you could provide more details.@havetisyan: One unfortunate limitation with the use of the RDL is that we're limited to a single authorization. At some point in the future we will move away from RDL but for now what we've been doing is that we're slowly moving the authorization from the RDL to the actual server code. So for example, if you look at the deleteMembership call, you'll see the RDL only shows an authentication call:

https://github.com/AthenZ/athenz/blob/master/core/zms/src/main/rdl/Role.rdli#L192-L208

There is only an authenticate call. And then if you look at the server code, then we carry out the actual authorization check in the server code itself:

https://github.com/AthenZ/athenz/blob/master/servers/zms/src/main/java/com/yahoo/athenz/zms/ZMSImpl.java#L5300

So when I say that we can extend the authorization for the deleteAssertion call, we won't replace it but rather we'll move it to the server and carry out two authorization checks:

("update", "{domainName}:policy.{policyName}"); 
("delete", "{domainName}:policy.{policyName}.assertion.{assertionId}");

So as long as one of those authorization checks passes, we're good to process the request. So you can add a policy and authorization some principal to have delete action on policy.{policyName}.assertion.* (for example) and they can delete any assertion in that policy and nothing else. @ysknkd: Thank you very much! I understand now. I will consider the PR!@ysknkd: This matter has been implemented by TakuyaMatsu! Thank you, TakuyaMatsu!

Solution patch:
diff --git a/clients/go/zms/zms_schema.go b/clients/go/zms/zms_schema.go
index a6c25e1a2..b4564c4b7 100644
--- a/clients/go/zms/zms_schema.go
+++ b/clients/go/zms/zms_schema.go
@@ -2116,13 +2116,13 @@ func init() {
 	sb.AddResource(mPutAssertionPolicyVersion.Build())
 
 	mDeleteAssertion := rdl.NewResourceBuilder("Assertion", "DELETE", "/domain/{domainName}/policy/{policyName}/assertion/{assertionId}")
-	mDeleteAssertion.Comment("Delete the specified policy assertion. Upon successful completion of this delete request, the server will return NO_CONTENT status code without any data (no object will be returned).")
+	mDeleteAssertion.Comment("Delete the specified policy assertion. Upon successful completion of this delete request, the server will return NO_CONTENT status code without any data (no object will be returned). The required authorization includes two options: 1. (\"update\", \"{domainName}:policy.{policyName}\") 2. (\"delete\", \"{domainName}:policy.{policyName}.assertion.{assertionId}\")")
 	mDeleteAssertion.Input("domainName", "DomainName", true, "", "", false, nil, "name of the domain")
 	mDeleteAssertion.Input("policyName", "EntityName", true, "", "", false, nil, "name of the policy")
 	mDeleteAssertion.Input("assertionId", "Int64", true, "", "", false, nil, "assertion id")
 	mDeleteAssertion.Input("auditRef", "String", false, "", "Y-Audit-Ref", false, nil, "Audit param required(not empty) if domain auditEnabled is true.")
 	mDeleteAssertion.Input("resourceOwner", "String", false, "", "Athenz-Resource-Owner", false, nil, "Resource owner for the request")
-	mDeleteAssertion.Auth("update", "{domainName}:policy.{policyName}", false, "")
+	mDeleteAssertion.Auth("", "", true, "")
 	mDeleteAssertion.Expected("NO_CONTENT")
 	mDeleteAssertion.Exception("BAD_REQUEST", "ResourceError", "")
 	mDeleteAssertion.Exception("CONFLICT", "ResourceError", "")
@@ -2133,7 +2133,7 @@ func init() {
 	sb.AddResource(mDeleteAssertion.Build())
 
 	mDeleteAssertionPolicyVersion := rdl.NewResourceBuilder("Assertion", "DELETE", "/domain/{domainName}/policy/{policyName}/version/{version}/assertion/{assertionId}")
-	mDeleteAssertionPolicyVersion.Comment("Delete the specified policy version assertion. Upon successful completion of this delete request, the server will return NO_CONTENT status code without any data (no object will be returned).")
+	mDeleteAssertionPolicyVersion.Comment("Delete the specified policy version assertion. Upon successful completion of this delete request, the server will return NO_CONTENT status code without any data (no object will be returned). The required authorization includes two options: 1. (\"update\", \"{domainName}:policy.{policyName}\") 2. (\"delete\", \"{domainName}:policy.{policyName}.assertion.{assertionId}\")")
 	mDeleteAssertionPolicyVersion.Name("deleteAssertionPolicyVersion")
 	mDeleteAssertionPolicyVersion.Input("domainName", "DomainName", true, "", "", false, nil, "name of the domain")
 	mDeleteAssertionPolicyVersion.Input("policyName", "EntityName", true, "", "", false, nil, "name of the policy")
@@ -2141,7 +2141,7 @@ func init() {
 	mDeleteAssertionPolicyVersion.Input("assertionId", "Int64", true, "", "", false, nil, "assertion id")
 	mDeleteAssertionPolicyVersion.Input("auditRef", "String", false, "", "Y-Audit-Ref", false, nil, "Audit param required(not empty) if domain auditEnabled is true.")
 	mDeleteAssertionPolicyVersion.Input("resourceOwner", "String", false, "", "Athenz-Resource-Owner", false, nil, "Resource owner for the request")
-	mDeleteAssertionPolicyVersion.Auth("update", "{domainName}:policy.{policyName}", false, "")
+	mDeleteAssertionPolicyVersion.Auth("", "", true, "")
 	mDeleteAssertionPolicyVersion.Expected("NO_CONTENT")
 	mDeleteAssertionPolicyVersion.Exception("BAD_REQUEST", "ResourceError", "")
 	mDeleteAssertionPolicyVersion.Exception("CONFLICT", "ResourceError", "")
diff --git a/core/zms/src/main/java/com/yahoo/athenz/zms/ZMSSchema.java b/core/zms/src/main/java/com/yahoo/athenz/zms/ZMSSchema.java
index 3774b6d5e..6f9fb9b61 100644
--- a/core/zms/src/main/java/com/yahoo/athenz/zms/ZMSSchema.java
+++ b/core/zms/src/main/java/com/yahoo/athenz/zms/ZMSSchema.java
@@ -2269,13 +2269,13 @@ public class ZMSSchema {
 ;
 
         sb.resource("Assertion", "DELETE", "/domain/{domainName}/policy/{policyName}/assertion/{assertionId}")
-            .comment("Delete the specified policy assertion. Upon successful completion of this delete request, the server will return NO_CONTENT status code without any data (no object will be returned).")
+            .comment("Delete the specified policy assertion. Upon successful completion of this delete request, the server will return NO_CONTENT status code without any data (no object will be returned). The required authorization includes two options: 1. (\"update\", \"{domainName}:policy.{policyName}\") 2. (\"delete\", \"{domainName}:policy.{policyName}.assertion.{assertionId}\")")
             .pathParam("domainName", "DomainName", "name of the domain")
             .pathParam("policyName", "EntityName", "name of the policy")
             .pathParam("assertionId", "Int64", "assertion id")
             .headerParam("Y-Audit-Ref", "auditRef", "String", null, "Audit param required(not empty) if domain auditEnabled is true.")
             .headerParam("Athenz-Resource-Owner", "resourceOwner", "String", null, "Resource owner for the request")
-            .auth("update", "{domainName}:policy.{policyName}")
+            .auth("", "", true)
             .expected("NO_CONTENT")
             .exception("BAD_REQUEST", "ResourceError", "")
 
@@ -2291,7 +2291,7 @@ public class ZMSSchema {
 ;
 
         sb.resource("Assertion", "DELETE", "/domain/{domainName}/policy/{policyName}/version/{version}/assertion/{assertionId}")
-            .comment("Delete the specified policy version assertion. Upon successful completion of this delete request, the server will return NO_CONTENT status code without any data (no object will be returned).")
+            .comment("Delete the specified policy version assertion. Upon successful completion of this delete request, the server will return NO_CONTENT status code without any data (no object will be returned). The required authorization includes two options: 1. (\"update\", \"{domainName}:policy.{policyName}\") 2. (\"delete\", \"{domainName}:policy.{policyName}.assertion.{assertionId}\")")
             .name("deleteAssertionPolicyVersion")
             .pathParam("domainName", "DomainName", "name of the domain")
             .pathParam("policyName", "EntityName", "name of the policy")
@@ -2299,7 +2299,7 @@ public class ZMSSchema {
             .pathParam("assertionId", "Int64", "assertion id")
             .headerParam("Y-Audit-Ref", "auditRef", "String", null, "Audit param required(not empty) if domain auditEnabled is true.")
             .headerParam("Athenz-Resource-Owner", "resourceOwner", "String", null, "Resource owner for the request")
-            .auth("update", "{domainName}:policy.{policyName}")
+            .auth("", "", true)
             .expected("NO_CONTENT")
             .exception("BAD_REQUEST", "ResourceError", "")
 
diff --git a/core/zms/src/main/rdl/Policy.rdli b/core/zms/src/main/rdl/Policy.rdli
index e5b96a26a..38afb121b 100644
--- a/core/zms/src/main/rdl/Policy.rdli
+++ b/core/zms/src/main/rdl/Policy.rdli
@@ -152,14 +152,16 @@ resource Assertion PUT "/domain/{domainName}/policy/{policyName}/version/{versio
 
 //Delete the specified policy assertion. Upon successful completion of this delete
 //request, the server will return NO_CONTENT status code without any data (no
-//object will be returned).
+//object will be returned). The required authorization includes two options:
+// 1. ("update", "{domainName}:policy.{policyName}")
+// 2. ("delete", "{domainName}:policy.{policyName}.assertion.{assertionId}")
 resource Assertion DELETE "/domain/{domainName}/policy/{policyName}/assertion/{assertionId}" {
     DomainName domainName; //name of the domain
     EntityName policyName; //name of the policy
     Int64 assertionId; //assertion id
     String auditRef (header="Y-Audit-Ref"); //Audit param required(not empty) if domain auditEnabled is true.
     String resourceOwner (header="Athenz-Resource-Owner"); //Resource owner for the request
-    authorize ("update", "{domainName}:policy.{policyName}");
+    authenticate;
     expected NO_CONTENT;
     exceptions {
         ResourceError NOT_FOUND;
@@ -173,7 +175,9 @@ resource Assertion DELETE "/domain/{domainName}/policy/{policyName}/assertion/{a
 
 //Delete the specified policy version assertion. Upon successful completion of this delete
 //request, the server will return NO_CONTENT status code without any data (no
-//object will be returned).
+//object will be returned). The required authorization includes two options:
+// 1. ("update", "{domainName}:policy.{policyName}")
+// 2. ("delete", "{domainName}:policy.{policyName}.assertion.{assertionId}")
 resource Assertion DELETE "/domain/{domainName}/policy/{policyName}/version/{version}/assertion/{assertionId}" (name=deleteAssertionPolicyVersion) {
     DomainName domainName; //name of the domain
     EntityName policyName; //name of the policy
@@ -181,7 +185,7 @@ resource Assertion DELETE "/domain/{domainName}/policy/{policyName}/version/{ver
     Int64 assertionId; //assertion id
     String auditRef (header="Y-Audit-Ref"); //Audit param required(not empty) if domain auditEnabled is true.
     String resourceOwner (header="Athenz-Resource-Owner"); //Resource owner for the request
-    authorize ("update", "{domainName}:policy.{policyName}");
+    authenticate;
     expected NO_CONTENT;
     exceptions {
         ResourceError NOT_FOUND;
diff --git a/libs/java/server_common/src/main/java/com/yahoo/athenz/common/ServerCommonConsts.java b/libs/java/server_common/src/main/java/com/yahoo/athenz/common/ServerCommonConsts.java
index c9886f762..9bff17193 100644
--- a/libs/java/server_common/src/main/java/com/yahoo/athenz/common/ServerCommonConsts.java
+++ b/libs/java/server_common/src/main/java/com/yahoo/athenz/common/ServerCommonConsts.java
@@ -21,6 +21,7 @@ public final class ServerCommonConsts {
     public static final String OBJECT_ROLE      = "role";
     public static final String OBJECT_GROUP     = "group";
     public static final String OBJECT_POLICY    = "policy";
+    public static final String OBJECT_ASSERTION = "assertion";
     public static final String OBJECT_ENTITY    = "entity";
     public static final String USER_DOMAIN      = "user";
 
diff --git a/libs/java/server_common/src/main/java/com/yahoo/athenz/common/server/util/ResourceUtils.java b/libs/java/server_common/src/main/java/com/yahoo/athenz/common/server/util/ResourceUtils.java
index 445b01597..8efafa123 100644
--- a/libs/java/server_common/src/main/java/com/yahoo/athenz/common/server/util/ResourceUtils.java
+++ b/libs/java/server_common/src/main/java/com/yahoo/athenz/common/server/util/ResourceUtils.java
@@ -43,6 +43,10 @@ public class ResourceUtils {
         return generateResourceName(domainName, policyName, ServerCommonConsts.OBJECT_POLICY);
     }
 
+    public static String assertionResourceName(String domainName, String policyName, Long assertionId) {
+        return domainName + ":" + ServerCommonConsts.OBJECT_POLICY + "." + policyName + "." + ServerCommonConsts.OBJECT_ASSERTION + "." + assertionId;
+    }
+
     public static String entityResourceName(String domainName, String entityName) {
         return generateResourceName(domainName, entityName, ServerCommonConsts.OBJECT_ENTITY);
     }
diff --git a/servers/zms/src/main/java/com/yahoo/athenz/zms/ZMSImpl.java b/servers/zms/src/main/java/com/yahoo/athenz/zms/ZMSImpl.java
index f4541d662..3e114df03 100644
--- a/servers/zms/src/main/java/com/yahoo/athenz/zms/ZMSImpl.java
+++ b/servers/zms/src/main/java/com/yahoo/athenz/zms/ZMSImpl.java
@@ -110,6 +110,7 @@ public class ZMSImpl implements Authorizer, KeyStore, ZMSHandler {
 
     private static final String ROLE_PREFIX = "role.";
     private static final String POLICY_PREFIX = "policy.";
+    private static final String ASSERTION_PREFIX = "assertion.";
 
     private static final String ADMIN_POLICY_NAME = "admin";
     private static final String ADMIN_ROLE_NAME = "admin";
@@ -5080,9 +5081,6 @@ public class ZMSImpl implements Authorizer, KeyStore, ZMSHandler {
             }
         }
 
-        // authorization check which also automatically updates
-        // the active and approved flags for the request
-
         if (!isAllowedPutMembership(principal, domain, role, roleMember)) {
             throw ZMSUtils.forbiddenError("putMembership: principal is not authorized to add members", caller);
         }
@@ -6105,6 +6103,8 @@ public class ZMSImpl implements Authorizer, KeyStore, ZMSHandler {
         setRequestDomain(ctx, domainName);
         policyName = policyName.toLowerCase();
 
+        final Principal principal = ((RsrcCtxWrapper) ctx).principal();
+
         // we are not going to allow any user to update
         // the admin policy since that is required
         // for standard domain operations */
@@ -6126,6 +6126,10 @@ public class ZMSImpl implements Authorizer, KeyStore, ZMSHandler {
             throw ZMSUtils.notFoundError("Invalid policy name specified", caller);
         }
 
+        if (!isAllowedDeleteAssertion(principal, domain, policyName, assertionId)) {
+            throw ZMSUtils.forbiddenError("deleteAssertion: principal is not authorized to delete assertion", caller);
+        }
+
         try {
             ResourceOwnership.verifyPolicyAssertionsDeleteResourceOwnership(policy, resourceOwner, caller);
         } catch (ServerResourceException ex) {
@@ -6160,6 +6164,8 @@ public class ZMSImpl implements Authorizer, KeyStore, ZMSHandler {
         policyName = policyName.toLowerCase();
         version = version.toLowerCase();
 
+        final Principal principal = ((RsrcCtxWrapper) ctx).principal();
+
         // we are not going to allow any user to update
         // the admin policy since that is required
         // for standard domain operations */
@@ -6170,7 +6176,14 @@ public class ZMSImpl implements Authorizer, KeyStore, ZMSHandler {
 
         // verify that request is properly authenticated for this request
 
-        verifyAuthorizedServiceOperation(((RsrcCtxWrapper) ctx).principal().getAuthorizedService(), caller);
+        verifyAuthorizedServiceOperation(principal.getAuthorizedService(), caller);
+        AthenzDomain domain = getAthenzDomain(domainName, false);
+
+        // authorization check which also automatically updates
+        // the active and approved flags for the request
+        if (!isAllowedDeleteAssertion(principal, domain, policyName, assertionId)) {
+            throw ZMSUtils.forbiddenError("deleteAssertion: principal is not authorized to delete assertions", caller);
+        }
 
         dbService.executeDeleteAssertion(ctx, domainName, policyName, version, assertionId, auditRef, caller);
     }
@@ -10763,6 +10776,16 @@ public class ZMSImpl implements Authorizer, KeyStore, ZMSHandler {
         return pendingMember != null && principal.getFullName().equals(pendingMember.getRequestPrincipal());
     }
 
+    boolean isAllowedDeleteAssertion(Principal principal, final AthenzDomain domain, final String policyName, final Long assertionId) {
+          if (hasAccess(domain, "update", ResourceUtils.policyResourceName(domain.getName(), policyName), principal, null) == AccessStatus.ALLOWED) {
+              return true;
+          }
+          if (hasAccess(domain, "delete", ResourceUtils.assertionResourceName(domain.getName(), policyName, assertionId), principal, null) == AccessStatus.ALLOWED) {
+              return true;
+          }
+          return false;
+    }
+
     @Override
     public DomainRoleMembership getPendingDomainRoleMembersList(ResourceContext ctx, String principal, String domainName) {
 
diff --git a/servers/zms/src/main/java/com/yahoo/athenz/zms/ZMSResources.java b/servers/zms/src/main/java/com/yahoo/athenz/zms/ZMSResources.java
index 5cf0e4a1d..5151900e9 100644
--- a/servers/zms/src/main/java/com/yahoo/athenz/zms/ZMSResources.java
+++ b/servers/zms/src/main/java/com/yahoo/athenz/zms/ZMSResources.java
@@ -2514,7 +2514,7 @@ public class ZMSResources {
     @DELETE
     @Path("/domain/{domainName}/policy/{policyName}/assertion/{assertionId}")
     @Produces(MediaType.APPLICATION_JSON)
-    @Operation(description = "Delete the specified policy assertion. Upon successful completion of this delete request, the server will return NO_CONTENT status code without any data (no object will be returned).")
+    @Operation(description = "Delete the specified policy assertion. Upon successful completion of this delete request, the server will return NO_CONTENT status code without any data (no object will be returned). The required authorization includes two options: 1. (\"update\", \"{domainName}:policy.{policyName}\") 2. (\"delete\", \"{domainName}:policy.{policyName}.assertion.{assertionId}\")")
     public void deleteAssertion(
         @Parameter(description = "name of the domain", required = true) @PathParam("domainName") String domainName,
         @Parameter(description = "name of the policy", required = true) @PathParam("policyName") String policyName,
@@ -2525,7 +2525,7 @@ public class ZMSResources {
         ResourceContext context = null;
         try {
             context = this.delegate.newResourceContext(this.servletContext, this.request, this.response, "deleteAssertion");
-            context.authorize("update", "" + domainName + ":policy." + policyName + "", null);
+            context.authenticate();
             this.delegate.deleteAssertion(context, domainName, policyName, assertionId, auditRef, resourceOwner);
         } catch (ResourceException e) {
             code = e.getCode();
@@ -2555,7 +2555,7 @@ public class ZMSResources {
     @DELETE
     @Path("/domain/{domainName}/policy/{policyName}/version/{version}/assertion/{assertionId}")
     @Produces(MediaType.APPLICATION_JSON)
-    @Operation(description = "Delete the specified policy version assertion. Upon successful completion of this delete request, the server will return NO_CONTENT status code without any data (no object will be returned).")
+    @Operation(description = "Delete the specified policy version assertion. Upon successful completion of this delete request, the server will return NO_CONTENT status code without any data (no object will be returned). The required authorization includes two options: 1. (\"update\", \"{domainName}:policy.{policyName}\") 2. (\"delete\", \"{domainName}:policy.{policyName}.assertion.{assertionId}\")")
     public void deleteAssertionPolicyVersion(
         @Parameter(description = "name of the domain", required = true) @PathParam("domainName") String domainName,
         @Parameter(description = "name of the policy", required = true) @PathParam("policyName") String policyName,
@@ -2567,7 +2567,7 @@ public class ZMSResources {
         ResourceContext context = null;
         try {
             context = this.delegate.newResourceContext(this.servletContext, this.request, this.response, "deleteAssertionPolicyVersion");
-            context.authorize("update", "" + domainName + ":policy." + policyName + "", null);
+            context.authenticate();
             this.delegate.deleteAssertionPolicyVersion(context, domainName, policyName, version, assertionId, auditRef, resourceOwner);
         } catch (ResourceException e) {
             code = e.getCode();



Test patch:
diff --git a/libs/java/server_common/src/test/java/com/yahoo/athenz/common/server/util/ResourceUtilsTest.java b/libs/java/server_common/src/test/java/com/yahoo/athenz/common/server/util/ResourceUtilsTest.java
index b206c5e81..9b84da0c6 100644
--- a/libs/java/server_common/src/test/java/com/yahoo/athenz/common/server/util/ResourceUtilsTest.java
+++ b/libs/java/server_common/src/test/java/com/yahoo/athenz/common/server/util/ResourceUtilsTest.java
@@ -50,4 +50,10 @@ public class ResourceUtilsTest {
         assertEquals(ResourceUtils.policyResourceName("athenz", "policy1"), "athenz:policy.policy1");
         assertEquals(ResourceUtils.policyResourceName("athenz.api", "policy1"), "athenz.api:policy.policy1");
     }
+
+    @Test
+    public void testAssertionResourceName() {
+        assertEquals(ResourceUtils.assertionResourceName("athenz", "policy1", 123l), "athenz:policy.policy1.assertion.123");
+        assertEquals(ResourceUtils.assertionResourceName("athenz.api", "policy1", 123l), "athenz.api:policy.policy1.assertion.123");
+    }
 }
diff --git a/servers/zms/src/test/java/com/yahoo/athenz/zms/ResourceOwnershipTest.java b/servers/zms/src/test/java/com/yahoo/athenz/zms/ResourceOwnershipTest.java
index 5bd59ce01..0a88fd239 100644
--- a/servers/zms/src/test/java/com/yahoo/athenz/zms/ResourceOwnershipTest.java
+++ b/servers/zms/src/test/java/com/yahoo/athenz/zms/ResourceOwnershipTest.java
@@ -2074,7 +2074,7 @@ public class ResourceOwnershipTest {
         final String roleName = "role1";
         final String policyName = "policy1";
         TopLevelDomain dom1 = zmsTestInitializer.createTopLevelDomainObject(domainName,
-                "Test Domain1", "testOrg", zmsTestInitializer.getAdminUser());
+                "Test Domain1", "testOrg", "user.user1");
         zmsImpl.postTopLevelDomain(ctx, auditRef, null, dom1);
 
         Role role1 = zmsTestInitializer.createRoleObject(domainName, roleName, null, null);
diff --git a/servers/zms/src/test/java/com/yahoo/athenz/zms/ZMSImplTest.java b/servers/zms/src/test/java/com/yahoo/athenz/zms/ZMSImplTest.java
index 6c6d184e0..620bd35a6 100644
--- a/servers/zms/src/test/java/com/yahoo/athenz/zms/ZMSImplTest.java
+++ b/servers/zms/src/test/java/com/yahoo/athenz/zms/ZMSImplTest.java
@@ -4222,7 +4222,7 @@ public class ZMSImplTest {
         final String auditRef = zmsTestInitializer.getAuditRef();
 
         TopLevelDomain dom1 = zmsTestInitializer.createTopLevelDomainObject(domainName,
-                "Test Domain1", "testOrg", zmsTestInitializer.getAdminUser());
+                "Test Domain1", "testOrg", "user.user1");
         when(ctx.getApiName()).thenReturn("posttopleveldomain").thenReturn("putpolicy");
         zmsImpl.postTopLevelDomain(ctx, auditRef, null, dom1);
 
@@ -4365,7 +4365,7 @@ public class ZMSImplTest {
         final String auditRef = zmsTestInitializer.getAuditRef();
 
         TopLevelDomain dom1 = zmsTestInitializer.createTopLevelDomainObject(domainName,
-                "Test Domain1", "testOrg", zmsTestInitializer.getAdminUser());
+                "Test Domain1", "testOrg", "user.user1");
         when(ctx.getApiName()).thenReturn("posttopleveldomain").thenReturn("putpolicy");
         zmsImpl.postTopLevelDomain(ctx, auditRef, null, dom1);
 
@@ -17131,7 +17131,7 @@ public class ZMSImplTest {
 
         final String domainName = "delete-assertion-single";
         TopLevelDomain dom1 = zmsTestInitializer.createTopLevelDomainObject(domainName,
-                "Test Domain1", "testOrg", zmsTestInitializer.getAdminUser());
+                "Test Domain1", "testOrg", "user.user1");
         zmsImpl.postTopLevelDomain(ctx, auditRef, null, dom1);
 
         Policy policy = zmsTestInitializer.createPolicyObject(domainName, "policy1");
@@ -17150,6 +17150,49 @@ public class ZMSImplTest {
         zmsImpl.deleteTopLevelDomain(ctx, domainName, auditRef, null);
     }
 
+    @Test
+    public void testDeleteAssertionAuthority() {
+        ZMSImpl zmsImpl = zmsTestInitializer.getZms();
+        RsrcCtxWrapper ctx = zmsTestInitializer.getMockDomRsrcCtx();
+        final String auditRef = zmsTestInitializer.getAuditRef();
+
+        final String domainName = "delete-assertion-authority";
+        TopLevelDomain dom1 = zmsTestInitializer.createTopLevelDomainObject(domainName,
+                "Test Domain1", "testOrg", zmsTestInitializer.getAdminUser());
+        zmsImpl.postTopLevelDomain(ctx, auditRef, null, dom1);
+
+        Role role = zmsTestInitializer.createRoleObject(domainName, "delete-assertion-role", null, "user.user1", null);
+        zmsImpl.putRole(ctx, domainName, "delete-assertion-role", auditRef, false, null, role);
+
+        Policy policy = zmsTestInitializer.createPolicyObject(domainName, "policy1");
+        zmsImpl.putPolicy(ctx, domainName, "policy1", auditRef, false, null, policy);
+
+        Policy policyRes = zmsImpl.getPolicy(ctx, domainName, "policy1");
+        Long assertionId = policyRes.getAssertions().get(0).getId();
+
+
+        try {
+            zmsImpl.deleteAssertion(ctx, domainName, "policy1", assertionId, auditRef, null);
+            fail();
+        } catch (ResourceException ex) {
+            assertEquals(ex.getCode(), 403);
+        }
+
+        Assertion assertion = new Assertion();
+        assertion.setAction("delete");
+        assertion.setEffect(AssertionEffect.ALLOW);
+        assertion.setResource(domainName + ":policy.policy1.assertion." + assertionId);
+        assertion.setRole(ResourceUtils.roleResourceName(domainName, "delete-assertion-role"));
+        zmsImpl.putAssertion(ctx, domainName, "policy1", auditRef, null, assertion);
+
+        zmsImpl.deleteAssertion(ctx, domainName, "policy1", assertionId, auditRef, null);
+
+        policyRes = zmsImpl.getPolicy(ctx, domainName, "policy1");
+        assertEquals(policyRes.getAssertions().size(), 1);
+
+        zmsImpl.deleteTopLevelDomain(ctx, domainName, auditRef, null);
+    }
+
     @Test
     public void testDeleteAssertionMultiple() {
 
@@ -17159,7 +17202,7 @@ public class ZMSImplTest {
 
         final String domainName = "delete-assertion-multiple";
         TopLevelDomain dom1 = zmsTestInitializer.createTopLevelDomainObject(domainName,
-                "Test Domain1", "testOrg", zmsTestInitializer.getAdminUser());
+                "Test Domain1", "testOrg", "user.user1");
         zmsImpl.postTopLevelDomain(ctx, auditRef, null, dom1);
 
         Policy policy = zmsTestInitializer.createPolicyObject(domainName, "policy1");
@@ -17222,7 +17265,7 @@ public class ZMSImplTest {
 
         final String domainName = "delete-assertion-unknown";
         TopLevelDomain dom1 = zmsTestInitializer.createTopLevelDomainObject(domainName,
-                "Test Domain1", "testOrg", zmsTestInitializer.getAdminUser());
+                "Test Domain1", "testOrg", "user.user1");
         zmsImpl.postTopLevelDomain(ctx, auditRef, null, dom1);
 
         Policy policy = zmsTestInitializer.createPolicyObject(domainName, "policy1");