diff --git a/clients/go/zms/client.go b/clients/go/zms/client.go index bf13a5252ff..852f98a48b3 100644 --- a/clients/go/zms/client.go +++ b/clients/go/zms/client.go @@ -4257,6 +4257,70 @@ func (client ZMSClient) GetDependentDomainList(service ServiceName) (*DomainList } } +func (client ZMSClient) GetRolesForReview(principal ResourceName) (*ReviewObjects, error) { + var data *ReviewObjects + url := client.URL + "/review/role" + encodeParams(encodeStringParam("principal", string(principal), "")) + resp, err := client.httpGet(url, nil) + if err != nil { + return data, err + } + defer resp.Body.Close() + switch resp.StatusCode { + case 200: + err = json.NewDecoder(resp.Body).Decode(&data) + if err != nil { + return data, err + } + return data, nil + default: + var errobj rdl.ResourceError + contentBytes, err := io.ReadAll(resp.Body) + if err != nil { + return data, err + } + json.Unmarshal(contentBytes, &errobj) + if errobj.Code == 0 { + errobj.Code = resp.StatusCode + } + if errobj.Message == "" { + errobj.Message = string(contentBytes) + } + return data, errobj + } +} + +func (client ZMSClient) GetGroupsForReview(principal ResourceName) (*ReviewObjects, error) { + var data *ReviewObjects + url := client.URL + "/review/group" + encodeParams(encodeStringParam("principal", string(principal), "")) + resp, err := client.httpGet(url, nil) + if err != nil { + return data, err + } + defer resp.Body.Close() + switch resp.StatusCode { + case 200: + err = json.NewDecoder(resp.Body).Decode(&data) + if err != nil { + return data, err + } + return data, nil + default: + var errobj rdl.ResourceError + contentBytes, err := io.ReadAll(resp.Body) + if err != nil { + return data, err + } + json.Unmarshal(contentBytes, &errobj) + if errobj.Code == 0 { + errobj.Code = resp.StatusCode + } + if errobj.Message == "" { + errobj.Message = string(contentBytes) + } + return data, errobj + } +} + func (client ZMSClient) GetInfo() (*Info, error) { var data *Info url := client.URL + "/sys/info" diff --git a/clients/go/zms/model.go b/clients/go/zms/model.go index 6ab32a94356..065f51aa661 100644 --- a/clients/go/zms/model.go +++ b/clients/go/zms/model.go @@ -7867,6 +7867,151 @@ func (self *DependentServiceResourceGroupList) Validate() error { return nil } +// ReviewObject - Details for the roles and/or groups that need to be reviewed +type ReviewObject struct { + + // + // name of the domain + // + DomainName DomainName `json:"domainName"` + + // + // name of the role and/or group + // + Name EntityName `json:"name"` + + // + // all user members in the object have specified max expiry days + // + MemberExpiryDays int32 `json:"memberExpiryDays"` + + // + // all user members in the object have specified max review days + // + MemberReviewDays int32 `json:"memberReviewDays"` + + // + // all services in the object have specified max expiry days + // + ServiceExpiryDays int32 `json:"serviceExpiryDays"` + + // + // all services in the object have specified max review days + // + ServiceReviewDays int32 `json:"serviceReviewDays"` + + // + // all groups in the object have specified max expiry days + // + GroupExpiryDays int32 `json:"groupExpiryDays"` + + // + // all groups in the object have specified max review days + // + GroupReviewDays int32 `json:"groupReviewDays"` + + // + // last review timestamp of the object + // + LastReviewedDate *rdl.Timestamp `json:"lastReviewedDate,omitempty" rdl:"optional" yaml:",omitempty"` +} + +// NewReviewObject - creates an initialized ReviewObject instance, returns a pointer to it +func NewReviewObject(init ...*ReviewObject) *ReviewObject { + var o *ReviewObject + if len(init) == 1 { + o = init[0] + } else { + o = new(ReviewObject) + } + return o +} + +type rawReviewObject ReviewObject + +// UnmarshalJSON is defined for proper JSON decoding of a ReviewObject +func (self *ReviewObject) UnmarshalJSON(b []byte) error { + var m rawReviewObject + err := json.Unmarshal(b, &m) + if err == nil { + o := ReviewObject(m) + *self = o + err = self.Validate() + } + return err +} + +// Validate - checks for missing required fields, etc +func (self *ReviewObject) Validate() error { + if self.DomainName == "" { + return fmt.Errorf("ReviewObject.domainName is missing but is a required field") + } else { + val := rdl.Validate(ZMSSchema(), "DomainName", self.DomainName) + if !val.Valid { + return fmt.Errorf("ReviewObject.domainName does not contain a valid DomainName (%v)", val.Error) + } + } + if self.Name == "" { + return fmt.Errorf("ReviewObject.name is missing but is a required field") + } else { + val := rdl.Validate(ZMSSchema(), "EntityName", self.Name) + if !val.Valid { + return fmt.Errorf("ReviewObject.name does not contain a valid EntityName (%v)", val.Error) + } + } + return nil +} + +// ReviewObjects - The representation for a list of objects with full details +type ReviewObjects struct { + + // + // list of review objects + // + List []*ReviewObject `json:"list"` +} + +// NewReviewObjects - creates an initialized ReviewObjects instance, returns a pointer to it +func NewReviewObjects(init ...*ReviewObjects) *ReviewObjects { + var o *ReviewObjects + if len(init) == 1 { + o = init[0] + } else { + o = new(ReviewObjects) + } + return o.Init() +} + +// Init - sets up the instance according to its default field values, if any +func (self *ReviewObjects) Init() *ReviewObjects { + if self.List == nil { + self.List = make([]*ReviewObject, 0) + } + return self +} + +type rawReviewObjects ReviewObjects + +// UnmarshalJSON is defined for proper JSON decoding of a ReviewObjects +func (self *ReviewObjects) UnmarshalJSON(b []byte) error { + var m rawReviewObjects + err := json.Unmarshal(b, &m) + if err == nil { + o := ReviewObjects(m) + *self = *((&o).Init()) + err = self.Validate() + } + return err +} + +// Validate - checks for missing required fields, etc +func (self *ReviewObjects) Validate() error { + if self.List == nil { + return fmt.Errorf("ReviewObjects: Missing required field: list") + } + return nil +} + // Info - Copyright The Athenz Authors Licensed under the terms of the Apache // version 2.0 license. See LICENSE file for terms. The representation for an // info object diff --git a/clients/go/zms/zms_schema.go b/clients/go/zms/zms_schema.go index 32fb2f43e9f..fb239b15009 100644 --- a/clients/go/zms/zms_schema.go +++ b/clients/go/zms/zms_schema.go @@ -810,6 +810,24 @@ func init() { tDependentServiceResourceGroupList.ArrayField("serviceAndResourceGroups", "DependentServiceResourceGroup", false, "collection of dependent services and resource groups for tenant domain") sb.AddType(tDependentServiceResourceGroupList.Build()) + tReviewObject := rdl.NewStructTypeBuilder("Struct", "ReviewObject") + tReviewObject.Comment("Details for the roles and/or groups that need to be reviewed") + tReviewObject.Field("domainName", "DomainName", false, nil, "name of the domain") + tReviewObject.Field("name", "EntityName", false, nil, "name of the role and/or group") + tReviewObject.Field("memberExpiryDays", "Int32", false, nil, "all user members in the object have specified max expiry days") + tReviewObject.Field("memberReviewDays", "Int32", false, nil, "all user members in the object have specified max review days") + tReviewObject.Field("serviceExpiryDays", "Int32", false, nil, "all services in the object have specified max expiry days") + tReviewObject.Field("serviceReviewDays", "Int32", false, nil, "all services in the object have specified max review days") + tReviewObject.Field("groupExpiryDays", "Int32", false, nil, "all groups in the object have specified max expiry days") + tReviewObject.Field("groupReviewDays", "Int32", false, nil, "all groups in the object have specified max review days") + tReviewObject.Field("lastReviewedDate", "Timestamp", true, nil, "last review timestamp of the object") + sb.AddType(tReviewObject.Build()) + + tReviewObjects := rdl.NewStructTypeBuilder("Struct", "ReviewObjects") + tReviewObjects.Comment("The representation for a list of objects with full details") + tReviewObjects.ArrayField("list", "ReviewObject", false, "list of review objects") + sb.AddType(tReviewObjects.Build()) + tInfo := rdl.NewStructTypeBuilder("Struct", "Info") tInfo.Comment("Copyright The Athenz Authors Licensed under the terms of the Apache version 2.0 license. See LICENSE file for terms. The representation for an info object") tInfo.Field("buildJdkSpec", "String", true, nil, "jdk build version") @@ -1448,7 +1466,7 @@ func init() { sb.AddResource(mGetDomainRoleMembers.Build()) mGetPrincipalRoles := rdl.NewResourceBuilder("DomainRoleMember", "GET", "/role") - mGetPrincipalRoles.Comment("Fetch all the roles across domains by either calling or specified principal The optional expand argument will include all direct and indirect roles, however, it will force authorization that you must be either the principal or for service accounts have update access to the service identity: 1. authenticated principal is the same as the check principal 2. system authorized (\"access\", \"sys.auth:meta.role.lookup\") 3. service admin (\"update\", \"{principal}\")") + mGetPrincipalRoles.Comment("Fetch all the roles across domains by either calling or specified principal The optional expand argument will include all direct and indirect roles, however, it will force authorization that you must be either the principal or for service accounts have update access to the service identity: 1. authenticated principal is the same as the check principal 2. system authorized (\"access\", \"sys.auth:meta.role.lookup\") 3. service admin (\"update\", \"{principal}\") 4. domain authorized (\"access\", \"{domainName}:meta.role.lookup\") if domainName is provided") mGetPrincipalRoles.Name("getPrincipalRoles") mGetPrincipalRoles.Input("principal", "ResourceName", false, "principal", "", true, nil, "If not present, will return roles for the user making the call") mGetPrincipalRoles.Input("domainName", "DomainName", false, "domain", "", true, nil, "If not present, will return roles from all domains") @@ -2732,6 +2750,30 @@ func init() { mGetDependentDomainList.Exception("UNAUTHORIZED", "ResourceError", "") sb.AddResource(mGetDependentDomainList.Build()) + mGetRolesForReview := rdl.NewResourceBuilder("ReviewObjects", "GET", "/review/role") + mGetRolesForReview.Comment("Fetch all the roles across domains for either the caller or specified principal that require a review based on the last reviewed date and configured attributes. The method requires the caller to be either the principal or authorized in system to carry out the operation for any principal (typically this would be system administrators) 1. authenticated principal is the same as the check principal 2. system authorized (\"access\", \"sys.auth:meta.review.lookup\")") + mGetRolesForReview.Name("GetRolesForReview") + mGetRolesForReview.Input("principal", "ResourceName", false, "principal", "", true, nil, "If not present, will return roles for the user making the call") + mGetRolesForReview.Auth("", "", true, "") + mGetRolesForReview.Exception("BAD_REQUEST", "ResourceError", "") + mGetRolesForReview.Exception("FORBIDDEN", "ResourceError", "") + mGetRolesForReview.Exception("NOT_FOUND", "ResourceError", "") + mGetRolesForReview.Exception("TOO_MANY_REQUESTS", "ResourceError", "") + mGetRolesForReview.Exception("UNAUTHORIZED", "ResourceError", "") + sb.AddResource(mGetRolesForReview.Build()) + + mGetGroupsForReview := rdl.NewResourceBuilder("ReviewObjects", "GET", "/review/group") + mGetGroupsForReview.Comment("Fetch all the groups across domains for either the caller or specified principal that require a review based on the last reviewed date and configured attributes. The method requires the caller to be either the principal or authorized in system to carry out the operation for any principal (typically this would be system administrators) 1. authenticated principal is the same as the check principal 2. system authorized (\"access\", \"sys.auth:meta.review.lookup\")") + mGetGroupsForReview.Name("GetGroupsForReview") + mGetGroupsForReview.Input("principal", "ResourceName", false, "principal", "", true, nil, "If not present, will return groups for the user making the call") + mGetGroupsForReview.Auth("", "", true, "") + mGetGroupsForReview.Exception("BAD_REQUEST", "ResourceError", "") + mGetGroupsForReview.Exception("FORBIDDEN", "ResourceError", "") + mGetGroupsForReview.Exception("NOT_FOUND", "ResourceError", "") + mGetGroupsForReview.Exception("TOO_MANY_REQUESTS", "ResourceError", "") + mGetGroupsForReview.Exception("UNAUTHORIZED", "ResourceError", "") + sb.AddResource(mGetGroupsForReview.Build()) + mGetInfo := rdl.NewResourceBuilder("Info", "GET", "/sys/info") mGetInfo.Comment("Retrieve the server info. Since we're exposing server version details, the request will require authorization") mGetInfo.Auth("get", "sys.auth:info", false, "") diff --git a/clients/java/zms/src/main/java/com/yahoo/athenz/zms/ZMSRDLGeneratedClient.java b/clients/java/zms/src/main/java/com/yahoo/athenz/zms/ZMSRDLGeneratedClient.java index 8ec6d8e9acd..bb62aa7adb6 100644 --- a/clients/java/zms/src/main/java/com/yahoo/athenz/zms/ZMSRDLGeneratedClient.java +++ b/clients/java/zms/src/main/java/com/yahoo/athenz/zms/ZMSRDLGeneratedClient.java @@ -4111,6 +4111,66 @@ public DomainList getDependentDomainList(String service) throws URISyntaxExcepti } } + public ReviewObjects getRolesForReview(String principal) throws URISyntaxException, IOException { + UriTemplateBuilder uriTemplateBuilder = new UriTemplateBuilder(baseUrl, "/review/role"); + URIBuilder uriBuilder = new URIBuilder(uriTemplateBuilder.getUri()); + if (principal != null) { + uriBuilder.setParameter("principal", principal); + } + HttpUriRequest httpUriRequest = RequestBuilder.get() + .setUri(uriBuilder.build()) + .build(); + if (credsHeader != null) { + httpUriRequest.addHeader(credsHeader, credsToken); + } + HttpEntity httpResponseEntity = null; + try (CloseableHttpResponse httpResponse = client.execute(httpUriRequest, httpContext)) { + int code = httpResponse.getStatusLine().getStatusCode(); + httpResponseEntity = httpResponse.getEntity(); + switch (code) { + case 200: + return jsonMapper.readValue(httpResponseEntity.getContent(), ReviewObjects.class); + default: + final String errorData = (httpResponseEntity == null) ? null : EntityUtils.toString(httpResponseEntity); + throw (errorData != null && !errorData.isEmpty()) + ? new ResourceException(code, jsonMapper.readValue(errorData, ResourceError.class)) + : new ResourceException(code); + } + } finally { + EntityUtils.consumeQuietly(httpResponseEntity); + } + } + + public ReviewObjects getGroupsForReview(String principal) throws URISyntaxException, IOException { + UriTemplateBuilder uriTemplateBuilder = new UriTemplateBuilder(baseUrl, "/review/group"); + URIBuilder uriBuilder = new URIBuilder(uriTemplateBuilder.getUri()); + if (principal != null) { + uriBuilder.setParameter("principal", principal); + } + HttpUriRequest httpUriRequest = RequestBuilder.get() + .setUri(uriBuilder.build()) + .build(); + if (credsHeader != null) { + httpUriRequest.addHeader(credsHeader, credsToken); + } + HttpEntity httpResponseEntity = null; + try (CloseableHttpResponse httpResponse = client.execute(httpUriRequest, httpContext)) { + int code = httpResponse.getStatusLine().getStatusCode(); + httpResponseEntity = httpResponse.getEntity(); + switch (code) { + case 200: + return jsonMapper.readValue(httpResponseEntity.getContent(), ReviewObjects.class); + default: + final String errorData = (httpResponseEntity == null) ? null : EntityUtils.toString(httpResponseEntity); + throw (errorData != null && !errorData.isEmpty()) + ? new ResourceException(code, jsonMapper.readValue(errorData, ResourceError.class)) + : new ResourceException(code); + } + } finally { + EntityUtils.consumeQuietly(httpResponseEntity); + } + } + public Info getInfo() throws URISyntaxException, IOException { UriTemplateBuilder uriTemplateBuilder = new UriTemplateBuilder(baseUrl, "/sys/info"); URIBuilder uriBuilder = new URIBuilder(uriTemplateBuilder.getUri()); diff --git a/core/zms/src/main/java/com/yahoo/athenz/zms/ReviewObject.java b/core/zms/src/main/java/com/yahoo/athenz/zms/ReviewObject.java new file mode 100644 index 00000000000..846a2d220b7 --- /dev/null +++ b/core/zms/src/main/java/com/yahoo/athenz/zms/ReviewObject.java @@ -0,0 +1,128 @@ +// +// This file generated by rdl 1.5.2. Do not modify! +// + +package com.yahoo.athenz.zms; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.yahoo.rdl.*; + +// +// ReviewObject - Details for the roles and/or groups that need to be reviewed +// +@JsonIgnoreProperties(ignoreUnknown = true) +public class ReviewObject { + public String domainName; + public String name; + public int memberExpiryDays; + public int memberReviewDays; + public int serviceExpiryDays; + public int serviceReviewDays; + public int groupExpiryDays; + public int groupReviewDays; + @RdlOptional + @JsonInclude(JsonInclude.Include.NON_EMPTY) + public Timestamp lastReviewedDate; + + public ReviewObject setDomainName(String domainName) { + this.domainName = domainName; + return this; + } + public String getDomainName() { + return domainName; + } + public ReviewObject setName(String name) { + this.name = name; + return this; + } + public String getName() { + return name; + } + public ReviewObject setMemberExpiryDays(int memberExpiryDays) { + this.memberExpiryDays = memberExpiryDays; + return this; + } + public int getMemberExpiryDays() { + return memberExpiryDays; + } + public ReviewObject setMemberReviewDays(int memberReviewDays) { + this.memberReviewDays = memberReviewDays; + return this; + } + public int getMemberReviewDays() { + return memberReviewDays; + } + public ReviewObject setServiceExpiryDays(int serviceExpiryDays) { + this.serviceExpiryDays = serviceExpiryDays; + return this; + } + public int getServiceExpiryDays() { + return serviceExpiryDays; + } + public ReviewObject setServiceReviewDays(int serviceReviewDays) { + this.serviceReviewDays = serviceReviewDays; + return this; + } + public int getServiceReviewDays() { + return serviceReviewDays; + } + public ReviewObject setGroupExpiryDays(int groupExpiryDays) { + this.groupExpiryDays = groupExpiryDays; + return this; + } + public int getGroupExpiryDays() { + return groupExpiryDays; + } + public ReviewObject setGroupReviewDays(int groupReviewDays) { + this.groupReviewDays = groupReviewDays; + return this; + } + public int getGroupReviewDays() { + return groupReviewDays; + } + public ReviewObject setLastReviewedDate(Timestamp lastReviewedDate) { + this.lastReviewedDate = lastReviewedDate; + return this; + } + public Timestamp getLastReviewedDate() { + return lastReviewedDate; + } + + @Override + public boolean equals(Object another) { + if (this != another) { + if (another == null || another.getClass() != ReviewObject.class) { + return false; + } + ReviewObject a = (ReviewObject) another; + if (domainName == null ? a.domainName != null : !domainName.equals(a.domainName)) { + return false; + } + if (name == null ? a.name != null : !name.equals(a.name)) { + return false; + } + if (memberExpiryDays != a.memberExpiryDays) { + return false; + } + if (memberReviewDays != a.memberReviewDays) { + return false; + } + if (serviceExpiryDays != a.serviceExpiryDays) { + return false; + } + if (serviceReviewDays != a.serviceReviewDays) { + return false; + } + if (groupExpiryDays != a.groupExpiryDays) { + return false; + } + if (groupReviewDays != a.groupReviewDays) { + return false; + } + if (lastReviewedDate == null ? a.lastReviewedDate != null : !lastReviewedDate.equals(a.lastReviewedDate)) { + return false; + } + } + return true; + } +} diff --git a/core/zms/src/main/java/com/yahoo/athenz/zms/ReviewObjects.java b/core/zms/src/main/java/com/yahoo/athenz/zms/ReviewObjects.java new file mode 100644 index 00000000000..03a8570b860 --- /dev/null +++ b/core/zms/src/main/java/com/yahoo/athenz/zms/ReviewObjects.java @@ -0,0 +1,38 @@ +// +// This file generated by rdl 1.5.2. Do not modify! +// + +package com.yahoo.athenz.zms; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import java.util.List; +import com.yahoo.rdl.*; + +// +// ReviewObjects - The representation for a list of objects with full details +// +@JsonIgnoreProperties(ignoreUnknown = true) +public class ReviewObjects { + public List list; + + public ReviewObjects setList(List list) { + this.list = list; + return this; + } + public List getList() { + return list; + } + + @Override + public boolean equals(Object another) { + if (this != another) { + if (another == null || another.getClass() != ReviewObjects.class) { + return false; + } + ReviewObjects a = (ReviewObjects) another; + if (list == null ? a.list != null : !list.equals(a.list)) { + 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 b9e60b9ec32..9422754cf58 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 @@ -698,6 +698,22 @@ private static Schema build() { sb.structType("DependentServiceResourceGroupList") .arrayField("serviceAndResourceGroups", "DependentServiceResourceGroup", false, "collection of dependent services and resource groups for tenant domain"); + sb.structType("ReviewObject") + .comment("Details for the roles and/or groups that need to be reviewed") + .field("domainName", "DomainName", false, "name of the domain") + .field("name", "EntityName", false, "name of the role and/or group") + .field("memberExpiryDays", "Int32", false, "all user members in the object have specified max expiry days") + .field("memberReviewDays", "Int32", false, "all user members in the object have specified max review days") + .field("serviceExpiryDays", "Int32", false, "all services in the object have specified max expiry days") + .field("serviceReviewDays", "Int32", false, "all services in the object have specified max review days") + .field("groupExpiryDays", "Int32", false, "all groups in the object have specified max expiry days") + .field("groupReviewDays", "Int32", false, "all groups in the object have specified max review days") + .field("lastReviewedDate", "Timestamp", true, "last review timestamp of the object"); + + sb.structType("ReviewObjects") + .comment("The representation for a list of objects with full details") + .arrayField("list", "ReviewObject", false, "list of review objects"); + sb.structType("Info") .comment("Copyright The Athenz Authors Licensed under the terms of the Apache version 2.0 license. See LICENSE file for terms. The representation for an info object") .field("buildJdkSpec", "String", true, "jdk build version") @@ -1442,7 +1458,7 @@ private static Schema build() { ; sb.resource("DomainRoleMember", "GET", "/role") - .comment("Fetch all the roles across domains by either calling or specified principal The optional expand argument will include all direct and indirect roles, however, it will force authorization that you must be either the principal or for service accounts have update access to the service identity: 1. authenticated principal is the same as the check principal 2. system authorized (\"access\", \"sys.auth:meta.role.lookup\") 3. service admin (\"update\", \"{principal}\")") + .comment("Fetch all the roles across domains by either calling or specified principal The optional expand argument will include all direct and indirect roles, however, it will force authorization that you must be either the principal or for service accounts have update access to the service identity: 1. authenticated principal is the same as the check principal 2. system authorized (\"access\", \"sys.auth:meta.role.lookup\") 3. service admin (\"update\", \"{principal}\") 4. domain authorized (\"access\", \"{domainName}:meta.role.lookup\") if domainName is provided") .name("getPrincipalRoles") .queryParam("principal", "principal", "ResourceName", null, "If not present, will return roles for the user making the call") .queryParam("domain", "domainName", "DomainName", null, "If not present, will return roles from all domains") @@ -3150,6 +3166,40 @@ private static Schema build() { .exception("UNAUTHORIZED", "ResourceError", "") ; + sb.resource("ReviewObjects", "GET", "/review/role") + .comment("Fetch all the roles across domains for either the caller or specified principal that require a review based on the last reviewed date and configured attributes. The method requires the caller to be either the principal or authorized in system to carry out the operation for any principal (typically this would be system administrators) 1. authenticated principal is the same as the check principal 2. system authorized (\"access\", \"sys.auth:meta.review.lookup\")") + .name("GetRolesForReview") + .queryParam("principal", "principal", "ResourceName", null, "If not present, will return roles for the user making the call") + .auth("", "", true) + .expected("OK") + .exception("BAD_REQUEST", "ResourceError", "") + + .exception("FORBIDDEN", "ResourceError", "") + + .exception("NOT_FOUND", "ResourceError", "") + + .exception("TOO_MANY_REQUESTS", "ResourceError", "") + + .exception("UNAUTHORIZED", "ResourceError", "") +; + + sb.resource("ReviewObjects", "GET", "/review/group") + .comment("Fetch all the groups across domains for either the caller or specified principal that require a review based on the last reviewed date and configured attributes. The method requires the caller to be either the principal or authorized in system to carry out the operation for any principal (typically this would be system administrators) 1. authenticated principal is the same as the check principal 2. system authorized (\"access\", \"sys.auth:meta.review.lookup\")") + .name("GetGroupsForReview") + .queryParam("principal", "principal", "ResourceName", null, "If not present, will return groups for the user making the call") + .auth("", "", true) + .expected("OK") + .exception("BAD_REQUEST", "ResourceError", "") + + .exception("FORBIDDEN", "ResourceError", "") + + .exception("NOT_FOUND", "ResourceError", "") + + .exception("TOO_MANY_REQUESTS", "ResourceError", "") + + .exception("UNAUTHORIZED", "ResourceError", "") +; + sb.resource("Info", "GET", "/sys/info") .comment("Retrieve the server info. Since we're exposing server version details, the request will require authorization") .auth("get", "sys.auth:info") diff --git a/core/zms/src/main/rdl/Review.rdli b/core/zms/src/main/rdl/Review.rdli new file mode 100644 index 00000000000..416e023c4ff --- /dev/null +++ b/core/zms/src/main/rdl/Review.rdli @@ -0,0 +1,58 @@ +// Copyright The Athenz Authors +// Licensed under the terms of the Apache version 2.0 license. See LICENSE file for terms. + +include "Names.tdl"; + +//Details for the roles and/or groups that need to be reviewed +type ReviewObject Struct { + DomainName domainName; //name of the domain + EntityName name; //name of the role and/or group + Int32 memberExpiryDays; //all user members in the object have specified max expiry days + Int32 memberReviewDays; //all user members in the object have specified max review days + Int32 serviceExpiryDays; //all services in the object have specified max expiry days + Int32 serviceReviewDays; //all services in the object have specified max review days + Int32 groupExpiryDays; //all groups in the object have specified max expiry days + Int32 groupReviewDays; //all groups in the object have specified max review days + Timestamp lastReviewedDate (optional); //last review timestamp of the object +} + +//The representation for a list of objects with full details +type ReviewObjects Struct { + Array list; // list of review objects +} + +// Fetch all the roles across domains for either the caller or specified principal +// that require a review based on the last reviewed date and configured attributes. +// The method requires the caller to be either the principal or authorized in system +// to carry out the operation for any principal (typically this would be system administrators) +// 1. authenticated principal is the same as the check principal +// 2. system authorized ("access", "sys.auth:meta.review.lookup") +resource ReviewObjects GET "/review/role?principal={principal}" (name=GetRolesForReview) { + ResourceName principal (optional); //If not present, will return roles for the user making the call + authenticate; + exceptions { + ResourceError BAD_REQUEST; + ResourceError NOT_FOUND; + ResourceError FORBIDDEN; + ResourceError UNAUTHORIZED; + ResourceError TOO_MANY_REQUESTS; + } +} + +// Fetch all the groups across domains for either the caller or specified principal +// that require a review based on the last reviewed date and configured attributes. +// The method requires the caller to be either the principal or authorized in system +// to carry out the operation for any principal (typically this would be system administrators) +// 1. authenticated principal is the same as the check principal +// 2. system authorized ("access", "sys.auth:meta.review.lookup") +resource ReviewObjects GET "/review/group?principal={principal}" (name=GetGroupsForReview) { + ResourceName principal (optional); //If not present, will return groups for the user making the call + authenticate; + exceptions { + ResourceError BAD_REQUEST; + ResourceError NOT_FOUND; + ResourceError FORBIDDEN; + ResourceError UNAUTHORIZED; + ResourceError TOO_MANY_REQUESTS; + } +} diff --git a/core/zms/src/main/rdl/ZMS.rdl b/core/zms/src/main/rdl/ZMS.rdl index a067c7a820f..00fdb2aab2c 100644 --- a/core/zms/src/main/rdl/ZMS.rdl +++ b/core/zms/src/main/rdl/ZMS.rdl @@ -28,5 +28,6 @@ include "DomainRoleMembership.rdli"; include "Authority.rdli"; include "Stats.rdli"; include "Dependency.rdli"; +include "Review.rdli"; include "Info.rdli"; include "Schema.rdli"; diff --git a/core/zms/src/test/java/com/yahoo/athenz/zms/ReviewTest.java b/core/zms/src/test/java/com/yahoo/athenz/zms/ReviewTest.java new file mode 100644 index 00000000000..887968bedbe --- /dev/null +++ b/core/zms/src/test/java/com/yahoo/athenz/zms/ReviewTest.java @@ -0,0 +1,136 @@ +/* + * 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; + +import com.yahoo.rdl.Timestamp; +import org.testng.annotations.Test; + +import java.util.Collections; + +import static org.testng.Assert.*; + +public class ReviewTest { + + @Test + public void testReviewObject() { + + ReviewObject object1 = new ReviewObject() + .setDomainName("domain1").setName("name1") + .setGroupReviewDays(10).setGroupExpiryDays(20) + .setMemberReviewDays(30).setMemberExpiryDays(40) + .setServiceReviewDays(50).setServiceExpiryDays(60) + .setLastReviewedDate(Timestamp.fromMillis(123456789)); + + ReviewObject object2 = new ReviewObject() + .setDomainName("domain1").setName("name1") + .setGroupReviewDays(10).setGroupExpiryDays(20) + .setMemberReviewDays(30).setMemberExpiryDays(40) + .setServiceReviewDays(50).setServiceExpiryDays(60) + .setLastReviewedDate(Timestamp.fromMillis(123456789)); + + assertEquals(object1, object1); + assertEquals(object1, object2); + assertNotEquals("data", object2); + + // verify getters + + assertEquals("domain1", object1.getDomainName()); + assertEquals("name1", object1.getName()); + assertEquals(10, object1.getGroupReviewDays()); + assertEquals(20, object1.getGroupExpiryDays()); + assertEquals(30, object1.getMemberReviewDays()); + assertEquals(40, object1.getMemberExpiryDays()); + assertEquals(50, object1.getServiceReviewDays()); + assertEquals(60, object1.getServiceExpiryDays()); + assertEquals(Timestamp.fromMillis(123456789), object1.getLastReviewedDate()); + + object1.setDomainName("domain2"); + assertNotEquals(object1, object2); + object1.setDomainName(null); + assertNotEquals(object1, object2); + object1.setDomainName("domain1"); + assertEquals(object1, object2); + + object1.setName("name2"); + assertNotEquals(object1, object2); + object1.setName(null); + assertNotEquals(object1, object2); + object1.setName("name1"); + assertEquals(object1, object2); + + object1.setGroupReviewDays(11); + assertNotEquals(object1, object2); + object1.setGroupReviewDays(10); + assertEquals(object1, object2); + + object1.setGroupExpiryDays(21); + assertNotEquals(object1, object2); + object1.setGroupExpiryDays(20); + assertEquals(object1, object2); + + object1.setMemberReviewDays(31); + assertNotEquals(object1, object2); + object1.setMemberReviewDays(30); + assertEquals(object1, object2); + + object1.setMemberExpiryDays(41); + assertNotEquals(object1, object2); + object1.setMemberExpiryDays(40); + assertEquals(object1, object2); + + object1.setServiceReviewDays(51); + assertNotEquals(object1, object2); + object1.setServiceReviewDays(50); + assertEquals(object1, object2); + + object1.setServiceExpiryDays(61); + assertNotEquals(object1, object2); + object1.setServiceExpiryDays(60); + assertEquals(object1, object2); + + object1.setLastReviewedDate(Timestamp.fromMillis(123456780)); + assertNotEquals(object1, object2); + object1.setLastReviewedDate(null); + assertNotEquals(object1, object2); + object1.setLastReviewedDate(Timestamp.fromMillis(123456789)); + assertEquals(object1, object2); + } + + @Test + public void testReviewObjects() { + + ReviewObjects objects1 = new ReviewObjects(); + ReviewObjects objects2 = new ReviewObjects(); + + assertEquals(objects1, objects1); + assertEquals(objects1, objects2); + assertNotEquals("data", objects2); + + // verify getters + + assertNull(objects1.getList()); + + ReviewObject object1 = new ReviewObject() + .setDomainName("domain1").setName("name1"); + objects1.setList(Collections.singletonList(object1)); + assertEquals(objects1.getList().size(), 1); + assertNotEquals(objects1, objects2); + + objects2.setList(Collections.singletonList(object1)); + assertEquals(objects1, objects2); + } +} diff --git a/libs/go/zmscli/cli.go b/libs/go/zmscli/cli.go index b381c47013a..ca0e7df99e0 100644 --- a/libs/go/zmscli/cli.go +++ b/libs/go/zmscli/cli.go @@ -386,6 +386,18 @@ func (cli Zms) EvalCommand(params []string) (*string, error) { } else if argc == 1 { return cli.ShowGroupsPrincipal(args[0], dn) } + case "list-roles-for-review": + if argc == 0 { + return cli.GetRolesForReview("") + } else if argc == 1 { + return cli.GetRolesForReview(args[0]) + } + case "list-groups-for-review": + if argc == 0 { + return cli.GetGroupsForReview("") + } else if argc == 1 { + return cli.GetGroupsForReview(args[0]) + } case "stats", "get-stats": if argc == 1 { //override the default domain @@ -3152,6 +3164,28 @@ func (cli Zms) HelpSpecificCommand(interactive bool, cmd string) string { buf.WriteString(" tag_value : optional, query all services with given tag key and value\n") buf.WriteString(" examples:\n") buf.WriteString(" " + domainExample + " show-services readers readers-tag-key reader-tag-value\n") + case "list-roles-for-review": + buf.WriteString(" syntax:\n") + buf.WriteString(" list-roles-for-review [principal]\n") + if !interactive { + buf.WriteString(" parameters:\n") + buf.WriteString(" principal : optional name of the principal to retrieve the list of roles for review\n") + buf.WriteString(" : if not specified will retrieve roles for current principal\n") + } + buf.WriteString(" examples:\n") + buf.WriteString(" list-roles-for-review\n") + buf.WriteString(" list-roles-for-review user.johndoe\n") + case "list-groups-for-review": + buf.WriteString(" syntax:\n") + buf.WriteString(" list-groups-for-review [principal]\n") + if !interactive { + buf.WriteString(" parameters:\n") + buf.WriteString(" principal : optional name of the principal to retrieve the list of groups for review\n") + buf.WriteString(" : if not specified will retrieve groups for current principal\n") + } + buf.WriteString(" examples:\n") + buf.WriteString(" list-groups-for-review\n") + buf.WriteString(" list-groups-for-review user.johndoe\n") default: if interactive { buf.WriteString("Unknown command. Type 'help' to see available commands") @@ -3244,6 +3278,7 @@ func (cli Zms) HelpListCommand() string { buf.WriteString(" show-role role [log | expand | pending]\n") buf.WriteString(" show-roles [tag_key] [tag_value]\n") buf.WriteString(" show-roles-principal [principal] [expand]\n") + buf.WriteString(" list-roles-for-review [principal]\n") buf.WriteString(" add-delegated-role role trusted_domain\n") buf.WriteString(" add-regular-role role member [member ... ]\n") buf.WriteString(" add-member regular_role user_or_service [user_or_service ...]\n") @@ -3283,7 +3318,8 @@ func (cli Zms) HelpListCommand() string { buf.WriteString(" list-group\n") buf.WriteString(" show-group group [log | pending]\n") buf.WriteString(" show-groups [tag_key] [tag_value]\n") - buf.WriteString(" show-groups-principal\n") + buf.WriteString(" show-groups-principal [principal]\n") + buf.WriteString(" list-groups-for-review [principal]\n") buf.WriteString(" add-group group member [member ... ]\n") buf.WriteString(" add-group-member group user_or_service [user_or_service ...]\n") buf.WriteString(" check-group-member group user_or_service [user_or_service ...]\n") diff --git a/libs/go/zmscli/review.go b/libs/go/zmscli/review.go new file mode 100644 index 00000000000..d17cf77d72e --- /dev/null +++ b/libs/go/zmscli/review.go @@ -0,0 +1,22 @@ +// Copyright The Athenz Authors +// Licensed under the terms of the Apache version 2.0 license. See LICENSE file for terms. + +package zmscli + +func (cli Zms) GetRolesForReview(principal string) (*string, error) { + reviewObjects, err := cli.GetRolesForReview(principal) + if err != nil { + return nil, err + } + + return cli.dumpByFormat(reviewObjects, cli.buildYAMLOutput) +} + +func (cli Zms) GetGroupsForReview(principal string) (*string, error) { + reviewObjects, err := cli.GetGroupsForReview(principal) + if err != nil { + return nil, err + } + + return cli.dumpByFormat(reviewObjects, cli.buildYAMLOutput) +} diff --git a/servers/zms/src/main/java/com/yahoo/athenz/zms/DBService.java b/servers/zms/src/main/java/com/yahoo/athenz/zms/DBService.java index 380fec60a1c..a9cf99143dd 100644 --- a/servers/zms/src/main/java/com/yahoo/athenz/zms/DBService.java +++ b/servers/zms/src/main/java/com/yahoo/athenz/zms/DBService.java @@ -3389,6 +3389,68 @@ DomainRoleMember getPrincipalRoles(String principal, String domainName, Boolean return principalRoles; } + ReviewObjects getRolesForReview(final String principal) { + try (ObjectStoreConnection con = store.getConnection(true, false)) { + return filterObjectsForReview(con.getRolesForReview(principal)); + } + } + + ReviewObjects getGroupsForReview(final String principal) { + try (ObjectStoreConnection con = store.getConnection(true, false)) { + return filterObjectsForReview(con.getGroupsForReview(principal)); + } + } + + ReviewObjects filterObjectsForReview(ReviewObjects reviewObjects) { + + List roles = reviewObjects.getList(); + if (roles == null || roles.isEmpty()) { + return reviewObjects; + } + List filteredRoles = new ArrayList<>(); + long now = System.currentTimeMillis(); + for (ReviewObject role : roles) { + + // if the role hasn't been reviewed before then we're going to add it to our list always + + if (role.getLastReviewedDate() == null) { + filteredRoles.add(role); + continue; + } + + // determine the lowest number of days that is configured for any of the objects in our list + + int minDays = minReviewDays(role); + + // we want to review before 1/3 period is left since the last review date. For example, + // if the min review period is 30 days then we want to review before 10 days. If the review + // period is 90 days, then we want to review before 30 days. We should never get a review + // period of 0 days since the connection store must return only objects where one of the + // expiry/review dates is not 0. + + if (now - role.getLastReviewedDate().millis() > ((long) minDays * 24 * 60 * 60 * 1000 / 3)) { + filteredRoles.add(role); + } + } + + reviewObjects.setList(filteredRoles); + return reviewObjects; + } + + int minReviewDay(int minDays, int checkDays) { + return (checkDays != 0 && checkDays < minDays) ? checkDays : minDays; + } + + int minReviewDays(ReviewObject object) { + int minDays = Integer.MAX_VALUE; + minDays = minReviewDay(minDays, object.getMemberExpiryDays()); + minDays = minReviewDay(minDays, object.getServiceExpiryDays()); + minDays = minReviewDay(minDays, object.getGroupExpiryDays()); + minDays = minReviewDay(minDays, object.getMemberReviewDays()); + minDays = minReviewDay(minDays, object.getServiceReviewDays()); + return minReviewDay(minDays, object.getGroupReviewDays()); + } + AthenzDomain getAthenzDomainFromLocalCache(Map domainCache, final String domainName) { // first we're going to check our local cache and if we find diff --git a/servers/zms/src/main/java/com/yahoo/athenz/zms/ZMSHandler.java b/servers/zms/src/main/java/com/yahoo/athenz/zms/ZMSHandler.java index 2774ae7a73b..0ef37c54546 100644 --- a/servers/zms/src/main/java/com/yahoo/athenz/zms/ZMSHandler.java +++ b/servers/zms/src/main/java/com/yahoo/athenz/zms/ZMSHandler.java @@ -132,6 +132,8 @@ public interface ZMSHandler { ServiceIdentityList getDependentServiceList(ResourceContext context, String domainName); DependentServiceResourceGroupList getDependentServiceResourceGroupList(ResourceContext context, String domainName); DomainList getDependentDomainList(ResourceContext context, String service); + ReviewObjects getRolesForReview(ResourceContext context, String principal); + ReviewObjects getGroupsForReview(ResourceContext context, String principal); Info getInfo(ResourceContext context); Schema getRdlSchema(ResourceContext context); ResourceContext newResourceContext(ServletContext servletContext, HttpServletRequest request, HttpServletResponse response, String apiName); diff --git a/servers/zms/src/main/java/com/yahoo/athenz/zms/ZMSImpl.java b/servers/zms/src/main/java/com/yahoo/athenz/zms/ZMSImpl.java index 060bc234e36..ea3afafb095 100644 --- a/servers/zms/src/main/java/com/yahoo/athenz/zms/ZMSImpl.java +++ b/servers/zms/src/main/java/com/yahoo/athenz/zms/ZMSImpl.java @@ -11102,4 +11102,80 @@ public List getGroupMembers(String groupName) { return group.getGroupMembers(); } } + + @Override + public ReviewObjects getRolesForReview(ResourceContext ctx, String principal) { + + final String caller = ctx.getApiName(); + logPrincipal(ctx); + + // If principal not specified, get roles for current user + + if (StringUtil.isEmpty(principal)) { + principal = ((RsrcCtxWrapper) ctx).principal().getFullName(); + } + validateRequest(ctx.request(), caller); + validate(principal, TYPE_RESOURCE_NAME, caller); + + // for consistent handling of all requests, we're going to convert + // all incoming object values into lower case + + principal = principal.toLowerCase(); + + if (!isAllowedObjectReviewLookup(((RsrcCtxWrapper) ctx).principal(), principal)) { + throw ZMSUtils.forbiddenError("principal is not authorized to request role review list", caller); + } + + return dbService.getRolesForReview(principal); + } + + @Override + public ReviewObjects getGroupsForReview(ResourceContext ctx, String principal) { + + final String caller = ctx.getApiName(); + logPrincipal(ctx); + + // If principal not specified, get groups for current user + + if (StringUtil.isEmpty(principal)) { + principal = ((RsrcCtxWrapper) ctx).principal().getFullName(); + } + validateRequest(ctx.request(), caller); + validate(principal, TYPE_RESOURCE_NAME, caller); + + // for consistent handling of all requests, we're going to convert + // all incoming object values into lower case + + principal = principal.toLowerCase(); + + if (!isAllowedObjectReviewLookup(((RsrcCtxWrapper) ctx).principal(), principal)) { + throw ZMSUtils.forbiddenError("principal is not authorized to request group review list", caller); + } + + return dbService.getGroupsForReview(principal); + } + + boolean isAllowedObjectReviewLookup(Principal principal, final String checkPrincipal) { + + // Role/Group Review list lookup requires one of these authorization checks + // 1. authenticated principal is the same as the check principal + // 2. system authorized ("access", "sys.auth:meta.review.lookup") + + if (checkPrincipal.equals(principal.getFullName())) { + return true; + } + + // if the check principal is another user, then this is not allowed + // unless the principal is authorized at system level + + AthenzDomain domain = getAthenzDomain(SYS_AUTH, true); + + // evaluate our domain's roles and policies to see if access + // is allowed or not for the given operation and resource + // our action are always converted to lowercase + + AccessStatus accessStatus = evaluateAccess(domain, principal.getFullName(), "access", + "sys.auth:meta.review.lookup", null, null, principal); + return accessStatus == AccessStatus.ALLOWED; + } } diff --git a/servers/zms/src/main/java/com/yahoo/athenz/zms/ZMSResources.java b/servers/zms/src/main/java/com/yahoo/athenz/zms/ZMSResources.java index 18eb3fdd948..e188e56f8d3 100644 --- a/servers/zms/src/main/java/com/yahoo/athenz/zms/ZMSResources.java +++ b/servers/zms/src/main/java/com/yahoo/athenz/zms/ZMSResources.java @@ -4482,6 +4482,74 @@ public DomainList getDependentDomainList( } } + @GET + @Path("/review/role") + @Produces(MediaType.APPLICATION_JSON) + @Operation(description = "Fetch all the roles across domains for either the caller or specified principal that require a review based on the last reviewed date and configured attributes. The method requires the caller to be either the principal or authorized in system to carry out the operation for any principal (typically this would be system administrators) 1. authenticated principal is the same as the check principal 2. system authorized (\"access\", \"sys.auth:meta.review.lookup\")") + public ReviewObjects getRolesForReview( + @Parameter(description = "If not present, will return roles for the user making the call", required = false) @QueryParam("principal") String principal) { + int code = ResourceException.OK; + ResourceContext context = null; + try { + context = this.delegate.newResourceContext(this.servletContext, this.request, this.response, "getRolesForReview"); + context.authenticate(); + return this.delegate.getRolesForReview(context, principal); + } catch (ResourceException e) { + code = e.getCode(); + switch (code) { + case ResourceException.BAD_REQUEST: + throw typedException(code, e, ResourceError.class); + case ResourceException.FORBIDDEN: + throw typedException(code, e, ResourceError.class); + case ResourceException.NOT_FOUND: + throw typedException(code, e, ResourceError.class); + case ResourceException.TOO_MANY_REQUESTS: + throw typedException(code, e, ResourceError.class); + case ResourceException.UNAUTHORIZED: + throw typedException(code, e, ResourceError.class); + default: + System.err.println("*** Warning: undeclared exception (" + code + ") for resource getRolesForReview"); + throw typedException(code, e, ResourceError.class); + } + } finally { + this.delegate.recordMetrics(context, code); + } + } + + @GET + @Path("/review/group") + @Produces(MediaType.APPLICATION_JSON) + @Operation(description = "Fetch all the groups across domains for either the caller or specified principal that require a review based on the last reviewed date and configured attributes. The method requires the caller to be either the principal or authorized in system to carry out the operation for any principal (typically this would be system administrators) 1. authenticated principal is the same as the check principal 2. system authorized (\"access\", \"sys.auth:meta.review.lookup\")") + public ReviewObjects getGroupsForReview( + @Parameter(description = "If not present, will return groups for the user making the call", required = false) @QueryParam("principal") String principal) { + int code = ResourceException.OK; + ResourceContext context = null; + try { + context = this.delegate.newResourceContext(this.servletContext, this.request, this.response, "getGroupsForReview"); + context.authenticate(); + return this.delegate.getGroupsForReview(context, principal); + } catch (ResourceException e) { + code = e.getCode(); + switch (code) { + case ResourceException.BAD_REQUEST: + throw typedException(code, e, ResourceError.class); + case ResourceException.FORBIDDEN: + throw typedException(code, e, ResourceError.class); + case ResourceException.NOT_FOUND: + throw typedException(code, e, ResourceError.class); + case ResourceException.TOO_MANY_REQUESTS: + throw typedException(code, e, ResourceError.class); + case ResourceException.UNAUTHORIZED: + throw typedException(code, e, ResourceError.class); + default: + System.err.println("*** Warning: undeclared exception (" + code + ") for resource getGroupsForReview"); + throw typedException(code, e, ResourceError.class); + } + } finally { + this.delegate.recordMetrics(context, code); + } + } + @GET @Path("/sys/info") @Produces(MediaType.APPLICATION_JSON) diff --git a/servers/zms/src/main/java/com/yahoo/athenz/zms/store/ObjectStoreConnection.java b/servers/zms/src/main/java/com/yahoo/athenz/zms/store/ObjectStoreConnection.java index a7cc55a78d5..d0cba0951d4 100644 --- a/servers/zms/src/main/java/com/yahoo/athenz/zms/store/ObjectStoreConnection.java +++ b/servers/zms/src/main/java/com/yahoo/athenz/zms/store/ObjectStoreConnection.java @@ -189,6 +189,11 @@ public interface ObjectStoreConnection extends Closeable { Stats getStats(String domainName); + // Review commands + + ReviewObjects getRolesForReview(String principal); + ReviewObjects getGroupsForReview(String principal); + Map> getPendingDomainRoleMembersByPrincipal(String principal); Map> getPendingDomainRoleMembersByDomain(String domainName); Map> getExpiredPendingDomainRoleMembers(int pendingRoleMemberLifespan); 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 0b452ba2485..8b17e00bddb 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 @@ -662,6 +662,23 @@ public class JDBCConnection implements ObjectStoreConnection { + " WHERE review_last_notified_time IS NOT NULL ORDER BY review_last_notified_time DESC LIMIT 1;"; private static final String SQL_GROUP_EXPIRY_LAST_NOTIFIED_TIME = "SELECT last_notified_time FROM principal_group_member" + " WHERE last_notified_time IS NOT NULL ORDER BY last_notified_time DESC LIMIT 1;"; + private static final String SQL_GET_ROLE_REVIEW_LIST = "SELECT domain.name AS domain_name, role.name AS role_name," + + " role.member_expiry_days, role.service_expiry_days, role.group_expiry_days, role.member_review_days," + + " role.service_review_days, role.group_review_days, role.last_reviewed_time FROM role" + + " JOIN domain ON role.domain_id=domain.domain_id WHERE role.trust='' AND" + + " (role.member_expiry_days!=0 OR role.service_expiry_days!=0 OR role.group_expiry_days!=0 OR" + + " role.member_review_days!=0 OR role.service_review_days!=0 OR role.group_review_days!=0) AND" + + " role.domain_id IN (SELECT domain.domain_id FROM domain JOIN role ON role.domain_id=domain.domain_id" + + " JOIN role_member ON role.role_id=role_member.role_id WHERE role_member.principal_id=? AND" + + " role_member.active=true AND role.name='admin') ORDER BY domain.name, role.name;"; + private static final String SQL_GET_GROUP_REVIEW_LIST = "SELECT domain.name AS domain_name, principal_group.name AS group_name," + + " principal_group.member_expiry_days, principal_group.service_expiry_days, principal_group.last_reviewed_time" + + " FROM principal_group JOIN domain ON principal_group.domain_id=domain.domain_id WHERE" + + " (principal_group.member_expiry_days!=0 OR principal_group.service_expiry_days!=0) AND" + + " principal_group.domain_id IN (SELECT domain.domain_id FROM domain JOIN role ON" + + " role.domain_id=domain.domain_id JOIN role_member ON role.role_id=role_member.role_id" + + " WHERE role_member.principal_id=? AND role_member.active=true AND role.name='admin')" + + " ORDER BY domain.name, principal_group.name;"; private static final String CACHE_DOMAIN = "d:"; private static final String CACHE_ROLE = "r:"; @@ -7591,4 +7608,74 @@ boolean isLastNotifyTimeWithinSpecifiedDays(final String sqlCmd, int delayDays) } return System.currentTimeMillis() - lastRunTime < TimeUnit.DAYS.toMillis(delayDays); } + + @Override + public ReviewObjects getRolesForReview(String principal) { + + final String caller = "getRolesForReview"; + + int principalId = getPrincipalId(principal); + if (principalId == 0) { + throw notFoundError(caller, ZMSConsts.OBJECT_PRINCIPAL, principal); + } + + List reviewRoles = new ArrayList<>(); + try (PreparedStatement ps = con.prepareStatement(SQL_GET_ROLE_REVIEW_LIST)) { + ps.setInt(1, principalId); + try (ResultSet rs = executeQuery(ps, caller)) { + while (rs.next()) { + ReviewObject reviewObject = new ReviewObject() + .setDomainName(rs.getString(ZMSConsts.DB_COLUMN_DOMAIN_NAME)) + .setName(rs.getString(DB_COLUMN_AS_ROLE_NAME)) + .setMemberExpiryDays(rs.getInt(ZMSConsts.DB_COLUMN_MEMBER_EXPIRY_DAYS)) + .setServiceExpiryDays(rs.getInt(ZMSConsts.DB_COLUMN_SERVICE_EXPIRY_DAYS)) + .setGroupExpiryDays(rs.getInt(ZMSConsts.DB_COLUMN_GROUP_EXPIRY_DAYS)) + .setMemberReviewDays(rs.getInt(ZMSConsts.DB_COLUMN_MEMBER_REVIEW_DAYS)) + .setServiceReviewDays(rs.getInt(ZMSConsts.DB_COLUMN_SERVICE_REVIEW_DAYS)) + .setGroupReviewDays(rs.getInt(ZMSConsts.DB_COLUMN_GROUP_REVIEW_DAYS)); + java.sql.Timestamp lastReviewedTime = rs.getTimestamp(ZMSConsts.DB_COLUMN_LAST_REVIEWED_TIME); + if (lastReviewedTime != null) { + reviewObject.setLastReviewedDate(Timestamp.fromMillis(lastReviewedTime.getTime())); + } + reviewRoles.add(reviewObject); + } + } + } catch (SQLException ex) { + throw sqlError(ex, caller); + } + return new ReviewObjects().setList(reviewRoles); + } + + @Override + public ReviewObjects getGroupsForReview(String principal) { + + final String caller = "getGroupsForReview"; + + int principalId = getPrincipalId(principal); + if (principalId == 0) { + throw notFoundError(caller, ZMSConsts.OBJECT_PRINCIPAL, principal); + } + + List reviewRoles = new ArrayList<>(); + try (PreparedStatement ps = con.prepareStatement(SQL_GET_GROUP_REVIEW_LIST)) { + ps.setInt(1, principalId); + try (ResultSet rs = executeQuery(ps, caller)) { + while (rs.next()) { + ReviewObject reviewObject = new ReviewObject() + .setDomainName(rs.getString(ZMSConsts.DB_COLUMN_DOMAIN_NAME)) + .setName(rs.getString(DB_COLUMN_AS_GROUP_NAME)) + .setMemberExpiryDays(rs.getInt(ZMSConsts.DB_COLUMN_MEMBER_EXPIRY_DAYS)) + .setServiceExpiryDays(rs.getInt(ZMSConsts.DB_COLUMN_SERVICE_EXPIRY_DAYS)); + java.sql.Timestamp lastReviewedTime = rs.getTimestamp(ZMSConsts.DB_COLUMN_LAST_REVIEWED_TIME); + if (lastReviewedTime != null) { + reviewObject.setLastReviewedDate(Timestamp.fromMillis(lastReviewedTime.getTime())); + } + reviewRoles.add(reviewObject); + } + } + } catch (SQLException ex) { + throw sqlError(ex, caller); + } + return new ReviewObjects().setList(reviewRoles); + } } 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 98937eda3d0..55f7a900b26 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 @@ -12722,4 +12722,76 @@ public void testGetDelegatedRoleNamesNullCases() { assertNull(zms.dbService.getDelegatedRoleNames(cache, "unknown-domain:role.trust-role", "trust-domain")); } + + @Test + public void testMinReviewDays() { + ReviewObject object = new ReviewObject().setServiceReviewDays(50); + assertEquals(zms.dbService.minReviewDays(object), 50); + + object.setMemberReviewDays(60); + assertEquals(zms.dbService.minReviewDays(object), 50); + object.setMemberReviewDays(45); + assertEquals(zms.dbService.minReviewDays(object), 45); + + object.setGroupReviewDays(46); + assertEquals(zms.dbService.minReviewDays(object), 45); + object.setGroupReviewDays(40); + assertEquals(zms.dbService.minReviewDays(object), 40); + + object.setMemberExpiryDays(41); + assertEquals(zms.dbService.minReviewDays(object), 40); + object.setMemberExpiryDays(35); + assertEquals(zms.dbService.minReviewDays(object), 35); + + object.setServiceExpiryDays(36); + assertEquals(zms.dbService.minReviewDays(object), 35); + object.setServiceExpiryDays(30); + assertEquals(zms.dbService.minReviewDays(object), 30); + + object.setGroupExpiryDays(31); + assertEquals(zms.dbService.minReviewDays(object), 30); + object.setMemberReviewDays(25); + assertEquals(zms.dbService.minReviewDays(object), 25); + } + + @Test + public void testFilterObjectsForReview() { + + ReviewObjects reviewObjects = new ReviewObjects(); + assertEquals(zms.dbService.filterObjectsForReview(reviewObjects), reviewObjects); + + reviewObjects.setList(Collections.emptyList()); + assertEquals(zms.dbService.filterObjectsForReview(reviewObjects), reviewObjects); + + // we're going to create three objects in our list + // object1 - no last reviewed date, so it must be included + // object2 - longer than min review days, so it must be excluded + // object2 - shorter than min review days, so it must be included + + List reviewObjectList = new ArrayList<>(); + reviewObjects.setList(reviewObjectList); + + ReviewObject object1 = new ReviewObject().setDomainName("domain1").setName("object1") + .setMemberExpiryDays(90); + reviewObjectList.add(object1); + + ReviewObject object2 = new ReviewObject().setDomainName("domain2").setName("object2") + .setMemberExpiryDays(90).setMemberReviewDays(120) + .setServiceReviewDays(90).setGroupReviewDays(120) + .setGroupReviewDays(110).setGroupReviewDays(150) + .setLastReviewedDate(Timestamp.fromMillis(System.currentTimeMillis() - 1000L * 60 * 60 * 24 * 30 + 60 * 1000)); + reviewObjectList.add(object2); + + ReviewObject object3 = new ReviewObject().setDomainName("domain3").setName("object3") + .setMemberExpiryDays(90).setMemberReviewDays(120) + .setServiceReviewDays(90).setGroupReviewDays(120) + .setGroupReviewDays(110).setGroupReviewDays(150) + .setLastReviewedDate(Timestamp.fromMillis(System.currentTimeMillis() - 1000L * 60 * 60 * 24 * 30 - 60 * 1000)); + reviewObjectList.add(object3); + + ReviewObjects filterObjects = zms.dbService.filterObjectsForReview(reviewObjects); + assertEquals(filterObjects.getList().size(), 2); + assertEquals(filterObjects.getList().get(0), object1); + assertEquals(filterObjects.getList().get(1), object3); + } } \ No newline at end of file diff --git a/servers/zms/src/test/java/com/yahoo/athenz/zms/ZMSObjectReviewTest.java b/servers/zms/src/test/java/com/yahoo/athenz/zms/ZMSObjectReviewTest.java new file mode 100644 index 00000000000..8190ee3b8d4 --- /dev/null +++ b/servers/zms/src/test/java/com/yahoo/athenz/zms/ZMSObjectReviewTest.java @@ -0,0 +1,363 @@ +/* + * 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; + +import com.yahoo.athenz.auth.Authority; +import com.yahoo.athenz.auth.Principal; +import com.yahoo.athenz.auth.impl.SimplePrincipal; +import org.mockito.MockitoAnnotations; +import org.testng.annotations.AfterClass; +import org.testng.annotations.BeforeClass; +import org.testng.annotations.BeforeMethod; +import org.testng.annotations.Test; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import static org.testng.Assert.*; + +public class ZMSObjectReviewTest { + + 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 testIsAllowedObjectReviewLookup() { + + Principal principal = getPrincipal("user", "john"); + assertNotNull(principal); + + ZMSImpl zmsImpl = zmsTestInitializer.getZms(); + RsrcCtxWrapper ctx = zmsTestInitializer.getMockDomRsrcCtx(); + final String auditRef = zmsTestInitializer.getAuditRef(); + + // without any setup, the principal will only work if the checkPrincipal + // matches the principal + + assertTrue(zmsImpl.isAllowedObjectReviewLookup(principal, "user.john")); + assertFalse(zmsImpl.isAllowedObjectReviewLookup(principal, "user.jane")); + + // invalid principals should return failure + + assertFalse(zmsImpl.isAllowedObjectReviewLookup(principal, "unknown-domain")); + + // asking for a domain that doesn't exist, must return failure + + assertFalse(zmsImpl.isAllowedObjectReviewLookup(principal, "unknown-domain.service")); + + // now let's set up the user as system role lookup user + + List roleMembers = new ArrayList<>(); + roleMembers.add(new RoleMember().setMemberName("user.john")); + + Role role = zmsTestInitializer.createRoleObject("sys.auth", "review-role", null, roleMembers); + zmsImpl.putRole(ctx, "sys.auth", "review-role", auditRef, false, role); + + Policy policy = zmsTestInitializer.createPolicyObject("sys.auth", "review-policy", "review-role", + "access", "sys.auth:meta.review.lookup", AssertionEffect.ALLOW); + zmsImpl.putPolicy(ctx, "sys.auth", "review-policy", auditRef, false, policy); + + // now our access check should work + + assertTrue(zmsImpl.isAllowedObjectReviewLookup(principal, "user.jane")); + + zmsImpl.deletePolicy(ctx, "sys.auth", "review-policy", auditRef); + zmsImpl.deleteRole(ctx, "sys.auth", "review-role", auditRef); + } + + private Principal getPrincipal(final String domainName, final String userName) { + Authority principalAuthority = new com.yahoo.athenz.common.server.debug.DebugPrincipalAuthority(); + final String unsignedCreds = "v=U1;d=" + domainName + ";n=" + userName; + return SimplePrincipal.create(domainName, userName, unsignedCreds + ";s=signature", 0, principalAuthority); + } + + @Test + public void testGetRolesForReviewUnauthorized() { + Principal principal = getPrincipal("user", "john"); + assertNotNull(principal); + + ZMSImpl zmsImpl = zmsTestInitializer.getZms(); + ResourceContext rsrcCtx1 = zmsTestInitializer.createResourceContext(principal); + + try { + zmsImpl.getRolesForReview(rsrcCtx1, zmsTestInitializer.getAdminUser()); + fail(); + } catch (ResourceException ex) { + assertEquals(ResourceException.FORBIDDEN, ex.getCode()); + } + } + + @Test + public void testGetRolesForReview() { + + Principal principal = getPrincipal("user", "john"); + assertNotNull(principal); + + createDomain("domain1", principal.getFullName()); + createDomain("domain2", principal.getFullName()); + createDomain("domain3", principal.getFullName()); + + ZMSImpl zmsImpl = zmsTestInitializer.getZms(); + RsrcCtxWrapper ctx = zmsTestInitializer.getMockDomRsrcCtx(); + final String auditRef = zmsTestInitializer.getAuditRef(); + + ResourceContext rsrcCtx1 = zmsTestInitializer.createResourceContext(principal); + + insertRecordsForRoleReviewTest(principal.getFullName()); + + // our roles without any config are not going to be returned + + ReviewObjects reviewObjects = zmsImpl.getRolesForReview(rsrcCtx1, null); + assertNotNull(reviewObjects); + assertNotNull(reviewObjects.getList()); + assertEquals(reviewObjects.getList().size(), 0); + + reviewObjects = zmsImpl.getRolesForReview(rsrcCtx1, principal.getFullName()); + assertNotNull(reviewObjects); + assertNotNull(reviewObjects.getList()); + assertEquals(reviewObjects.getList().size(), 0); + + // now let us set up 2 of the roles with expiry settings and + // make sure both of them are returned without any review date + + RoleMeta meta = new RoleMeta().setMemberExpiryDays(30).setServiceExpiryDays(60); + zmsImpl.putRoleMeta(rsrcCtx1, "domain1", "role1", auditRef, meta); + + meta = new RoleMeta().setMemberReviewDays(30); + zmsImpl.putRoleMeta(rsrcCtx1, "domain3", "role1", auditRef, meta); + + // we should get back our 2 roles in domain1 and domain3 + + reviewObjects = zmsImpl.getRolesForReview(rsrcCtx1, principal.getFullName()); + assertNotNull(reviewObjects); + assertNotNull(reviewObjects.getList()); + assertEquals(reviewObjects.getList().size(), 2); + + assertTrue(verifyReviewObjectExists(reviewObjects, "domain1", "role1")); + assertTrue(verifyReviewObjectExists(reviewObjects, "domain3", "role1")); + + // we're going to set last reviewed date on the role in domain1 to current + // value thus it should not be returned in our list + + Role role = new Role().setName("domain1:role.role1").setRoleMembers(Collections.emptyList()); + zmsImpl.putRoleReview(rsrcCtx1, "domain1", "role1", auditRef, false, role); + + // we should get back our domain3 role only + + reviewObjects = zmsImpl.getRolesForReview(rsrcCtx1, principal.getFullName()); + assertNotNull(reviewObjects); + assertNotNull(reviewObjects.getList()); + assertEquals(reviewObjects.getList().size(), 1); + + assertTrue(verifyReviewObjectExists(reviewObjects, "domain3", "role1")); + + zmsImpl.deleteTopLevelDomain(ctx,"domain1", auditRef); + zmsImpl.deleteTopLevelDomain(ctx,"domain2", auditRef); + zmsImpl.deleteTopLevelDomain(ctx,"domain3", auditRef); + } + + boolean verifyReviewObjectExists(ReviewObjects objects, final String domainName, final String objectName) { + for (ReviewObject object : objects.getList()) { + if (object.getDomainName().equals(domainName) && object.getName().equals(objectName)) { + return true; + } + } + return false; + } + + @Test + public void testGetGroupsForReviewUnauthorized() { + Principal principal = getPrincipal("user", "john"); + assertNotNull(principal); + + ZMSImpl zmsImpl = zmsTestInitializer.getZms(); + ResourceContext rsrcCtx1 = zmsTestInitializer.createResourceContext(principal); + + try { + zmsImpl.getGroupsForReview(rsrcCtx1, zmsTestInitializer.getAdminUser()); + fail(); + } catch (ResourceException ex) { + assertEquals(ResourceException.FORBIDDEN, ex.getCode()); + } + } + + @Test + public void testGetGroupsForReview() { + + Principal principal = getPrincipal("user", "john"); + assertNotNull(principal); + + createDomain("domain1", principal.getFullName()); + createDomain("domain2", principal.getFullName()); + createDomain("domain3", principal.getFullName()); + + ZMSImpl zmsImpl = zmsTestInitializer.getZms(); + RsrcCtxWrapper ctx = zmsTestInitializer.getMockDomRsrcCtx(); + final String auditRef = zmsTestInitializer.getAuditRef(); + + ResourceContext rsrcCtx1 = zmsTestInitializer.createResourceContext(principal); + + insertRecordsForGroupReviewTest(principal.getFullName()); + + // our roles without any config are not going to be returned + + ReviewObjects reviewObjects = zmsImpl.getGroupsForReview(rsrcCtx1, null); + assertNotNull(reviewObjects); + assertNotNull(reviewObjects.getList()); + assertEquals(reviewObjects.getList().size(), 0); + + reviewObjects = zmsImpl.getGroupsForReview(rsrcCtx1, principal.getFullName()); + assertNotNull(reviewObjects); + assertNotNull(reviewObjects.getList()); + assertEquals(reviewObjects.getList().size(), 0); + + // now let us setup 2 of the groups with expiry settings and + // make sure both of them are returned without any review date + + GroupMeta meta = new GroupMeta().setMemberExpiryDays(30).setServiceExpiryDays(60); + zmsImpl.putGroupMeta(rsrcCtx1, "domain1", "group1", auditRef, meta); + + meta = new GroupMeta().setServiceExpiryDays(30); + zmsImpl.putGroupMeta(rsrcCtx1, "domain3", "group1", auditRef, meta); + + // we should get back our 2 groups in domain1 and domain3 + + reviewObjects = zmsImpl.getGroupsForReview(rsrcCtx1, principal.getFullName()); + assertNotNull(reviewObjects); + assertNotNull(reviewObjects.getList()); + assertEquals(reviewObjects.getList().size(), 2); + + assertTrue(verifyReviewObjectExists(reviewObjects, "domain1", "group1")); + assertTrue(verifyReviewObjectExists(reviewObjects, "domain3", "group1")); + + // we're going to set last reviewed date on the group in domain1 to current + // value thus it should not be returned in our list + + Group group = new Group().setName("domain1:group.group1").setGroupMembers(Collections.emptyList()); + zmsImpl.putGroupReview(rsrcCtx1, "domain1", "group1", auditRef, false, group); + + // we should get back our domain3 group only + + reviewObjects = zmsImpl.getGroupsForReview(rsrcCtx1, principal.getFullName()); + assertNotNull(reviewObjects); + assertNotNull(reviewObjects.getList()); + assertEquals(reviewObjects.getList().size(), 1); + + assertTrue(verifyReviewObjectExists(reviewObjects, "domain3", "group1")); + + zmsImpl.deleteTopLevelDomain(ctx,"domain1", auditRef); + zmsImpl.deleteTopLevelDomain(ctx,"domain2", auditRef); + zmsImpl.deleteTopLevelDomain(ctx,"domain3", auditRef); + } + + private void insertRecordsForRoleReviewTest(final String principal) { + + ZMSImpl zmsImpl = zmsTestInitializer.getZms(); + RsrcCtxWrapper ctx = zmsTestInitializer.getMockDomRsrcCtx(); + final String auditRef = zmsTestInitializer.getAuditRef(); + + List roleMembers = new ArrayList<>(); + roleMembers.add(new RoleMember().setMemberName("user.test1")); + roleMembers.add(new RoleMember().setMemberName("user.test2")); + roleMembers.add(new RoleMember().setMemberName(principal)); + + // Create role1 in domain1 with members and principal + Role role = zmsTestInitializer.createRoleObject("domain1", "role1", null, roleMembers); + zmsImpl.putRole(ctx, "domain1", "Role1", auditRef, false, role); + + // Create role2 in domain1 with members and principal + role = zmsTestInitializer.createRoleObject("domain1", "role2", null, roleMembers); + zmsImpl.putRole(ctx, "domain1", "Role2", auditRef, false, role); + + roleMembers = new ArrayList<>(); + roleMembers.add(new RoleMember().setMemberName("user.test1")); + roleMembers.add(new RoleMember().setMemberName("user.test2")); + + // Create role1 in domain2 with members but without the principal + role = zmsTestInitializer.createRoleObject("domain2", "role1", null, roleMembers); + zmsImpl.putRole(ctx, "domain2", "Role1", auditRef, false, role); + + roleMembers = new ArrayList<>(); + roleMembers.add(new RoleMember().setMemberName(principal)); + + // Create role1 in domain3 only principal + role = zmsTestInitializer.createRoleObject("domain3", "role1", null, roleMembers); + zmsImpl.putRole(ctx, "domain3", "role1", auditRef, false, role); + } + + private void insertRecordsForGroupReviewTest(final String principal) { + + ZMSImpl zmsImpl = zmsTestInitializer.getZms(); + RsrcCtxWrapper ctx = zmsTestInitializer.getMockDomRsrcCtx(); + final String auditRef = zmsTestInitializer.getAuditRef(); + + List groupMembers = new ArrayList<>(); + groupMembers.add(new GroupMember().setMemberName("user.test1")); + groupMembers.add(new GroupMember().setMemberName("user.test2")); + groupMembers.add(new GroupMember().setMemberName(principal)); + + // Create role1 in domain1 with members and principal + Group group = zmsTestInitializer.createGroupObject("domain1", "group1", groupMembers); + zmsImpl.putGroup(ctx, "domain1", "group1", auditRef, false, group); + + // Create role2 in domain1 with members and principal + group = zmsTestInitializer.createGroupObject("domain1", "group2", groupMembers); + zmsImpl.putGroup(ctx, "domain1", "group2", auditRef, false, group); + + groupMembers = new ArrayList<>(); + groupMembers.add(new GroupMember().setMemberName("user.test1")); + groupMembers.add(new GroupMember().setMemberName("user.test2")); + + // Create role1 in domain2 with members but without the principal + group = zmsTestInitializer.createGroupObject("domain2", "group1", groupMembers); + zmsImpl.putGroup(ctx, "domain2", "group1", auditRef, false, group); + + groupMembers = new ArrayList<>(); + groupMembers.add(new GroupMember().setMemberName(principal)); + + // Create role1 in domain3 only principal + group = zmsTestInitializer.createGroupObject("domain3", "group1", groupMembers); + zmsImpl.putGroup(ctx, "domain3", "group1", auditRef, false, group); + } + + private void createDomain(final String domainName, final String principal) { + ZMSImpl zmsImpl = zmsTestInitializer.getZms(); + RsrcCtxWrapper ctx = zmsTestInitializer.getMockDomRsrcCtx(); + final String auditRef = zmsTestInitializer.getAuditRef(); + + TopLevelDomain dom = zmsTestInitializer.createTopLevelDomainObject(domainName, + "Test " + domainName, "testOrg", zmsTestInitializer.getAdminUser()); + dom.getAdminUsers().add(principal); + zmsImpl.postTopLevelDomain(ctx, auditRef, dom); + } +} diff --git a/servers/zms/src/test/java/com/yahoo/athenz/zms/ZMSPrincipalRolesTest.java b/servers/zms/src/test/java/com/yahoo/athenz/zms/ZMSPrincipalRolesTest.java index 83b34d1fdd4..2abc12e1c67 100644 --- a/servers/zms/src/test/java/com/yahoo/athenz/zms/ZMSPrincipalRolesTest.java +++ b/servers/zms/src/test/java/com/yahoo/athenz/zms/ZMSPrincipalRolesTest.java @@ -488,7 +488,6 @@ public void testIsAllowedExpandedRoleLookup() { assertTrue(zmsImpl.isAllowedExpandedRoleLookup(principal, domainName + ".api", null)); // clean up our system domain - zmsImpl.deletePolicy(ctx, "sys.auth", "service-policy", auditRef); zmsImpl.deleteRole(ctx, "sys.auth", "service-role", auditRef); 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 b8af59d0ddc..b0080835531 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 @@ -15315,4 +15315,144 @@ public void testDependencyExceptions() throws Exception { } jdbcConn.close(); } + + @Test + public void testGetRolesForReview() throws SQLException { + + // first result set is for principal id lookup + Mockito.when(mockResultSet.next()).thenReturn(true).thenReturn(true).thenReturn(true).thenReturn(false); + Mockito.when(mockResultSet.getInt(1)).thenReturn(10); // for principal id + Mockito.when(mockResultSet.getString(ZMSConsts.DB_COLUMN_DOMAIN_NAME)).thenReturn("domain1").thenReturn("domain2"); + Mockito.when(mockResultSet.getString(ZMSConsts.DB_COLUMN_AS_ROLE_NAME)).thenReturn("role1").thenReturn("role2"); + Mockito.when(mockResultSet.getInt(ZMSConsts.DB_COLUMN_MEMBER_EXPIRY_DAYS)).thenReturn(11).thenReturn(12); + Mockito.when(mockResultSet.getInt(ZMSConsts.DB_COLUMN_SERVICE_EXPIRY_DAYS)).thenReturn(21).thenReturn(22); + Mockito.when(mockResultSet.getInt(ZMSConsts.DB_COLUMN_GROUP_EXPIRY_DAYS)).thenReturn(31).thenReturn(32); + Mockito.when(mockResultSet.getInt(ZMSConsts.DB_COLUMN_MEMBER_REVIEW_DAYS)).thenReturn(41).thenReturn(42); + Mockito.when(mockResultSet.getInt(ZMSConsts.DB_COLUMN_SERVICE_REVIEW_DAYS)).thenReturn(51).thenReturn(52); + Mockito.when(mockResultSet.getInt(ZMSConsts.DB_COLUMN_GROUP_REVIEW_DAYS)).thenReturn(61).thenReturn(62); + Mockito.when(mockResultSet.getTimestamp(ZMSConsts.DB_COLUMN_LAST_REVIEWED_TIME)) + .thenReturn(new java.sql.Timestamp(101)).thenReturn(null); + + JDBCConnection jdbcConn = new JDBCConnection(mockConn, true); + ReviewObjects reviewObjects = jdbcConn.getRolesForReview("user.joe"); + assertNotNull(reviewObjects); + List objects = reviewObjects.getList(); + assertNotNull(objects); + assertEquals(objects.size(), 2); + + ReviewObject object1 = new ReviewObject().setDomainName("domain1").setName("role1").setMemberExpiryDays(11) + .setServiceExpiryDays(21).setGroupExpiryDays(31).setMemberReviewDays(41).setServiceReviewDays(51) + .setGroupReviewDays(61).setLastReviewedDate(Timestamp.fromMillis(101)); + assertEquals(objects.get(0), object1); + + ReviewObject object2 = new ReviewObject().setDomainName("domain2").setName("role2").setMemberExpiryDays(12) + .setServiceExpiryDays(22).setGroupExpiryDays(32).setMemberReviewDays(42).setServiceReviewDays(52) + .setGroupReviewDays(62); + assertEquals(objects.get(1), object2); + } + + @Test + public void testGetRolesForReviewInvalidPrincipal() throws Exception { + + JDBCConnection jdbcConn = new JDBCConnection(mockConn, true); + + // first query is for the principal id + Mockito.when(mockPrepStmt.executeQuery()).thenThrow(new SQLException("failed operation", "state", 1001)); + + try { + jdbcConn.getRolesForReview("user.joe"); + fail(); + } catch (ResourceException ex) { + assertEquals(ex.getCode(), ResourceException.NOT_FOUND); + } + + jdbcConn.close(); + } + + @Test + public void testGetRolesForReviewFailure() throws Exception { + + // first result set is for principal id lookup + Mockito.when(mockResultSet.next()).thenReturn(true) + .thenThrow(new SQLException("failed operation", "state", 1001)); + Mockito.when(mockResultSet.getInt(1)).thenReturn(10); // for principal id + + JDBCConnection jdbcConn = new JDBCConnection(mockConn, true); + + try { + jdbcConn.getRolesForReview("user.joe"); + fail(); + } catch (ResourceException ex) { + assertEquals(ex.getCode(), ResourceException.INTERNAL_SERVER_ERROR); + } + + jdbcConn.close(); + } + + @Test + public void testGetGroupsForReview() throws SQLException { + + // first result set is for principal id lookup + Mockito.when(mockResultSet.next()).thenReturn(true).thenReturn(true).thenReturn(true).thenReturn(false); + Mockito.when(mockResultSet.getInt(1)).thenReturn(10); // for principal id + Mockito.when(mockResultSet.getString(ZMSConsts.DB_COLUMN_DOMAIN_NAME)).thenReturn("domain1").thenReturn("domain2"); + Mockito.when(mockResultSet.getString(ZMSConsts.DB_COLUMN_AS_GROUP_NAME)).thenReturn("group1").thenReturn("group2"); + Mockito.when(mockResultSet.getInt(ZMSConsts.DB_COLUMN_MEMBER_EXPIRY_DAYS)).thenReturn(11).thenReturn(12); + Mockito.when(mockResultSet.getInt(ZMSConsts.DB_COLUMN_SERVICE_EXPIRY_DAYS)).thenReturn(21).thenReturn(22); + Mockito.when(mockResultSet.getTimestamp(ZMSConsts.DB_COLUMN_LAST_REVIEWED_TIME)) + .thenReturn(new java.sql.Timestamp(101)).thenReturn(null); + + JDBCConnection jdbcConn = new JDBCConnection(mockConn, true); + ReviewObjects reviewObjects = jdbcConn.getGroupsForReview("user.joe"); + assertNotNull(reviewObjects); + List objects = reviewObjects.getList(); + assertNotNull(objects); + assertEquals(objects.size(), 2); + + ReviewObject object1 = new ReviewObject().setDomainName("domain1").setName("group1").setMemberExpiryDays(11) + .setServiceExpiryDays(21).setLastReviewedDate(Timestamp.fromMillis(101)); + assertEquals(objects.get(0), object1); + + ReviewObject object2 = new ReviewObject().setDomainName("domain2").setName("group2").setMemberExpiryDays(12) + .setServiceExpiryDays(22); + assertEquals(objects.get(1), object2); + } + + @Test + public void testGetGroupsForReviewInvalidPrincipal() throws Exception { + + JDBCConnection jdbcConn = new JDBCConnection(mockConn, true); + + // first query is for the principal id + Mockito.when(mockPrepStmt.executeQuery()).thenThrow(new SQLException("failed operation", "state", 1001)); + + try { + jdbcConn.getGroupsForReview("user.joe"); + fail(); + } catch (ResourceException ex) { + assertEquals(ex.getCode(), ResourceException.NOT_FOUND); + } + + jdbcConn.close(); + } + + @Test + public void testGetGroupsForReviewFailure() throws Exception { + + // first result set is for principal id lookup + Mockito.when(mockResultSet.next()).thenReturn(true) + .thenThrow(new SQLException("failed operation", "state", 1001)); + Mockito.when(mockResultSet.getInt(1)).thenReturn(10); // for principal id + + JDBCConnection jdbcConn = new JDBCConnection(mockConn, true); + + try { + jdbcConn.getGroupsForReview("user.joe"); + fail(); + } catch (ResourceException ex) { + assertEquals(ex.getCode(), ResourceException.INTERNAL_SERVER_ERROR); + } + + jdbcConn.close(); + } }