diff --git a/.gitignore b/.gitignore
index ce3cda5189e..ad196ec7ea4 100644
--- a/.gitignore
+++ b/.gitignore
@@ -109,6 +109,9 @@ utils/zts-svccert/src/
utils/zts-accesstoken/bin/
utils/zts-accesstoken/pkg/
utils/zts-accesstoken/src/
+utils/zms-domainattrs/bin/
+utils/zms-domainattrs/pkg/
+utils/zms-domainattrs/src/
utils/zts-idtoken/bin/
utils/zts-idtoken/pkg/
utils/zts-idtoken/src/
diff --git a/assembly/utils/utils.xml b/assembly/utils/utils.xml
index 6baaadfd3f7..43bf56a5261 100644
--- a/assembly/utils/utils.xml
+++ b/assembly/utils/utils.xml
@@ -34,6 +34,10 @@
${basedir}/../../utils/zms-svctoken/target
bin
+
+ ${basedir}/../../utils/zms-domainattrs/target
+ bin
+
${basedir}/../../utils/zts-roletoken/target
bin
diff --git a/clients/go/zms/model.go b/clients/go/zms/model.go
index b3dad5b5d21..2ba06e29682 100644
--- a/clients/go/zms/model.go
+++ b/clients/go/zms/model.go
@@ -3429,6 +3429,13 @@ type ServiceIdentities struct {
// list of services
//
List []*ServiceIdentity `json:"list"`
+
+ //
+ // if set, the value indicates the total number of services in the system that
+ // match the query criteria but not returned due to limit constraints; thus, the
+ // result in the list is a partial set.
+ //
+ ServiceMatchCount int64 `json:"serviceMatchCount"`
}
// NewServiceIdentities - creates an initialized ServiceIdentities instance, returns a pointer to it
diff --git a/clients/go/zms/zms_schema.go b/clients/go/zms/zms_schema.go
index befb10284de..6d3bfce5979 100644
--- a/clients/go/zms/zms_schema.go
+++ b/clients/go/zms/zms_schema.go
@@ -446,6 +446,7 @@ func init() {
tServiceIdentities := rdl.NewStructTypeBuilder("Struct", "ServiceIdentities")
tServiceIdentities.Comment("The representation of list of services")
tServiceIdentities.ArrayField("list", "ServiceIdentity", false, "list of services")
+ tServiceIdentities.Field("serviceMatchCount", "Int64", false, nil, "if set, the value indicates the total number of services in the system that match the query criteria but not returned due to limit constraints; thus, the result in the list is a partial set.")
sb.AddType(tServiceIdentities.Build())
tServiceIdentityList := rdl.NewStructTypeBuilder("Struct", "ServiceIdentityList")
diff --git a/pom.xml b/pom.xml
index 6c9b3355508..29f6eca4b4a 100644
--- a/pom.xml
+++ b/pom.xml
@@ -203,6 +203,7 @@
provider/harness/sia-harness
utils/zms-cli
utils/athenz-conf
+ utils/zms-domainattrs
utils/zms-svctoken
utils/zpe-updater
utils/zts-roletoken
@@ -499,6 +500,7 @@
libs/go/athenzconf
utils/zms-cli
utils/athenz-conf
+ utils/zms-domainattrs
utils/zms-svctoken
utils/zpe-updater
utils/zts-roletoken
diff --git a/utils/zms-domainattrs/Makefile b/utils/zms-domainattrs/Makefile
new file mode 100644
index 00000000000..5682ff9262f
--- /dev/null
+++ b/utils/zms-domainattrs/Makefile
@@ -0,0 +1,58 @@
+#
+# Makefile to build ZMS Domain Attributes utility
+# Prerequisite: Go development environment
+#
+# Copyright The Athenz Authors
+# Licensed under the Apache License, Version 2.0 - http://www.apache.org/licenses/LICENSE-2.0
+#
+
+GOPKGNAME = github.com/AthenZ/athenz/utils/zms-domainattrs
+PKG_DATE=$(shell date '+%Y-%m-%dT%H:%M:%S')
+BINARY=zms-domainattrs
+SRC=zms-domainattrs.go
+
+# check to see if go utility is installed
+GO := $(shell command -v go 2> /dev/null)
+GOPATH := $(shell pwd)
+export $(GOPATH)
+
+ifdef GO
+
+# we need to make sure we have go 1.19+
+# the output for the go version command is:
+# go version go1.19 darwin/amd64
+
+GO_VER_GTEQ := $(shell expr `go version | cut -f 3 -d' ' | cut -f2 -d.` \>= 19)
+ifneq "$(GO_VER_GTEQ)" "1"
+all:
+ @echo "Please install 1.19.x or newer version of golang"
+else
+
+.PHONY: vet fmt linux darwin
+all: vet fmt linux darwin
+
+endif
+
+else
+
+all:
+ @echo "go is not available please install golang"
+
+endif
+
+vet:
+ go vet .
+
+fmt:
+ go fmt .
+
+darwin:
+ @echo "Building darwin client..."
+ GOOS=darwin go build -ldflags "-X main.VERSION=$(PKG_VERSION) -X main.BUILD_DATE=$(PKG_DATE)" -o target/darwin/$(BINARY) $(SRC)
+
+linux:
+ @echo "Building linux client..."
+ GOOS=linux go build -ldflags "-X main.VERSION=$(PKG_VERSION) -X main.BUILD_DATE=$(PKG_DATE)" -o target/linux/$(BINARY) $(SRC)
+
+clean:
+ rm -rf target
diff --git a/utils/zms-domainattrs/README.md b/utils/zms-domainattrs/README.md
new file mode 100644
index 00000000000..e5779153e7b
--- /dev/null
+++ b/utils/zms-domainattrs/README.md
@@ -0,0 +1,58 @@
+zms-domainattrs
+===============
+
+The utility looks at the domains specified in a given file (one domain per line) and
+for each domain, it retrieves and displays the requested attributes associated with
+the domain.
+
+The utility supports the following list of attributes:
+
+- businessService
+- productId
+- account
+- gcpProject
+- gcpProjectNumber
+- azureSubscription
+- azureTenant
+- azureClient
+- org
+- slackChannel
+- environment
+
+For businessService and productId attributes, if the given domain does not have the
+attribute set, the utility will look at the parent domain to see if the attribute
+is set there. If the attribute is set in the parent domain, the utility will display
+the value from the parent domain. It will continue to look at the parent domains until
+it finds the attribute set, or it reaches the top level domain.
+
+## Usage
+
+```
+zms-domainattrs -svc-key-file ./key.pem -svc-cert-file ./cert.pem -zms https://athenz.io:4443/zms/v1 -domain-file ./domain.txt -attrs businessService,account
+```
+
+where domain.txt might contain:
+
+```
+weather
+sports.prod
+sports.nhl
+sys.auth
+```
+
+And the output might look like ('weather' domain does not have a businessService attribute and
+'sports.prod' domain does not have an account attribute):
+
+```
+Domain,businessService,account
+weather,,123456
+sports.prod,sports-service,
+sports.nhl,sports-service,123456
+sys.auth,athenz,456789
+```
+
+## License
+
+Copyright The Athenz Authors
+
+Licensed under the [Apache License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0)
diff --git a/utils/zms-domainattrs/doc.go b/utils/zms-domainattrs/doc.go
new file mode 100644
index 00000000000..3f6be9fd429
--- /dev/null
+++ b/utils/zms-domainattrs/doc.go
@@ -0,0 +1,7 @@
+// Copyright The Athenz Authors
+// Licensed under the terms of the Apache version 2.0 license. See LICENSE file for terms.
+
+// The utility looks at the domains specified in a given file (one domain per line) and
+// for each domain, it retrieves and displays the requested attributes associated with
+// the domain.
+package main
diff --git a/utils/zms-domainattrs/pom.xml b/utils/zms-domainattrs/pom.xml
new file mode 100644
index 00000000000..31294319ec4
--- /dev/null
+++ b/utils/zms-domainattrs/pom.xml
@@ -0,0 +1,73 @@
+
+
+
+ 4.0.0
+
+
+ com.yahoo.athenz
+ athenz
+ 1.12.5-SNAPSHOT
+ ../../pom.xml
+
+
+ zms-domainattrs
+ jar
+ zms-domainattrs
+ ZMS Domain Attribute Lookup Utility
+
+
+ true
+ true
+
+
+
+
+
+ org.codehaus.mojo
+ exec-maven-plugin
+ ${maven-exec-plugin.version}
+
+
+
+ exec
+
+ compile
+
+
+
+ make
+
+ PKG_VERSION=${project.parent.version}
+ clean
+ all
+
+
+
+
+ org.apache.maven.plugins
+ maven-jar-plugin
+ ${maven-jar-plugin.version}
+
+
+ default-jar
+
+
+
+
+
+
+
+
diff --git a/utils/zms-domainattrs/zms-domainattrs.go b/utils/zms-domainattrs/zms-domainattrs.go
new file mode 100644
index 00000000000..05387c24d44
--- /dev/null
+++ b/utils/zms-domainattrs/zms-domainattrs.go
@@ -0,0 +1,194 @@
+// Copyright The Athenz Authors
+// Licensed under the terms of the Apache version 2.0 license. See LICENSE file for terms.
+
+package main
+
+import (
+ "flag"
+ "fmt"
+ "log"
+ "os"
+ "strings"
+
+ "github.com/AthenZ/athenz/clients/go/zms"
+ "github.com/AthenZ/athenz/libs/go/athenzutils"
+)
+
+var (
+ // VERSION gets set by the build script via the LDFLAGS.
+ VERSION string
+
+ // BUILD_DATE gets set by the build script via the LDFLAGS.
+ BUILD_DATE string
+)
+
+func usage() {
+ fmt.Println("usage: zms-domainattrs -svc-key-file -svc-cert-file -zms -domain-file [-attrs ]")
+ os.Exit(1)
+}
+
+func printVersion() {
+ if VERSION == "" {
+ fmt.Println("zms-domainattrs (development version)")
+ } else {
+ fmt.Println("zms-domainattrs " + VERSION + " " + BUILD_DATE)
+ }
+}
+
+func main() {
+ var domainFile, attrs, svcKeyFile, svcCertFile, svcCACertFile, zmsURL string
+ var showVersion bool
+ flag.StringVar(&domainFile, "domain-file", "", "domain file with list of domains")
+ flag.StringVar(&svcCACertFile, "svc-cacert-file", "", "CA Certificates file")
+ flag.StringVar(&svcKeyFile, "svc-key-file", "", "service identity private key file")
+ flag.StringVar(&svcCertFile, "svc-cert-file", "", "service identity certificate file")
+ flag.StringVar(&zmsURL, "zms", "", "url of the ZMS Service")
+ flag.StringVar(&attrs, "attrs", "businessService,productId,account,gcpProject,gcpProjectNumber,azureSubscription,azureTenant,azureClient,org,slackChannel,environment", "comma separated list of domain attribute names")
+ flag.BoolVar(&showVersion, "version", false, "Show version")
+ flag.Parse()
+
+ if showVersion {
+ printVersion()
+ return
+ }
+
+ if domainFile == "" || svcKeyFile == "" || svcCertFile == "" || zmsURL == "" {
+ usage()
+ }
+
+ // first get the list of domains from the file
+
+ domains, err := getDomainList(domainFile)
+ if err != nil {
+ log.Fatalf("unable to get domain list from file: %s error: %v\n", domainFile, err)
+ }
+
+ fetchDomainAttrs(zmsURL, svcKeyFile, svcCertFile, svcCACertFile, domains, attrs)
+}
+
+func getDomainList(domainFile string) ([]string, error) {
+
+ bytes, err := os.ReadFile(domainFile)
+ if err != nil {
+ return nil, err
+ }
+
+ return strings.Split(string(bytes), "\n"), nil
+}
+
+func fetchDomainAttrs(zmsURL, svcKeyFile, svcCertFile, svcCACertFile string, domains []string, attrs string) {
+
+ client, err := athenzutils.ZmsClient(zmsURL, svcKeyFile, svcCertFile, svcCACertFile, false)
+ if err != nil {
+ log.Fatalf("unable to create zms client: %v\n", err)
+ }
+
+ signedDomains, _, err := client.GetSignedDomains("", "true", "all", nil, nil, "")
+ if err != nil {
+ log.Fatalf("unable to fetch domains with list of attributes from ZMS: %v\n", err)
+ }
+
+ // put the results in a map
+
+ domainMap := make(map[string]*zms.SignedDomain, len(signedDomains.Domains))
+ for _, signedDomain := range signedDomains.Domains {
+ domainMap[string(signedDomain.Domain.Name)] = signedDomain
+ }
+
+ // convert the attributes to list
+
+ attrList := strings.Split(attrs, ",")
+
+ // write the header line
+
+ fmt.Print("Domain")
+ for _, attr := range attrList {
+ fmt.Print("," + attr)
+ }
+ fmt.Println()
+
+ // go through the list of domains and print the requested attributes
+
+ for _, domain := range domains {
+
+ if domain == "" {
+ continue
+ }
+
+ fmt.Print(domain)
+ signedDomain, ok := domainMap[domain]
+ if !ok {
+ fmt.Println(",")
+ continue
+ }
+
+ // now go through the list of attributes and print the values
+
+ for _, attr := range attrList {
+ attrName := strings.ToLower(attr)
+ attrVal := getDomainAttributeValue(signedDomain.Domain, attrName)
+ if attrVal == "" && isRecursiveAttribute(attrName) {
+ attrVal = getDomainAttributeValueRecursive(domain, domainMap, attrName)
+ }
+ fmt.Print("," + attrVal)
+ }
+ fmt.Println()
+ }
+ fmt.Println()
+}
+
+func getParentDomainName(domainName string) string {
+ idx := strings.LastIndex(domainName, ".")
+ if idx == -1 {
+ return ""
+ }
+ return domainName[:idx]
+}
+
+func getDomainAttributeValueRecursive(domainName string, domainMap map[string]*zms.SignedDomain, attrName string) string {
+ // first get the parent domain name
+ parentDomainName := getParentDomainName(domainName)
+ if parentDomainName == "" {
+ return ""
+ }
+ signedDomain, ok := domainMap[parentDomainName]
+ if !ok {
+ return ""
+ }
+ attrValue := getDomainAttributeValue(signedDomain.Domain, attrName)
+ if attrValue == "" {
+ attrValue = getDomainAttributeValueRecursive(parentDomainName, domainMap, attrName)
+ }
+ return attrValue
+}
+
+func isRecursiveAttribute(attrName string) bool {
+ return attrName == "businessservice" || attrName == "productid"
+}
+
+func getDomainAttributeValue(domainData *zms.DomainData, attrName string) string {
+ switch attrName {
+ case "businessservice":
+ return domainData.BusinessService
+ case "productid":
+ return domainData.ProductId
+ case "account":
+ return domainData.Account
+ case "gcpproject":
+ return domainData.GcpProject
+ case "gcpprojectnumber":
+ return domainData.GcpProjectNumber
+ case "azuresubscription":
+ return domainData.AzureSubscription
+ case "azuretenant":
+ return domainData.AzureTenant
+ case "azureclient":
+ return domainData.AzureClient
+ case "org":
+ return string(domainData.Org)
+ case "slackchannel":
+ return domainData.SlackChannel
+ }
+
+ return ""
+}