From 121d3e8d0f29870ded5234ecd407e057c4d87713 Mon Sep 17 00:00:00 2001 From: Fergal Mc Carthy Date: Tue, 30 Jul 2024 11:06:53 -0400 Subject: [PATCH] Add cmd/authenticator and misc command tweaks Add an authenticator command that will register, and re-authenticate, a telemetry client system, and will report the current auth token's issuer and expiration date. Extended TelemtryClient with methods to get the ExpirationDate() and Issuer() for the currently active token. Added internal helper methods to support this. Quietened some log messages by switching them to debug level and tweaked some others to be more useful. Added output messages to cmd/generator describing the steps that it has performed. Similarly updated cmd/clientds to say that nothing was found if the client datastore is empty. Note that this change pulls support for JWT processing into the client side library. --- cmd/authenticator/go.mod | 15 +++++ cmd/authenticator/go.sum | 18 ++++++ cmd/authenticator/main.go | 124 ++++++++++++++++++++++++++++++++++++++ cmd/clientds/go.mod | 1 + cmd/clientds/go.sum | 2 + cmd/clientds/main.go | 23 +++++-- cmd/generator/go.mod | 1 + cmd/generator/go.sum | 2 + cmd/generator/main.go | 14 +++++ go.mod | 1 + go.sum | 2 + pkg/client/client.go | 96 +++++++++++++++++++++++++---- pkg/config/config.go | 2 +- pkg/logging/logging.go | 2 +- 14 files changed, 283 insertions(+), 20 deletions(-) create mode 100644 cmd/authenticator/go.mod create mode 100644 cmd/authenticator/go.sum create mode 100644 cmd/authenticator/main.go diff --git a/cmd/authenticator/go.mod b/cmd/authenticator/go.mod new file mode 100644 index 0000000..ab35f5d --- /dev/null +++ b/cmd/authenticator/go.mod @@ -0,0 +1,15 @@ +module github.com/SUSE/telemetry/cmd/authenticator + +go 1.21 + +replace github.com/SUSE/telemetry => ../../ + +require github.com/SUSE/telemetry v0.0.0-00010101000000-000000000000 + +require ( + github.com/golang-jwt/jwt/v5 v5.2.1 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/mattn/go-sqlite3 v1.14.22 // indirect + github.com/xyproto/randomstring v1.0.5 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) diff --git a/cmd/authenticator/go.sum b/cmd/authenticator/go.sum new file mode 100644 index 0000000..93900fd --- /dev/null +++ b/cmd/authenticator/go.sum @@ -0,0 +1,18 @@ +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/golang-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17wHk= +github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU= +github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU= +github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/cmd/authenticator/main.go b/cmd/authenticator/main.go new file mode 100644 index 0000000..f79265a --- /dev/null +++ b/cmd/authenticator/main.go @@ -0,0 +1,124 @@ +package main + +import ( + "flag" + "fmt" + "log/slog" + + "github.com/SUSE/telemetry/pkg/client" + "github.com/SUSE/telemetry/pkg/config" + "github.com/SUSE/telemetry/pkg/logging" +) + +// options is a struct of the options +type options struct { + config string + dryrun bool + noregister bool + authenticate bool + debug bool +} + +var opts options + +func main() { + if err := logging.SetupBasicLogging(opts.debug); err != nil { + panic(err) + } + + slog.Debug("Authenticator", slog.Any("options", opts)) + + cfg, err := config.NewConfig(opts.config) + if err != nil { + slog.Error( + "Failed to load config", + slog.String("config", opts.config), + slog.String("error", err.Error()), + ) + panic(err) + } + + // setup logging based upon config settings + lm := logging.NewLogManager() + if err := lm.Config(&cfg.Logging); err != nil { + panic(err) + } + + // override config log level to debug if option specified + if opts.debug { + lm.SetLevel("DEBUG") + slog.Debug("Debug mode enabled") + } + + if err := lm.Setup(); err != nil { + panic(err) + } + + tc, err := client.NewTelemetryClient(cfg) + if err != nil { + slog.Error( + "Failed to instantiate TelemetryClient", + slog.String("config", opts.config), + slog.String("error", err.Error()), + ) + panic(err) + } + + if !opts.noregister { + err = tc.Register() + if err != nil { + slog.Error( + "Failed to register TelemetryClient", + slog.String("error", err.Error()), + ) + panic(err) + } + } + + if opts.authenticate { + err = tc.Authenticate() + if err != nil { + slog.Error( + "Failed to (re)uthenticate TelemetryClient", + slog.String("error", err.Error()), + ) + panic(err) + } + } + + issuer, err := tc.AuthIssuer() + if err != nil { + slog.Error( + "AuthIssuer() failed", + slog.String("err", err.Error()), + ) + } + + expiration, err := tc.AuthExpiration() + if err != nil { + slog.Error( + "AuthExpiration() failed", + slog.String("err", err.Error()), + ) + } + + fmt.Printf( + "Current Auth Token:\n %-[1]*[2]s %[3]s\n %-[1]*[4]s %[5]s\n %-[1]*[6]s %[7]s\n", + 19, + "Issuer:", + issuer, + "Expiration (UTC):", + expiration.UTC().Format("2006-01-02T15:04:05.000000"), + "Expiration (local):", + expiration.Format("2006-01-02T15:04:05.000000Z07:00"), + ) +} + +func init() { + flag.StringVar(&opts.config, "config", client.CONFIG_PATH, "Path to config file to read") + flag.BoolVar(&opts.debug, "debug", false, "Whether to enable debug level logging.") + flag.BoolVar(&opts.dryrun, "dryrun", false, "Process provided JSON files but do add them to the telemetry staging area.") + flag.BoolVar(&opts.noregister, "noregister", false, "Whether to skip registering the telemetry client if it is needed.") + flag.BoolVar(&opts.authenticate, "authenticate", false, "Whether to (re)authenticate the telemetry client.") + flag.Parse() +} diff --git a/cmd/clientds/go.mod b/cmd/clientds/go.mod index 1949c24..ba0e794 100644 --- a/cmd/clientds/go.mod +++ b/cmd/clientds/go.mod @@ -7,6 +7,7 @@ replace github.com/SUSE/telemetry => ../../../telemetry require github.com/SUSE/telemetry v0.0.0-00010101000000-000000000000 require ( + github.com/golang-jwt/jwt/v5 v5.2.1 // indirect github.com/google/uuid v1.6.0 // indirect github.com/mattn/go-sqlite3 v1.14.22 // indirect github.com/xyproto/randomstring v1.0.5 // indirect diff --git a/cmd/clientds/go.sum b/cmd/clientds/go.sum index 34efe85..93900fd 100644 --- a/cmd/clientds/go.sum +++ b/cmd/clientds/go.sum @@ -1,5 +1,7 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/golang-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17wHk= +github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU= diff --git a/cmd/clientds/main.go b/cmd/clientds/main.go index 1fe1329..4d06ee8 100644 --- a/cmd/clientds/main.go +++ b/cmd/clientds/main.go @@ -19,14 +19,13 @@ type options struct { debug bool } -func (o options) String() string { - return fmt.Sprintf("config=%v, items=%v, bundles=%v, reports=%v, debug=%v", o.config, o.items, o.bundles, o.reports, o.debug) -} - var opts options func main() { - fmt.Printf("clientds: %s\n", opts) + slog.Debug( + "clientds", + slog.Any("options", opts), + ) if err := logging.SetupBasicLogging(opts.debug); err != nil { panic(err) @@ -41,7 +40,6 @@ func main() { ) panic(err) } - fmt.Printf("Config: %+v\n", cfg) // setup logging based upon config settings lm := logging.NewLogManager() @@ -71,6 +69,9 @@ func main() { processor := tc.Processor() + // this will be toggled to true if items, bundles or reports were found + foundEntries := false + if opts.items { itemRows, err := processor.GetItemRows() if err != nil { @@ -87,6 +88,8 @@ func main() { for i, dataItemRow := range itemRows { fmt.Printf("Data Item[%d]: %q\n", i, dataItemRow.ItemId) } + + foundEntries = true } } @@ -106,6 +109,8 @@ func main() { for i, bundleRow := range bundleRows { fmt.Printf("Bundle[%d]: %q\n", i, bundleRow.BundleId) } + + foundEntries = true } } @@ -125,8 +130,14 @@ func main() { for i, reportRow := range reportRows { fmt.Printf("Reports[%d]: %q\n", i, reportRow.ReportId) } + + foundEntries = true } } + + if !foundEntries { + fmt.Println("No items, bundles or reports found in client datastore") + } } func init() { diff --git a/cmd/generator/go.mod b/cmd/generator/go.mod index 753de6b..4aa5f18 100644 --- a/cmd/generator/go.mod +++ b/cmd/generator/go.mod @@ -7,6 +7,7 @@ replace github.com/SUSE/telemetry => ../../ require github.com/SUSE/telemetry v0.0.0-00010101000000-000000000000 require ( + github.com/golang-jwt/jwt/v5 v5.2.1 // indirect github.com/google/uuid v1.6.0 // indirect github.com/mattn/go-sqlite3 v1.14.22 // indirect github.com/xyproto/randomstring v1.0.5 // indirect diff --git a/cmd/generator/go.sum b/cmd/generator/go.sum index 34efe85..93900fd 100644 --- a/cmd/generator/go.sum +++ b/cmd/generator/go.sum @@ -1,5 +1,7 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/golang-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17wHk= +github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU= diff --git a/cmd/generator/main.go b/cmd/generator/main.go index 624b7d4..a00b0ca 100644 --- a/cmd/generator/main.go +++ b/cmd/generator/main.go @@ -5,6 +5,7 @@ import ( "fmt" "log/slog" "os" + "path/filepath" "github.com/SUSE/telemetry/pkg/client" "github.com/SUSE/telemetry/pkg/config" @@ -114,6 +115,13 @@ func main() { ) panic(err) } + + fmt.Printf( + "Added telemetry data from %q as type %q with tags %s to local datastore\n", + filepath.Base(jsonFile), + opts.telemetry, + opts.tags, + ) } // create one or more bundles from available data items @@ -125,6 +133,8 @@ func main() { ) panic(err) } + + fmt.Println("Created telemetry bundles from pending telemetry data items") } // create one or more reports from available bundles @@ -136,6 +146,8 @@ func main() { ) panic(err) } + + fmt.Println("Created telemetry reports from pending telemetry bundles") } // create one or more reports from available bundles and then @@ -148,6 +160,8 @@ func main() { ) panic(err) } + + fmt.Println("Submitted pending telemetry reports") } } diff --git a/go.mod b/go.mod index 381caaa..b6a3540 100644 --- a/go.mod +++ b/go.mod @@ -5,6 +5,7 @@ go 1.21 replace github.com/SUSE/telemetry => ../telemetry require ( + github.com/golang-jwt/jwt/v5 v5.2.1 github.com/google/uuid v1.6.0 github.com/mattn/go-sqlite3 v1.14.22 github.com/stretchr/testify v1.9.0 diff --git a/go.sum b/go.sum index 34efe85..93900fd 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,7 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/golang-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17wHk= +github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU= diff --git a/pkg/client/client.go b/pkg/client/client.go index c99cc18..bafd7f3 100644 --- a/pkg/client/client.go +++ b/pkg/client/client.go @@ -11,11 +11,13 @@ import ( "log/slog" "net/http" "os" + "time" "github.com/SUSE/telemetry/pkg/config" telemetrylib "github.com/SUSE/telemetry/pkg/lib" "github.com/SUSE/telemetry/pkg/restapi" "github.com/SUSE/telemetry/pkg/types" + "github.com/golang-jwt/jwt/v5" ) const ( @@ -33,9 +35,10 @@ type TelemetryAuth struct { } type TelemetryClient struct { - cfg *config.Config - auth TelemetryAuth - processor telemetrylib.TelemetryProcessor + cfg *config.Config + auth TelemetryAuth + authLoaded bool + processor telemetrylib.TelemetryProcessor } func NewTelemetryClient(cfg *config.Config) (tc *TelemetryClient, err error) { @@ -70,7 +73,7 @@ func checkFileReadAccessible(filePath string) bool { func ensureInstanceIdExists(instIdPath string) error { - slog.Info("ensuring existence of instIdPath", slog.String("instIdPath", instIdPath)) + slog.Debug("ensuring existence of instIdPath", slog.String("instIdPath", instIdPath)) _, err := os.Stat(instIdPath) if !os.IsNotExist(err) { return nil @@ -94,6 +97,71 @@ func ensureInstanceIdExists(instIdPath string) error { return nil } +func (tc *TelemetryClient) authParsedToken() (token *jwt.Token, err error) { + if !tc.authLoaded { + if err = tc.loadTelemetryAuth(); err != nil { + slog.Error( + "Failed to load authToken", + slog.String("error", err.Error()), + ) + return + } + } + + // only the server can validate the signing key, so parse unverified + token, _, err = jwt.NewParser().ParseUnverified( + string(tc.auth.Token), jwt.MapClaims{}, + ) + + if err != nil { + slog.Error( + "Failed to parse JWT", + slog.String("error", err.Error()), + ) + return + } + + return +} + +func (tc *TelemetryClient) AuthIssuer() (issuer string, err error) { + token, err := tc.authParsedToken() + if err != nil { + return + } + + issuer, err = token.Claims.GetIssuer() + if err != nil { + slog.Error( + "Filed to retrieve issuer from token claims", + slog.String("err", err.Error()), + ) + return + } + + return +} + +func (tc *TelemetryClient) AuthExpiration() (expiration time.Time, err error) { + token, err := tc.authParsedToken() + if err != nil { + return + } + + expTime, err := token.Claims.GetExpirationTime() + if err != nil { + slog.Error( + "Filed to retrieve expiration date from token claims", + slog.String("err", err.Error()), + ) + return + } + + expiration = expTime.Time + + return +} + func (tc *TelemetryClient) AuthAccessible() bool { return checkFileReadAccessible(tc.AuthPath()) } @@ -157,7 +225,7 @@ func (tc *TelemetryClient) getInstanceId() (instId types.ClientInstanceId, err e func (tc *TelemetryClient) loadTelemetryAuth() (err error) { authPath := tc.AuthPath() - slog.Info("Checking auth file existence", slog.String("authPath", authPath)) + slog.Debug("Checking auth file existence", slog.String("authPath", authPath)) _, err = os.Stat(authPath) if os.IsNotExist(err) { slog.Warn( @@ -211,6 +279,8 @@ func (tc *TelemetryClient) loadTelemetryAuth() (err error) { return } + tc.authLoaded = true + return } @@ -283,7 +353,7 @@ func (tc *TelemetryClient) submitReport(report *telemetrylib.TelemetryReport) (e return } - slog.Info( + slog.Debug( "successfully submitted report", slog.String("report", report.Header.ReportId), slog.String("processing", trResp.ProcessingInfo()), @@ -392,7 +462,7 @@ func (tc *TelemetryClient) Authenticate() (err error) { return } - slog.Info( + slog.Debug( "successfully authenticated", slog.Int64("clientId", tc.auth.ClientId), ) @@ -404,7 +474,7 @@ func (tc *TelemetryClient) Register() (err error) { // get the saved TelemetryAuth, returning success if found err = tc.loadTelemetryAuth() if err == nil { - slog.Info("telemetry auth found, client already registered, skipping", slog.Int64("clientId", tc.auth.ClientId)) + slog.Debug("telemetry auth found, client already registered, skipping", slog.Int64("clientId", tc.auth.ClientId)) return } @@ -487,6 +557,8 @@ func (tc *TelemetryClient) Register() (err error) { return } + tc.authLoaded = true + err = tc.saveTelemetryAuth() if err != nil { slog.Error( @@ -496,7 +568,7 @@ func (tc *TelemetryClient) Register() (err error) { return } - slog.Info( + slog.Debug( "successfully registered as client", slog.Int64("clientId", tc.auth.ClientId), ) @@ -513,7 +585,7 @@ func (tc *TelemetryClient) Generate(telemetry types.TelemetryType, content []byt } // Add telemetry data item to DataItem data store - slog.Info( + slog.Debug( "Generated Telemetry", slog.String("name", telemetry.String()), slog.String("tags", tags.String()), @@ -525,7 +597,7 @@ func (tc *TelemetryClient) Generate(telemetry types.TelemetryType, content []byt func (tc *TelemetryClient) CreateBundles(tags types.Tags) error { // Bundle existing telemetry data items found in DataItem data store into one or more bundles in the Bundle data store - slog.Info("Bundle", slog.String("Tags", tags.String())) + slog.Debug("Bundle", slog.String("Tags", tags.String())) tc.processor.GenerateBundle(tc.auth.ClientId, tc.cfg.CustomerID, tags) return nil @@ -533,7 +605,7 @@ func (tc *TelemetryClient) CreateBundles(tags types.Tags) error { func (tc *TelemetryClient) CreateReports(tags types.Tags) (err error) { // Generate reports from available bundles - slog.Info("CreateReports", slog.String("Tags", tags.String())) + slog.Debug("CreateReports", slog.String("Tags", tags.String())) tc.processor.GenerateReport(tc.auth.ClientId, tags) return diff --git a/pkg/config/config.go b/pkg/config/config.go index 0071d3b..c1a74de 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -93,7 +93,7 @@ func NewConfig(cfgFile string) (*Config, error) { return cfg, fmt.Errorf("failed to read contents of config file '%s': %s", cfgFile, err) } - slog.Debug("Contents", slog.String("contents", string(contents))) + slog.Debug("Config", slog.String("contents", string(contents))) err = yaml.Unmarshal(contents, &cfg) if err != nil { return cfg, fmt.Errorf("failed to parse contents of config file '%s': %s", cfgFile, err) diff --git a/pkg/logging/logging.go b/pkg/logging/logging.go index 6efa7fa..3b50d22 100644 --- a/pkg/logging/logging.go +++ b/pkg/logging/logging.go @@ -354,7 +354,7 @@ func (lm *LogManager) Setup() (err error) { // set our handler as the default slog handler slog.SetDefault(lm.logger) - slog.Info( + slog.Debug( "Logging initialised", slog.Any("level", lm.logLevel), slog.String("dest", lm.logPath),