From 2cc04da549b94fa1c68810a23e09bfef121cecf8 Mon Sep 17 00:00:00 2001 From: Henry Avetisyan Date: Sat, 25 May 2024 14:25:43 -0700 Subject: [PATCH] support principal domain filter for role/group members Signed-off-by: Henry Avetisyan --- clients/go/zms/model.go | 44 ++ clients/go/zms/zms_schema.go | 2 + .../main/java/com/yahoo/athenz/zms/Group.java | 13 + .../java/com/yahoo/athenz/zms/GroupMeta.java | 13 + .../main/java/com/yahoo/athenz/zms/Role.java | 13 + .../java/com/yahoo/athenz/zms/RoleMeta.java | 13 + .../java/com/yahoo/athenz/zms/ZMSSchema.java | 6 +- core/zms/src/main/rdl/Group.tdl | 1 + core/zms/src/main/rdl/Role.tdl | 1 + .../java/com/yahoo/athenz/zms/GroupTest.java | 28 +- .../java/com/yahoo/athenz/zms/RoleTest.java | 28 +- libs/go/zmscli/cli.go | 32 ++ libs/go/zmscli/group.go | 22 + libs/go/zmscli/role.go | 22 + servers/zms/pom.xml | 2 +- .../zms/schema/updates/update-20240525.sql | 2 + servers/zms/schema/zms_server.mwb | Bin 53874 -> 54528 bytes servers/zms/schema/zms_server.sql | 4 +- .../java/com/yahoo/athenz/zms/DBService.java | 18 +- .../java/com/yahoo/athenz/zms/ZMSConsts.java | 1 + .../java/com/yahoo/athenz/zms/ZMSImpl.java | 131 +++-- .../zms/store/impl/jdbc/JDBCConnection.java | 26 +- .../zms/utils/PrincipalDomainFilter.java | 139 +++++ .../com/yahoo/athenz/zms/DBServiceTest.java | 5 +- .../com/yahoo/athenz/zms/ZMSImplTest.java | 126 +++-- .../provider/ServiceProviderManagerTest.java | 6 +- .../store/impl/jdbc/JDBCConnectionTest.java | 19 +- .../zms/utils/PrincipalDomainFilterTest.java | 489 ++++++++++++++++++ 28 files changed, 1089 insertions(+), 117 deletions(-) create mode 100644 servers/zms/schema/updates/update-20240525.sql create mode 100644 servers/zms/src/main/java/com/yahoo/athenz/zms/utils/PrincipalDomainFilter.java create mode 100644 servers/zms/src/test/java/com/yahoo/athenz/zms/utils/PrincipalDomainFilterTest.java diff --git a/clients/go/zms/model.go b/clients/go/zms/model.go index d4aeedd2d73..5e11527b13b 100644 --- a/clients/go/zms/model.go +++ b/clients/go/zms/model.go @@ -1466,6 +1466,11 @@ type RoleMeta struct { // ownership information for the role (read-only attribute) // ResourceOwnership *ResourceRoleOwnership `json:"resourceOwnership,omitempty" rdl:"optional" yaml:",omitempty"` + + // + // membership filtered based on configured principal domains + // + PrincipalDomainFilter string `json:"principalDomainFilter" rdl:"optional" yaml:",omitempty"` } // NewRoleMeta - creates an initialized RoleMeta instance, returns a pointer to it @@ -1525,6 +1530,12 @@ func (self *RoleMeta) Validate() error { return fmt.Errorf("RoleMeta.description does not contain a valid String (%v)", val.Error) } } + if self.PrincipalDomainFilter != "" { + val := rdl.Validate(ZMSSchema(), "String", self.PrincipalDomainFilter) + if !val.Valid { + return fmt.Errorf("RoleMeta.principalDomainFilter does not contain a valid String (%v)", val.Error) + } + } return nil } @@ -1658,6 +1669,11 @@ type Role struct { // ResourceOwnership *ResourceRoleOwnership `json:"resourceOwnership,omitempty" rdl:"optional" yaml:",omitempty"` + // + // membership filtered based on configured principal domains + // + PrincipalDomainFilter string `json:"principalDomainFilter" rdl:"optional" yaml:",omitempty"` + // // name of the role // @@ -1747,6 +1763,12 @@ func (self *Role) Validate() error { return fmt.Errorf("Role.description does not contain a valid String (%v)", val.Error) } } + if self.PrincipalDomainFilter != "" { + val := rdl.Validate(ZMSSchema(), "String", self.PrincipalDomainFilter) + if !val.Valid { + return fmt.Errorf("Role.principalDomainFilter does not contain a valid String (%v)", val.Error) + } + } if self.Name == "" { return fmt.Errorf("Role.name is missing but is a required field") } else { @@ -5905,6 +5927,11 @@ type GroupMeta struct { // ownership information for the group (read-only attribute) // ResourceOwnership *ResourceGroupOwnership `json:"resourceOwnership,omitempty" rdl:"optional" yaml:",omitempty"` + + // + // membership filtered based on configured principal domains + // + PrincipalDomainFilter string `json:"principalDomainFilter" rdl:"optional" yaml:",omitempty"` } // NewGroupMeta - creates an initialized GroupMeta instance, returns a pointer to it @@ -5952,6 +5979,12 @@ func (self *GroupMeta) Validate() error { return fmt.Errorf("GroupMeta.userAuthorityExpiration does not contain a valid String (%v)", val.Error) } } + if self.PrincipalDomainFilter != "" { + val := rdl.Validate(ZMSSchema(), "String", self.PrincipalDomainFilter) + if !val.Valid { + return fmt.Errorf("GroupMeta.principalDomainFilter does not contain a valid String (%v)", val.Error) + } + } return nil } @@ -6039,6 +6072,11 @@ type Group struct { // ResourceOwnership *ResourceGroupOwnership `json:"resourceOwnership,omitempty" rdl:"optional" yaml:",omitempty"` + // + // membership filtered based on configured principal domains + // + PrincipalDomainFilter string `json:"principalDomainFilter" rdl:"optional" yaml:",omitempty"` + // // name of the group // @@ -6105,6 +6143,12 @@ func (self *Group) Validate() error { return fmt.Errorf("Group.userAuthorityExpiration does not contain a valid String (%v)", val.Error) } } + if self.PrincipalDomainFilter != "" { + val := rdl.Validate(ZMSSchema(), "String", self.PrincipalDomainFilter) + if !val.Valid { + return fmt.Errorf("Group.principalDomainFilter does not contain a valid String (%v)", val.Error) + } + } if self.Name == "" { return fmt.Errorf("Group.name is missing but is a required field") } else { diff --git a/clients/go/zms/zms_schema.go b/clients/go/zms/zms_schema.go index a0431166b81..d1f77e928c7 100644 --- a/clients/go/zms/zms_schema.go +++ b/clients/go/zms/zms_schema.go @@ -270,6 +270,7 @@ func init() { tRoleMeta.Field("selfRenewMins", "Int32", true, nil, "Number of minutes members can renew their membership if self review option is enabled") tRoleMeta.Field("maxMembers", "Int32", true, nil, "Maximum number of members allowed in the group") tRoleMeta.Field("resourceOwnership", "ResourceRoleOwnership", true, nil, "ownership information for the role (read-only attribute)") + tRoleMeta.Field("principalDomainFilter", "String", true, nil, "membership filtered based on configured principal domains") sb.AddType(tRoleMeta.Build()) tRole := rdl.NewStructTypeBuilder("RoleMeta", "Role") @@ -638,6 +639,7 @@ func init() { tGroupMeta.Field("selfRenewMins", "Int32", true, nil, "Number of minutes members can renew their membership if self review option is enabled") tGroupMeta.Field("maxMembers", "Int32", true, nil, "Maximum number of members allowed in the group") tGroupMeta.Field("resourceOwnership", "ResourceGroupOwnership", true, nil, "ownership information for the group (read-only attribute)") + tGroupMeta.Field("principalDomainFilter", "String", true, nil, "membership filtered based on configured principal domains") sb.AddType(tGroupMeta.Build()) tGroup := rdl.NewStructTypeBuilder("GroupMeta", "Group") diff --git a/core/zms/src/main/java/com/yahoo/athenz/zms/Group.java b/core/zms/src/main/java/com/yahoo/athenz/zms/Group.java index 817dcb39e24..33ec9b2031e 100644 --- a/core/zms/src/main/java/com/yahoo/athenz/zms/Group.java +++ b/core/zms/src/main/java/com/yahoo/athenz/zms/Group.java @@ -59,6 +59,9 @@ public class Group { @RdlOptional @JsonInclude(JsonInclude.Include.NON_EMPTY) public ResourceGroupOwnership resourceOwnership; + @RdlOptional + @JsonInclude(JsonInclude.Include.NON_NULL) + public String principalDomainFilter; public String name; @RdlOptional @JsonInclude(JsonInclude.Include.NON_EMPTY) @@ -175,6 +178,13 @@ public Group setResourceOwnership(ResourceGroupOwnership resourceOwnership) { public ResourceGroupOwnership getResourceOwnership() { return resourceOwnership; } + public Group setPrincipalDomainFilter(String principalDomainFilter) { + this.principalDomainFilter = principalDomainFilter; + return this; + } + public String getPrincipalDomainFilter() { + return principalDomainFilter; + } public Group setName(String name) { this.name = name; return this; @@ -256,6 +266,9 @@ public boolean equals(Object another) { if (resourceOwnership == null ? a.resourceOwnership != null : !resourceOwnership.equals(a.resourceOwnership)) { return false; } + if (principalDomainFilter == null ? a.principalDomainFilter != null : !principalDomainFilter.equals(a.principalDomainFilter)) { + return false; + } if (name == null ? a.name != null : !name.equals(a.name)) { return false; } diff --git a/core/zms/src/main/java/com/yahoo/athenz/zms/GroupMeta.java b/core/zms/src/main/java/com/yahoo/athenz/zms/GroupMeta.java index 10790807549..14c9f2c8c5d 100644 --- a/core/zms/src/main/java/com/yahoo/athenz/zms/GroupMeta.java +++ b/core/zms/src/main/java/com/yahoo/athenz/zms/GroupMeta.java @@ -59,6 +59,9 @@ public class GroupMeta { @RdlOptional @JsonInclude(JsonInclude.Include.NON_EMPTY) public ResourceGroupOwnership resourceOwnership; + @RdlOptional + @JsonInclude(JsonInclude.Include.NON_NULL) + public String principalDomainFilter; public GroupMeta setSelfServe(Boolean selfServe) { this.selfServe = selfServe; @@ -165,6 +168,13 @@ public GroupMeta setResourceOwnership(ResourceGroupOwnership resourceOwnership) public ResourceGroupOwnership getResourceOwnership() { return resourceOwnership; } + public GroupMeta setPrincipalDomainFilter(String principalDomainFilter) { + this.principalDomainFilter = principalDomainFilter; + return this; + } + public String getPrincipalDomainFilter() { + return principalDomainFilter; + } @Override public boolean equals(Object another) { @@ -218,6 +228,9 @@ public boolean equals(Object another) { if (resourceOwnership == null ? a.resourceOwnership != null : !resourceOwnership.equals(a.resourceOwnership)) { return false; } + if (principalDomainFilter == null ? a.principalDomainFilter != null : !principalDomainFilter.equals(a.principalDomainFilter)) { + return false; + } } return true; } diff --git a/core/zms/src/main/java/com/yahoo/athenz/zms/Role.java b/core/zms/src/main/java/com/yahoo/athenz/zms/Role.java index 55025b29fd9..bcba698423d 100644 --- a/core/zms/src/main/java/com/yahoo/athenz/zms/Role.java +++ b/core/zms/src/main/java/com/yahoo/athenz/zms/Role.java @@ -89,6 +89,9 @@ public class Role { @RdlOptional @JsonInclude(JsonInclude.Include.NON_EMPTY) public ResourceRoleOwnership resourceOwnership; + @RdlOptional + @JsonInclude(JsonInclude.Include.NON_NULL) + public String principalDomainFilter; public String name; @RdlOptional @JsonInclude(JsonInclude.Include.NON_EMPTY) @@ -267,6 +270,13 @@ public Role setResourceOwnership(ResourceRoleOwnership resourceOwnership) { public ResourceRoleOwnership getResourceOwnership() { return resourceOwnership; } + public Role setPrincipalDomainFilter(String principalDomainFilter) { + this.principalDomainFilter = principalDomainFilter; + return this; + } + public String getPrincipalDomainFilter() { + return principalDomainFilter; + } public Role setName(String name) { this.name = name; return this; @@ -386,6 +396,9 @@ public boolean equals(Object another) { if (resourceOwnership == null ? a.resourceOwnership != null : !resourceOwnership.equals(a.resourceOwnership)) { return false; } + if (principalDomainFilter == null ? a.principalDomainFilter != null : !principalDomainFilter.equals(a.principalDomainFilter)) { + return false; + } if (name == null ? a.name != null : !name.equals(a.name)) { return false; } diff --git a/core/zms/src/main/java/com/yahoo/athenz/zms/RoleMeta.java b/core/zms/src/main/java/com/yahoo/athenz/zms/RoleMeta.java index 380ed5da9f2..7b753d4afab 100644 --- a/core/zms/src/main/java/com/yahoo/athenz/zms/RoleMeta.java +++ b/core/zms/src/main/java/com/yahoo/athenz/zms/RoleMeta.java @@ -83,6 +83,9 @@ public class RoleMeta { @RdlOptional @JsonInclude(JsonInclude.Include.NON_EMPTY) public ResourceRoleOwnership resourceOwnership; + @RdlOptional + @JsonInclude(JsonInclude.Include.NON_NULL) + public String principalDomainFilter; public RoleMeta setSelfServe(Boolean selfServe) { this.selfServe = selfServe; @@ -245,6 +248,13 @@ public RoleMeta setResourceOwnership(ResourceRoleOwnership resourceOwnership) { public ResourceRoleOwnership getResourceOwnership() { return resourceOwnership; } + public RoleMeta setPrincipalDomainFilter(String principalDomainFilter) { + this.principalDomainFilter = principalDomainFilter; + return this; + } + public String getPrincipalDomainFilter() { + return principalDomainFilter; + } @Override public boolean equals(Object another) { @@ -322,6 +332,9 @@ public boolean equals(Object another) { if (resourceOwnership == null ? a.resourceOwnership != null : !resourceOwnership.equals(a.resourceOwnership)) { return false; } + if (principalDomainFilter == null ? a.principalDomainFilter != null : !principalDomainFilter.equals(a.principalDomainFilter)) { + return false; + } } return true; } 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 a7cd85af099..a9300d9f520 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 @@ -234,7 +234,8 @@ private static Schema build() { .field("selfRenew", "Bool", true, "Flag indicates whether to allow expired members to renew their membership") .field("selfRenewMins", "Int32", true, "Number of minutes members can renew their membership if self review option is enabled") .field("maxMembers", "Int32", true, "Maximum number of members allowed in the group") - .field("resourceOwnership", "ResourceRoleOwnership", true, "ownership information for the role (read-only attribute)"); + .field("resourceOwnership", "ResourceRoleOwnership", true, "ownership information for the role (read-only attribute)") + .field("principalDomainFilter", "String", true, "membership filtered based on configured principal domains"); sb.structType("Role", "RoleMeta") .comment("The representation for a Role with set of members. The members (Array) field is deprecated and not used in role objects since it incorrectly lists all the members in the role without taking into account if the member is expired or possibly disabled. Thus, using this attribute will result in incorrect authorization checks by the client and, thus, it's no longer being populated. All applications must use the roleMembers field and take into account all the attributes of the member.") @@ -553,7 +554,8 @@ private static Schema build() { .field("selfRenew", "Bool", true, "Flag indicates whether to allow expired members to renew their membership") .field("selfRenewMins", "Int32", true, "Number of minutes members can renew their membership if self review option is enabled") .field("maxMembers", "Int32", true, "Maximum number of members allowed in the group") - .field("resourceOwnership", "ResourceGroupOwnership", true, "ownership information for the group (read-only attribute)"); + .field("resourceOwnership", "ResourceGroupOwnership", true, "ownership information for the group (read-only attribute)") + .field("principalDomainFilter", "String", true, "membership filtered based on configured principal domains"); sb.structType("Group", "GroupMeta") .comment("The representation for a Group with set of members.") diff --git a/core/zms/src/main/rdl/Group.tdl b/core/zms/src/main/rdl/Group.tdl index fa0ebeacd9c..994c0ad7f1f 100644 --- a/core/zms/src/main/rdl/Group.tdl +++ b/core/zms/src/main/rdl/Group.tdl @@ -67,6 +67,7 @@ type GroupMeta Struct { Int32 selfRenewMins (optional); //Number of minutes members can renew their membership if self review option is enabled Int32 maxMembers (optional); //Maximum number of members allowed in the group ResourceGroupOwnership resourceOwnership (optional); //ownership information for the group (read-only attribute) + String principalDomainFilter (optional, x_allowempty="true"); //membership filtered based on configured principal domains } //The representation for a Group with set of members. diff --git a/core/zms/src/main/rdl/Role.tdl b/core/zms/src/main/rdl/Role.tdl index 3d2a735e9ea..d0191a7a4ba 100644 --- a/core/zms/src/main/rdl/Role.tdl +++ b/core/zms/src/main/rdl/Role.tdl @@ -66,6 +66,7 @@ type RoleMeta Struct { Int32 selfRenewMins (optional); //Number of minutes members can renew their membership if self review option is enabled Int32 maxMembers (optional); //Maximum number of members allowed in the group ResourceRoleOwnership resourceOwnership (optional); //ownership information for the role (read-only attribute) + String principalDomainFilter (optional, x_allowempty="true"); //membership filtered based on configured principal domains } //The representation for a Role with set of members. diff --git a/core/zms/src/test/java/com/yahoo/athenz/zms/GroupTest.java b/core/zms/src/test/java/com/yahoo/athenz/zms/GroupTest.java index bb4b8033d37..10fea7ae9b2 100644 --- a/core/zms/src/test/java/com/yahoo/athenz/zms/GroupTest.java +++ b/core/zms/src/test/java/com/yahoo/athenz/zms/GroupTest.java @@ -62,7 +62,8 @@ public void testGroupsMethod() { .setSelfRenew(true) .setSelfRenewMins(180) .setMaxMembers(5) - .setResourceOwnership(new ResourceGroupOwnership().setMetaOwner("TF")); + .setResourceOwnership(new ResourceGroupOwnership().setMetaOwner("TF")) + .setPrincipalDomainFilter("user,+unix.test,-home"); Group r2 = new Group() .setName("sys.auth:group.admin") @@ -83,7 +84,8 @@ public void testGroupsMethod() { .setSelfRenew(true) .setSelfRenewMins(180) .setMaxMembers(5) - .setResourceOwnership(new ResourceGroupOwnership().setMetaOwner("TF")); + .setResourceOwnership(new ResourceGroupOwnership().setMetaOwner("TF")) + .setPrincipalDomainFilter("user,+unix.test,-home"); assertEquals(r, r2); assertEquals(r, r); @@ -107,6 +109,7 @@ public void testGroupsMethod() { assertEquals(r.getSelfRenew(), Boolean.TRUE); assertEquals(r.getMaxMembers(), 5); assertEquals(r.getResourceOwnership(), new ResourceGroupOwnership().setMetaOwner("TF")); + assertEquals(r.getPrincipalDomainFilter(), "user,+unix.test,-home"); r2.setLastReviewedDate(Timestamp.fromMillis(123456789124L)); assertNotEquals(r, r2); @@ -206,6 +209,13 @@ public void testGroupsMethod() { r2.setResourceOwnership(new ResourceGroupOwnership().setMetaOwner("TF")); assertEquals(r, r2); + r2.setPrincipalDomainFilter("user"); + assertNotEquals(r, r2); + r2.setPrincipalDomainFilter(null); + assertNotEquals(r, r2); + r2.setPrincipalDomainFilter("user,+unix.test,-home"); + assertEquals(r, r2); + r2.setAuditLog(null); assertNotEquals(r, r2); r2.setGroupMembers(null); @@ -539,7 +549,8 @@ public void testGroupMetaMethod() { .setSelfRenew(true) .setSelfRenewMins(180) .setMaxMembers(5) - .setResourceOwnership(new ResourceGroupOwnership().setMetaOwner("TF")); + .setResourceOwnership(new ResourceGroupOwnership().setMetaOwner("TF")) + .setPrincipalDomainFilter("user,+unix.test,-home"); assertFalse(gm1.getSelfServe()); assertEquals(gm1.getNotifyRoles(), "role1,domain:role.role2"); @@ -556,6 +567,7 @@ public void testGroupMetaMethod() { assertEquals(gm1.getSelfRenew(), Boolean.TRUE); assertEquals(gm1.getMaxMembers(), 5); assertEquals(gm1.getResourceOwnership(), new ResourceGroupOwnership().setMetaOwner("TF")); + assertEquals(gm1.getPrincipalDomainFilter(), "user,+unix.test,-home"); GroupMeta gm2 = new GroupMeta() .setSelfServe(false) @@ -572,7 +584,8 @@ public void testGroupMetaMethod() { .setSelfRenew(true) .setSelfRenewMins(180) .setMaxMembers(5) - .setResourceOwnership(new ResourceGroupOwnership().setMetaOwner("TF")); + .setResourceOwnership(new ResourceGroupOwnership().setMetaOwner("TF")) + .setPrincipalDomainFilter("user,+unix.test,-home"); assertEquals(gm1, gm2); assertEquals(gm1, gm1); @@ -684,6 +697,13 @@ public void testGroupMetaMethod() { gm2.setResourceOwnership(new ResourceGroupOwnership().setMetaOwner("TF")); assertEquals(gm2, gm1); + gm2.setPrincipalDomainFilter("user"); + assertNotEquals(gm2, gm1); + gm2.setPrincipalDomainFilter(null); + assertNotEquals(gm2, gm1); + gm2.setPrincipalDomainFilter("user,+unix.test,-home"); + assertEquals(gm2, gm1); + Schema schema = ZMSSchema.instance(); Validator validator = new Validator(schema); Result result = validator.validate(gm1, "GroupMeta"); diff --git a/core/zms/src/test/java/com/yahoo/athenz/zms/RoleTest.java b/core/zms/src/test/java/com/yahoo/athenz/zms/RoleTest.java index 74ba394cc9f..65e7fb11a8e 100644 --- a/core/zms/src/test/java/com/yahoo/athenz/zms/RoleTest.java +++ b/core/zms/src/test/java/com/yahoo/athenz/zms/RoleTest.java @@ -55,7 +55,8 @@ public void testRoleMetaMethod() { .setSelfRenew(true) .setSelfRenewMins(180) .setMaxMembers(5) - .setResourceOwnership(new ResourceRoleOwnership().setMetaOwner("TF")); + .setResourceOwnership(new ResourceRoleOwnership().setMetaOwner("TF")) + .setPrincipalDomainFilter("user,+unix.test,-home"); assertFalse(rm1.getSelfServe()); assertEquals(rm1.getMemberExpiryDays(), Integer.valueOf(30)); @@ -80,6 +81,7 @@ public void testRoleMetaMethod() { assertEquals(rm1.getSelfRenew(), Boolean.TRUE); assertEquals(rm1.getMaxMembers(), 5); assertEquals(rm1.getResourceOwnership(), new ResourceRoleOwnership().setMetaOwner("TF")); + assertEquals(rm1.getPrincipalDomainFilter(), "user,+unix.test,-home"); RoleMeta rm2 = new RoleMeta() .setMemberExpiryDays(30) @@ -104,7 +106,8 @@ public void testRoleMetaMethod() { .setSelfRenew(true) .setSelfRenewMins(180) .setMaxMembers(5) - .setResourceOwnership(new ResourceRoleOwnership().setMetaOwner("TF")); + .setResourceOwnership(new ResourceRoleOwnership().setMetaOwner("TF")) + .setPrincipalDomainFilter("user,+unix.test,-home"); assertEquals(rm1, rm2); assertEquals(rm1, rm1); @@ -272,6 +275,13 @@ public void testRoleMetaMethod() { rm2.setResourceOwnership(new ResourceRoleOwnership().setMetaOwner("TF")); assertEquals(rm2, rm1); + rm2.setPrincipalDomainFilter("user"); + assertNotEquals(rm2, rm1); + rm2.setPrincipalDomainFilter(null); + assertNotEquals(rm2, rm1); + rm2.setPrincipalDomainFilter("user,+unix.test,-home"); + assertEquals(rm2, rm1); + Schema schema = ZMSSchema.instance(); Validator validator = new Validator(schema); Validator.Result result = validator.validate(rm1, "RoleMeta"); @@ -322,7 +332,8 @@ public void testRolesMethod() { .setSelfRenew(true) .setSelfRenewMins(180) .setMaxMembers(5) - .setResourceOwnership(new ResourceRoleOwnership().setMetaOwner("TF")); + .setResourceOwnership(new ResourceRoleOwnership().setMetaOwner("TF")) + .setPrincipalDomainFilter("user,+unix.test,-home"); assertEquals(r.getName(), "sys.auth:role.admin"); assertEquals(r.getModified(), Timestamp.fromMillis(123456789123L)); @@ -353,6 +364,7 @@ public void testRolesMethod() { assertEquals(r.getSelfRenewMins(), 180); assertEquals(r.getMaxMembers(), 5); assertEquals(r.getResourceOwnership(), new ResourceRoleOwnership().setMetaOwner("TF")); + assertEquals(r.getPrincipalDomainFilter(), "user,+unix.test,-home"); Role r2 = new Role() .setName("sys.auth:role.admin") @@ -383,7 +395,8 @@ public void testRolesMethod() { .setSelfRenew(true) .setSelfRenewMins(180) .setMaxMembers(5) - .setResourceOwnership(new ResourceRoleOwnership().setMetaOwner("TF")); + .setResourceOwnership(new ResourceRoleOwnership().setMetaOwner("TF")) + .setPrincipalDomainFilter("user,+unix.test,-home"); assertEquals(r, r2); assertEquals(r, r); @@ -542,6 +555,13 @@ public void testRolesMethod() { r2.setResourceOwnership(new ResourceRoleOwnership().setMetaOwner("TF")); assertEquals(r2, r); + r2.setPrincipalDomainFilter("user"); + assertNotEquals(r2, r); + r2.setPrincipalDomainFilter(null); + assertNotEquals(r2, r); + r2.setPrincipalDomainFilter("user,+unix.test,-home"); + assertEquals(r2, r); + r2.setAuditLog(null); assertNotEquals(r, r2); r2.setTrust(null); diff --git a/libs/go/zmscli/cli.go b/libs/go/zmscli/cli.go index 91b355b6996..59e83a9a641 100644 --- a/libs/go/zmscli/cli.go +++ b/libs/go/zmscli/cli.go @@ -1003,6 +1003,10 @@ func (cli Zms) EvalCommand(params []string) (*string, error) { if argc == 2 { return cli.SetRoleResourceOwnership(dn, args[0], args[1]) } + case "set-role-principal-domain-filter": + if argc == 2 { + return cli.SetRolePrincipalDomainFilter(dn, args[0], args[1]) + } case "set-role-self-renew": if argc == 2 { selfRenew, err := strconv.ParseBool(args[1]) @@ -1165,6 +1169,10 @@ func (cli Zms) EvalCommand(params []string) (*string, error) { if argc == 2 { return cli.SetGroupResourceOwnership(dn, args[0], args[1]) } + case "set-group-principal-domain-filter": + if argc == 2 { + return cli.SetGroupPrincipalDomainFilter(dn, args[0], args[1]) + } case "set-group-self-renew": if argc == 2 { selfRenew, err := strconv.ParseBool(args[1]) @@ -3057,6 +3065,17 @@ func (cli Zms) HelpSpecificCommand(interactive bool, cmd string) string { buf.WriteString(" resource-owner : resource owner in objectowner:{owner},metaowner:{owner},membersowner:{owner} format\n") buf.WriteString(" examples:\n") buf.WriteString(" " + domainExample + " set-role-resource-ownership writers metaowner:TF,membersowner:MSD\n") + case "set-role-principal-domain-filter": + buf.WriteString(" syntax:\n") + buf.WriteString(" " + domainParam + " set-role-principal-domain-filter role principal-domain-filter\n") + buf.WriteString(" parameters:\n") + if !interactive { + buf.WriteString(" domain : name of the domain being updated\n") + } + buf.WriteString(" role : name of the role to be modified\n") + buf.WriteString(" principal-domain-filter : domain filter [+|-]{domainName}[,[+|-]{domainName}]...\n") + buf.WriteString(" examples:\n") + buf.WriteString(" " + domainExample + " set-role-principal-domain-filter writers user,+sports,-sports.dev\n") case "set-role-description": buf.WriteString(" syntax:\n") buf.WriteString(" " + domainParam + " set-role-description role \"description\"\n") @@ -3176,6 +3195,17 @@ func (cli Zms) HelpSpecificCommand(interactive bool, cmd string) string { buf.WriteString(" resource-owner : resource owner in objectowner:{owner},metaowner:{owner},membersowner:{owner} format\n") buf.WriteString(" examples:\n") buf.WriteString(" " + domainExample + " set-group-resource-ownership writers metaowner:TF,membersowner:MSD\n") + case "set-group-principal-domain-filter": + buf.WriteString(" syntax:\n") + buf.WriteString(" " + domainParam + " set-group-principal-domain-filter group principal-domain-filter\n") + buf.WriteString(" parameters:\n") + if !interactive { + buf.WriteString(" domain : name of the domain being updated\n") + } + buf.WriteString(" group : name of the group to be modified\n") + buf.WriteString(" principal-domain-filter : domain filter [+|-]{domainName}[,[+|-]{domainName}]...\n") + buf.WriteString(" examples:\n") + buf.WriteString(" " + domainExample + " set-group-principal-domain-filter writers user,+sports,-sports.dev\n") case "set-group-audit-enabled": buf.WriteString(" syntax:\n") buf.WriteString(" " + domainParam + " set-group-audit-enabled group audit-enabled\n") @@ -3614,6 +3644,7 @@ func (cli Zms) HelpListCommand() string { buf.WriteString(" set-role-user-authority-expiration regular_role attribute\n") buf.WriteString(" set-role-description regular_role description\n") buf.WriteString(" set-role-resource-ownership regular_role resource-owner\n") + buf.WriteString(" set-role-principal-domain-filter regular_role domain-filter\n") buf.WriteString(" add-role-tag regular_role tag_key tag_value [tag_value ...]\n") buf.WriteString(" delete-role-tag regular_role tag_key [tag_value]\n") buf.WriteString(" put-membership-decision regular_role user_or_service [expiration] decision\n") @@ -3644,6 +3675,7 @@ func (cli Zms) HelpListCommand() string { buf.WriteString(" set-group-user-authority-filter group attribute[,attribute...]\n") buf.WriteString(" set-group-user-authority-expiration group attribute\n") buf.WriteString(" set-group-resource-ownership group resource-owner\n") + buf.WriteString(" set-group-principal-domain-filter group domain-filter\n") buf.WriteString(" add-group-tag group tag_key tag_value [tag_value ...]\n") buf.WriteString(" delete-group-tag group tag_key [tag_value]\n") buf.WriteString(" put-group-membership-decision group user_or_service [expiration] decision\n") diff --git a/libs/go/zmscli/group.go b/libs/go/zmscli/group.go index 3628b21a86e..42f64589ea0 100644 --- a/libs/go/zmscli/group.go +++ b/libs/go/zmscli/group.go @@ -381,6 +381,7 @@ func getGroupMetaObject(group *zms.Group) zms.GroupMeta { MaxMembers: group.MaxMembers, SelfRenew: group.SelfRenew, SelfRenewMins: group.SelfRenewMins, + PrincipalDomainFilter: group.PrincipalDomainFilter, } } @@ -677,3 +678,24 @@ func (cli Zms) SetGroupResourceOwnership(dn, gn, resourceOwner string) (*string, return cli.dumpByFormat(message, cli.buildYAMLOutput) } + +func (cli Zms) SetGroupPrincipalDomainFilter(dn, gn, domainFilter string) (*string, error) { + group, err := cli.Zms.GetGroup(zms.DomainName(dn), zms.EntityName(gn), nil, nil) + if err != nil { + return nil, err + } + meta := getGroupMetaObject(group) + meta.PrincipalDomainFilter = domainFilter + + err = cli.Zms.PutGroupMeta(zms.DomainName(dn), zms.EntityName(gn), cli.AuditRef, cli.ResourceOwner, &meta) + if err != nil { + return nil, err + } + s := "[domain " + dn + " group " + gn + " principal-domain-filter attribute successfully updated]\n" + message := SuccessMessage{ + Status: 200, + Message: s, + } + + return cli.dumpByFormat(message, cli.buildYAMLOutput) +} diff --git a/libs/go/zmscli/role.go b/libs/go/zmscli/role.go index bf796a50bac..5cf22c36a22 100644 --- a/libs/go/zmscli/role.go +++ b/libs/go/zmscli/role.go @@ -478,6 +478,7 @@ func getRoleMetaObject(role *zms.Role) zms.RoleMeta { MaxMembers: role.MaxMembers, SelfRenew: role.SelfRenew, SelfRenewMins: role.SelfRenewMins, + PrincipalDomainFilter: role.PrincipalDomainFilter, } } @@ -1015,3 +1016,24 @@ func (cli Zms) SetRoleResourceOwnership(dn, rn, resourceOwner string) (*string, return cli.dumpByFormat(message, cli.buildYAMLOutput) } + +func (cli Zms) SetRolePrincipalDomainFilter(dn string, rn string, domainFilter string) (*string, error) { + role, err := cli.Zms.GetRole(zms.DomainName(dn), zms.EntityName(rn), nil, nil, nil) + if err != nil { + return nil, err + } + meta := getRoleMetaObject(role) + meta.PrincipalDomainFilter = domainFilter + + err = cli.Zms.PutRoleMeta(zms.DomainName(dn), zms.EntityName(rn), cli.AuditRef, cli.ResourceOwner, &meta) + if err != nil { + return nil, err + } + s := "[domain " + dn + " role " + rn + " principal-domain-filter attribute successfully updated]\n" + message := SuccessMessage{ + Status: 200, + Message: s, + } + + return cli.dumpByFormat(message, cli.buildYAMLOutput) +} diff --git a/servers/zms/pom.xml b/servers/zms/pom.xml index e171fc1a5d3..a94f98dd1cb 100644 --- a/servers/zms/pom.xml +++ b/servers/zms/pom.xml @@ -46,7 +46,7 @@ - 0.9813 + 0.9814 diff --git a/servers/zms/schema/updates/update-20240525.sql b/servers/zms/schema/updates/update-20240525.sql new file mode 100644 index 00000000000..8fe9be5f82a --- /dev/null +++ b/servers/zms/schema/updates/update-20240525.sql @@ -0,0 +1,2 @@ +ALTER TABLE `zms_server`.`role` ADD `principal_domain_filter` VARCHAR(1024) NOT NULL DEFAULT ''; +ALTER TABLE `zms_server`.`principal_group` ADD `principal_domain_filter` VARCHAR(1024) NOT NULL DEFAULT ''; diff --git a/servers/zms/schema/zms_server.mwb b/servers/zms/schema/zms_server.mwb index 1911dcc05ef438dbf5df23a6af6cd8fd33470eaa..14ca71ead9e1504baa358a1fd190829f288107ac 100644 GIT binary patch literal 54528 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 z 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 z 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 adminUsers = new ArrayList<>(); adminUsers.add(userDomainAdmin); @@ -2340,8 +2342,28 @@ void validateDomainContacts(Map contacts, final String caller) { } } + void validateDomainFilterValue(final String domainFilter, final String caller) { + + if (!StringUtil.isEmpty(domainFilter)) { + + String[] domainNames = domainFilter.split(","); + for (String domainName : domainNames) { + + // supported format is domainName,+domainName,-domainName + + if (domainName.startsWith("+") || domainName.startsWith("-")) { + domainName = domainName.substring(1); + } + validate(domainName, TYPE_DOMAIN_NAME, caller); + if (dbService.getDomain(domainName, false) == null) { + throw ZMSUtils.requestError("No such domain: " + domainName, caller); + } + } + } + } + void validateString(final String value, final String type, final String caller) { - if (value != null && !value.isEmpty()) { + if (!StringUtil.isEmpty(value)) { validate(value, type, caller); } } @@ -2640,6 +2662,8 @@ void validateRoleMetaValues(RoleMeta meta) { validateString(meta.getNotifyRoles(), TYPE_RESOURCE_NAMES, caller); validateString(meta.getUserAuthorityFilter(), TYPE_AUTHORITY_KEYWORDS, caller); validateString(meta.getUserAuthorityExpiration(), TYPE_AUTHORITY_KEYWORD, caller); + + validateDomainFilterValue(meta.getPrincipalDomainFilter(), caller); } void validateRoleValues(Role role) { @@ -2660,6 +2684,8 @@ void validateRoleValues(Role role) { validateString(role.getNotifyRoles(), TYPE_RESOURCE_NAMES, caller); validateString(role.getUserAuthorityFilter(), TYPE_AUTHORITY_KEYWORDS, caller); validateString(role.getUserAuthorityExpiration(), TYPE_AUTHORITY_KEYWORD, caller); + + validateDomainFilterValue(role.getPrincipalDomainFilter(), caller); } void validateGroupValues(Group group) { @@ -2674,6 +2700,8 @@ void validateGroupValues(Group group) { validateString(group.getNotifyRoles(), TYPE_RESOURCE_NAMES, caller); validateString(group.getUserAuthorityFilter(), TYPE_AUTHORITY_KEYWORDS, caller); validateString(group.getUserAuthorityExpiration(), TYPE_AUTHORITY_KEYWORD, caller); + + validateDomainFilterValue(group.getPrincipalDomainFilter(), caller); } void validateGroupMetaValues(GroupMeta meta) { @@ -2688,6 +2716,8 @@ void validateGroupMetaValues(GroupMeta meta) { validateString(meta.getNotifyRoles(), TYPE_RESOURCE_NAMES, caller); validateString(meta.getUserAuthorityFilter(), TYPE_AUTHORITY_KEYWORDS, caller); validateString(meta.getUserAuthorityExpiration(), TYPE_AUTHORITY_KEYWORD, caller); + + validateDomainFilterValue(meta.getPrincipalDomainFilter(), caller); } @Override @@ -3993,7 +4023,7 @@ List normalizedAdminUsers(List admins, final String domainUserAu for (String admin : normalizedAdmins) { validateRoleMemberPrincipal(admin, principalType(admin), domainUserAuthorityFilter, null, - null, disallowGroupsInAdminRole.get(), caller); + null, null, disallowGroupsInAdminRole.get(), caller); } return new ArrayList<>(normalizedAdmins); @@ -4153,7 +4183,8 @@ public Response putRole(ResourceContext ctx, String domainName, String roleName, // be specified as members. boolean disallowGroups = disallowGroupsInAdminRole.get() == Boolean.TRUE && ADMIN_ROLE_NAME.equals(roleName); - validateRoleMemberPrincipals(role, domain.getUserAuthorityFilter(), disallowGroups, caller); + PrincipalDomainFilter principalDomainFilter = new PrincipalDomainFilter(role.getPrincipalDomainFilter()); + validateRoleMemberPrincipals(role, domain.getUserAuthorityFilter(), principalDomainFilter, disallowGroups, caller); // validate role review-enabled and/or audit-enabled flags @@ -4259,8 +4290,8 @@ void validateRoleStructure(final Role role, final String domainName, final Strin } } - void validateRoleMemberPrincipals(final Role role, final String domainUserAuthorityFilter, boolean disallowGroups, - final String caller) { + void validateRoleMemberPrincipals(final Role role, final String domainUserAuthorityFilter, + PrincipalDomainFilter principalDomainFilter, boolean disallowGroups, final String caller) { // extract the user authority filter for the role @@ -4269,8 +4300,8 @@ void validateRoleMemberPrincipals(final Role role, final String domainUserAuthor for (RoleMember roleMember : role.getRoleMembers()) { validateRoleMemberPrincipal(roleMember.getMemberName(), roleMember.getPrincipalType(), - userAuthorityFilter, role.getUserAuthorityExpiration(), role.getAuditEnabled(), - disallowGroups, caller); + userAuthorityFilter, role.getUserAuthorityExpiration(), principalDomainFilter, + role.getAuditEnabled(), disallowGroups, caller); } } @@ -4394,10 +4425,25 @@ void validateGroupPrincipal(final String memberName, final String userAuthorityF } void validateRoleMemberPrincipal(final String memberName, int principalType, final String userAuthorityFilter, - final String userAuthorityExpiration, Boolean roleAuditEnabled, - boolean disallowGroups, final String caller) { + final String userAuthorityExpiration, PrincipalDomainFilter principalDomainFilter, + Boolean roleAuditEnabled, boolean disallowGroups, final String caller) { + + Principal.Type type = Principal.Type.getType(principalType); + + if (type == Principal.Type.UNKNOWN) { + throw ZMSUtils.requestError("Principal " + memberName + " is not valid", caller); + } - switch (Principal.Type.getType(principalType)) { + // if we have a principal domain filter then we need to make sure + // that the principal is within the allowed domain list + + if (principalDomainFilter != null && !principalDomainFilter.validate(memberName, type)) { + throw ZMSUtils.requestError("Principal " + memberName + " is not allowed for the role", caller); + } + + // now let's carry out further validation based on the principal type + + switch (type) { case USER: case USER_HEADLESS: @@ -4430,23 +4476,30 @@ void validateRoleMemberPrincipal(final String memberName, int principalType, fin validateGroupPrincipal(memberName, userAuthorityFilter, userAuthorityExpiration, roleAuditEnabled, caller); break; - - default: - - throw ZMSUtils.requestError("Principal " + memberName + " is not valid", caller); } } void validateGroupMemberPrincipal(final String memberName, int principalType, final String userAuthorityFilter, - final String caller) { + PrincipalDomainFilter principalDomainFilter, final String caller) { - // we do not support any type of wildcards in group members + Principal.Type type = Principal.Type.getType(principalType); - if (memberName.indexOf('*') != -1) { + // we do not support group members and any type of wildcards in group members + + if (type == Principal.Type.UNKNOWN || type == Principal.Type.GROUP || memberName.indexOf('*') != -1) { throw ZMSUtils.requestError("Principal " + memberName + " is not valid", caller); } - switch (Principal.Type.getType(principalType)) { + // if we have a principal domain filter then we need to make sure + // that the principal is within the allowed domain list + + if (principalDomainFilter != null && !principalDomainFilter.validate(memberName, type)) { + throw ZMSUtils.requestError("Principal " + memberName + " is not allowed for the group", caller); + } + + // now let's carry out further validation based on the principal type + + switch (type) { case USER: case USER_HEADLESS: @@ -4460,10 +4513,6 @@ void validateGroupMemberPrincipal(final String memberName, int principalType, fi validateServicePrincipal(memberName, caller); break; - - default: - - throw ZMSUtils.requestError("Principal " + memberName + " is not valid", caller); } } @@ -4762,8 +4811,10 @@ public Response putMembership(ResourceContext ctx, String domainName, String rol final String userAuthorityFilter = enforcedUserAuthorityFilter(role.getUserAuthorityFilter(), domain.getDomain().getUserAuthorityFilter()); boolean disallowGroups = disallowGroupsInAdminRole.get() == Boolean.TRUE && ADMIN_ROLE_NAME.equals(roleName); + PrincipalDomainFilter principalDomainFilter = new PrincipalDomainFilter(role.getPrincipalDomainFilter()); validateRoleMemberPrincipal(roleMember.getMemberName(), roleMember.getPrincipalType(), userAuthorityFilter, - role.getUserAuthorityExpiration(), role.getAuditEnabled(), disallowGroups, caller); + role.getUserAuthorityExpiration(), principalDomainFilter, role.getAuditEnabled(), + disallowGroups, caller); // authorization check which also automatically updates // the active and approved flags for the request @@ -9846,9 +9897,10 @@ public void putMembershipDecision(ResourceContext ctx, String domainName, String final String userAuthorityFilter = enforcedUserAuthorityFilter(role.getUserAuthorityFilter(), domain.getDomain().getUserAuthorityFilter()); boolean disallowGroups = disallowGroupsInAdminRole.get() == Boolean.TRUE && ADMIN_ROLE_NAME.equals(roleName); + PrincipalDomainFilter principalDomainFilter = new PrincipalDomainFilter(role.getPrincipalDomainFilter()); validateRoleMemberPrincipal(roleMember.getMemberName(), roleMember.getPrincipalType(), - userAuthorityFilter, role.getUserAuthorityExpiration(), role.getAuditEnabled(), - disallowGroups, caller); + userAuthorityFilter, role.getUserAuthorityExpiration(), principalDomainFilter, + role.getAuditEnabled(), disallowGroups, caller); } dbService.executePutMembershipDecision(ctx, domainName, roleName, roleMember, auditRef, caller); @@ -10262,7 +10314,8 @@ public Response putRoleReview(ResourceContext ctx, String domainName, String rol // validate all members specified in the review request - validateRoleMemberPrincipals(role, domain.getDomain().getUserAuthorityFilter(), false, caller); + PrincipalDomainFilter principalDomainFilter = new PrincipalDomainFilter(dbRole.getPrincipalDomainFilter()); + validateRoleMemberPrincipals(role, domain.getDomain().getUserAuthorityFilter(), principalDomainFilter, false, caller); // update role expiry based on our configurations @@ -10464,7 +10517,8 @@ void normalizeGroupMembers(Group group) { group.setGroupMembers(new ArrayList<>(normalizedMembers.values())); } - void validateGroupMemberPrincipals(final Group group, final String domainUserAuthorityFilter, final String caller) { + void validateGroupMemberPrincipals(final Group group, final String domainUserAuthorityFilter, + PrincipalDomainFilter principalDomainFilter, final String caller) { // make sure we have either one of the options enabled for verification @@ -10473,7 +10527,7 @@ void validateGroupMemberPrincipals(final Group group, final String domainUserAut for (GroupMember groupMember : group.getGroupMembers()) { validateGroupMemberPrincipal(groupMember.getMemberName(), groupMember.getPrincipalType(), - userAuthorityFilter, caller); + userAuthorityFilter, principalDomainFilter, caller); } } @@ -10576,7 +10630,8 @@ public Response putGroup(ResourceContext ctx, String domainName, String groupNam // check to see if we need to validate user and service members // and possibly user authority filter restrictions - validateGroupMemberPrincipals(group, domain.getUserAuthorityFilter(), caller); + PrincipalDomainFilter principalDomainFilter = new PrincipalDomainFilter(group.getPrincipalDomainFilter()); + validateGroupMemberPrincipals(group, domain.getUserAuthorityFilter(), principalDomainFilter, caller); // validate group review-enabled and/or audit-enabled flags @@ -10813,7 +10868,7 @@ void setGroupMemberExpiration(final AthenzDomain domain, final Group group, fina } boolean isAllowedPutGroupMembership(Principal principal, final AthenzDomain domain, final Group group, - final GroupMember groupMember) { + final GroupMember groupMember) { // first lets check if the principal has update access on the group @@ -10983,7 +11038,9 @@ public Response putGroupMembership(ResourceContext ctx, String domainName, Strin final String userAuthorityFilter = enforcedUserAuthorityFilter(group.getUserAuthorityFilter(), domain.getDomain().getUserAuthorityFilter()); - validateGroupMemberPrincipal(groupMember.getMemberName(), groupMember.getPrincipalType(), userAuthorityFilter, caller); + PrincipalDomainFilter principalDomainFilter = new PrincipalDomainFilter(group.getPrincipalDomainFilter()); + validateGroupMemberPrincipal(groupMember.getMemberName(), groupMember.getPrincipalType(), userAuthorityFilter, + principalDomainFilter, caller); // authorization check which also automatically updates // the active and approved flags for the request @@ -11333,8 +11390,8 @@ public void putGroupMembershipDecision(ResourceContext ctx, String domainName, S } // initially create the group member and only set the - // user name which is all we need in case we need to - // lookup the pending entry for review approval + // username which is all we need in case we need to + // look up the pending entry for review approval // we'll set the state and expiration after the // authorization check is successful @@ -11363,8 +11420,9 @@ public void putGroupMembershipDecision(ResourceContext ctx, String domainName, S final String userAuthorityFilter = enforcedUserAuthorityFilter(group.getUserAuthorityFilter(), domain.getDomain().getUserAuthorityFilter()); + PrincipalDomainFilter principalDomainFilter = new PrincipalDomainFilter(group.getPrincipalDomainFilter()); validateGroupMemberPrincipal(groupMember.getMemberName(), groupMember.getPrincipalType(), - userAuthorityFilter, caller); + userAuthorityFilter, principalDomainFilter, caller); } dbService.executePutGroupMembershipDecision(ctx, domainName, group, groupMember, auditRef); @@ -11437,7 +11495,8 @@ public Response putGroupReview(ResourceContext ctx, String domainName, String gr // validate all members specified in the review request - validateGroupMemberPrincipals(group, domain.getDomain().getUserAuthorityFilter(), caller); + PrincipalDomainFilter principalDomainFilter = new PrincipalDomainFilter(dbGroup.getPrincipalDomainFilter()); + validateGroupMemberPrincipals(group, domain.getDomain().getUserAuthorityFilter(), principalDomainFilter, caller); // update group expiry based on our configurations diff --git a/servers/zms/src/main/java/com/yahoo/athenz/zms/store/impl/jdbc/JDBCConnection.java b/servers/zms/src/main/java/com/yahoo/athenz/zms/store/impl/jdbc/JDBCConnection.java index 151a4f948da..836183bfe55 100644 --- a/servers/zms/src/main/java/com/yahoo/athenz/zms/store/impl/jdbc/JDBCConnection.java +++ b/servers/zms/src/main/java/com/yahoo/athenz/zms/store/impl/jdbc/JDBCConnection.java @@ -116,12 +116,14 @@ public class JDBCConnection implements ObjectStoreConnection { + " member_expiry_days, token_expiry_mins, cert_expiry_mins, sign_algorithm, service_expiry_days," + " member_review_days, service_review_days, group_review_days, review_enabled, notify_roles, user_authority_filter," + " user_authority_expiration, description, group_expiry_days, delete_protection, last_reviewed_time," - + " max_members, self_renew, self_renew_mins) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?);"; + + " max_members, self_renew, self_renew_mins, principal_domain_filter)" + + " VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?);"; private static final String SQL_UPDATE_ROLE = "UPDATE role SET trust=?, audit_enabled=?, self_serve=?, " + "member_expiry_days=?, token_expiry_mins=?, cert_expiry_mins=?, sign_algorithm=?, " + "service_expiry_days=?, member_review_days=?, service_review_days=?, group_review_days=?, review_enabled=?, notify_roles=?, " + "user_authority_filter=?, user_authority_expiration=?, description=?, group_expiry_days=?, " - + "delete_protection=?, last_reviewed_time=?, max_members=?, self_renew=?, self_renew_mins=? WHERE role_id=?;"; + + "delete_protection=?, last_reviewed_time=?, max_members=?, self_renew=?, self_renew_mins=?, " + + "principal_domain_filter=? WHERE role_id=?;"; private static final String SQL_DELETE_ROLE = "DELETE FROM role WHERE domain_id=? AND name=?;"; private static final String SQL_UPDATE_ROLE_MOD_TIMESTAMP = "UPDATE role " + "SET modified=CURRENT_TIMESTAMP(3) WHERE role_id=?;"; @@ -411,12 +413,12 @@ public class JDBCConnection implements ObjectStoreConnection { + "WHERE domain.name=? AND principal_group.name=?;"; private static final String SQL_INSERT_GROUP = "INSERT INTO principal_group (name, domain_id, audit_enabled, self_serve, " + "review_enabled, notify_roles, user_authority_filter, user_authority_expiration, member_expiry_days, " - + "service_expiry_days, delete_protection, last_reviewed_time, max_members, self_renew, self_renew_mins) " - + "VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?);"; + + "service_expiry_days, delete_protection, last_reviewed_time, max_members, self_renew, self_renew_mins, " + + "principal_domain_filter) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?);"; private static final String SQL_UPDATE_GROUP = "UPDATE principal_group SET audit_enabled=?, self_serve=?, " + "review_enabled=?, notify_roles=?, user_authority_filter=?, user_authority_expiration=?, " + "member_expiry_days=?, service_expiry_days=?, delete_protection=?, last_reviewed_time=?, " - + "max_members=?, self_renew=?, self_renew_mins=? WHERE group_id=?;"; + + "max_members=?, self_renew=?, self_renew_mins=?, principal_domain_filter=? WHERE group_id=?;"; private static final String SQL_GET_GROUP_ID = "SELECT group_id FROM principal_group WHERE domain_id=? AND name=?;"; private static final String SQL_DELETE_GROUP = "DELETE FROM principal_group WHERE domain_id=? AND name=?;"; private static final String SQL_UPDATE_GROUP_MOD_TIMESTAMP = "UPDATE principal_group " @@ -2048,6 +2050,7 @@ public boolean insertRole(String domainName, Role role) { ps.setInt(22, processInsertValue(role.getMaxMembers())); ps.setBoolean(23, processInsertValue(role.getSelfRenew(), false)); ps.setInt(24, processInsertValue(role.getSelfRenewMins())); + ps.setString(25, processInsertValue(role.getPrincipalDomainFilter())); affectedRows = executeUpdate(ps, caller); } catch (SQLException ex) { throw sqlError(ex, caller); @@ -2102,7 +2105,8 @@ public boolean updateRole(String domainName, Role role) { ps.setInt(20, processInsertValue(role.getMaxMembers())); ps.setBoolean(21, processInsertValue(role.getSelfRenew(), false)); ps.setInt(22, processInsertValue(role.getSelfRenewMins())); - ps.setInt(23, roleId); + ps.setString(23, processInsertValue(role.getPrincipalDomainFilter())); + ps.setInt(24, roleId); affectedRows = executeUpdate(ps, caller); } catch (SQLException ex) { throw sqlError(ex, caller); @@ -3976,7 +3980,8 @@ Role retrieveRole(ResultSet rs, final String domainName, final String roleName) .setMaxMembers(nullIfDefaultValue(rs.getInt(ZMSConsts.DB_COLUMN_MAX_MEMBERS), 0)) .setSelfRenew(nullIfDefaultValue(rs.getBoolean(ZMSConsts.DB_COLUMN_SELF_RENEW), false)) .setSelfRenewMins(nullIfDefaultValue(rs.getInt(ZMSConsts.DB_COLUMN_SELF_RENEW_MINS), 0)) - .setResourceOwnership(ResourceOwnership.getResourceRoleOwnership(rs.getString(ZMSConsts.DB_COLUMN_RESOURCE_OWNER))); + .setResourceOwnership(ResourceOwnership.getResourceRoleOwnership(rs.getString(ZMSConsts.DB_COLUMN_RESOURCE_OWNER))) + .setPrincipalDomainFilter(saveValue(rs.getString(ZMSConsts.DB_COLUMN_PRINCIPAL_DOMAIN_FILTER))); java.sql.Timestamp lastReviewedTime = rs.getTimestamp(ZMSConsts.DB_COLUMN_LAST_REVIEWED_TIME); if (lastReviewedTime != null) { role.setLastReviewedDate(Timestamp.fromMillis(lastReviewedTime.getTime())); @@ -6026,7 +6031,8 @@ Group retrieveGroup(ResultSet rs, final String domainName, final String groupNam .setMaxMembers(nullIfDefaultValue(rs.getInt(ZMSConsts.DB_COLUMN_MAX_MEMBERS), 0)) .setSelfRenew(nullIfDefaultValue(rs.getBoolean(ZMSConsts.DB_COLUMN_SELF_RENEW), false)) .setSelfRenewMins(nullIfDefaultValue(rs.getInt(ZMSConsts.DB_COLUMN_SELF_RENEW_MINS), 0)) - .setResourceOwnership(ResourceOwnership.getResourceGroupOwnership(rs.getString(ZMSConsts.DB_COLUMN_RESOURCE_OWNER))); + .setResourceOwnership(ResourceOwnership.getResourceGroupOwnership(rs.getString(ZMSConsts.DB_COLUMN_RESOURCE_OWNER))) + .setPrincipalDomainFilter(saveValue(rs.getString(ZMSConsts.DB_COLUMN_PRINCIPAL_DOMAIN_FILTER))); java.sql.Timestamp lastReviewedTime = rs.getTimestamp(ZMSConsts.DB_COLUMN_LAST_REVIEWED_TIME); if (lastReviewedTime != null) { group.setLastReviewedDate(Timestamp.fromMillis(lastReviewedTime.getTime())); @@ -6087,6 +6093,7 @@ public boolean insertGroup(String domainName, Group group) { ps.setInt(13, processInsertValue(group.getMaxMembers())); ps.setBoolean(14, processInsertValue(group.getSelfRenew(), false)); ps.setInt(15, processInsertValue(group.getSelfRenewMins())); + ps.setString(16, processInsertValue(group.getPrincipalDomainFilter())); affectedRows = executeUpdate(ps, caller); } catch (SQLException ex) { throw sqlError(ex, caller); @@ -6131,7 +6138,8 @@ public boolean updateGroup(String domainName, Group group) { ps.setInt(11, processInsertValue(group.getMaxMembers())); ps.setBoolean(12, processInsertValue(group.getSelfRenew(), false)); ps.setInt(13, processInsertValue(group.getSelfRenewMins())); - ps.setInt(14, groupId); + ps.setString(14, processInsertValue(group.getPrincipalDomainFilter())); + ps.setInt(15, groupId); affectedRows = executeUpdate(ps, caller); } catch (SQLException ex) { throw sqlError(ex, caller); diff --git a/servers/zms/src/main/java/com/yahoo/athenz/zms/utils/PrincipalDomainFilter.java b/servers/zms/src/main/java/com/yahoo/athenz/zms/utils/PrincipalDomainFilter.java new file mode 100644 index 00000000000..0416bc2e226 --- /dev/null +++ b/servers/zms/src/main/java/com/yahoo/athenz/zms/utils/PrincipalDomainFilter.java @@ -0,0 +1,139 @@ +/* + * Copyright The Athenz Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.yahoo.athenz.zms.utils; + +import com.yahoo.athenz.auth.AuthorityConsts; +import com.yahoo.athenz.auth.Principal; +import org.eclipse.jetty.util.StringUtil; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +public class PrincipalDomainFilter { + + Set allowedDomains; + List disallowedSubDomains; + List allowedSubDomains; + + public PrincipalDomainFilter(final String domainFilter) { + + // supported format is domainName,+domainName,-domainName + + if (StringUtil.isEmpty(domainFilter)) { + return; + } + + // for matching with domains and not substrings we're going + // to automatically add a '.' at the end of each domain name + + String[] domainNames = domainFilter.split(","); + for (String domainName : domainNames) { + if (domainName.startsWith("+")) { + if (allowedSubDomains == null) { + allowedSubDomains = new ArrayList<>(); + } + allowedSubDomains.add(domainName.substring(1) + "."); + } else if (domainName.startsWith("-")) { + if (disallowedSubDomains == null) { + disallowedSubDomains = new ArrayList<>(); + } + disallowedSubDomains.add(domainName.substring(1) + "."); + } else { + if (allowedDomains == null) { + allowedDomains = new HashSet<>(); + } + allowedDomains.add(domainName + "."); + } + } + } + + public boolean validate(final String principalName, Principal.Type type) { + + // if we have no filter then we're good + + if (allowedDomains == null && allowedSubDomains == null && disallowedSubDomains == null) { + return true; + } + + // let's first extract our domain name: special handling + // for groups while all other types are standard service names + // since we're given the principal type, it's already been + // verified that the principal name is valid so no need to + // check for error cases + + int idx = getIdx(principalName, type); + final String domainName = principalName.substring(0, idx) + "."; + + // if we have disallowed domains then we need to make sure + // that the principal domain is not in the disallowed list + + if (disallowedSubDomains != null) { + + for (String disallowedDomain : disallowedSubDomains) { + if (domainName.startsWith(disallowedDomain)) { + return false; + } + } + + // at this time we don't have any allowed list specified + // it means all other entries are allowed + + if (allowedDomains == null && allowedSubDomains == null) { + return true; + } + } + + // if we have allowed domains then we need to make sure + // that the principal domain is in the allowed list + + if (allowedDomains != null) { + if (allowedDomains.contains(domainName)) { + return true; + } + + // at this time we don't have any subdomains specified + // it means all other entries are disallowed + + if (allowedSubDomains == null) { + return false; + } + } + + // if we got here it means we have configured allowed subdomains, + // so we need to make sure the principal domain is in the allowed list + + for (String allowedDomain : allowedSubDomains) { + if (domainName.startsWith(allowedDomain)) { + return true; + } + } + + return false; + } + + private int getIdx(String principalName, Principal.Type type) { + int idx; + if (type == Principal.Type.GROUP) { + idx = principalName.indexOf(AuthorityConsts.GROUP_SEP); + } else { + idx = principalName.lastIndexOf('.'); + } + return idx; + } +} diff --git a/servers/zms/src/test/java/com/yahoo/athenz/zms/DBServiceTest.java b/servers/zms/src/test/java/com/yahoo/athenz/zms/DBServiceTest.java index 02bdd9b2292..04d7170dea8 100644 --- a/servers/zms/src/test/java/com/yahoo/athenz/zms/DBServiceTest.java +++ b/servers/zms/src/test/java/com/yahoo/athenz/zms/DBServiceTest.java @@ -5934,7 +5934,7 @@ public void testUpdateRoleMetaFields() { public void testAuditLogRoleMeta() { StringBuilder auditDetails = new StringBuilder(); Role role = new Role().setName("dom1:role.role1").setSelfServe(true).setReviewEnabled(false) - .setSelfRenew(true).setSelfRenewMins(10); + .setSelfRenew(true).setSelfRenewMins(10).setPrincipalDomainFilter("domain1"); zms.dbService.auditLogRoleMeta(auditDetails, role, "role1", true); assertEquals(auditDetails.toString(), "{\"name\": \"role1\", \"selfServe\": \"true\", \"memberExpiryDays\": \"null\"," @@ -5944,7 +5944,8 @@ public void testAuditLogRoleMeta() { + " \"reviewEnabled\": \"false\", \"notifyRoles\": \"null\", \"signAlgorithm\": \"null\"," + " \"userAuthorityFilter\": \"null\", \"userAuthorityExpiration\": \"null\"," + " \"description\": \"null\", \"deleteProtection\": \"null\", \"lastReviewedDate\": \"null\"," - + " \"maxMembers\": \"null\", \"selfRenew\": \"true\", \"selfRenewMins\": \"10\"}"); + + " \"maxMembers\": \"null\", \"selfRenew\": \"true\", \"selfRenewMins\": \"10\"," + + " \"principalDomainFilter\": \"domain1\"}"); } @Test 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 29e73dec606..3fedfa251d7 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 @@ -2253,7 +2253,7 @@ public void testCreateRole() { + "\"signAlgorithm\": \"null\", \"userAuthorityFilter\": \"null\", " + "\"userAuthorityExpiration\": \"null\", \"description\": \"null\", " + "\"deleteProtection\": \"false\", \"lastReviewedDate\": \"null\", \"maxMembers\": \"null\", " - + "\"selfRenew\": \"null\", \"selfRenewMins\": \"null\", \"trust\": \"null\", " + + "\"selfRenew\": \"null\", \"selfRenewMins\": \"null\", \"principalDomainFilter\": \"null\", \"trust\": \"null\", " + "\"deleted-members\": [{\"member\": \"user.jane\", \"approved\": true, \"system-disabled\": 0}], " + "\"added-members\": []}"); assertTrue(index2 > index, msg); @@ -21301,6 +21301,7 @@ public void testPutRoleMeta() { rm.setMaxMembers(25); rm.setSelfRenew(true); rm.setSelfRenewMins(99); + rm.setPrincipalDomainFilter("user,sys.auth"); zmsImpl.putRoleMeta(ctx, "rolemetadom1", "role1", auditRef, null, rm); Role resRole1 = zmsImpl.getRole(ctx, "rolemetadom1", "role1", true, false, false); @@ -21320,6 +21321,7 @@ public void testPutRoleMeta() { assertTrue(resRole1.getSelfRenew()); assertEquals(resRole1.getMaxMembers(), 25); assertEquals(resRole1.getSelfRenewMins(), 99); + assertEquals(resRole1.getPrincipalDomainFilter(), "user,sys.auth"); // if we pass a null for the expiry days (e.g. old client) // then we're not going to modify the value @@ -21342,6 +21344,7 @@ public void testPutRoleMeta() { assertTrue(resRole1.getSelfRenew()); assertEquals(resRole1.getMaxMembers(), 25); assertEquals(resRole1.getSelfRenewMins(), 99); + assertEquals(resRole1.getPrincipalDomainFilter(), "user,sys.auth"); // now let's reset to 0 @@ -21357,6 +21360,7 @@ public void testPutRoleMeta() { rm3.setSelfRenewMins(0); rm3.setSelfRenew(false); rm3.setMaxMembers(0); + rm3.setPrincipalDomainFilter(""); zmsImpl.putRoleMeta(ctx, "rolemetadom1", "role1", auditRef, null, rm3); resRole1 = zmsImpl.getRole(ctx, "rolemetadom1", "role1", true, false, false); @@ -21374,6 +21378,7 @@ public void testPutRoleMeta() { assertNull(resRole1.getSelfRenew()); assertNull(resRole1.getMaxMembers()); assertNull(resRole1.getSelfRenewMins()); + assertNull(resRole1.getPrincipalDomainFilter()); // invalid negative values @@ -21452,6 +21457,16 @@ public void testPutRoleMeta() { assertEquals(ex.getCode(), ResourceException.BAD_REQUEST); } + rm4.setServiceReviewDays(10); + rm4.setPrincipalDomainFilter("some invalid domain"); + + try { + zmsImpl.putRoleMeta(ctx, "rolemetadom1", "role1", auditRef, null, rm4); + fail(); + } catch (ResourceException ex) { + assertEquals(ex.getCode(), ResourceException.BAD_REQUEST); + } + zmsImpl.deleteTopLevelDomain(ctx, "rolemetadom1", auditRef, null); } @@ -22907,7 +22922,7 @@ public void testValidateRoleMemberPrincipals() { roleMembers.add(new RoleMember().setMemberName("coretech.backend").setPrincipalType(Principal.Type.SERVICE.getValue())); Role role = new Role().setRoleMembers(roleMembers); - zmsImpl.validateRoleMemberPrincipals(role, null, false, "unittest"); + zmsImpl.validateRoleMemberPrincipals(role, null, null, false, "unittest"); // enable user authority check @@ -22923,13 +22938,13 @@ public void testValidateRoleMemberPrincipals() { roleMembers.add(new RoleMember().setMemberName("user.jane").setPrincipalType(Principal.Type.USER.getValue())); role.setRoleMembers(roleMembers); - zmsImpl.validateRoleMemberPrincipals(role, null, false, "unittest"); + zmsImpl.validateRoleMemberPrincipals(role, null, null, false, "unittest"); // add one more invalid user roleMembers.add(new RoleMember().setMemberName("user.john").setPrincipalType(Principal.Type.USER.getValue())); try { - zmsImpl.validateRoleMemberPrincipals(role, null, false, "unittest"); + zmsImpl.validateRoleMemberPrincipals(role, null, null, false, "unittest"); fail(); } catch (ResourceException ex) { assertEquals(ex.getCode(), ResourceException.BAD_REQUEST); @@ -22943,7 +22958,7 @@ public void testValidateRoleMemberPrincipals() { roleMembers.add(new RoleMember().setMemberName("coretech:group.dev-team").setPrincipalType(Principal.Type.GROUP.getValue())); role.setRoleMembers(roleMembers); try { - zmsImpl.validateRoleMemberPrincipals(role, null, true, "unittest"); + zmsImpl.validateRoleMemberPrincipals(role, null, null, true, "unittest"); fail(); } catch (ResourceException ex) { assertEquals(ex.getCode(), ResourceException.BAD_REQUEST); @@ -22955,7 +22970,7 @@ public void testValidateRoleMemberPrincipals() { roleMembers.add(new RoleMember().setMemberName("unknown").setPrincipalType(Principal.Type.UNKNOWN.getValue())); role.setRoleMembers(roleMembers); try { - zmsImpl.validateRoleMemberPrincipals(role, null, false, "unittest"); + zmsImpl.validateRoleMemberPrincipals(role, null, null, false, "unittest"); fail(); } catch (ResourceException ex) { assertEquals(ex.getCode(), ResourceException.BAD_REQUEST); @@ -22975,13 +22990,16 @@ public void testValidateRoleMemberPrincipalUser() { // valid users no exception - zmsImpl.validateRoleMemberPrincipal("user.joe", Principal.Type.USER.getValue(), null, null, null, false, "unittest"); - zmsImpl.validateRoleMemberPrincipal("user.jane", Principal.Type.USER.getValue(), null, null, null, false, "unittest"); + zmsImpl.validateRoleMemberPrincipal("user.joe", Principal.Type.USER.getValue(), null, null, + null, null, false, "unittest"); + zmsImpl.validateRoleMemberPrincipal("user.jane", Principal.Type.USER.getValue(), null, null, + null, null, false, "unittest"); // invalid user request error try { - zmsImpl.validateRoleMemberPrincipal("user.john", Principal.Type.USER.getValue(), null, null, null, false, "unittest"); + zmsImpl.validateRoleMemberPrincipal("user.john", Principal.Type.USER.getValue(), null, null, + null, null, false, "unittest"); fail(); } catch (ResourceException ex) { assertEquals(ex.getCode(), ResourceException.BAD_REQUEST); @@ -22990,31 +23008,31 @@ public void testValidateRoleMemberPrincipalUser() { // non - user principals by default are accepted zmsImpl.validateRoleMemberPrincipal("coretech.api", Principal.Type.SERVICE.getValue(), - null, null, null, false, "unittest"); + null, null, null, null, false, "unittest"); // valid employee and contractor users zmsImpl.validateRoleMemberPrincipal("user.joe", Principal.Type.USER.getValue(), "employee", - null, null, false, "unittest"); + null, null, null, false, "unittest"); zmsImpl.validateRoleMemberPrincipal("user.jane", Principal.Type.USER.getValue(), "employee", - null, null, false, "unittest"); + null, null, null, false, "unittest"); zmsImpl.validateRoleMemberPrincipal("user.jack", Principal.Type.USER.getValue(), "contractor", - null, null, false, "unittest"); + null, null, null, false, "unittest"); // valid multiple attribute users zmsImpl.validateRoleMemberPrincipal("user.joe", Principal.Type.USER.getValue(), "employee,local", - null, null, false, "unittest"); + null, null, null, false, "unittest"); zmsImpl.validateRoleMemberPrincipal("user.jane", Principal.Type.USER.getValue(), "employee,local", - null, null, false, "unittest"); + null, null, null, false, "unittest"); zmsImpl.validateRoleMemberPrincipal("user.jack", Principal.Type.USER.getValue(), "contractor,local", - null, null, false, "unittest"); + null, null, null, false, "unittest"); // invalid employee type try { zmsImpl.validateRoleMemberPrincipal("user.jack", Principal.Type.USER.getValue(), "employee", - null, null, false, "unittest"); + null, null, null, false, "unittest"); fail(); } catch (ResourceException ex) { assertEquals(ex.getCode(), ResourceException.BAD_REQUEST); @@ -23024,7 +23042,7 @@ public void testValidateRoleMemberPrincipalUser() { try { zmsImpl.validateRoleMemberPrincipal("user.jack", Principal.Type.USER.getValue(), "local,employee", - null, null, false, "unittest"); + null, null, null, false, "unittest"); fail(); } catch (ResourceException ex) { assertEquals(ex.getCode(), ResourceException.BAD_REQUEST); @@ -23047,15 +23065,15 @@ public void testValidateRoleMemberPrincipalService() { // wildcards are always valid with no exception zmsImpl.validateRoleMemberPrincipal("athenz.api*", Principal.Type.SERVICE.getValue(), - null, null, null, false, "unittest"); + null, null, null, null, false, "unittest"); zmsImpl.validateRoleMemberPrincipal("coretech.*", Principal.Type.SERVICE.getValue(), - null, null, null, false, "unittest"); + null, null, null, null, false, "unittest"); // should get back invalid request since service does not exist try { zmsImpl.validateRoleMemberPrincipal("coretech.api", Principal.Type.SERVICE.getValue(), "employee", - null, null, false, "unittest"); + null, null, null, false, "unittest"); fail(); } catch (ResourceException ex) { assertEquals(ex.getCode(), ResourceException.BAD_REQUEST); @@ -23065,7 +23083,7 @@ public void testValidateRoleMemberPrincipalService() { try { zmsImpl.validateRoleMemberPrincipal("coretech", Principal.Type.SERVICE.getValue(), - null, null, null, false, "unittest"); + null, null, null, null, false, "unittest"); fail(); } catch (ResourceException ex) { assertEquals(ex.getCode(), ResourceException.BAD_REQUEST); @@ -23088,13 +23106,13 @@ public void testValidateRoleMemberPrincipalService() { // known service - no exception zmsImpl.validateRoleMemberPrincipal("coretech.api", Principal.Type.SERVICE.getValue(), - null, null, null, false, "unittest"); + null, null, null, null, false, "unittest"); // unknown service - exception try { zmsImpl.validateRoleMemberPrincipal("coretech.backend", Principal.Type.SERVICE.getValue(), - null, null, null, false, "unittest"); + null, null, null, null, false, "unittest"); fail(); } catch (ResourceException ex) { assertEquals(ex.getCode(), ResourceException.BAD_REQUEST); @@ -23111,13 +23129,13 @@ public void testValidateRoleMemberPrincipalService() { // coretech is now accepted zmsImpl.validateRoleMemberPrincipal("coretech.backend", Principal.Type.SERVICE.getValue(), - null, null, null, false, "unittest"); + null, null, null, null, false, "unittest"); // but coretech2 is rejected try { zmsImpl.validateRoleMemberPrincipal("coretech2.backend", Principal.Type.SERVICE.getValue(), - null, null, null, false, "unittest"); + null, null, null, null, false, "unittest"); fail(); } catch (ResourceException ex) { assertEquals(ex.getCode(), ResourceException.BAD_REQUEST); @@ -23126,11 +23144,12 @@ public void testValidateRoleMemberPrincipalService() { // rbac.sre does not exists, but is accepted because rbac.* is included in skipDomains zmsImpl.validateRoleMemberPrincipal("rbac.sre.backend", Principal.Type.SERVICE.getValue(), - null, null, null ,false, "unittest"); + null, null, null, null ,false, "unittest"); // user principals by default are accepted - zmsImpl.validateRoleMemberPrincipal("user.john", Principal.Type.USER.getValue(), null, null, null, false, "unittest"); + zmsImpl.validateRoleMemberPrincipal("user.john", Principal.Type.USER.getValue(), null, null, + null, null, false, "unittest"); // reset our setting @@ -23152,21 +23171,23 @@ public void testValidateGroupMemberPrincipal() { // wildcards are always rejected try { - zmsImpl.validateGroupMemberPrincipal("athenz.api*", Principal.Type.SERVICE.getValue(), null, "unittest"); + zmsImpl.validateGroupMemberPrincipal("athenz.api*", Principal.Type.SERVICE.getValue(), + null, null, "unittest"); fail(); } catch (ResourceException ex) { assertEquals(ex.getCode(), ResourceException.BAD_REQUEST); } try { - zmsImpl.validateGroupMemberPrincipal("athenz.api*", Principal.Type.SERVICE.getValue(), null, "unittest"); + zmsImpl.validateGroupMemberPrincipal("athenz.api*", Principal.Type.SERVICE.getValue(), + null, null, "unittest"); fail(); } catch (ResourceException ex) { assertEquals(ex.getCode(), ResourceException.BAD_REQUEST); } try { - zmsImpl.validateGroupMemberPrincipal("*", Principal.Type.SERVICE.getValue(), null, "unittest"); + zmsImpl.validateGroupMemberPrincipal("*", Principal.Type.SERVICE.getValue(), null, null, "unittest"); fail(); } catch (ResourceException ex) { assertEquals(ex.getCode(), ResourceException.BAD_REQUEST); @@ -23175,7 +23196,8 @@ public void testValidateGroupMemberPrincipal() { // should get back invalid request since service does not exist try { - zmsImpl.validateGroupMemberPrincipal("coretech.api", Principal.Type.SERVICE.getValue(), "employee", "unittest"); + zmsImpl.validateGroupMemberPrincipal("coretech.api", Principal.Type.SERVICE.getValue(), + "employee", null, "unittest"); fail(); } catch (ResourceException ex) { assertEquals(ex.getCode(), ResourceException.BAD_REQUEST); @@ -23184,13 +23206,15 @@ public void testValidateGroupMemberPrincipal() { // invalid service request error try { - zmsImpl.validateGroupMemberPrincipal("coretech", Principal.Type.SERVICE.getValue(), null, "unittest"); + zmsImpl.validateGroupMemberPrincipal("coretech", Principal.Type.SERVICE.getValue(), null, + null, "unittest"); fail(); } catch (ResourceException ex) { assertEquals(ex.getCode(), ResourceException.BAD_REQUEST); } - TopLevelDomain dom1 = zmsTestInitializer.createTopLevelDomainObject("coretech", "Test Domain1", "testorg", zmsTestInitializer.getAdminUser()); + TopLevelDomain dom1 = zmsTestInitializer.createTopLevelDomainObject("coretech", "Test Domain1", + "testorg", zmsTestInitializer.getAdminUser()); zmsImpl.postTopLevelDomain(ctx, auditRef, null, dom1); ServiceIdentity service1 = zmsTestInitializer.createServiceObject("coretech", @@ -23201,12 +23225,14 @@ public void testValidateGroupMemberPrincipal() { // known service - no exception - zmsImpl.validateGroupMemberPrincipal("coretech.api", Principal.Type.SERVICE.getValue(), null, "unittest"); + zmsImpl.validateGroupMemberPrincipal("coretech.api", Principal.Type.SERVICE.getValue(), + null, null, "unittest"); // unknown service - exception try { - zmsImpl.validateGroupMemberPrincipal("coretech.backend", Principal.Type.SERVICE.getValue(), null, "unittest"); + zmsImpl.validateGroupMemberPrincipal("coretech.backend", Principal.Type.SERVICE.getValue(), + null, null, "unittest"); fail(); } catch (ResourceException ex) { assertEquals(ex.getCode(), ResourceException.BAD_REQUEST); @@ -23214,13 +23240,13 @@ public void testValidateGroupMemberPrincipal() { // known user principals are accepted - zmsImpl.validateGroupMemberPrincipal("user.joe", Principal.Type.USER.getValue(), null, "unittest"); - zmsImpl.validateGroupMemberPrincipal("user.jane", Principal.Type.USER.getValue(), null, "unittest"); + zmsImpl.validateGroupMemberPrincipal("user.joe", Principal.Type.USER.getValue(), null, null, "unittest"); + zmsImpl.validateGroupMemberPrincipal("user.jane", Principal.Type.USER.getValue(), null, null, "unittest"); // unknown users are rejected try { - zmsImpl.validateGroupMemberPrincipal("user.john", Principal.Type.USER.getValue(), null, "unittest"); + zmsImpl.validateGroupMemberPrincipal("user.john", Principal.Type.USER.getValue(), null, null, "unittest"); fail(); } catch (ResourceException ex) { assertEquals(ex.getCode(), ResourceException.BAD_REQUEST); @@ -23229,7 +23255,7 @@ public void testValidateGroupMemberPrincipal() { // groups and unknown types are rejected try { - zmsImpl.validateGroupMemberPrincipal("user", Principal.Type.UNKNOWN.getValue(), null, "unittest"); + zmsImpl.validateGroupMemberPrincipal("user", Principal.Type.UNKNOWN.getValue(), null, null, "unittest"); fail(); } catch (ResourceException ex) { assertEquals(ex.getCode(), ResourceException.BAD_REQUEST); @@ -23239,7 +23265,8 @@ public void testValidateGroupMemberPrincipal() { zmsImpl.putGroup(ctx, "coretech", "dev-team", auditRef, false, null, group); try { - zmsImpl.validateGroupMemberPrincipal("coretech:group.dev-team", Principal.Type.GROUP.getValue(), null, "unittest"); + zmsImpl.validateGroupMemberPrincipal("coretech:group.dev-team", Principal.Type.GROUP.getValue(), + null, null, "unittest"); fail(); } catch (ResourceException ex) { assertEquals(ex.getCode(), ResourceException.BAD_REQUEST); @@ -25135,7 +25162,7 @@ public void testCreateGroup() { + "\"serviceExpiryDays\": \"null\", \"reviewEnabled\": \"false\", \"notifyRoles\": \"null\", " + "\"userAuthorityFilter\": \"null\", \"userAuthorityExpiration\": \"null\", " + "\"deleteProtection\": \"false\", \"lastReviewedDate\": \"null\", \"maxMembers\": \"null\", " - + "\"selfRenew\": \"null\", \"selfRenewMins\": \"null\", " + + "\"selfRenew\": \"null\", \"selfRenewMins\": \"null\", \"principalDomainFilter\": \"null\", " + "\"deleted-members\": [{\"member\": \"user.jane\", \"approved\": true, \"system-disabled\": 0}], " + "\"added-members\": []}"); assertTrue(index2 > index, msg); @@ -25960,7 +25987,7 @@ public void testValidateGroupMemberPrincipals() { Group group = new Group().setGroupMembers(groupMembers); try { - zmsImpl.validateGroupMemberPrincipals(group, null, "unittest"); + zmsImpl.validateGroupMemberPrincipals(group, null, null, "unittest"); fail(); } catch (ResourceException ex) { assertEquals(ex.getCode(), ResourceException.BAD_REQUEST); @@ -25979,13 +26006,13 @@ public void testValidateGroupMemberPrincipals() { groupMembers.add(new GroupMember().setMemberName("sys.auth.zms").setPrincipalType(Principal.Type.SERVICE.getValue())); group.setGroupMembers(groupMembers); - zmsImpl.validateGroupMemberPrincipals(group, null, "unittest"); + zmsImpl.validateGroupMemberPrincipals(group, null, null, "unittest"); // add one more invalid user groupMembers.add(new GroupMember().setMemberName("user.john").setPrincipalType(Principal.Type.USER.getValue())); try { - zmsImpl.validateGroupMemberPrincipals(group, null, "unittest"); + zmsImpl.validateGroupMemberPrincipals(group, null, null, "unittest"); fail(); } catch (ResourceException ex) { assertEquals(ex.getCode(), ResourceException.BAD_REQUEST); @@ -27942,7 +27969,8 @@ public void testPutGroupMeta() { final String domainName = "put-group-meta"; final String groupName = "group1"; - TopLevelDomain dom1 = zmsTestInitializer.createTopLevelDomainObject(domainName, "Group Meta Test Domain1", "testOrg", zmsTestInitializer.getAdminUser()); + TopLevelDomain dom1 = zmsTestInitializer.createTopLevelDomainObject(domainName, + "Group Meta Test Domain1", "testOrg", zmsTestInitializer.getAdminUser()); zmsImpl.postTopLevelDomain(ctx, auditRef, null, dom1); Group group1 = zmsTestInitializer.createGroupObject(domainName, groupName, "user.john", "user.jane"); @@ -27976,6 +28004,7 @@ public void testPutGroupMeta() { assertNull(resGroup1.getMaxMembers()); assertNull(resGroup1.getSelfRenew()); assertNull(resGroup1.getSelfRenewMins()); + assertNull(resGroup1.getPrincipalDomainFilter()); groupMeta = new GroupMeta() .setSelfServe(true) @@ -27987,7 +28016,8 @@ public void testPutGroupMeta() { .setServiceExpiryDays(45) .setSelfRenew(true) .setSelfRenewMins(99) - .setMaxMembers(23); + .setMaxMembers(23) + .setPrincipalDomainFilter("user,sys.auth"); zmsImpl.putGroupMeta(ctx, domainName, groupName, auditRef, null, groupMeta); resGroup1 = zmsImpl.getGroup(ctx, domainName, groupName, true, false); @@ -28002,6 +28032,7 @@ public void testPutGroupMeta() { assertEquals(resGroup1.getMaxMembers(), 23); assertTrue(resGroup1.getSelfRenew()); assertEquals(resGroup1.getSelfRenewMins(), 99); + assertEquals(resGroup1.getPrincipalDomainFilter(), "user,sys.auth"); groupMeta = new GroupMeta().setNotifyRoles("role2,role3"); zmsImpl.putGroupMeta(ctx, domainName, groupName, auditRef, null, groupMeta); @@ -28013,6 +28044,7 @@ public void testPutGroupMeta() { assertEquals(resGroup1.getNotifyRoles(), "role2,role3"); assertEquals(resGroup1.getUserAuthorityExpiration(), "elevated-clearance"); assertEquals(resGroup1.getUserAuthorityFilter(), "OnShore-US"); + assertEquals(resGroup1.getPrincipalDomainFilter(), "user,sys.auth"); zmsImpl.dbService.zmsConfig.setUserAuthority(savedAuthority); zmsImpl.userAuthority = savedAuthority; diff --git a/servers/zms/src/test/java/com/yahoo/athenz/zms/provider/ServiceProviderManagerTest.java b/servers/zms/src/test/java/com/yahoo/athenz/zms/provider/ServiceProviderManagerTest.java index 6c131ad8c69..db099737f50 100644 --- a/servers/zms/src/test/java/com/yahoo/athenz/zms/provider/ServiceProviderManagerTest.java +++ b/servers/zms/src/test/java/com/yahoo/athenz/zms/provider/ServiceProviderManagerTest.java @@ -31,10 +31,7 @@ import org.testng.annotations.Test; import java.lang.reflect.Field; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; -import java.util.Map; +import java.util.*; import java.util.concurrent.*; import static com.yahoo.athenz.zms.ZMSConsts.*; @@ -115,6 +112,7 @@ public void testIsServiceProvider() throws InterruptedException, ExecutionExcept } serviceProviderManager.shutdown(); + serviceProviderManager.setServiceProviders(Collections.emptyMap()); System.clearProperty(ZMS_PROP_SERVICE_PROVIDER_MANAGER_FREQUENCY_SECONDS); System.clearProperty(ZMS_PROP_SERVICE_PROVIDER_MANAGER_DOMAIN); System.clearProperty(ZMS_PROP_SERVICE_PROVIDER_MANAGER_ROLE); diff --git a/servers/zms/src/test/java/com/yahoo/athenz/zms/store/impl/jdbc/JDBCConnectionTest.java b/servers/zms/src/test/java/com/yahoo/athenz/zms/store/impl/jdbc/JDBCConnectionTest.java index b89dc650f32..c0dd73f7dc7 100644 --- a/servers/zms/src/test/java/com/yahoo/athenz/zms/store/impl/jdbc/JDBCConnectionTest.java +++ b/servers/zms/src/test/java/com/yahoo/athenz/zms/store/impl/jdbc/JDBCConnectionTest.java @@ -1027,6 +1027,7 @@ public void testGetRole() throws Exception { Mockito.doReturn("").when(mockResultSet).getString(ZMSConsts.DB_COLUMN_USER_AUTHORITY_EXPIRATION); Mockito.doReturn("").when(mockResultSet).getString(ZMSConsts.DB_COLUMN_USER_AUTHORITY_FILTER); Mockito.doReturn("").when(mockResultSet).getString(ZMSConsts.DB_COLUMN_DESCRIPTION); + Mockito.doReturn("user,-home").when(mockResultSet).getString(ZMSConsts.DB_COLUMN_PRINCIPAL_DOMAIN_FILTER); JDBCConnection jdbcConn = new JDBCConnection(mockConn, true); Role role = jdbcConn.getRole("my-domain", "role1"); @@ -1046,6 +1047,7 @@ public void testGetRole() throws Exception { assertEquals(role.getNotifyRoles(), "role1,role2"); assertTrue(role.getReviewEnabled()); assertEquals(role.getLastReviewedDate(), Timestamp.fromMillis(1454358917)); + assertEquals(role.getPrincipalDomainFilter(), "user,-home"); Mockito.verify(mockPrepStmt, times(1)).setString(1, "my-domain"); Mockito.verify(mockPrepStmt, times(1)).setString(2, "role1"); @@ -1077,6 +1079,7 @@ public void testGetRoleWithDueDates() throws Exception { Mockito.doReturn("expiry").when(mockResultSet).getString(ZMSConsts.DB_COLUMN_USER_AUTHORITY_EXPIRATION); Mockito.doReturn("filter").when(mockResultSet).getString(ZMSConsts.DB_COLUMN_USER_AUTHORITY_FILTER); Mockito.doReturn("description").when(mockResultSet).getString(ZMSConsts.DB_COLUMN_DESCRIPTION); + Mockito.doReturn("user,-home").when(mockResultSet).getString(ZMSConsts.DB_COLUMN_PRINCIPAL_DOMAIN_FILTER); JDBCConnection jdbcConn = new JDBCConnection(mockConn, true); Role role = jdbcConn.getRole("my-domain", "role1"); @@ -1093,6 +1096,7 @@ public void testGetRoleWithDueDates() throws Exception { assertEquals(role.getMemberReviewDays(), Integer.valueOf(70)); assertEquals(role.getServiceReviewDays(), Integer.valueOf(80)); assertEquals(role.getGroupReviewDays(), Integer.valueOf(90)); + assertEquals(role.getPrincipalDomainFilter(), "user,-home"); Mockito.verify(mockPrepStmt, times(1)).setString(1, "my-domain"); Mockito.verify(mockPrepStmt, times(1)).setString(2, "role1"); @@ -1117,6 +1121,7 @@ public void testGetRoleWithoutSelfServe() throws Exception { Mockito.doReturn("").when(mockResultSet).getString(ZMSConsts.DB_COLUMN_USER_AUTHORITY_EXPIRATION); Mockito.doReturn("").when(mockResultSet).getString(ZMSConsts.DB_COLUMN_USER_AUTHORITY_FILTER); Mockito.doReturn("").when(mockResultSet).getString(ZMSConsts.DB_COLUMN_DESCRIPTION); + Mockito.doReturn("").when(mockResultSet).getString(ZMSConsts.DB_COLUMN_PRINCIPAL_DOMAIN_FILTER); JDBCConnection jdbcConn = new JDBCConnection(mockConn, true); Role role = jdbcConn.getRole("my-domain", "role1"); @@ -1127,6 +1132,7 @@ public void testGetRoleWithoutSelfServe() throws Exception { assertNull(role.getUserAuthorityExpiration()); assertNull(role.getUserAuthorityFilter()); assertNull(role.getDescription()); + assertNull(role.getPrincipalDomainFilter()); Mockito.verify(mockPrepStmt, times(1)).setString(1, "my-domain"); Mockito.verify(mockPrepStmt, times(1)).setString(2, "role1"); @@ -1160,6 +1166,7 @@ public void testGetRoleTrust() throws Exception { Mockito.doReturn("").when(mockResultSet).getString(ZMSConsts.DB_COLUMN_USER_AUTHORITY_EXPIRATION); Mockito.doReturn("").when(mockResultSet).getString(ZMSConsts.DB_COLUMN_USER_AUTHORITY_FILTER); Mockito.doReturn("").when(mockResultSet).getString(ZMSConsts.DB_COLUMN_DESCRIPTION); + Mockito.doReturn("").when(mockResultSet).getString(ZMSConsts.DB_COLUMN_PRINCIPAL_DOMAIN_FILTER); JDBCConnection jdbcConn = new JDBCConnection(mockConn, true); Role role = jdbcConn.getRole("my-domain", "role1"); @@ -1168,6 +1175,7 @@ public void testGetRoleTrust() throws Exception { assertEquals("trust.domain", role.getTrust()); assertNull(role.getUserAuthorityExpiration()); assertNull(role.getUserAuthorityFilter()); + assertNull(role.getPrincipalDomainFilter()); jdbcConn.close(); } @@ -1383,7 +1391,7 @@ public void testUpdateRole() throws Exception { .setReviewEnabled(true).setNotifyRoles("role1,role2") .setUserAuthorityFilter("filter").setUserAuthorityExpiration("expiry") .setDescription("description").setLastReviewedDate(Timestamp.fromMillis(100)) - .setMaxMembers(10).setSelfRenew(true).setSelfRenewMins(99); + .setMaxMembers(10).setSelfRenew(true).setSelfRenewMins(99).setPrincipalDomainFilter("user"); Mockito.doReturn(1).when(mockPrepStmt).executeUpdate(); Mockito.when(mockResultSet.next()).thenReturn(true); @@ -1421,7 +1429,8 @@ public void testUpdateRole() throws Exception { Mockito.verify(mockPrepStmt, times(1)).setInt(20, 10); Mockito.verify(mockPrepStmt, times(1)).setBoolean(21, true); Mockito.verify(mockPrepStmt, times(1)).setInt(22, 99); - Mockito.verify(mockPrepStmt, times(1)).setInt(23, 4); + Mockito.verify(mockPrepStmt, times(1)).setString(23, "user"); + Mockito.verify(mockPrepStmt, times(1)).setInt(24, 4); jdbcConn.close(); } @@ -1470,7 +1479,8 @@ public void testUpdateRoleWithTrust() throws Exception { Mockito.verify(mockPrepStmt, times(1)).setInt(20, 0); Mockito.verify(mockPrepStmt, times(1)).setBoolean(21, false); Mockito.verify(mockPrepStmt, times(1)).setInt(22, 0); - Mockito.verify(mockPrepStmt, times(1)).setInt(23, 7); + Mockito.verify(mockPrepStmt, times(1)).setString(23, ""); + Mockito.verify(mockPrepStmt, times(1)).setInt(24, 7); jdbcConn.close(); } @@ -6598,6 +6608,7 @@ public void testGetAthenzDomain() throws Exception { Mockito.when(mockResultSet.getString(ZMSConsts.DB_COLUMN_BUSINESS_SERVICE)).thenReturn(""); Mockito.when(mockResultSet.getString(ZMSConsts.DB_COLUMN_PRODUCT_ID)).thenReturn(""); Mockito.when(mockResultSet.getString(ZMSConsts.DB_COLUMN_ENVIRONMENT)).thenReturn(""); + Mockito.when(mockResultSet.getString(ZMSConsts.DB_COLUMN_PRINCIPAL_DOMAIN_FILTER)).thenReturn(""); AthenzDomain athenzDomain = jdbcConn.getAthenzDomain("my-domain"); assertNotNull(athenzDomain); @@ -9133,6 +9144,7 @@ public void testGetRoleDefaultAuditEnabledAsNull() throws Exception { Mockito.doReturn("").when(mockResultSet).getString(ZMSConsts.DB_COLUMN_USER_AUTHORITY_EXPIRATION); Mockito.doReturn("").when(mockResultSet).getString(ZMSConsts.DB_COLUMN_USER_AUTHORITY_FILTER); Mockito.doReturn("").when(mockResultSet).getString(ZMSConsts.DB_COLUMN_DESCRIPTION); + Mockito.doReturn("").when(mockResultSet).getString(ZMSConsts.DB_COLUMN_PRINCIPAL_DOMAIN_FILTER); JDBCConnection jdbcConn = new JDBCConnection(mockConn, true); Role role = jdbcConn.getRole("my-domain", "role1"); @@ -9146,6 +9158,7 @@ public void testGetRoleDefaultAuditEnabledAsNull() throws Exception { assertNull(role.getUserAuthorityExpiration()); assertNull(role.getUserAuthorityFilter()); assertNull(role.getDescription()); + assertNull(role.getPrincipalDomainFilter()); Mockito.verify(mockPrepStmt, times(1)).setString(1, "my-domain"); Mockito.verify(mockPrepStmt, times(1)).setString(2, "role1"); diff --git a/servers/zms/src/test/java/com/yahoo/athenz/zms/utils/PrincipalDomainFilterTest.java b/servers/zms/src/test/java/com/yahoo/athenz/zms/utils/PrincipalDomainFilterTest.java new file mode 100644 index 00000000000..1ce2542ab20 --- /dev/null +++ b/servers/zms/src/test/java/com/yahoo/athenz/zms/utils/PrincipalDomainFilterTest.java @@ -0,0 +1,489 @@ +/* + * Copyright The Athenz Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.yahoo.athenz.zms.utils; + +import com.yahoo.athenz.auth.Principal; +import com.yahoo.athenz.zms.*; +import org.mockito.MockitoAnnotations; +import org.testng.annotations.*; + +import java.lang.reflect.Member; +import java.util.ArrayList; +import java.util.List; + +import static org.testng.Assert.*; + +public class PrincipalDomainFilterTest { + + private final ZMSTestInitializer zmsTestInitializer = new ZMSTestInitializer(); + + @BeforeClass + public void startMemoryMySQL() { + zmsTestInitializer.startMemoryMySQL(); + } + + @AfterClass + public void stopMemoryMySQL() { + zmsTestInitializer.stopMemoryMySQL(); + } + + @BeforeMethod + public void setUp() throws Exception { + MockitoAnnotations.openMocks(this); + zmsTestInitializer.setUp(); + } + + @Test + public void testPrincipalDomainFilter() { + + // empty filter has no processing and always returns true for validation + + PrincipalDomainFilter filter = new PrincipalDomainFilter(""); + assertNull(filter.allowedDomains); + assertNull(filter.disallowedSubDomains); + assertNull(filter.allowedSubDomains); + assertTrue(filter.validate(null, Principal.Type.USER)); + assertTrue(filter.validate(null, Principal.Type.GROUP)); + + filter = new PrincipalDomainFilter(null); + assertNull(filter.allowedDomains); + assertNull(filter.disallowedSubDomains); + assertNull(filter.allowedSubDomains); + assertTrue(filter.validate(null, Principal.Type.USER)); + assertTrue(filter.validate(null, Principal.Type.GROUP)); + + // now let's test some valid filters + + filter = new PrincipalDomainFilter("domain1,domain2"); + assertNotNull(filter.allowedDomains); + assertEquals(filter.allowedDomains.size(), 2); + assertTrue(filter.allowedDomains.contains("domain1.")); + assertTrue(filter.allowedDomains.contains("domain2.")); + assertNull(filter.disallowedSubDomains); + assertNull(filter.allowedSubDomains); + + filter = new PrincipalDomainFilter("domain1,domain2.api,+domain3,-domain4"); + assertNotNull(filter.allowedDomains); + assertEquals(filter.allowedDomains.size(), 2); + assertTrue(filter.allowedDomains.contains("domain1.")); + assertTrue(filter.allowedDomains.contains("domain2.api.")); + assertNotNull(filter.allowedSubDomains); + assertEquals(filter.allowedSubDomains.size(), 1); + assertTrue(filter.allowedSubDomains.contains("domain3.")); + assertNotNull(filter.disallowedSubDomains); + assertEquals(filter.disallowedSubDomains.size(), 1); + assertTrue(filter.disallowedSubDomains.contains("domain4.")); + + filter = new PrincipalDomainFilter("domain2.api,+domain1,+domain2,-domain1.api,-domain2.prod"); + assertNotNull(filter.allowedDomains); + assertEquals(filter.allowedDomains.size(), 1); + assertTrue(filter.allowedDomains.contains("domain2.api.")); + assertNotNull(filter.allowedSubDomains); + assertEquals(filter.allowedSubDomains.size(), 2); + assertTrue(filter.allowedSubDomains.contains("domain1.")); + assertTrue(filter.allowedSubDomains.contains("domain2.")); + assertNotNull(filter.disallowedSubDomains); + assertEquals(filter.disallowedSubDomains.size(), 2); + assertTrue(filter.disallowedSubDomains.contains("domain1.api.")); + assertTrue(filter.disallowedSubDomains.contains("domain2.prod.")); + } + + @DataProvider(name = "DomainFilterData") + public static Object[][] domainFilterData() { + return new Object[][] { + { "user", "user.joe", Principal.Type.USER, true }, + { "user", "sports.api", Principal.Type.SERVICE, false }, + { "user", "athenz:group.dev-team", Principal.Type.GROUP, false }, + { "-home", "user.joe", Principal.Type.USER, true }, + { "-home", "sports.api", Principal.Type.SERVICE, true }, + { "-home", "athenz:group.dev-team", Principal.Type.GROUP, true }, + { "-home", "home.api", Principal.Type.SERVICE, false }, + { "-home", "home.prod.api", Principal.Type.SERVICE, false }, + { "+sports.prod", "user.joe", Principal.Type.USER, false }, + { "+sports.prod", "sports.api", Principal.Type.SERVICE, false }, + { "+sports.prod", "athenz:group.dev-team", Principal.Type.GROUP, false }, + { "+sports.prod", "sports.prod.api", Principal.Type.SERVICE, true }, + { "+sports.prod", "sports.prod.west2.api", Principal.Type.SERVICE, true }, + { "+sports.prod", "weather.api", Principal.Type.SERVICE, false }, + { "user,+sports,-sports.prod", "user.joe", Principal.Type.USER, true }, + { "user,+sports,-sports.prod", "sports.api", Principal.Type.SERVICE, true }, + { "user,+sports,-sports.prod", "sports.dev.api", Principal.Type.SERVICE, true }, + { "user,+sports,-sports.prod", "sports.prod.api", Principal.Type.SERVICE, false }, + { "user,+sports,-sports.prod", "sports.prod.west2.api", Principal.Type.SERVICE, false }, + { "user,+sports,-sports.prod", "weather.api", Principal.Type.SERVICE, false }, + { "user,+sports,-sports.prod", "athenz:group.dev-team", Principal.Type.GROUP, false }, + { "+sports,-sports.prod", "user.joe", Principal.Type.USER, false }, + { "+sports,-sports.prod", "sports.api", Principal.Type.SERVICE, true }, + { "+sports,-sports.prod", "sports.dev.api", Principal.Type.SERVICE, true }, + { "+sports,-sports.prod", "sports.prod.api", Principal.Type.SERVICE, false }, + { "+sports,-sports.prod", "sports.prod.west2.api", Principal.Type.SERVICE, false }, + { "+sports,-sports.prod", "weather.api", Principal.Type.SERVICE, false }, + { "+sports,-sports.prod", "athenz:group.dev-team", Principal.Type.GROUP, false }, + }; + } + @Test(dataProvider = "DomainFilterData") + public void testDomainFilterValidation(String filter, String principalName, Principal.Type type, boolean expectedResult) { + PrincipalDomainFilter domainFilter = new PrincipalDomainFilter(filter); + assertEquals(domainFilter.validate(principalName, type), expectedResult); + } + + @Test + public void testPutRoleWithDomainFilter() { + + ZMSImpl zmsImpl = zmsTestInitializer.getZms(); + RsrcCtxWrapper ctx = zmsTestInitializer.getMockDomRsrcCtx(); + final String auditRef = zmsTestInitializer.getAuditRef(); + + final String domainName = "role-with-domain-filter"; + TopLevelDomain dom1 = zmsTestInitializer.createTopLevelDomainObject(domainName, + "Test Domain1", "testOrg", "user.user1"); + zmsImpl.postTopLevelDomain(ctx, auditRef, null, dom1); + + TopLevelDomain dom2 = zmsTestInitializer.createTopLevelDomainObject("sports", + "Test Domain1", "testOrg", "user.user1"); + zmsImpl.postTopLevelDomain(ctx, auditRef, null, dom2); + + ServiceIdentity service1 = zmsTestInitializer.createServiceObject("sports", "api", + "http://localhost:8080", null, null, null, null); + zmsImpl.putServiceIdentity(ctx, "sports", "api", auditRef, false, null, service1); + + Group group1 = zmsTestInitializer.createGroupObject("sports", "group1", null, null); + zmsImpl.putGroup(ctx, "sports", "group1", auditRef, false, null, group1); + + SubDomain subDom1 = zmsTestInitializer.createSubDomainObject("prod", "sports", "Test Domain1", + "testOrg", "user.user1"); + zmsImpl.postSubDomain(ctx, "sports", auditRef, null, subDom1); + + ServiceIdentity service2 = zmsTestInitializer.createServiceObject("sports.prod", "api", + "http://localhost:8080", null, null, null, null); + zmsImpl.putServiceIdentity(ctx, "sports.prod", "api", auditRef, false, null, service2); + + SubDomain subDom2 = zmsTestInitializer.createSubDomainObject("dev", "sports", "Test Domain1", + "testOrg", "user.user1"); + zmsImpl.postSubDomain(ctx, "sports", auditRef, null, subDom2); + + ServiceIdentity service3 = zmsTestInitializer.createServiceObject("sports.dev", "api", + "http://localhost:8080", null, null, null, null); + zmsImpl.putServiceIdentity(ctx, "sports.dev", "api", auditRef, false, null, service3); + + // add a role with the domain filter and allowed members + + List roleMembers = new ArrayList<>(); + roleMembers.add(new RoleMember().setMemberName("user.user1")); + roleMembers.add(new RoleMember().setMemberName("sports.api")); + roleMembers.add(new RoleMember().setMemberName("sports:group.group1")); + roleMembers.add(new RoleMember().setMemberName("sports.dev.api")); + + final String roleName1 = "filter-role1"; + Role role1 = zmsTestInitializer.createRoleObject(domainName, roleName1, null, roleMembers); + role1.setPrincipalDomainFilter("user,+sports,-sports.prod"); + zmsImpl.putRole(ctx, domainName, roleName1, auditRef, false, null, role1); + + // add a role with the domain filter and no allowed members + + roleMembers = new ArrayList<>(); + roleMembers.add(new RoleMember().setMemberName("user.user1")); + roleMembers.add(new RoleMember().setMemberName("sports.api")); + roleMembers.add(new RoleMember().setMemberName("sports:group.group1")); + roleMembers.add(new RoleMember().setMemberName("sports.dev.api")); + roleMembers.add(new RoleMember().setMemberName("sports.prod.api")); + role1.setRoleMembers(roleMembers); + + // sports.prod.api should be rejected + + try { + zmsImpl.putRole(ctx, domainName, roleName1, auditRef, false, null, role1); + fail(); + } catch (ResourceException ex) { + assertEquals(ex.getCode(), 400); + assertEquals(ex.getMessage().contains("Principal sports.prod.api is not allowed for the role"), true); + } + + zmsImpl.deleteSubDomain(ctx, "sports", "dev", auditRef, null); + zmsImpl.deleteSubDomain(ctx, "sports", "prod", auditRef, null); + zmsImpl.deleteMembership(ctx, domainName, roleName1, "sports:group.group1", auditRef, null); + zmsImpl.deleteTopLevelDomain(ctx, "sports", auditRef, null); + zmsImpl.deleteTopLevelDomain(ctx, domainName, auditRef, null); + } + + @Test + public void testPutRoleMembershipWithDomainFilter() { + + ZMSImpl zmsImpl = zmsTestInitializer.getZms(); + RsrcCtxWrapper ctx = zmsTestInitializer.getMockDomRsrcCtx(); + final String auditRef = zmsTestInitializer.getAuditRef(); + + final String domainName = "role-mbr-with-domain-filter"; + TopLevelDomain dom1 = zmsTestInitializer.createTopLevelDomainObject(domainName, + "Test Domain1", "testOrg", "user.user1"); + zmsImpl.postTopLevelDomain(ctx, auditRef, null, dom1); + + TopLevelDomain dom2 = zmsTestInitializer.createTopLevelDomainObject("sports", + "Test Domain1", "testOrg", "user.user1"); + zmsImpl.postTopLevelDomain(ctx, auditRef, null, dom2); + + ServiceIdentity service1 = zmsTestInitializer.createServiceObject("sports", "api", + "http://localhost:8080", null, null, null, null); + zmsImpl.putServiceIdentity(ctx, "sports", "api", auditRef, false, null, service1); + + Group group1 = zmsTestInitializer.createGroupObject("sports", "group1", null, null); + zmsImpl.putGroup(ctx, "sports", "group1", auditRef, false, null, group1); + + SubDomain subDom1 = zmsTestInitializer.createSubDomainObject("prod", "sports", "Test Domain1", + "testOrg", "user.user1"); + zmsImpl.postSubDomain(ctx, "sports", auditRef, null, subDom1); + + ServiceIdentity service2 = zmsTestInitializer.createServiceObject("sports.prod", "api", + "http://localhost:8080", null, null, null, null); + zmsImpl.putServiceIdentity(ctx, "sports.prod", "api", auditRef, false, null, service2); + + SubDomain subDom2 = zmsTestInitializer.createSubDomainObject("dev", "sports", "Test Domain1", + "testOrg", "user.user1"); + zmsImpl.postSubDomain(ctx, "sports", auditRef, null, subDom2); + + ServiceIdentity service3 = zmsTestInitializer.createServiceObject("sports.dev", "api", + "http://localhost:8080", null, null, null, null); + zmsImpl.putServiceIdentity(ctx, "sports.dev", "api", auditRef, false, null, service3); + + // add a role with the domain filter + + final String roleName1 = "filter-role1"; + Role role1 = zmsTestInitializer.createRoleObject(domainName, roleName1, null, null); + role1.setPrincipalDomainFilter("user,+sports,-sports.prod"); + zmsImpl.putRole(ctx, domainName, roleName1, auditRef, false, null, role1); + + // user.user1 should be able to be added to the role + + Membership membership = new Membership().setMemberName("user.user1"); + zmsImpl.putMembership(ctx, domainName, roleName1, "user.user1", auditRef, false, null, membership); + + // sports.api should be able to be added to the role + + membership = new Membership().setMemberName("sports.api"); + zmsImpl.putMembership(ctx, domainName, roleName1, "sports.api", auditRef, false, null, membership); + + // sports:group.group1 should be allowed to be added to the role + + membership = new Membership().setMemberName("sports:group.group1"); + zmsImpl.putMembership(ctx, domainName, roleName1, "sports:group.group1", auditRef, false, null, membership); + + // sports.dev.api should be able to be added to the role + + membership = new Membership().setMemberName("sports.dev.api"); + zmsImpl.putMembership(ctx, domainName, roleName1, "sports.dev.api", auditRef, false, null, membership); + + // sports.prod.api should be rejected + + membership = new Membership().setMemberName("sports.prod.api"); + try { + zmsImpl.putMembership(ctx, domainName, roleName1, "sports.prod.api", auditRef, false, null, membership); + fail(); + } catch (ResourceException ex) { + assertEquals(ex.getCode(), 400); + assertEquals(ex.getMessage().contains("Principal sports.prod.api is not allowed for the role"), true); + } + + zmsImpl.deleteSubDomain(ctx, "sports", "dev", auditRef, null); + zmsImpl.deleteSubDomain(ctx, "sports", "prod", auditRef, null); + zmsImpl.deleteMembership(ctx, domainName, roleName1, "sports:group.group1", auditRef, null); + zmsImpl.deleteTopLevelDomain(ctx, "sports", auditRef, null); + zmsImpl.deleteTopLevelDomain(ctx, domainName, auditRef, null); + } + + @Test + public void testPutGroupWithDomainFilter() { + + ZMSImpl zmsImpl = zmsTestInitializer.getZms(); + RsrcCtxWrapper ctx = zmsTestInitializer.getMockDomRsrcCtx(); + final String auditRef = zmsTestInitializer.getAuditRef(); + + final String domainName = "group-with-domain-filter"; + TopLevelDomain dom1 = zmsTestInitializer.createTopLevelDomainObject(domainName, + "Test Domain1", "testOrg", "user.user1"); + zmsImpl.postTopLevelDomain(ctx, auditRef, null, dom1); + + TopLevelDomain dom2 = zmsTestInitializer.createTopLevelDomainObject("sports", + "Test Domain1", "testOrg", "user.user1"); + zmsImpl.postTopLevelDomain(ctx, auditRef, null, dom2); + + ServiceIdentity service1 = zmsTestInitializer.createServiceObject("sports", "api", + "http://localhost:8080", null, null, null, null); + zmsImpl.putServiceIdentity(ctx, "sports", "api", auditRef, false, null, service1); + + SubDomain subDom1 = zmsTestInitializer.createSubDomainObject("prod", "sports", "Test Domain1", + "testOrg", "user.user1"); + zmsImpl.postSubDomain(ctx, "sports", auditRef, null, subDom1); + + ServiceIdentity service2 = zmsTestInitializer.createServiceObject("sports.prod", "api", + "http://localhost:8080", null, null, null, null); + zmsImpl.putServiceIdentity(ctx, "sports.prod", "api", auditRef, false, null, service2); + + SubDomain subDom2 = zmsTestInitializer.createSubDomainObject("dev", "sports", "Test Domain1", + "testOrg", "user.user1"); + zmsImpl.postSubDomain(ctx, "sports", auditRef, null, subDom2); + + ServiceIdentity service3 = zmsTestInitializer.createServiceObject("sports.dev", "api", + "http://localhost:8080", null, null, null, null); + zmsImpl.putServiceIdentity(ctx, "sports.dev", "api", auditRef, false, null, service3); + + // add a group with the domain filter and allowed members + + List groupMembers = new ArrayList<>(); + groupMembers.add(new GroupMember().setMemberName("user.user1")); + groupMembers.add(new GroupMember().setMemberName("sports.api")); + groupMembers.add(new GroupMember().setMemberName("sports.dev.api")); + + final String groupName1 = "filter-group1"; + Group group1 = zmsTestInitializer.createGroupObject(domainName, groupName1, groupMembers); + group1.setPrincipalDomainFilter("user,+sports,-sports.prod"); + zmsImpl.putGroup(ctx, domainName, groupName1, auditRef, false, null, group1); + + // add a group with the domain filter and no allowed members + + groupMembers = new ArrayList<>(); + groupMembers.add(new GroupMember().setMemberName("user.user1")); + groupMembers.add(new GroupMember().setMemberName("sports.api")); + groupMembers.add(new GroupMember().setMemberName("sports.dev.api")); + groupMembers.add(new GroupMember().setMemberName("sports.prod.api")); + group1.setGroupMembers(groupMembers); + + // sports.prod.api should be rejected + + try { + zmsImpl.putGroup(ctx, domainName, groupName1, auditRef, false, null, group1); + fail(); + } catch (ResourceException ex) { + assertEquals(ex.getCode(), 400); + assertEquals(ex.getMessage().contains("Principal sports.prod.api is not allowed for the group"), true); + } + + zmsImpl.deleteSubDomain(ctx, "sports", "dev", auditRef, null); + zmsImpl.deleteSubDomain(ctx, "sports", "prod", auditRef, null); + zmsImpl.deleteTopLevelDomain(ctx, "sports", auditRef, null); + zmsImpl.deleteTopLevelDomain(ctx, domainName, auditRef, null); + } + + @Test + public void testPutGroupMembershipWithDomainFilter() { + + ZMSImpl zmsImpl = zmsTestInitializer.getZms(); + RsrcCtxWrapper ctx = zmsTestInitializer.getMockDomRsrcCtx(); + final String auditRef = zmsTestInitializer.getAuditRef(); + + final String domainName = "group-mbr-with-domain-filter"; + TopLevelDomain dom1 = zmsTestInitializer.createTopLevelDomainObject(domainName, + "Test Domain1", "testOrg", "user.user1"); + zmsImpl.postTopLevelDomain(ctx, auditRef, null, dom1); + + TopLevelDomain dom2 = zmsTestInitializer.createTopLevelDomainObject("sports", + "Test Domain1", "testOrg", "user.user1"); + zmsImpl.postTopLevelDomain(ctx, auditRef, null, dom2); + + ServiceIdentity service1 = zmsTestInitializer.createServiceObject("sports", "api", + "http://localhost:8080", null, null, null, null); + zmsImpl.putServiceIdentity(ctx, "sports", "api", auditRef, false, null, service1); + + SubDomain subDom1 = zmsTestInitializer.createSubDomainObject("prod", "sports", "Test Domain1", + "testOrg", "user.user1"); + zmsImpl.postSubDomain(ctx, "sports", auditRef, null, subDom1); + + ServiceIdentity service2 = zmsTestInitializer.createServiceObject("sports.prod", "api", + "http://localhost:8080", null, null, null, null); + zmsImpl.putServiceIdentity(ctx, "sports.prod", "api", auditRef, false, null, service2); + + SubDomain subDom2 = zmsTestInitializer.createSubDomainObject("dev", "sports", "Test Domain1", + "testOrg", "user.user1"); + zmsImpl.postSubDomain(ctx, "sports", auditRef, null, subDom2); + + ServiceIdentity service3 = zmsTestInitializer.createServiceObject("sports.dev", "api", + "http://localhost:8080", null, null, null, null); + zmsImpl.putServiceIdentity(ctx, "sports.dev", "api", auditRef, false, null, service3); + + // add a group with the domain filter + + final String groupName1 = "filter-group1"; + Group group1 = zmsTestInitializer.createGroupObject(domainName, groupName1, null, null); + group1.setPrincipalDomainFilter("user,+sports,-sports.prod"); + zmsImpl.putGroup(ctx, domainName, groupName1, auditRef, false, null, group1); + + // user.user1 should be able to be added to the group + + GroupMembership membership = new GroupMembership().setMemberName("user.user1"); + zmsImpl.putGroupMembership(ctx, domainName, groupName1, "user.user1", auditRef, false, null, membership); + + // sports.api should be able to be added to the group + + membership = new GroupMembership().setMemberName("sports.api"); + zmsImpl.putGroupMembership(ctx, domainName, groupName1, "sports.api", auditRef, false, null, membership); + + // sports.dev.api should be able to be added to the group + + membership = new GroupMembership().setMemberName("sports.dev.api"); + zmsImpl.putGroupMembership(ctx, domainName, groupName1, "sports.dev.api", auditRef, false, null, membership); + + // sports.prod.api should be rejected + + membership = new GroupMembership().setMemberName("sports.prod.api"); + try { + zmsImpl.putGroupMembership(ctx, domainName, groupName1, "sports.prod.api", auditRef, false, null, membership); + fail(); + } catch (ResourceException ex) { + assertEquals(ex.getCode(), 400); + assertEquals(ex.getMessage().contains("Principal sports.prod.api is not allowed for the group"), true); + } + + zmsImpl.deleteSubDomain(ctx, "sports", "dev", auditRef, null); + zmsImpl.deleteSubDomain(ctx, "sports", "prod", auditRef, null); + zmsImpl.deleteTopLevelDomain(ctx, "sports", auditRef, null); + zmsImpl.deleteTopLevelDomain(ctx, domainName, auditRef, null); + } + + @Test + public void testUnknownDomainFilterName() { + + ZMSImpl zmsImpl = zmsTestInitializer.getZms(); + RsrcCtxWrapper ctx = zmsTestInitializer.getMockDomRsrcCtx(); + final String auditRef = zmsTestInitializer.getAuditRef(); + + final String domainName = "unknown-domain-filter-name"; + TopLevelDomain dom1 = zmsTestInitializer.createTopLevelDomainObject(domainName, + "Test Domain1", "testOrg", "user.user1"); + zmsImpl.postTopLevelDomain(ctx, auditRef, null, dom1); + + try { + Role role1 = zmsTestInitializer.createRoleObject(domainName, "role1", null, null); + role1.setPrincipalDomainFilter("user,unknown-domain-name"); + zmsImpl.putRole(ctx, domainName, "role1", auditRef, false, null, role1); + fail(); + } catch (ResourceException ex) { + assertEquals(ex.getCode(), 400); + assertTrue(ex.getMessage().contains("No such domain: unknown-domain-name")); + } + + try { + Group group1 = zmsTestInitializer.createGroupObject(domainName, "group1", null, null); + group1.setPrincipalDomainFilter("user,unknown-domain-name"); + zmsImpl.putGroup(ctx, domainName, "group1", auditRef, false, null, group1); + fail(); + } catch (ResourceException ex) { + assertEquals(ex.getCode(), 400); + assertTrue(ex.getMessage().contains("No such domain: unknown-domain-name")); + } + + zmsImpl.deleteTopLevelDomain(ctx, domainName, auditRef, null); + } +}