From d2ac728d5df718e9377511f9b57d5f0072ce3d7e Mon Sep 17 00:00:00 2001 From: Henry Avetisyan Date: Wed, 11 Feb 2026 00:04:06 -0800 Subject: [PATCH 1/2] A utility to retrieve and report authorization history dependencies for services in a specified Athenz domain. Signed-off-by: Henry Avetisyan --- pom.xml | 1 + utils/zms-authhistory/Makefile | 58 +++++ utils/zms-authhistory/README.md | 95 ++++++++ utils/zms-authhistory/doc.go | 8 + utils/zms-authhistory/pom.xml | 73 ++++++ utils/zms-authhistory/zms-authhistory.go | 271 +++++++++++++++++++++++ 6 files changed, 506 insertions(+) create mode 100644 utils/zms-authhistory/Makefile create mode 100644 utils/zms-authhistory/README.md create mode 100644 utils/zms-authhistory/doc.go create mode 100644 utils/zms-authhistory/pom.xml create mode 100644 utils/zms-authhistory/zms-authhistory.go diff --git a/pom.xml b/pom.xml index 63c806469ae..d78b8046f92 100644 --- a/pom.xml +++ b/pom.xml @@ -212,6 +212,7 @@ utils/athenz-conf utils/zms-domainattrs utils/zms-svctoken + utils/zms-authhistory utils/zpe-updater utils/zts-roletoken utils/zts-accesstoken diff --git a/utils/zms-authhistory/Makefile b/utils/zms-authhistory/Makefile new file mode 100644 index 00000000000..375a4edf05e --- /dev/null +++ b/utils/zms-authhistory/Makefile @@ -0,0 +1,58 @@ +# +# Makefile to build ZMS Token Access 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-authhistory +PKG_DATE=$(shell date '+%Y-%m-%dT%H:%M:%S') +BINARY=zms-authhistory +SRC=zms-authhistory.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-authhistory/README.md b/utils/zms-authhistory/README.md new file mode 100644 index 00000000000..b986a11521d --- /dev/null +++ b/utils/zms-authhistory/README.md @@ -0,0 +1,95 @@ +zms-authhistory +=============== + +A utility to retrieve and report authorization history dependencies for services in a specified Athenz domain. It connects to the ZMS server using mTLS authentication and generates a report showing both **incoming** and **outgoing** dependencies for each service in the domain. + +- **Outgoing dependencies**: Services in your domain that have accessed resources in other domains (which domains they call). +- **Incoming dependencies**: Principals from other domains that have accessed resources in your domain (who calls your domain). + +## Usage + +``` +zms-authhistory -domain -zms -svc-key-file -svc-cert-file [-svc-cacert-file ] [-days ] [-domains-only] +``` + +### Required Options + +| Option | Description | +|--------|-------------| +| `-domain` | Athenz domain name to report on | +| `-zms` | ZMS server URL (e.g. `https://athenz.io:4443/zms/v1`) | +| `-svc-key-file` | Service identity private key file (PEM) | +| `-svc-cert-file` | Service identity certificate file (PEM) | + +### Optional Options + +| Option | Description | +|--------|-------------| +| `-svc-cacert-file` | CA certificates file for verifying the ZMS server | +| `-days` | Number of days to look back; records older than this are ignored (0 = no filter) | +| `-domains-only` | For dependencies, show only the domain name (no service name) | +| `-version` | Print version and exit | + +### Example + +```bash +zms-authhistory -domain mydomain -zms https://athenz.example.com:4443/zms/v1 \ + -svc-key-file /path/to/key.pem -svc-cert-file /path/to/cert.pem \ + -svc-cacert-file /path/to/ca.pem -days 30 +``` + +With `-domains-only` to get a compact list of domains only: + +```bash +zms-authhistory -domain mydomain -zms https://athenz.example.com:4443/zms/v1 \ + -svc-key-file ./key.pem -svc-cert-file ./cert.pem -domains-only -days 7 +``` + +## Output + +The report is printed as CSV to stdout. + +**Default format** (with service names): + +- **Outgoing**: `Service,Target-Domain,Last-Access` — services in your domain and which external domains they accessed. +- **Incoming**: `Source-Domain,Source-Service,Last-Access` — external principals that accessed your domain. + +**With `-domains-only`**: + +- **Outgoing**: `Target-Domain,Last-Access` +- **Incoming**: `Source-Domain,Last-Access` + +Example (default): + +``` +Service,Target-Domain,Last-Access +api,other.domain,2025-02-10T12:00:00Z +api,third.domain,2025-02-09T08:30:00Z +worker,other.domain,2025-02-08T14:00:00Z + +Source-Domain,Source-Service,Last-Access +caller.domain,frontend,2025-02-10T11:00:00Z +caller.domain,ingest,2025-02-09T09:00:00Z +``` + +## Building + +Prerequisites: Go 1.19 or newer. + +```bash +# Build for current OS and run checks +make + +# Build for specific platforms +make darwin # target/darwin/zms-authhistory +make linux # target/linux/zms-authhistory + +# Clean build artifacts +make clean +``` + +## 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-authhistory/doc.go b/utils/zms-authhistory/doc.go new file mode 100644 index 00000000000..e46f6a29ca2 --- /dev/null +++ b/utils/zms-authhistory/doc.go @@ -0,0 +1,8 @@ +// Copyright The Athenz Authors +// Licensed under the terms of the Apache version 2.0 license. See LICENSE file for terms. + +// zms-authhistory is a utility program to retrieve and report authorization +// history dependencies for services in a specified domain. It connects to +// the ZMS server using mTLS authentication and generates a report showing +// both incoming and outgoing dependencies for each service in the domain. +package main diff --git a/utils/zms-authhistory/pom.xml b/utils/zms-authhistory/pom.xml new file mode 100644 index 00000000000..f1f14b3bc61 --- /dev/null +++ b/utils/zms-authhistory/pom.xml @@ -0,0 +1,73 @@ + + + + 4.0.0 + + + com.yahoo.athenz + athenz + 1.12.35-SNAPSHOT + ../../pom.xml + + + zms-authhistory + jar + zms-authhistory + Utility to report all auth dependencies + + + 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-authhistory/zms-authhistory.go b/utils/zms-authhistory/zms-authhistory.go new file mode 100644 index 00000000000..1743e165c17 --- /dev/null +++ b/utils/zms-authhistory/zms-authhistory.go @@ -0,0 +1,271 @@ +// 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" + "sort" + "time" + + "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 printVersion() { + if VERSION == "" { + fmt.Println("zms-authhistory (development version)") + } else { + fmt.Println("zms-authhistory " + VERSION + " " + BUILD_DATE) + } +} + +// ServiceDependency represents a dependency for a service +type ServiceDependency struct { + Domain string + Service string + LastAccess string +} + +// ServiceReport represents the report for a single service +type ServiceReport struct { + ServiceName string + OutgoingDependencies []ServiceDependency +} + +func main() { + var domain, zmsURL, keyFile, certFile, caCertFile string + var showVersion, domainsOnly bool + var days int + flag.StringVar(&domain, "domain", "", "domain name") + flag.StringVar(&zmsURL, "zms", "", "ZMS server URL") + flag.StringVar(&keyFile, "svc-key-file", "", "service identity private key file") + flag.StringVar(&certFile, "svc-cert-file", "", "service identity certificate file") + flag.StringVar(&caCertFile, "svc-cacert-file", "", "CA Certificates file") + flag.BoolVar(&showVersion, "version", false, "Show version") + flag.BoolVar(&domainsOnly, "domains-only", false, "For dependencies, only show the domain without the service name") + flag.IntVar(&days, "days", 0, "Number of days to look back (ignore records older than this)") + flag.Parse() + + if showVersion { + printVersion() + return + } + + if domain == "" || zmsURL == "" || keyFile == "" || certFile == "" { + log.Fatalln("usage: zms-authhistory -domain -zms -svc-key-file -svc-cert-file [-svc-cacert-file ] [-days ]") + } + + // Create ZMS client with mTLS + client, err := athenzutils.ZmsClient(zmsURL, keyFile, certFile, caCertFile, false) + if err != nil { + log.Fatalf("Failed to create ZMS client: %v\n", err) + } + + // Call GetAuthHistoryDependencies API + authHistory, err := client.GetAuthHistoryDependencies(zms.DomainName(domain)) + if err != nil { + log.Fatalf("Failed to get auth history dependencies: %v\n", err) + } + + // Process dependencies and generate report + outgoing, incoming := processDependencies(domain, authHistory, days, domainsOnly) + + // Print report + printReport(outgoing, incoming, domainsOnly) +} + +func processDependencies(targetDomain string, authHistory *zms.AuthHistoryDependencies, days int, domainsOnly bool) (map[string]*ServiceReport, []ServiceDependency) { + serviceReports := make(map[string]*ServiceReport) + + // Calculate cutoff time if days filter is specified + var cutoffTime time.Time + if days > 0 { + cutoffTime = time.Now().UTC().AddDate(0, 0, -days) + } + + // Process incoming dependencies + incomingMap := make(map[string]ServiceDependency) + + if authHistory.IncomingDependencies != nil { + for _, dep := range authHistory.IncomingDependencies { + // Skip if timestamp is before cutoff + if days > 0 && dep.Timestamp != nil { + if dep.Timestamp.Time.Before(cutoffTime) { + continue + } + } + + // Skip if the principal domain points back to itself + if dep.PrincipalDomain == zms.DomainName(targetDomain) { + continue + } + + // Create unique key for deduplication: accessing service + var depKey string + if domainsOnly { + depKey = string(dep.PrincipalDomain) + } else { + depKey = string(dep.PrincipalDomain) + "." + string(dep.PrincipalName) + } + + // Keep the most recent entry if duplicate + existing, exists := incomingMap[depKey] + if !exists || (dep.Timestamp != nil && existing.LastAccess < dep.Timestamp.String()) { + timestamp := "" + if dep.Timestamp != nil { + timestamp = dep.Timestamp.String() + } + incomingMap[depKey] = ServiceDependency{ + Domain: string(dep.PrincipalDomain), + Service: string(dep.PrincipalName), + LastAccess: timestamp, + } + } + } + } + + // Process outgoing dependencies + // These are services from the target domain accessing resources in other domains + // PrincipalDomain == targetDomain, PrincipalName is the service making the access + outgoingMap := make(map[string]map[string]ServiceDependency) + + if authHistory.OutgoingDependencies != nil { + for _, dep := range authHistory.OutgoingDependencies { + // Skip if timestamp is before cutoff + if days > 0 && dep.Timestamp != nil { + if dep.Timestamp.Time.Before(cutoffTime) { + continue + } + } + + if dep.PrincipalDomain == zms.DomainName(targetDomain) && dep.PrincipalName != "" { + // Skip if the URI domain points back to itself + if dep.UriDomain == zms.DomainName(targetDomain) { + continue + } + serviceName := string(dep.PrincipalName) + + // Create unique key for deduplication: uriDomain + depKey := string(dep.UriDomain) + + if outgoingMap[serviceName] == nil { + outgoingMap[serviceName] = make(map[string]ServiceDependency) + } + + // Keep the most recent entry if duplicate + existing, exists := outgoingMap[serviceName][depKey] + if !exists || (dep.Timestamp != nil && existing.LastAccess < dep.Timestamp.String()) { + timestamp := "" + if dep.Timestamp != nil { + timestamp = dep.Timestamp.String() + } + outgoingMap[serviceName][depKey] = ServiceDependency{ + Domain: string(dep.UriDomain), + Service: "", // For outgoing, we don't have a target service name + LastAccess: timestamp, + } + } + } + } + } + + allServices := make(map[string]bool) + for serviceName := range outgoingMap { + allServices[serviceName] = true + } + + // Build service reports + for serviceName := range allServices { + report := &ServiceReport{ + ServiceName: serviceName, + OutgoingDependencies: make([]ServiceDependency, 0), + } + + // Add outgoing dependencies + if outgoingDeps, exists := outgoingMap[serviceName]; exists { + for _, dep := range outgoingDeps { + report.OutgoingDependencies = append(report.OutgoingDependencies, dep) + } + } + + sort.Slice(report.OutgoingDependencies, func(i, j int) bool { + return report.OutgoingDependencies[i].Domain < report.OutgoingDependencies[j].Domain + }) + + // Only add services that have at least one dependency + if len(report.OutgoingDependencies) > 0 { + serviceReports[serviceName] = report + } + } + + // Convert incomingMap to a slice and sort by Domain, then Service + incomingSlice := make([]ServiceDependency, 0, len(incomingMap)) + for _, dep := range incomingMap { + incomingSlice = append(incomingSlice, dep) + } + sort.Slice(incomingSlice, func(i, j int) bool { + if incomingSlice[i].Domain != incomingSlice[j].Domain { + return incomingSlice[i].Domain < incomingSlice[j].Domain + } + return incomingSlice[i].Service < incomingSlice[j].Service + }) + + return serviceReports, incomingSlice +} + +func printReport(outgoing map[string]*ServiceReport, incoming []ServiceDependency, domainsOnly bool) { + if len(outgoing) == 0 && len(incoming) == 0 { + fmt.Println("No services with dependencies found.") + return + } + + // Sort service names for consistent output + serviceNames := make([]string, 0, len(outgoing)) + for serviceName := range outgoing { + serviceNames = append(serviceNames, serviceName) + } + sort.Strings(serviceNames) + + if domainsOnly { + fmt.Println("\nTarget-Domain,Last-Access") + } else { + fmt.Println("\nService,Target-Domain,Last-Access") + } + for _, serviceName := range serviceNames { + report := outgoing[serviceName] + if len(report.OutgoingDependencies) > 0 { + for _, dep := range report.OutgoingDependencies { + if domainsOnly { + fmt.Printf("%s,%s\n", dep.Domain, dep.LastAccess) + } else { + fmt.Printf("%s,%s,%s\n", report.ServiceName, dep.Domain, dep.LastAccess) + } + } + } + } + + if len(incoming) > 0 { + if domainsOnly { + fmt.Println("\nSource-Domain,Last-Access") + for _, dep := range incoming { + fmt.Printf("%s,%s\n", dep.Domain, dep.LastAccess) + } + } else { + fmt.Println("\nSource-Domain,Source-Service,Last-Access") + for _, dep := range incoming { + fmt.Printf("%s,%s,%s\n", dep.Domain, dep.Service, dep.LastAccess) + } + } + } +} From ce1bee17e779997a4df6837b853253e8fc37a732 Mon Sep 17 00:00:00 2001 From: Henry Avetisyan Date: Wed, 11 Feb 2026 13:21:33 -0800 Subject: [PATCH 2/2] address review comments Signed-off-by: Henry Avetisyan --- .gitignore | 3 + utils/zms-authhistory/zms-authhistory.go | 76 +++++++++++------------- 2 files changed, 39 insertions(+), 40 deletions(-) diff --git a/.gitignore b/.gitignore index 339a0e2f679..4c740594a65 100644 --- a/.gitignore +++ b/.gitignore @@ -97,6 +97,9 @@ utils/hostdoc/target/ utils/zms-svctoken/bin/ utils/zms-svctoken/pkg/ utils/zms-svctoken/src/ +utils/zms-authhistory/bin/ +utils/zms-authhistory/pkg/ +utils/zms-authhistory/src/ utils/zpe-updater/pkg/ utils/zpe-updater/src/ utils/zpe-updater/bin/ diff --git a/utils/zms-authhistory/zms-authhistory.go b/utils/zms-authhistory/zms-authhistory.go index 1743e165c17..4b6605223a5 100644 --- a/utils/zms-authhistory/zms-authhistory.go +++ b/utils/zms-authhistory/zms-authhistory.go @@ -69,13 +69,13 @@ func main() { // Create ZMS client with mTLS client, err := athenzutils.ZmsClient(zmsURL, keyFile, certFile, caCertFile, false) if err != nil { - log.Fatalf("Failed to create ZMS client: %v\n", err) + log.Fatalf("Failed to create ZMS client: %v", err) } // Call GetAuthHistoryDependencies API authHistory, err := client.GetAuthHistoryDependencies(zms.DomainName(domain)) if err != nil { - log.Fatalf("Failed to get auth history dependencies: %v\n", err) + log.Fatalf("Failed to get auth history dependencies: %v", err) } // Process dependencies and generate report @@ -180,33 +180,22 @@ func processDependencies(targetDomain string, authHistory *zms.AuthHistoryDepend } } - allServices := make(map[string]bool) - for serviceName := range outgoingMap { - allServices[serviceName] = true - } - // Build service reports - for serviceName := range allServices { + for serviceName, outgoingDeps := range outgoingMap { + if len(outgoingDeps) == 0 { + continue + } report := &ServiceReport{ ServiceName: serviceName, - OutgoingDependencies: make([]ServiceDependency, 0), + OutgoingDependencies: make([]ServiceDependency, 0, len(outgoingDeps)), } - - // Add outgoing dependencies - if outgoingDeps, exists := outgoingMap[serviceName]; exists { - for _, dep := range outgoingDeps { - report.OutgoingDependencies = append(report.OutgoingDependencies, dep) - } + for _, dep := range outgoingDeps { + report.OutgoingDependencies = append(report.OutgoingDependencies, dep) } - sort.Slice(report.OutgoingDependencies, func(i, j int) bool { return report.OutgoingDependencies[i].Domain < report.OutgoingDependencies[j].Domain }) - - // Only add services that have at least one dependency - if len(report.OutgoingDependencies) > 0 { - serviceReports[serviceName] = report - } + serviceReports[serviceName] = report } // Convert incomingMap to a slice and sort by Domain, then Service @@ -237,33 +226,40 @@ func printReport(outgoing map[string]*ServiceReport, incoming []ServiceDependenc } sort.Strings(serviceNames) - if domainsOnly { - fmt.Println("\nTarget-Domain,Last-Access") - } else { - fmt.Println("\nService,Target-Domain,Last-Access") - } - for _, serviceName := range serviceNames { - report := outgoing[serviceName] - if len(report.OutgoingDependencies) > 0 { - for _, dep := range report.OutgoingDependencies { - if domainsOnly { - fmt.Printf("%s,%s\n", dep.Domain, dep.LastAccess) - } else { - fmt.Printf("%s,%s,%s\n", report.ServiceName, dep.Domain, dep.LastAccess) + if len(outgoing) > 0 { + if domainsOnly { + fmt.Println("Target-Domain,Last-Access") + } else { + fmt.Println("Service,Target-Domain,Last-Access") + } + + for _, serviceName := range serviceNames { + report := outgoing[serviceName] + if len(report.OutgoingDependencies) > 0 { + for _, dep := range report.OutgoingDependencies { + if domainsOnly { + fmt.Printf("%s,%s\n", dep.Domain, dep.LastAccess) + } else { + fmt.Printf("%s,%s,%s\n", report.ServiceName, dep.Domain, dep.LastAccess) + } } } } } if len(incoming) > 0 { + if len(outgoing) > 0 { + fmt.Println() + } if domainsOnly { - fmt.Println("\nSource-Domain,Last-Access") - for _, dep := range incoming { - fmt.Printf("%s,%s\n", dep.Domain, dep.LastAccess) - } + fmt.Println("Source-Domain,Last-Access") } else { - fmt.Println("\nSource-Domain,Source-Service,Last-Access") - for _, dep := range incoming { + fmt.Println("Source-Domain,Source-Service,Last-Access") + } + for _, dep := range incoming { + if domainsOnly { + fmt.Printf("%s,%s\n", dep.Domain, dep.LastAccess) + } else { fmt.Printf("%s,%s,%s\n", dep.Domain, dep.Service, dep.LastAccess) } }