From d19344a43e69f981cf0c65d2a7db1388d83a8175 Mon Sep 17 00:00:00 2001 From: Hoeg Date: Sun, 5 Nov 2023 10:23:15 +0100 Subject: [PATCH 01/42] add oauth2 client and login --- cmd/artifact/command/push.go | 26 +++++-- cmd/daemon/command/start.go | 17 ++++- cmd/hamctl/command/login.go | 17 +++++ cmd/hamctl/command/root.go | 39 ++++------- cmd/hamctl/command/root_test.go | 73 ------------------- go.mod | 14 ++-- go.sum | 17 +++-- internal/http/client.go | 23 ++++-- internal/http/login.go | 120 ++++++++++++++++++++++++++++++++ 9 files changed, 223 insertions(+), 123 deletions(-) create mode 100644 cmd/hamctl/command/login.go create mode 100644 internal/http/login.go diff --git a/cmd/artifact/command/push.go b/cmd/artifact/command/push.go index 8cecaff2..137088db 100644 --- a/cmd/artifact/command/push.go +++ b/cmd/artifact/command/push.go @@ -10,6 +10,7 @@ import ( httpinternal "github.com/lunarway/release-manager/internal/http" intslack "github.com/lunarway/release-manager/internal/slack" "github.com/nlopes/slack" + "github.com/pkg/errors" "github.com/spf13/cobra" ) @@ -24,12 +25,26 @@ func pushCommand(options *Options) *cobra.Command { var err error ctx := context.Background() - if releaseManagerClient.Metadata.AuthToken != "" { - artifactID, err = flow.PushArtifactToReleaseManager(ctx, &releaseManagerClient, options.FileName, options.RootPath) - if err != nil { - return err - } + idpURL := os.Getenv("HAMCTL_OAUTH_IDP_URL") + if idpURL == "" { + return errors.New("no HAMCTL_OAUTH_IDP_URL env var set") } + clientID := os.Getenv("HAMCTL_OAUTH_CLIENT_ID") + if clientID == "" { + return errors.New("no HAMCTL_OAUTH_CLIENT_ID env var set") + } + clientSecret := os.Getenv("HAMCTL_OAUTH_CLIENT_SECRET") + if clientID == "" { + return errors.New("no HAMCTL_OAUTH_CLIENT_SECRET env var set") + } + daemonGate := httpinternal.NewDaemonGate(clientID, clientSecret, idpURL) + releaseManagerClient.Auth = &daemonGate + + artifactID, err = flow.PushArtifactToReleaseManager(ctx, &releaseManagerClient, options.FileName, options.RootPath) + if err != nil { + return err + } + client, err := intslack.NewClient(slack.New(options.SlackToken), options.UserMappings, options.EmailSuffix) if err != nil { fmt.Printf("Error, not able to create Slack client in successful command: %v", err) @@ -47,7 +62,6 @@ func pushCommand(options *Options) *cobra.Command { }, } command.Flags().StringVar(&releaseManagerClient.BaseURL, "http-base-url", os.Getenv("ARTIFACT_URL"), "address of the http release manager server") - command.Flags().StringVar(&releaseManagerClient.Metadata.AuthToken, "http-auth-token", "", "auth token for the http service") // errors are skipped here as the only case they can occour are if thee flag // does not exist on the command. diff --git a/cmd/daemon/command/start.go b/cmd/daemon/command/start.go index f6197308..1b576711 100644 --- a/cmd/daemon/command/start.go +++ b/cmd/daemon/command/start.go @@ -34,6 +34,22 @@ func StartDaemon() *cobra.Command { logConfiguration.ParseFromEnvironmnet() log.Init(logConfiguration) + idpURL := os.Getenv("HAMCTL_OAUTH_IDP_URL") + if idpURL == "" { + return errors.New("no HAMCTL_OAUTH_IDP_URL env var set") + } + clientID := os.Getenv("HAMCTL_OAUTH_CLIENT_ID") + if clientID == "" { + return errors.New("no HAMCTL_OAUTH_CLIENT_ID env var set") + } + clientSecret := os.Getenv("HAMCTL_OAUTH_CLIENT_SECRET") + if clientID == "" { + return errors.New("no HAMCTL_OAUTH_CLIENT_SECRET env var set") + } + + daemonGate := httpinternal.NewDaemonGate(clientID, clientSecret, idpURL) + client.Auth = &daemonGate + exporter := &kubernetes.ReleaseManagerExporter{ Log: log.With("type", "k8s-exporter"), Client: client, @@ -98,7 +114,6 @@ func StartDaemon() *cobra.Command { }, } command.Flags().StringVar(&client.BaseURL, "release-manager-url", os.Getenv("RELEASE_MANAGER_ADDRESS"), "address of the release-manager, e.g. http://release-manager") - command.Flags().StringVar(&client.Metadata.AuthToken, "auth-token", os.Getenv("DAEMON_AUTH_TOKEN"), "token to be used to communicate with the release-manager") command.Flags().DurationVar(&client.Timeout, "http-timeout", 20*time.Second, "HTTP request timeout") command.Flags().StringVar(&environment, "environment", "", "environment where release-daemon is running") command.Flags().StringVar(&kubeConfigPath, "kubeconfig", "", "path to kubeconfig file. If not specified, then daemon is expected to run inside kubernetes") diff --git a/cmd/hamctl/command/login.go b/cmd/hamctl/command/login.go new file mode 100644 index 00000000..ecda72ba --- /dev/null +++ b/cmd/hamctl/command/login.go @@ -0,0 +1,17 @@ +package command + +import ( + "github.com/lunarway/release-manager/internal/http" + "github.com/spf13/cobra" +) + +func Login(gate http.Gate) *cobra.Command { + return &cobra.Command{ + Use: "login", + Short: `Log into the configured IdP`, + Args: cobra.ExactArgs(0), + RunE: func(c *cobra.Command, args []string) error { + return gate.Authenticate() + }, + } +} diff --git a/cmd/hamctl/command/root.go b/cmd/hamctl/command/root.go index 75758bf8..39a3a799 100644 --- a/cmd/hamctl/command/root.go +++ b/cmd/hamctl/command/root.go @@ -17,7 +17,18 @@ import ( // NewRoot returns a new instance of a hamctl command. func NewRoot(version *string) (*cobra.Command, error) { - var service, email string + idpURL := os.Getenv("HAMCTL_OAUTH_IDP_URL") + if idpURL == "" { + return nil, errors.New("no HAMCTL_OAUTH_IDP_URL env var set") + } + clientID := os.Getenv("HAMCTL_OAUTH_CLIENT_ID") + if clientID == "" { + return nil, errors.New("no HAMCTL_OAUTH_CLIENT_ID env var set") + } + + gate := http.NewGate(clientID, idpURL) + + var service string client := http.Client{ Metadata: http.Metadata{ CLIVersion: *version, @@ -34,7 +45,7 @@ func NewRoot(version *string) (*cobra.Command, error) { PersistentPreRunE: func(c *cobra.Command, args []string) error { // all commands but version and completion requires the "service" flag // if this is one of them, skip the check - if c.Name() == "version" || c.Name() == "completion" { + if c.Name() == "version" || c.Name() == "completion" || c.Name() == "login" { return nil } defaultShuttleString(shuttleSpecFromFile, &service, func(s *shuttleSpec) string { @@ -45,17 +56,10 @@ func NewRoot(version *string) (*cobra.Command, error) { client.BaseURL = os.Getenv("HAMCTL_URL") } - if client.Metadata.AuthToken == "" { - client.Metadata.AuthToken = os.Getenv("HAMCTL_AUTH_TOKEN") - } - var missingFlags []string if service == "" { missingFlags = append(missingFlags, "service") } - if err := setCallerEmail(gitConfigAPI, &client, email); err != nil { - missingFlags = append(missingFlags, "user-email") - } if len(missingFlags) != 0 { return errors.Errorf(`required flag(s) "%s" not set`, strings.Join(missingFlags, `", "`)) @@ -78,12 +82,11 @@ func NewRoot(version *string) (*cobra.Command, error) { NewRollback(&client, &service, loggerFunc, SelectRollbackReleaseFunc, releaseClient), NewStatus(&client, &service), NewVersion(*version), + Login(gate), ) command.PersistentFlags().DurationVar(&client.Timeout, "http-timeout", 120*time.Second, "HTTP request timeout") command.PersistentFlags().StringVar(&client.BaseURL, "http-base-url", "", "address of the http release manager server") - command.PersistentFlags().StringVar(&client.Metadata.AuthToken, "http-auth-token", "", "auth token for the http service") command.PersistentFlags().StringVar(&service, "service", "", "service name to execute commands for") - command.PersistentFlags().StringVar(&email, "user-email", "", "email of user performing the command (defaults to Git configurated user.email)") return command, nil } @@ -133,17 +136,3 @@ func defaultShuttleString(shuttleLocator func() (shuttleSpec, bool), flagValue * *flagValue = t } } - -func setCallerEmail(gitConfigAPI GitConfigAPI, client *http.Client, email string) error { - if email != "" { - client.Metadata.CallerEmail = email - } else { - committer, err := gitConfigAPI.CommitterDetails() - if err != nil { - return fmt.Errorf("could not get committer from git: %w", err) - } - client.Metadata.CallerEmail = committer.Email - } - - return nil -} diff --git a/cmd/hamctl/command/root_test.go b/cmd/hamctl/command/root_test.go index 21c69b73..6f62f726 100644 --- a/cmd/hamctl/command/root_test.go +++ b/cmd/hamctl/command/root_test.go @@ -1,12 +1,8 @@ package command import ( - "fmt" "testing" - "github.com/lunarway/release-manager/internal/git" - "github.com/lunarway/release-manager/internal/http" - "github.com/pkg/errors" "github.com/stretchr/testify/assert" ) @@ -69,72 +65,3 @@ func TestDefaultShuttleString_noSpec(t *testing.T) { }) assert.Equal(t, "", flagValue, "flag value not as expected") } - -func TestSetCallerEmail(t *testing.T) { - tt := []struct { - name string - gitConfigApiMock GitConfigAPI - email string - expectedEmail string - }{ - { - name: "with valid email from committer", - gitConfigApiMock: &GitConfigAPIMock{ - CommitterDetailsFunc: func() (*git.CommitterDetails, error) { - return &git.CommitterDetails{Email: "some@email"}, nil - }, - }, - expectedEmail: "some@email", - }, - { - name: "with empty email", - gitConfigApiMock: &GitConfigAPIMock{ - CommitterDetailsFunc: func() (*git.CommitterDetails, error) { - return nil, errors.New("could not find email") - }, - }, - expectedEmail: "", - }, - { - name: "with valid email", - email: "some@email", - expectedEmail: "some@email", - }, - } - - for _, tc := range tt { - t.Run(tc.name, func(t *testing.T) { - client := &http.Client{} - _ = setCallerEmail(tc.gitConfigApiMock, client, tc.email) - assert.Equal(t, tc.expectedEmail, client.Metadata.CallerEmail) - }) - } -} - -func TestSetCallerEmailReturnsError(t *testing.T) { - tt := []struct { - name string - gitConfigApiMock GitConfigAPI - email string - expectedError error - }{ - { - name: "with no email", - gitConfigApiMock: &GitConfigAPIMock{ - CommitterDetailsFunc: func() (*git.CommitterDetails, error) { - return nil, errors.New("could not find email") - }, - }, - expectedError: fmt.Errorf("could not get committer from git: %w", errors.New("could not find email")), - }, - } - - for _, tc := range tt { - t.Run(tc.name, func(t *testing.T) { - client := &http.Client{} - err := setCallerEmail(tc.gitConfigApiMock, client, tc.email) - assert.ErrorContains(t, err, tc.expectedError.Error()) - assert.Equal(t, "", client.Metadata.CallerEmail) - }) - } -} diff --git a/go.mod b/go.mod index b26879bf..1b6bc56c 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module github.com/lunarway/release-manager -go 1.18 +go 1.20 require ( github.com/HdrHistogram/hdrhistogram-go v1.1.2 // indirect @@ -24,6 +24,7 @@ require ( github.com/uber/jaeger-lib v2.4.1+incompatible go.uber.org/multierr v1.8.0 go.uber.org/zap v1.23.0 + golang.org/x/oauth2 v0.13.0 gopkg.in/go-playground/webhooks.v5 v5.17.0 gopkg.in/yaml.v2 v2.4.0 k8s.io/api v0.25.4 @@ -31,7 +32,10 @@ require ( k8s.io/client-go v0.25.4 ) -require github.com/gorilla/mux v1.8.0 +require ( + github.com/gorilla/mux v1.8.0 + github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8 +) require ( github.com/Microsoft/go-winio v0.4.16 // indirect @@ -52,9 +56,9 @@ require ( github.com/go-openapi/jsonreference v0.19.5 // indirect github.com/go-openapi/swag v0.19.14 // indirect github.com/gogo/protobuf v1.3.2 // indirect - github.com/golang/protobuf v1.5.2 // indirect + github.com/golang/protobuf v1.5.3 // indirect github.com/google/gnostic v0.5.7-v3refs // indirect - github.com/google/go-cmp v0.5.8 // indirect + github.com/google/go-cmp v0.5.9 // indirect github.com/google/gofuzz v1.1.0 // indirect github.com/gorilla/websocket v1.4.2 // indirect github.com/imdario/mergo v0.3.12 // indirect @@ -89,7 +93,7 @@ require ( golang.org/x/time v0.0.0-20220210224613-90d013bbcef8 // indirect golang.org/x/tools v0.6.0 // indirect google.golang.org/appengine v1.6.7 // indirect - google.golang.org/protobuf v1.28.1 // indirect + google.golang.org/protobuf v1.31.0 // indirect gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/warnings.v0 v0.1.2 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect diff --git a/go.sum b/go.sum index 30ba333d..64502c2d 100644 --- a/go.sum +++ b/go.sum @@ -163,8 +163,9 @@ github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QD github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= -github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw= github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= +github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/gnostic v0.5.7-v3refs h1:FhTMOKj2VhjpouxvWJAV1TL304uMlb9zcDqkl6cEI54= @@ -178,8 +179,8 @@ github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/ github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.8 h1:e6P7q2lk1O+qJJb4BtCQXlK8vWEO8V1ZeuEdJNOqZyg= -github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= +github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/gofuzz v1.1.0 h1:Hsa8mG0dQ46ij8Sl2AYJDUv1oA9/d6Vk+3LG99Oe02g= github.com/google/gofuzz v1.1.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= @@ -280,6 +281,8 @@ github.com/onsi/ginkgo/v2 v2.1.6 h1:Fx2POJZfKRQcM1pH49qSZiYeu319wji004qX+GDovrU= github.com/onsi/gomega v1.20.1 h1:PA/3qinGoukvymdIDV8pii6tiZgC8kbmJO6Z5+b002Q= github.com/opentracing/opentracing-go v1.2.0 h1:uEJPy/1a5RIPAJ0Ov+OIO8OxWu77jEv+1B0VhjKrZUs= github.com/opentracing/opentracing-go v1.2.0/go.mod h1:GxEUsuufX4nBwe+T+Wl9TAgYrxe9dPLANfrWvHYVTgc= +github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8 h1:KoWmjvw+nsYOo29YJK9vDA65RGE3NrOnUtO7a+RF9HU= +github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8/go.mod h1:HKlIX3XHQyzLZPlr7++PzdhaXEj94dEiJgZDTsxEqUI= github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= @@ -471,8 +474,9 @@ golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4Iltr golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20210514164344-f6687ab2804c/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.0.0-20220223155221-ee480838109b h1:clP8eMhB30EHdc0bd2Twtq6kgU7yl5ub2cQLSdrv1Dg= golang.org/x/oauth2 v0.0.0-20220223155221-ee480838109b/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc= +golang.org/x/oauth2 v0.13.0 h1:jDDenyj+WgFtmV3zYVoi8aE2BwtXFLWOA67ZfNWftiY= +golang.org/x/oauth2 v0.13.0/go.mod h1:/JMhi4ZRXAf4HG9LiNmxvk+45+96RUlVThiH8FzNBn0= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -529,6 +533,7 @@ golang.org/x/sys v0.0.0-20210502180810-71e4cd670f79/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210616045830-e2b7044e8c71/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220114195835-da31bd327af9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -692,8 +697,8 @@ google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGj google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= -google.golang.org/protobuf v1.28.1 h1:d0NfwRgPtno5B1Wa6L2DAG+KivqkdutMf1UhdNx175w= -google.golang.org/protobuf v1.28.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= +google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8= +google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/internal/http/client.go b/internal/http/client.go index 42e725a4..ecce3fd6 100644 --- a/internal/http/client.go +++ b/internal/http/client.go @@ -2,6 +2,7 @@ package http import ( "bytes" + "context" "encoding/json" stderrors "errors" "fmt" @@ -15,16 +16,23 @@ import ( "github.com/pkg/errors" ) +type Authenticator interface { + AuthenticatedClient(context context.Context) (*http.Client, error) +} + type Client struct { BaseURL string Timeout time.Duration Metadata Metadata + Auth Authenticator } type Metadata struct { - AuthToken string - CLIVersion string - CallerEmail string + CLIVersion string +} + +func NewClient(baseURL string) Client { + return Client{} } // URL returns a URL with provided path added to the client's base URL. @@ -52,9 +60,12 @@ func (c *Client) URLWithQuery(path string, queryParams url.Values) (string, erro // the server returns a status code above 399 the response is parsed as an // ErrorResponse object and returned as the error. func (c *Client) Do(method string, path string, requestBody, responseBody interface{}) error { - client := &http.Client{ - Timeout: c.Timeout, + ctx := context.Background() + client, err := c.Auth.AuthenticatedClient(ctx) + if err != nil { + return errors.Wrap(err, "please log in again to refresh the token") } + client.Timeout = c.Timeout var b io.ReadWriter if requestBody != nil { @@ -72,9 +83,7 @@ func (c *Client) Do(method string, path string, requestBody, responseBody interf if err == nil { req.Header.Set("x-request-id", id.String()) } - req.Header.Set("Authorization", "Bearer "+c.Metadata.AuthToken) req.Header.Set("X-Cli-Version", c.Metadata.CLIVersion) - req.Header.Set("X-Caller-Email", c.Metadata.CallerEmail) resp, err := client.Do(req) if err != nil { var dnsError *net.DNSError diff --git a/internal/http/login.go b/internal/http/login.go new file mode 100644 index 00000000..bfb93168 --- /dev/null +++ b/internal/http/login.go @@ -0,0 +1,120 @@ +package http + +import ( + "context" + "encoding/json" + "fmt" + "net/http" + "os" + "path/filepath" + + "github.com/pkg/browser" + "golang.org/x/oauth2" + "golang.org/x/oauth2/clientcredentials" +) + +type Gate struct { + conf *oauth2.Config +} + +func NewGate(clientID, idpURL string) Gate { + conf := &oauth2.Config{ + ClientID: clientID, + Endpoint: oauth2.Endpoint{ + TokenURL: fmt.Sprintf("%s/v1/token", idpURL), + DeviceAuthURL: fmt.Sprintf("%s/v1/device/authorize", idpURL), + }, + Scopes: []string{"openid profile"}, + } + return Gate{ + conf: conf, + } +} + +func (g *Gate) Authenticate() error { + ctx := context.Background() + response, err := g.conf.DeviceAuth(ctx) + if err != nil { + return err + } + fmt.Printf("please enter code %s at %s\n", response.UserCode, response.VerificationURIComplete) + err = browser.OpenURL(response.VerificationURIComplete) + if err != nil { + return err + } + + token, err := g.conf.DeviceAccessToken(ctx, response) + if err != nil { + return err + } + return storeAccessToken(token) +} + +func (g *Gate) AuthenticatedClient(ctx context.Context) (*http.Client, error) { + token, err := readAccessToken() + if err != nil { + return nil, err + } + return g.conf.Client(ctx, token), nil +} + +const tokenFile string = ".Config/hamctl/token.json" + +func tokenFilePath() string { + return filepath.Join(os.Getenv("HOME"), tokenFile) +} + +func readAccessToken() (*oauth2.Token, error) { + data, err := os.ReadFile(tokenFilePath()) + if err != nil { + return nil, err + } + var token oauth2.Token + err = json.Unmarshal(data, &token) + if err != nil { + return nil, err + } + return &token, nil +} + +func storeAccessToken(token *oauth2.Token) error { + accessToken, err := json.Marshal(token) + if err != nil { + return err + } + p := tokenFilePath() + dir := filepath.Dir(p) + if err := os.MkdirAll(dir, 0755); err != nil { + return err + } + f, err := os.Create(p) + if err != nil { + return err + } + defer f.Close() + _, err = f.Write(accessToken) + if err != nil { + return err + } + return nil +} + +type DaemonGate struct { + conf *clientcredentials.Config +} + +func NewDaemonGate(clientID, clientSecret, idpURL string) DaemonGate { + conf := &clientcredentials.Config{ + ClientID: clientID, + ClientSecret: clientSecret, + TokenURL: fmt.Sprintf("%s/v1/token", idpURL), + Scopes: []string{""}, + } + return DaemonGate{ + conf: conf, + } +} + +func (g *DaemonGate) AuthenticatedClient(ctx context.Context) (*http.Client, error) { + return g.conf.Client(ctx), nil +} From 4fa7485bd348c26199e3ed6731a7cc0db6d70da6 Mon Sep 17 00:00:00 2001 From: Hoeg Date: Sun, 5 Nov 2023 10:25:58 +0100 Subject: [PATCH 02/42] unused func --- internal/http/client.go | 4 ---- 1 file changed, 4 deletions(-) diff --git a/internal/http/client.go b/internal/http/client.go index ecce3fd6..f71f6578 100644 --- a/internal/http/client.go +++ b/internal/http/client.go @@ -31,10 +31,6 @@ type Metadata struct { CLIVersion string } -func NewClient(baseURL string) Client { - return Client{} -} - // URL returns a URL with provided path added to the client's base URL. func (c *Client) URL(path string) (string, error) { requestURL, err := url.Parse(fmt.Sprintf("%s/%s", c.BaseURL, path)) From eeb80fc7af9f53294dfb2ffc58501e0aa6676aee Mon Sep 17 00:00:00 2001 From: Hoeg Date: Sun, 5 Nov 2023 10:38:20 +0100 Subject: [PATCH 03/42] tidy up --- go.mod | 1 - 1 file changed, 1 deletion(-) diff --git a/go.mod b/go.mod index 1b6bc56c..24f8effd 100644 --- a/go.mod +++ b/go.mod @@ -86,7 +86,6 @@ require ( go.uber.org/atomic v1.9.0 // indirect golang.org/x/crypto v0.14.0 // indirect golang.org/x/net v0.17.0 // indirect - golang.org/x/oauth2 v0.0.0-20220223155221-ee480838109b // indirect golang.org/x/sys v0.13.0 // indirect golang.org/x/term v0.13.0 // indirect golang.org/x/text v0.13.0 From 86885b5d4c6dfd2388a7aa14821b2ad3d9ce326b Mon Sep 17 00:00:00 2001 From: Hoeg Date: Sun, 5 Nov 2023 10:46:22 +0100 Subject: [PATCH 04/42] test-token --- cmd/server/http/http.go | 5 ++--- internal/http/client.go | 1 + 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/cmd/server/http/http.go b/cmd/server/http/http.go index b2f14b45..d0586d1c 100644 --- a/cmd/server/http/http.go +++ b/cmd/server/http/http.go @@ -148,9 +148,8 @@ func trace(tracer tracing.Tracer) func(http.Handler) http.Handler { func authenticate(token string) func(http.Handler) http.Handler { return func(h http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - authorization := r.Header.Get("Authorization") - t := strings.TrimPrefix(authorization, "Bearer ") - t = strings.TrimSpace(t) + authorization := r.Header.Get("X-HAM-TOKEN") + t := strings.TrimSpace(authorization) if t != token { httpinternal.Error(w, "please provide a valid authentication token", http.StatusUnauthorized) return diff --git a/internal/http/client.go b/internal/http/client.go index f71f6578..3931e529 100644 --- a/internal/http/client.go +++ b/internal/http/client.go @@ -80,6 +80,7 @@ func (c *Client) Do(method string, path string, requestBody, responseBody interf req.Header.Set("x-request-id", id.String()) } req.Header.Set("X-Cli-Version", c.Metadata.CLIVersion) + req.Header.Set("X-HAM-TOKEN", "test") resp, err := client.Do(req) if err != nil { var dnsError *net.DNSError From 75c63313d1fcfefce272fdb8a64b146662003f5d Mon Sep 17 00:00:00 2001 From: Hoeg Date: Sun, 5 Nov 2023 10:55:47 +0100 Subject: [PATCH 05/42] check if the auth is the problem --- cmd/hamctl/command/release_test.go | 10 +++++++++- cmd/hamctl/command/rollback_test.go | 1 + 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/cmd/hamctl/command/release_test.go b/cmd/hamctl/command/release_test.go index 09eea718..150fefd2 100644 --- a/cmd/hamctl/command/release_test.go +++ b/cmd/hamctl/command/release_test.go @@ -1,6 +1,7 @@ package command_test import ( + "context" "encoding/json" "fmt" "net/http" @@ -18,6 +19,12 @@ import ( "github.com/stretchr/testify/require" ) +type NoAuthClient struct{} + +func (NoAuthClient) AuthenticatedClient(ctx context.Context) (*http.Client, error) { + return &http.Client{}, nil +} + func TestRelease(t *testing.T) { var ( serviceName = "service-name" @@ -61,6 +68,7 @@ func TestRelease(t *testing.T) { c := internalhttp.Client{ BaseURL: server.URL, + Auth: NoAuthClient{}, } releaseClient := actions.NewReleaseHttpClient(git.NewLocalGitConfigAPI(), &c) @@ -194,7 +202,7 @@ func maskGUID(output []string) []string { func TestRelease_emptyEnvValue(t *testing.T) { serviceName := "service-name" - c := internalhttp.Client{} + c := internalhttp.Client{Auth: NoAuthClient{}} releaseClient := actions.NewReleaseHttpClient(git.NewLocalGitConfigAPI(), &c) cmd := command.NewRelease(&c, &serviceName, func(f string, args ...interface{}) { diff --git a/cmd/hamctl/command/rollback_test.go b/cmd/hamctl/command/rollback_test.go index 41f13c7f..1e4a5aab 100644 --- a/cmd/hamctl/command/rollback_test.go +++ b/cmd/hamctl/command/rollback_test.go @@ -70,6 +70,7 @@ func TestRollback(t *testing.T) { c := internalhttp.Client{ BaseURL: server.URL, + Auth: NoAuthClient{}, } gitConfigAPI := command.GitConfigAPIMock{ From ae03a80de4ed730ef29e4d152e5c1853b7f2f6e0 Mon Sep 17 00:00:00 2001 From: Hoeg Date: Sun, 5 Nov 2023 11:33:13 +0100 Subject: [PATCH 06/42] fix auth test --- cmd/server/http/http_test.go | 16 +++++----------- 1 file changed, 5 insertions(+), 11 deletions(-) diff --git a/cmd/server/http/http_test.go b/cmd/server/http/http_test.go index 383fb3e0..fcd4720f 100644 --- a/cmd/server/http/http_test.go +++ b/cmd/server/http/http_test.go @@ -27,34 +27,28 @@ func TestAuthenticate(t *testing.T) { authorization: " ", status: http.StatusUnauthorized, }, - { - name: "non-bearer authorization", - serverToken: "token", - authorization: "non-bearer-token", - status: http.StatusUnauthorized, - }, { name: "empty bearer authorization", serverToken: "token", - authorization: "Bearer ", + authorization: " ", status: http.StatusUnauthorized, }, { name: "whitespace bearer authorization", serverToken: "token", - authorization: "Bearer ", + authorization: " ", status: http.StatusUnauthorized, }, { name: "wrong bearer authorization", serverToken: "token", - authorization: "Bearer another-token", + authorization: "another-token", status: http.StatusUnauthorized, }, { name: "correct bearer authorization", serverToken: "token", - authorization: "Bearer token", + authorization: "token", status: http.StatusOK, }, } @@ -64,7 +58,7 @@ func TestAuthenticate(t *testing.T) { w.WriteHeader(http.StatusOK) }) req := httptest.NewRequest(http.MethodGet, "/", nil) - req.Header.Set("Authorization", tc.authorization) + req.Header.Set("X-HAM-TOKEN", tc.authorization) w := httptest.NewRecorder() authenticate(tc.serverToken)(handler).ServeHTTP(w, req) From ab7769b416515b5f6c8f6517e5484e3b2ef87d5b Mon Sep 17 00:00:00 2001 From: Hoeg Date: Sun, 5 Nov 2023 11:46:12 +0100 Subject: [PATCH 07/42] renaming --- cmd/artifact/command/push.go | 4 ++-- cmd/daemon/command/start.go | 4 ++-- cmd/hamctl/command/login.go | 4 ++-- cmd/hamctl/command/release_test.go | 2 +- cmd/hamctl/command/root.go | 4 ++-- internal/http/{login.go => authenticator.go} | 18 +++++++++--------- internal/http/client.go | 4 ++-- 7 files changed, 20 insertions(+), 20 deletions(-) rename internal/http/{login.go => authenticator.go} (81%) diff --git a/cmd/artifact/command/push.go b/cmd/artifact/command/push.go index 137088db..b4a6a099 100644 --- a/cmd/artifact/command/push.go +++ b/cmd/artifact/command/push.go @@ -37,8 +37,8 @@ func pushCommand(options *Options) *cobra.Command { if clientID == "" { return errors.New("no HAMCTL_OAUTH_CLIENT_SECRET env var set") } - daemonGate := httpinternal.NewDaemonGate(clientID, clientSecret, idpURL) - releaseManagerClient.Auth = &daemonGate + authenticator := httpinternal.NewClientAuthenticator(clientID, clientSecret, idpURL) + releaseManagerClient.Auth = &authenticator artifactID, err = flow.PushArtifactToReleaseManager(ctx, &releaseManagerClient, options.FileName, options.RootPath) if err != nil { diff --git a/cmd/daemon/command/start.go b/cmd/daemon/command/start.go index 1b576711..73e09b63 100644 --- a/cmd/daemon/command/start.go +++ b/cmd/daemon/command/start.go @@ -47,8 +47,8 @@ func StartDaemon() *cobra.Command { return errors.New("no HAMCTL_OAUTH_CLIENT_SECRET env var set") } - daemonGate := httpinternal.NewDaemonGate(clientID, clientSecret, idpURL) - client.Auth = &daemonGate + authenticator := httpinternal.NewClientAuthenticator(clientID, clientSecret, idpURL) + client.Auth = &authenticator exporter := &kubernetes.ReleaseManagerExporter{ Log: log.With("type", "k8s-exporter"), diff --git a/cmd/hamctl/command/login.go b/cmd/hamctl/command/login.go index ecda72ba..8498ce12 100644 --- a/cmd/hamctl/command/login.go +++ b/cmd/hamctl/command/login.go @@ -5,13 +5,13 @@ import ( "github.com/spf13/cobra" ) -func Login(gate http.Gate) *cobra.Command { +func Login(authenticator http.UserAuthenticator) *cobra.Command { return &cobra.Command{ Use: "login", Short: `Log into the configured IdP`, Args: cobra.ExactArgs(0), RunE: func(c *cobra.Command, args []string) error { - return gate.Authenticate() + return authenticator.Login() }, } } diff --git a/cmd/hamctl/command/release_test.go b/cmd/hamctl/command/release_test.go index 150fefd2..00a2f3f2 100644 --- a/cmd/hamctl/command/release_test.go +++ b/cmd/hamctl/command/release_test.go @@ -21,7 +21,7 @@ import ( type NoAuthClient struct{} -func (NoAuthClient) AuthenticatedClient(ctx context.Context) (*http.Client, error) { +func (NoAuthClient) Access(ctx context.Context) (*http.Client, error) { return &http.Client{}, nil } diff --git a/cmd/hamctl/command/root.go b/cmd/hamctl/command/root.go index 39a3a799..de9b68fa 100644 --- a/cmd/hamctl/command/root.go +++ b/cmd/hamctl/command/root.go @@ -26,7 +26,7 @@ func NewRoot(version *string) (*cobra.Command, error) { return nil, errors.New("no HAMCTL_OAUTH_CLIENT_ID env var set") } - gate := http.NewGate(clientID, idpURL) + authenticator := http.NewUserAuthenticator(clientID, idpURL) var service string client := http.Client{ @@ -82,7 +82,7 @@ func NewRoot(version *string) (*cobra.Command, error) { NewRollback(&client, &service, loggerFunc, SelectRollbackReleaseFunc, releaseClient), NewStatus(&client, &service), NewVersion(*version), - Login(gate), + Login(authenticator), ) command.PersistentFlags().DurationVar(&client.Timeout, "http-timeout", 120*time.Second, "HTTP request timeout") command.PersistentFlags().StringVar(&client.BaseURL, "http-base-url", "", "address of the http release manager server") diff --git a/internal/http/login.go b/internal/http/authenticator.go similarity index 81% rename from internal/http/login.go rename to internal/http/authenticator.go index bfb93168..c1902348 100644 --- a/internal/http/login.go +++ b/internal/http/authenticator.go @@ -13,11 +13,11 @@ import ( "golang.org/x/oauth2/clientcredentials" ) -type Gate struct { +type UserAuthenticator struct { conf *oauth2.Config } -func NewGate(clientID, idpURL string) Gate { +func NewUserAuthenticator(clientID, idpURL string) UserAuthenticator { conf := &oauth2.Config{ ClientID: clientID, Endpoint: oauth2.Endpoint{ @@ -26,12 +26,12 @@ func NewGate(clientID, idpURL string) Gate { }, Scopes: []string{"openid profile"}, } - return Gate{ + return UserAuthenticator{ conf: conf, } } -func (g *Gate) Authenticate() error { +func (g *UserAuthenticator) Login() error { ctx := context.Background() response, err := g.conf.DeviceAuth(ctx) if err != nil { @@ -50,7 +50,7 @@ func (g *Gate) Authenticate() error { return storeAccessToken(token) } -func (g *Gate) AuthenticatedClient(ctx context.Context) (*http.Client, error) { +func (g *UserAuthenticator) Access(ctx context.Context) (*http.Client, error) { token, err := readAccessToken() if err != nil { return nil, err @@ -99,22 +99,22 @@ func storeAccessToken(token *oauth2.Token) error { return nil } -type DaemonGate struct { +type ClientAuthenticator struct { conf *clientcredentials.Config } -func NewDaemonGate(clientID, clientSecret, idpURL string) DaemonGate { +func NewClientAuthenticator(clientID, clientSecret, idpURL string) ClientAuthenticator { conf := &clientcredentials.Config{ ClientID: clientID, ClientSecret: clientSecret, TokenURL: fmt.Sprintf("%s/v1/token", idpURL), Scopes: []string{""}, } - return DaemonGate{ + return ClientAuthenticator{ conf: conf, } } -func (g *DaemonGate) AuthenticatedClient(ctx context.Context) (*http.Client, error) { +func (g *ClientAuthenticator) Access(ctx context.Context) (*http.Client, error) { return g.conf.Client(ctx), nil } diff --git a/internal/http/client.go b/internal/http/client.go index 3931e529..a7bd71f4 100644 --- a/internal/http/client.go +++ b/internal/http/client.go @@ -17,7 +17,7 @@ import ( ) type Authenticator interface { - AuthenticatedClient(context context.Context) (*http.Client, error) + Access(context context.Context) (*http.Client, error) } type Client struct { @@ -57,7 +57,7 @@ func (c *Client) URLWithQuery(path string, queryParams url.Values) (string, erro // ErrorResponse object and returned as the error. func (c *Client) Do(method string, path string, requestBody, responseBody interface{}) error { ctx := context.Background() - client, err := c.Auth.AuthenticatedClient(ctx) + client, err := c.Auth.Access(ctx) if err != nil { return errors.Wrap(err, "please log in again to refresh the token") } From 511643c1b3efe1ae65b54db8163b0b9e115f45d6 Mon Sep 17 00:00:00 2001 From: Hoeg Date: Sun, 5 Nov 2023 19:07:06 +0100 Subject: [PATCH 08/42] remove git config --- cmd/hamctl/command/actions/release.go | 22 ++++++---------- cmd/hamctl/command/policy.go | 6 ++--- cmd/hamctl/command/policy/apply.go | 36 ++++++++------------------- cmd/hamctl/command/policy/delete.go | 13 +++------- cmd/hamctl/command/release_test.go | 5 ++-- cmd/hamctl/command/rollback_test.go | 9 +------ cmd/hamctl/command/root.go | 8 ++---- cmd/server/http/release.go | 4 +-- internal/http/types.go | 16 +++--------- 9 files changed, 35 insertions(+), 84 deletions(-) diff --git a/cmd/hamctl/command/actions/release.go b/cmd/hamctl/command/actions/release.go index af2bb130..ec1822aa 100644 --- a/cmd/hamctl/command/actions/release.go +++ b/cmd/hamctl/command/actions/release.go @@ -20,14 +20,12 @@ type ReleaseResult struct { } type ReleaseHttpClient struct { - gitConfigAPI GitConfigAPI - client *httpinternal.Client + client *httpinternal.Client } -func NewReleaseHttpClient(gitConfigAPI GitConfigAPI, client *httpinternal.Client) *ReleaseHttpClient { +func NewReleaseHttpClient(client *httpinternal.Client) *ReleaseHttpClient { return &ReleaseHttpClient{ - gitConfigAPI: gitConfigAPI, - client: client, + client: client, } } @@ -44,10 +42,6 @@ func (hc *ReleaseHttpClient) ReleaseArtifactID(service, environment string, arti // environments. func (hc *ReleaseHttpClient) ReleaseArtifactIDMultipleEnvironments(service string, environments []string, artifactID string, intent intent.Intent) ([]ReleaseResult, error) { var results []ReleaseResult - committer, err := hc.gitConfigAPI.CommitterDetails() - if err != nil { - return nil, err - } path, err := hc.client.URL("release") if err != nil { return nil, err @@ -55,12 +49,10 @@ func (hc *ReleaseHttpClient) ReleaseArtifactIDMultipleEnvironments(service strin for _, environment := range environments { var resp httpinternal.ReleaseResponse err = hc.client.Do(http.MethodPost, path, httpinternal.ReleaseRequest{ - Service: service, - Environment: environment, - ArtifactID: artifactID, - CommitterName: committer.Name, - CommitterEmail: committer.Email, - Intent: intent, + Service: service, + Environment: environment, + ArtifactID: artifactID, + Intent: intent, }, &resp) results = append(results, ReleaseResult{ diff --git a/cmd/hamctl/command/policy.go b/cmd/hamctl/command/policy.go index eb8a88c0..7008d209 100644 --- a/cmd/hamctl/command/policy.go +++ b/cmd/hamctl/command/policy.go @@ -7,7 +7,7 @@ import ( "github.com/spf13/cobra" ) -func NewPolicy(client *http.Client, service *string, gitConfigAPI GitConfigAPI) *cobra.Command { +func NewPolicy(client *http.Client, service *string) *cobra.Command { var command = &cobra.Command{ Use: "policy", Short: "Manage release policies for services.", @@ -28,8 +28,8 @@ func NewPolicy(client *http.Client, service *string, gitConfigAPI GitConfigAPI) c.HelpFunc()(c, args) }, } - command.AddCommand(policy.NewApply(client, service, gitConfigAPI)) + command.AddCommand(policy.NewApply(client, service)) command.AddCommand(policy.NewList(client, service)) - command.AddCommand(policy.NewDelete(client, service, gitConfigAPI)) + command.AddCommand(policy.NewDelete(client, service)) return command } diff --git a/cmd/hamctl/command/policy/apply.go b/cmd/hamctl/command/policy/apply.go index 547ef550..c637a8d4 100644 --- a/cmd/hamctl/command/policy/apply.go +++ b/cmd/hamctl/command/policy/apply.go @@ -16,7 +16,7 @@ type GitConfigAPI interface { CommitterDetails() (*git.CommitterDetails, error) } -func NewApply(client *httpinternal.Client, service *string, gitConfigAPI GitConfigAPI) *cobra.Command { +func NewApply(client *httpinternal.Client, service *string) *cobra.Command { var command = &cobra.Command{ Use: "apply", Short: "Apply a release policy for a service. See available commands for specific policies.", @@ -37,34 +37,27 @@ func NewApply(client *httpinternal.Client, service *string, gitConfigAPI GitConf c.HelpFunc()(c, args) }, } - command.AddCommand(autoRelease(client, service, gitConfigAPI)) - command.AddCommand(branchRestriction(client, service, gitConfigAPI)) + command.AddCommand(autoRelease(client, service)) + command.AddCommand(branchRestriction(client, service)) return command } -func autoRelease(client *httpinternal.Client, service *string, gitConfigAPI GitConfigAPI) *cobra.Command { +func autoRelease(client *httpinternal.Client, service *string) *cobra.Command { var branch, env string var command = &cobra.Command{ Use: "auto-release", Short: "Auto-release policy for releasing branch artifacts to an environment", Args: cobra.ExactArgs(0), RunE: func(c *cobra.Command, args []string) error { - committer, err := gitConfigAPI.CommitterDetails() - if err != nil { - return err - } - var resp httpinternal.ApplyPolicyResponse path, err := client.URL(pathAutoRelease) if err != nil { return err } err = client.Do(http.MethodPatch, path, httpinternal.ApplyAutoReleasePolicyRequest{ - Service: *service, - Branch: branch, - Environment: env, - CommitterEmail: committer.Email, - CommitterName: committer.Name, + Service: *service, + Branch: branch, + Environment: env, }, &resp) if err != nil { return err @@ -87,7 +80,7 @@ func autoRelease(client *httpinternal.Client, service *string, gitConfigAPI GitC return command } -func branchRestriction(client *httpinternal.Client, service *string, gitConfigAPI GitConfigAPI) *cobra.Command { +func branchRestriction(client *httpinternal.Client, service *string) *cobra.Command { var branchRegex, env string var command = &cobra.Command{ Use: "branch-restriction", @@ -95,22 +88,15 @@ func branchRestriction(client *httpinternal.Client, service *string, gitConfigAP Long: "Branch restriction policy for limiting releases of artifacts by their origin branch to specific environments", Args: cobra.ExactArgs(0), RunE: func(c *cobra.Command, args []string) error { - committer, err := gitConfigAPI.CommitterDetails() - if err != nil { - return err - } - var resp httpinternal.ApplyBranchRestrictionPolicyResponse path, err := client.URL(pathBranchRestrction) if err != nil { return err } err = client.Do(http.MethodPatch, path, httpinternal.ApplyBranchRestrictionPolicyRequest{ - Service: *service, - BranchRegex: branchRegex, - Environment: env, - CommitterEmail: committer.Email, - CommitterName: committer.Name, + Service: *service, + BranchRegex: branchRegex, + Environment: env, }, &resp) if err != nil { return err diff --git a/cmd/hamctl/command/policy/delete.go b/cmd/hamctl/command/policy/delete.go index 495ee30f..ab6b7aeb 100644 --- a/cmd/hamctl/command/policy/delete.go +++ b/cmd/hamctl/command/policy/delete.go @@ -9,7 +9,7 @@ import ( "github.com/spf13/cobra" ) -func NewDelete(client *httpinternal.Client, service *string, gitConfigAPI GitConfigAPI) *cobra.Command { +func NewDelete(client *httpinternal.Client, service *string) *cobra.Command { var command = &cobra.Command{ Use: "delete", Short: "Delete one or more policies by their id.", @@ -41,21 +41,14 @@ Delete multiple policies: $ hamctl --service product policy delete auto-release-master-dev auto-release-master-prod `, RunE: func(c *cobra.Command, args []string) error { - committer, err := gitConfigAPI.CommitterDetails() - if err != nil { - return err - } - var resp httpinternal.DeletePolicyResponse path, err := client.URL(path) if err != nil { return err } err = client.Do(http.MethodDelete, path, httpinternal.DeletePolicyRequest{ - Service: *service, - PolicyIDs: args, - CommitterName: committer.Name, - CommitterEmail: committer.Email, + Service: *service, + PolicyIDs: args, }, &resp) if err != nil { return err diff --git a/cmd/hamctl/command/release_test.go b/cmd/hamctl/command/release_test.go index 00a2f3f2..ce314e75 100644 --- a/cmd/hamctl/command/release_test.go +++ b/cmd/hamctl/command/release_test.go @@ -13,7 +13,6 @@ import ( "github.com/lunarway/release-manager/cmd/hamctl/command" "github.com/lunarway/release-manager/cmd/hamctl/command/actions" "github.com/lunarway/release-manager/internal/artifact" - "github.com/lunarway/release-manager/internal/git" internalhttp "github.com/lunarway/release-manager/internal/http" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -71,7 +70,7 @@ func TestRelease(t *testing.T) { Auth: NoAuthClient{}, } - releaseClient := actions.NewReleaseHttpClient(git.NewLocalGitConfigAPI(), &c) + releaseClient := actions.NewReleaseHttpClient(&c) runCommand := func(t *testing.T, args ...string) []string { var output []string @@ -203,7 +202,7 @@ func maskGUID(output []string) []string { func TestRelease_emptyEnvValue(t *testing.T) { serviceName := "service-name" c := internalhttp.Client{Auth: NoAuthClient{}} - releaseClient := actions.NewReleaseHttpClient(git.NewLocalGitConfigAPI(), &c) + releaseClient := actions.NewReleaseHttpClient(&c) cmd := command.NewRelease(&c, &serviceName, func(f string, args ...interface{}) { t.Logf(f, args...) diff --git a/cmd/hamctl/command/rollback_test.go b/cmd/hamctl/command/rollback_test.go index 1e4a5aab..000342cf 100644 --- a/cmd/hamctl/command/rollback_test.go +++ b/cmd/hamctl/command/rollback_test.go @@ -11,7 +11,6 @@ import ( "github.com/lunarway/release-manager/cmd/hamctl/command" "github.com/lunarway/release-manager/cmd/hamctl/command/actions" "github.com/lunarway/release-manager/internal/artifact" - "github.com/lunarway/release-manager/internal/git" internalhttp "github.com/lunarway/release-manager/internal/http" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -73,13 +72,7 @@ func TestRollback(t *testing.T) { Auth: NoAuthClient{}, } - gitConfigAPI := command.GitConfigAPIMock{ - CommitterDetailsFunc: func() (*git.CommitterDetails, error) { - return &git.CommitterDetails{Name: "some-name", Email: "some-email"}, nil - }, - } - - releaseClient := actions.NewReleaseHttpClient(&gitConfigAPI, &c) + releaseClient := actions.NewReleaseHttpClient(&c) runCommand := func(selectRollback command.SelectRollbackRelease, args ...string) ([]string, error) { var output []string diff --git a/cmd/hamctl/command/root.go b/cmd/hamctl/command/root.go index de9b68fa..928cc141 100644 --- a/cmd/hamctl/command/root.go +++ b/cmd/hamctl/command/root.go @@ -8,7 +8,6 @@ import ( "github.com/lunarway/release-manager/cmd/hamctl/command/actions" "github.com/lunarway/release-manager/cmd/hamctl/command/completion" - "github.com/lunarway/release-manager/internal/git" "github.com/lunarway/release-manager/internal/http" "github.com/pkg/errors" "github.com/spf13/cobra" @@ -35,16 +34,13 @@ func NewRoot(version *string) (*cobra.Command, error) { }, } - gitConfigAPI := git.NewLocalGitConfigAPI() - releaseClient := actions.NewReleaseHttpClient(gitConfigAPI, &client) + releaseClient := actions.NewReleaseHttpClient(&client) var command = &cobra.Command{ Use: "hamctl", Short: "hamctl controls a release manager server", BashCompletionFunction: completion.Hamctl, PersistentPreRunE: func(c *cobra.Command, args []string) error { - // all commands but version and completion requires the "service" flag - // if this is one of them, skip the check if c.Name() == "version" || c.Name() == "completion" || c.Name() == "login" { return nil } @@ -76,7 +72,7 @@ func NewRoot(version *string) (*cobra.Command, error) { command.AddCommand( NewCompletion(command), NewDescribe(&client, &service), - NewPolicy(&client, &service, gitConfigAPI), + NewPolicy(&client, &service), NewPromote(&client, &service, releaseClient), NewRelease(&client, &service, loggerFunc, releaseClient), NewRollback(&client, &service, loggerFunc, SelectRollbackReleaseFunc, releaseClient), diff --git a/cmd/server/http/release.go b/cmd/server/http/release.go index 6293a092..ddf94ebf 100644 --- a/cmd/server/http/release.go +++ b/cmd/server/http/release.go @@ -33,8 +33,8 @@ func release(payload *payload, flowSvc *flow.Service) http.HandlerFunc { logger.Infof("http: release: service '%s' environment '%s' artifact id '%s': releasing artifact", req.Service, req.Environment, req.ArtifactID) releaseID, err := flowSvc.ReleaseArtifactID(ctx, flow.Actor{ - Name: req.CommitterName, - Email: req.CommitterEmail, + Name: "Subject", //req.CommitterName, + Email: "Subject", //req.CommitterEmail, }, req.Environment, req.Service, req.ArtifactID, req.Intent) var statusString string diff --git a/internal/http/types.go b/internal/http/types.go index ee36686a..8d334ae5 100644 --- a/internal/http/types.go +++ b/internal/http/types.go @@ -32,12 +32,10 @@ type Environment struct { } type ReleaseRequest struct { - Service string `json:"service,omitempty"` - Environment string `json:"environment,omitempty"` - ArtifactID string `json:"artifactId,omitempty"` - CommitterName string `json:"committerName,omitempty"` - CommitterEmail string `json:"committerEmail,omitempty"` - Intent intent.Intent `json:"intent,omitempty"` + Service string `json:"service,omitempty"` + Environment string `json:"environment,omitempty"` + ArtifactID string `json:"artifactId,omitempty"` + Intent intent.Intent `json:"intent,omitempty"` } func (r ReleaseRequest) Validate(w http.ResponseWriter) bool { @@ -48,12 +46,6 @@ func (r ReleaseRequest) Validate(w http.ResponseWriter) bool { if emptyString(r.Environment) { errs.Append(requiredField("environment")) } - if emptyString(r.CommitterName) { - errs.Append(requiredField("committerName")) - } - if emptyString(r.CommitterEmail) { - errs.Append(requiredField("committerEmail")) - } if emptyString(r.ArtifactID) { errs.Append("required field artifact id is not specified") } From 8dd7dc0af3e6ba3572536cfaf82f665de5fd492f Mon Sep 17 00:00:00 2001 From: Hoeg Date: Mon, 6 Nov 2023 21:06:55 +0100 Subject: [PATCH 09/42] add token verification --- cmd/server/command/root.go | 5 ++ cmd/server/command/start.go | 15 +++- cmd/server/http/http.go | 28 ++------ cmd/server/http/http_test.go | 68 ------------------ cmd/server/http/jwt.go | 134 +++++++++++++++++++++++++++++++++++ go.mod | 14 +++- go.sum | 34 ++++++++- 7 files changed, 203 insertions(+), 95 deletions(-) delete mode 100644 cmd/server/http/http_test.go create mode 100644 cmd/server/http/jwt.go diff --git a/cmd/server/command/root.go b/cmd/server/command/root.go index dd6088fc..20987716 100644 --- a/cmd/server/command/root.go +++ b/cmd/server/command/root.go @@ -23,6 +23,7 @@ func NewRoot(version string) (*cobra.Command, error) { var githubAPIToken string var configRepoOpts configRepoOptions var gitConfigOpts git.GitConfig + var jwtVerifierOpts jwtVerifierOptions var gpgKeyPaths []string var users []string var userMappings map[string]string @@ -68,6 +69,7 @@ func NewRoot(version string) (*cobra.Command, error) { gitConfigOpts: &gitConfigOpts, s3storage: &s3storageOpts, http: &httpOpts, + jwtVerifier: &jwtVerifierOpts, gpgKeyPaths: &gpgKeyPaths, broker: &brokerOpts, slackMutes: &slackMuteOpts, @@ -85,6 +87,9 @@ func NewRoot(version string) (*cobra.Command, error) { command.PersistentFlags().StringVar(&configRepoOpts.ConfigRepo, "config-repo", os.Getenv("CONFIG_REPO"), "ssh url for the git config repository") command.PersistentFlags().StringVar(&configRepoOpts.ArtifactFileName, "artifact-filename", "artifact.json", "the filename of the artifact to be used") command.PersistentFlags().StringVar(&configRepoOpts.SSHPrivateKeyPath, "ssh-private-key", "/etc/release-manager/ssh/identity", "ssh-private-key for the config repo") + command.PersistentFlags().StringVar(&jwtVerifierOpts.JwksLocation, "jwks-urls", "", "URL of the JWKS for the IdP") + command.PersistentFlags().StringVar(&jwtVerifierOpts.Audience, "jwt-audience", "release-manager", "the expected audience of the access token") + command.PersistentFlags().StringVar(&jwtVerifierOpts.Issuer, "jwt-issuer", "", "the issuer of the access tokens") command.PersistentFlags().StringVar(&httpOpts.GithubWebhookSecret, "github-webhook-secret", os.Getenv("GITHUB_WEBHOOK_SECRET"), "github webhook secret") command.PersistentFlags().StringVar(&githubAPIToken, "github-api-token", os.Getenv("GITHUB_API_TOKEN"), "github api token for tagging releases") command.PersistentFlags().StringVar(&slackAuthToken, "slack-token", os.Getenv("SLACK_TOKEN"), "token to be used to communicate with the slack api") diff --git a/cmd/server/command/start.go b/cmd/server/command/start.go index 54d6064c..af907eaa 100644 --- a/cmd/server/command/start.go +++ b/cmd/server/command/start.go @@ -96,6 +96,12 @@ type s3storageOptions struct { S3BucketName string } +type jwtVerifierOptions struct { + JwksLocation string + Issuer string + Audience string +} + type startOptions struct { slackAuthToken *string githubAPIToken *string @@ -106,6 +112,7 @@ type startOptions struct { broker *brokerOptions s3storage *s3storageOptions slackMutes *intslack.MuteOptions + jwtVerifier *jwtVerifierOptions gpgKeyPaths *[]string userMappings *map[string]string branchRestrictionPolicies *[]policy.BranchRestriction @@ -509,7 +516,12 @@ func NewStart(startOptions *startOptions) *cobra.Command { } }() go func() { - err := http.NewServer( + jwtVerifier, err := http.NewVerifier(startOptions.jwtVerifier.JwksLocation, time.Duration(30)*time.Second, startOptions.jwtVerifier.Issuer, startOptions.jwtVerifier.Audience) + if err != nil { + done <- errors.WithMessage(err, "new jwt verifier") + return + } + err = http.NewServer( startOptions.http, slackClient, &flowSvc, @@ -517,6 +529,7 @@ func NewStart(startOptions *startOptions) *cobra.Command { &gitSvc, s3storageSvc, tracer, + jwtVerifier, ) if err != nil { done <- errors.WithMessage(err, "new http server") diff --git a/cmd/server/http/http.go b/cmd/server/http/http.go index d0586d1c..e47469f7 100644 --- a/cmd/server/http/http.go +++ b/cmd/server/http/http.go @@ -4,14 +4,12 @@ import ( "fmt" "net/http" "net/http/pprof" - "strings" "time" "github.com/google/uuid" "github.com/gorilla/mux" "github.com/lunarway/release-manager/internal/flow" "github.com/lunarway/release-manager/internal/git" - httpinternal "github.com/lunarway/release-manager/internal/http" "github.com/lunarway/release-manager/internal/log" policyinternal "github.com/lunarway/release-manager/internal/policy" "github.com/lunarway/release-manager/internal/slack" @@ -30,7 +28,7 @@ type Options struct { S3WebhookSecret string } -func NewServer(opts *Options, slackClient *slack.Client, flowSvc *flow.Service, policySvc *policyinternal.Service, gitSvc *git.Service, artifactWriteStorage ArtifactWriteStorage, tracer tracing.Tracer) error { +func NewServer(opts *Options, slackClient *slack.Client, flowSvc *flow.Service, policySvc *policyinternal.Service, gitSvc *git.Service, artifactWriteStorage ArtifactWriteStorage, tracer tracing.Tracer, jwtVerifier *Verifier) error { payloader := payload{ tracer: tracer, } @@ -45,7 +43,7 @@ func NewServer(opts *Options, slackClient *slack.Client, flowSvc *flow.Service, m.Use(reqrespLogger) hamctlMux := m.NewRoute().Subrouter() - hamctlMux.Use(authenticate(opts.HamCtlAuthToken)) + hamctlMux.Use(jwtVerifier.authentication(opts.HamCtlAuthToken)) hamctlMux.Methods(http.MethodPost).Path("/release").Handler(release(&payloader, flowSvc)) hamctlMux.Methods(http.MethodGet).Path("/status").Handler(status(&payloader, flowSvc)) @@ -60,14 +58,14 @@ func NewServer(opts *Options, slackClient *slack.Client, flowSvc *flow.Service, hamctlMux.Methods(http.MethodGet).Path("/describe/latest-artifact/{service}").Handler(describeLatestArtifacts(&payloader, flowSvc)) daemonMux := m.NewRoute().Subrouter() - daemonMux.Use(authenticate(opts.DaemonAuthToken)) + daemonMux.Use(jwtVerifier.authentication(opts.DaemonAuthToken)) daemonMux.Methods(http.MethodPost).Path("/webhook/daemon/k8s/deploy").Handler(daemonk8sDeployWebhook(&payloader, flowSvc)) daemonMux.Methods(http.MethodPost).Path("/webhook/daemon/k8s/error").Handler(daemonk8sPodErrorWebhook(&payloader, flowSvc)) daemonMux.Methods(http.MethodPost).Path("/webhook/daemon/k8s/joberror").Handler(daemonk8sJobErrorWebhook(&payloader, flowSvc)) // s3 endpoints artifactMux := m.NewRoute().Subrouter() - artifactMux.Use(authenticate(opts.ArtifactAuthToken)) + artifactMux.Use(jwtVerifier.authentication(opts.ArtifactAuthToken)) artifactMux.Methods(http.MethodPost).Path("/artifacts/create").Handler(createArtifact(&payloader, artifactWriteStorage)) // profiling endpoints @@ -140,21 +138,3 @@ func trace(tracer tracing.Tracer) func(http.Handler) http.Handler { }) } } - -// authenticate authenticates the handler against a Bearer token. -// -// If authentication fails a 401 Unauthorized HTTP status is returned with an -// ErrorResponse body. -func authenticate(token string) func(http.Handler) http.Handler { - return func(h http.Handler) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - authorization := r.Header.Get("X-HAM-TOKEN") - t := strings.TrimSpace(authorization) - if t != token { - httpinternal.Error(w, "please provide a valid authentication token", http.StatusUnauthorized) - return - } - h.ServeHTTP(w, r) - }) - } -} diff --git a/cmd/server/http/http_test.go b/cmd/server/http/http_test.go deleted file mode 100644 index fcd4720f..00000000 --- a/cmd/server/http/http_test.go +++ /dev/null @@ -1,68 +0,0 @@ -package http - -import ( - "net/http" - "net/http/httptest" - "testing" - - "github.com/stretchr/testify/assert" -) - -func TestAuthenticate(t *testing.T) { - tt := []struct { - name string - serverToken string - authorization string - status int - }{ - { - name: "empty authorization", - serverToken: "token", - authorization: "", - status: http.StatusUnauthorized, - }, - { - name: "whitespace token", - serverToken: "token", - authorization: " ", - status: http.StatusUnauthorized, - }, - { - name: "empty bearer authorization", - serverToken: "token", - authorization: " ", - status: http.StatusUnauthorized, - }, - { - name: "whitespace bearer authorization", - serverToken: "token", - authorization: " ", - status: http.StatusUnauthorized, - }, - { - name: "wrong bearer authorization", - serverToken: "token", - authorization: "another-token", - status: http.StatusUnauthorized, - }, - { - name: "correct bearer authorization", - serverToken: "token", - authorization: "token", - status: http.StatusOK, - }, - } - for _, tc := range tt { - t.Run(tc.name, func(t *testing.T) { - handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - w.WriteHeader(http.StatusOK) - }) - req := httptest.NewRequest(http.MethodGet, "/", nil) - req.Header.Set("X-HAM-TOKEN", tc.authorization) - w := httptest.NewRecorder() - authenticate(tc.serverToken)(handler).ServeHTTP(w, req) - - assert.Equal(t, tc.status, w.Result().StatusCode, "status code not as expected") - }) - } -} diff --git a/cmd/server/http/jwt.go b/cmd/server/http/jwt.go new file mode 100644 index 00000000..65a40ffb --- /dev/null +++ b/cmd/server/http/jwt.go @@ -0,0 +1,134 @@ +package http + +import ( + "context" + "net/http" + "strings" + "time" + + "github.com/lestrrat-go/jwx/v2/jwk" + "github.com/lestrrat-go/jwx/v2/jwt" + httpinternal "github.com/lunarway/release-manager/internal/http" + "github.com/pkg/errors" +) + +const keyNotFoundMsg = "failed to find key with key ID" + +type JwkCache interface { + Get(ctx context.Context, url string) (jwk.Set, error) + Refresh(ctx context.Context, url string) (jwk.Set, error) +} + +type Verifier struct { + jwksLocation string + issuer string + audience string + + jwkCache JwkCache +} + +func NewVerifier(jwksLocation string, jwkFetchTimeout time.Duration, issuer string, audience string) (*Verifier, error) { + ctx := context.Background() + + cache := jwk.NewCache(ctx) + err := cache.Register(jwksLocation, jwk.WithMinRefreshInterval(24*time.Hour)) + if err != nil { + return nil, err + } + _, err = cache.Refresh(ctx, jwksLocation) + if err != nil { + return nil, err + } + + return &Verifier{ + jwksLocation: jwksLocation, + jwkCache: cache, + issuer: issuer, + audience: audience, + }, nil +} + +func ParseBearerToken(token string) (string, error) { + jwt := strings.TrimPrefix(token, "Bearer") + + tokenParts := strings.Split(jwt, ".") + + if len(tokenParts) != 3 { + return "", errors.New("invalid token format") + } + + return strings.TrimSpace(jwt), nil +} + +const AUTH_USER_KEY = "AUTH_USER_KEY" + +// authenticate authenticates the handler against a Bearer token. +// +// If authentication fails a 401 Unauthorized HTTP status is returned with an +// ErrorResponse body. +func (v *Verifier) authentication(token string) func(http.Handler) http.Handler { + return func(h http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + bearerToken, err := ParseBearerToken(token) + if err != nil { + authorization := r.Header.Get("Authorization") + t := strings.TrimPrefix(authorization, "Bearer ") + t = strings.TrimSpace(t) + if t != token { + httpinternal.Error(w, "please provide a valid authentication token", http.StatusUnauthorized) + return + } + } else { + keySet, err := v.jwkCache.Get(context.Background(), v.jwksLocation) + if err != nil { + httpinternal.Error(w, "please provide a valid authentication token", http.StatusUnauthorized) + return + } + + parsedToken, err := v.verify(bearerToken, keySet) + if err != nil { + if strings.Contains(err.Error(), keyNotFoundMsg) { + freshKeys, err := v.jwkCache.Refresh(context.Background(), v.jwksLocation) + if err != nil { + httpinternal.Error(w, "please provide a valid authentication token", http.StatusUnauthorized) + return + } + parsedToken, err = v.verify(bearerToken, freshKeys) + if err != nil { + httpinternal.Error(w, "please provide a valid authentication token", http.StatusUnauthorized) + return + } + } else { + httpinternal.Error(w, "please provide a valid authentication token", http.StatusUnauthorized) + return + } + } + context.WithValue(r.Context(), AUTH_USER_KEY, parsedToken.Subject()) + } + h.ServeHTTP(w, r) + }) + } +} + +func (j *Verifier) verify(token string, keySet jwk.Set) (jwt.Token, error) { + parseOptions := []jwt.ParseOption{ + jwt.WithKeySet(keySet), + jwt.WithValidate(true), + jwt.WithVerify(true), + jwt.WithIssuer(j.issuer), + jwt.WithAcceptableSkew(time.Second), + } + if j.audience != "" { + parseOptions = append(parseOptions, jwt.WithAudience(j.audience)) + } + + parsedToken, err := jwt.ParseString(token, parseOptions...) + if err != nil { + return nil, err + } + + if parsedToken.Subject() == "" { + return nil, jwt.ErrMissingRequiredClaim("sub") + } + return parsedToken, nil +} diff --git a/go.mod b/go.mod index 24f8effd..d5ec18c7 100644 --- a/go.mod +++ b/go.mod @@ -19,7 +19,7 @@ require ( github.com/rabbitmq/amqp091-go v1.5.0 github.com/spf13/cobra v1.6.1 github.com/spf13/pflag v1.0.5 - github.com/stretchr/testify v1.8.1 + github.com/stretchr/testify v1.8.4 github.com/uber/jaeger-client-go v2.30.0+incompatible github.com/uber/jaeger-lib v2.4.1+incompatible go.uber.org/multierr v1.8.0 @@ -37,6 +37,17 @@ require ( github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8 ) +require ( + github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0 // indirect + github.com/goccy/go-json v0.10.2 // indirect + github.com/lestrrat-go/blackmagic v1.0.2 // indirect + github.com/lestrrat-go/httpcc v1.0.1 // indirect + github.com/lestrrat-go/httprc v1.0.4 // indirect + github.com/lestrrat-go/iter v1.0.2 // indirect + github.com/lestrrat-go/option v1.0.1 // indirect + github.com/segmentio/asm v1.2.0 // indirect +) + require ( github.com/Microsoft/go-winio v0.4.16 // indirect github.com/ProtonMail/go-crypto v0.0.0-20210428141323-04723f9f07d7 // indirect @@ -68,6 +79,7 @@ require ( github.com/josharian/intern v1.0.0 // indirect github.com/json-iterator/go v1.1.12 // indirect github.com/kevinburke/ssh_config v0.0.0-20201106050909-4977a11b4351 // indirect + github.com/lestrrat-go/jwx/v2 v2.0.16 github.com/mailru/easyjson v0.7.6 // indirect github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369 // indirect github.com/mitchellh/go-homedir v1.1.0 // indirect diff --git a/go.sum b/go.sum index 64502c2d..08f1e05c 100644 --- a/go.sum +++ b/go.sum @@ -83,6 +83,9 @@ github.com/cyphar/filepath-securejoin v0.2.4/go.mod h1:aPGpWjXOXUn2NCNjFvBE6aRxG github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 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/decred/dcrd/crypto/blake256 v1.0.1/go.mod h1:2OfgNZ5wDpcsFmHmCK5gZTPcCXqlm2ArzUIkw9czNJo= +github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0 h1:8UrgZ3GkP4i/CLijOJx79Yu+etlyjdBU4sfcs2WYQMs= +github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0/go.mod h1:v57UDF4pDQJcEfFUCRop3lJL149eHGSe9Jvczhzjo/0= github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815/go.mod h1:WwZ+bS3ebgob9U8Nd0kOddGdZWjyMGR8Wziv+TBNwSE= github.com/dustin/go-humanize v1.0.0 h1:VSnTsYCnlFHaM2/igO1h6X3HA71jcobQuxemgkq4zYo= github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= @@ -131,6 +134,8 @@ github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh github.com/go-openapi/swag v0.19.14 h1:gm3vOOXfiuw5i9p5N9xJvfjvuofpyvLA9Wr6QfK5Fng= github.com/go-openapi/swag v0.19.14/go.mod h1:QYRuS/SOXUCsnplDa677K7+DxSOj6IPNl/eQntq43wQ= github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= +github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU= +github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= @@ -248,6 +253,19 @@ github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/lestrrat-go/blackmagic v1.0.2 h1:Cg2gVSc9h7sz9NOByczrbUvLopQmXrfFx//N+AkAr5k= +github.com/lestrrat-go/blackmagic v1.0.2/go.mod h1:UrEqBzIR2U6CnzVyUtfM6oZNMt/7O7Vohk2J0OGSAtU= +github.com/lestrrat-go/httpcc v1.0.1 h1:ydWCStUeJLkpYyjLDHihupbn2tYmZ7m22BGkcvZZrIE= +github.com/lestrrat-go/httpcc v1.0.1/go.mod h1:qiltp3Mt56+55GPVCbTdM9MlqhvzyuL6W/NMDA8vA5E= +github.com/lestrrat-go/httprc v1.0.4 h1:bAZymwoZQb+Oq8MEbyipag7iSq6YIga8Wj6GOiJGdI8= +github.com/lestrrat-go/httprc v1.0.4/go.mod h1:mwwz3JMTPBjHUkkDv/IGJ39aALInZLrhBp0X7KGUZlo= +github.com/lestrrat-go/iter v1.0.2 h1:gMXo1q4c2pHmC3dn8LzRhJfP1ceCbgSiT9lUydIzltI= +github.com/lestrrat-go/iter v1.0.2/go.mod h1:Momfcq3AnRlRjI5b5O8/G5/BvpzrhoFTZcn06fEOPt4= +github.com/lestrrat-go/jwx/v2 v2.0.16 h1:TuH3dBkYTy2giQg/9D8f20znS3JtMRuQJ372boS3lWk= +github.com/lestrrat-go/jwx/v2 v2.0.16/go.mod h1:jBHyESp4e7QxfERM0UKkQ80/94paqNIEcdEfiUYz5zE= +github.com/lestrrat-go/option v1.0.0/go.mod h1:5ZHFbivi4xwXxhxY9XHDe2FHo6/Z7WWmtT7T5nBBp3I= +github.com/lestrrat-go/option v1.0.1 h1:oAzP2fvZGQKWkvHa1/SAcFolBEca1oN+mQ7eooNBEYU= +github.com/lestrrat-go/option v1.0.1/go.mod h1:5ZHFbivi4xwXxhxY9XHDe2FHo6/Z7WWmtT7T5nBBp3I= github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= github.com/mailru/easyjson v0.7.6 h1:8yTIVnZgCoiM1TgqoeTl+LfU5Jg6/xL3QhGQnimLYnA= @@ -321,6 +339,8 @@ github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFR github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/ryszard/goskiplist v0.0.0-20150312221310-2dfbae5fcf46 h1:GHRpF1pTW19a8tTFrMLUcfWwyC0pnifVo2ClaLq+hP8= github.com/ryszard/goskiplist v0.0.0-20150312221310-2dfbae5fcf46/go.mod h1:uAQ5PCi+MFsC7HjREoAz1BU+Mq60+05gifQSsHSDG/8= +github.com/segmentio/asm v1.2.0 h1:9BQrFxC+YOHJlTlHGkTrFWf59nbL3XnCoFLTwDCI7ys= +github.com/segmentio/asm v1.2.0/go.mod h1:BqMnlJP91P8d+4ibuonYZw9mfnzI9HfxselHZr5aAcs= github.com/sergi/go-diff v1.1.0 h1:we8PVUC3FE2uYfodKH/nBHMSetSfHDR6scGdBi+erh0= github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM= github.com/shabbyrobe/gocovmerge v0.0.0-20180507124511-f6ea450bfb63 h1:J6qvD6rbmOil46orKqJaRPG+zTpoGlBTUdyv8ki63L0= @@ -349,8 +369,9 @@ github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/ github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= -github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/uber/jaeger-client-go v2.30.0+incompatible h1:D6wyKGCecFaSRUpo8lCVbaOOb6ThwMmTEbhRwtKR97o= github.com/uber/jaeger-client-go v2.30.0+incompatible/go.mod h1:WVhlPFC8FDjOFMMWRy2pZqQJSXxYSwNYOkTr/Z6d3Kk= github.com/uber/jaeger-lib v2.4.1+incompatible h1:td4jdvLcExb4cBISKIpHuGoVXh+dVKhn2Um6rjCsSsg= @@ -427,6 +448,7 @@ golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -466,6 +488,8 @@ golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco= +golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= +golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM= golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= @@ -489,6 +513,7 @@ golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -536,14 +561,19 @@ golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20210616045830-e2b7044e8c71/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220114195835-da31bd327af9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE= golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= +golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= golang.org/x/term v0.13.0 h1:bb+I9cTfFazGW51MZqBVmZy7+JEJMouUHTUSKVQLBek= golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= @@ -554,6 +584,8 @@ golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/text v0.13.0 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k= golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= From 6c7cfff6ddda3d97bbd3617a86c2cbfdc41d0217 Mon Sep 17 00:00:00 2001 From: Hoeg Date: Tue, 7 Nov 2023 06:43:51 +0100 Subject: [PATCH 10/42] fixed auth tests --- cmd/server/http/http_test.go | 75 ++++++++++++++++++++++++++++++++++++ 1 file changed, 75 insertions(+) create mode 100644 cmd/server/http/http_test.go diff --git a/cmd/server/http/http_test.go b/cmd/server/http/http_test.go new file mode 100644 index 00000000..8abe8d29 --- /dev/null +++ b/cmd/server/http/http_test.go @@ -0,0 +1,75 @@ +package http + +import ( + "net/http" + "net/http/httptest" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestAuthenticate(t *testing.T) { + tt := []struct { + name string + serverToken string + authorization string + status int + }{ + { + name: "empty authorization", + serverToken: "token", + authorization: "", + status: http.StatusUnauthorized, + }, + { + name: "whitespace token", + serverToken: "token", + authorization: " ", + status: http.StatusUnauthorized, + }, + { + name: "non-bearer authorization", + serverToken: "token", + authorization: "non-bearer-token", + status: http.StatusUnauthorized, + }, + { + name: "empty bearer authorization", + serverToken: "token", + authorization: "Bearer ", + status: http.StatusUnauthorized, + }, + { + name: "whitespace bearer authorization", + serverToken: "token", + authorization: "Bearer ", + status: http.StatusUnauthorized, + }, + { + name: "wrong bearer authorization", + serverToken: "token", + authorization: "Bearer another-token", + status: http.StatusUnauthorized, + }, + { + name: "correct bearer authorization", + serverToken: "token", + authorization: "Bearer token", + status: http.StatusOK, + }, + } + for _, tc := range tt { + t.Run(tc.name, func(t *testing.T) { + verifier := Verifier{} + handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + }) + req := httptest.NewRequest(http.MethodGet, "/", nil) + req.Header.Set("Authorization", tc.authorization) + w := httptest.NewRecorder() + verifier.authentication(tc.serverToken)(handler).ServeHTTP(w, req) + + assert.Equal(t, tc.status, w.Result().StatusCode, "status code not as expected") + }) + } +} From 9b49538dc9ed5eb6067dcbf002320e8198fc15b7 Mon Sep 17 00:00:00 2001 From: Hoeg Date: Tue, 7 Nov 2023 17:33:18 +0100 Subject: [PATCH 11/42] do not require name and email --- internal/http/types.go | 27 ++++++--------------------- 1 file changed, 6 insertions(+), 21 deletions(-) diff --git a/internal/http/types.go b/internal/http/types.go index 8d334ae5..0d6da6b9 100644 --- a/internal/http/types.go +++ b/internal/http/types.go @@ -32,10 +32,12 @@ type Environment struct { } type ReleaseRequest struct { - Service string `json:"service,omitempty"` - Environment string `json:"environment,omitempty"` - ArtifactID string `json:"artifactId,omitempty"` - Intent intent.Intent `json:"intent,omitempty"` + Service string `json:"service,omitempty"` + Environment string `json:"environment,omitempty"` + ArtifactID string `json:"artifactId,omitempty"` + CommitterName string `json:"committerName,omitempty"` + CommitterEmail string `json:"committerEmail,omitempty"` + Intent intent.Intent `json:"intent,omitempty"` } func (r ReleaseRequest) Validate(w http.ResponseWriter) bool { @@ -162,12 +164,6 @@ func (r ApplyBranchRestrictionPolicyRequest) Validate(w http.ResponseWriter) boo if emptyString(r.BranchRegex) { errs.Append(requiredField("branch regex")) } - if emptyString(r.CommitterName) { - errs.Append(requiredField("committerName")) - } - if emptyString(r.CommitterEmail) { - errs.Append(requiredField("committerEmail")) - } return errs.Evaluate(w) } @@ -197,12 +193,6 @@ func (r ApplyAutoReleasePolicyRequest) Validate(w http.ResponseWriter) bool { if emptyString(r.Environment) { errs.Append(requiredField("environment")) } - if emptyString(r.CommitterName) { - errs.Append(requiredField("committerName")) - } - if emptyString(r.CommitterEmail) { - errs.Append(requiredField("committerEmail")) - } return errs.Evaluate(w) } @@ -224,11 +214,6 @@ func (r DeletePolicyRequest) Validate(w http.ResponseWriter) bool { var errs validationErrors if emptyString(r.Service) { errs.Append(requiredField("service")) - } - if emptyString(r.CommitterName) { - errs.Append(requiredField("committerName")) - } - if emptyString(r.CommitterEmail) { errs.Append(requiredField("committerEmail")) } ids := filterEmptyStrings(r.PolicyIDs) From 7ddb6c18194626156c3c5b34b5d92ecfe366ec0e Mon Sep 17 00:00:00 2001 From: Hoeg Date: Tue, 7 Nov 2023 17:57:52 +0100 Subject: [PATCH 12/42] get email from subject --- cmd/server/http/policy.go | 41 ++++++++++++++++++++++++++++---------- cmd/server/http/release.go | 16 +++++++++++---- 2 files changed, 43 insertions(+), 14 deletions(-) diff --git a/cmd/server/http/policy.go b/cmd/server/http/policy.go index 24f59417..ec429293 100644 --- a/cmd/server/http/policy.go +++ b/cmd/server/http/policy.go @@ -30,12 +30,19 @@ func applyAutoReleasePolicy(payload *payload, policySvc *policyinternal.Service) return } - logger = logger.WithFields("service", req.Service, "req", req) - logger.Infof("http: policy: apply: service '%s' branch '%s' environment '%s': apply auto-release policy started", req.Service, req.Branch, req.Environment) - id, err := policySvc.ApplyAutoRelease(ctx, policyinternal.Actor{ + actor := policyinternal.Actor{ Name: req.CommitterName, Email: req.CommitterEmail, - }, req.Service, req.Branch, req.Environment) + } + subject := r.Context().Value(AUTH_USER_KEY).(string) + if subject != "" { + actor.Email = subject + actor.Name = subject + } + + logger = logger.WithFields("service", req.Service, "req", req) + logger.Infof("http: policy: apply: service '%s' branch '%s' environment '%s': apply auto-release policy started", req.Service, req.Branch, req.Environment) + id, err := policySvc.ApplyAutoRelease(ctx, actor, req.Service, req.Branch, req.Environment) if err != nil { if ctx.Err() == context.Canceled { logger.Infof("http: policy: apply: service '%s' branch '%s' environment '%s': apply auto-release cancelled", req.Service, req.Branch, req.Environment) @@ -88,12 +95,19 @@ func applyBranchRestrictionPolicy(payload *payload, policySvc *policyinternal.Se return } - logger = logger.WithFields("service", req.Service, "req", req) - logger.Infof("http: policy: apply: service '%s' branch regex '%s' environment '%s': apply branch-restriction policy started", req.Service, req.BranchRegex, req.Environment) - id, err := policySvc.ApplyBranchRestriction(ctx, policyinternal.Actor{ + actor := policyinternal.Actor{ Name: req.CommitterName, Email: req.CommitterEmail, - }, req.Service, req.BranchRegex, req.Environment) + } + subject := r.Context().Value(AUTH_USER_KEY).(string) + if subject != "" { + actor.Email = subject + actor.Name = subject + } + + logger = logger.WithFields("service", req.Service, "req", req) + logger.Infof("http: policy: apply: service '%s' branch regex '%s' environment '%s': apply branch-restriction policy started", req.Service, req.BranchRegex, req.Environment) + id, err := policySvc.ApplyBranchRestriction(ctx, actor, req.Service, req.BranchRegex, req.Environment) if err != nil { if ctx.Err() == context.Canceled { logger.Infof("http: policy: apply: service '%s' branch regex '%s' environment '%s': apply branch-restriction cancelled", req.Service, req.BranchRegex, req.Environment) @@ -219,10 +233,17 @@ func deletePolicies(payload *payload, policySvc *policyinternal.Service) http.Ha ids := filterEmptyStrings(req.PolicyIDs) logger = logger.WithFields("service", req.Service, "req", req) - deleted, err := policySvc.Delete(ctx, policyinternal.Actor{ + actor := policyinternal.Actor{ Name: req.CommitterName, Email: req.CommitterEmail, - }, req.Service, ids) + } + subject := r.Context().Value(AUTH_USER_KEY).(string) + if subject != "" { + actor.Email = subject + actor.Name = subject + } + + deleted, err := policySvc.Delete(ctx, actor, req.Service, ids) if err != nil { if ctx.Err() == context.Canceled { logger.Errorf("http: policy: delete: service '%s' ids %v: delete cancelled", req.Service, ids) diff --git a/cmd/server/http/release.go b/cmd/server/http/release.go index ddf94ebf..3cfaf8ca 100644 --- a/cmd/server/http/release.go +++ b/cmd/server/http/release.go @@ -26,16 +26,24 @@ func release(payload *payload, flowSvc *flow.Service) http.HandlerFunc { if !req.Validate(w) { return } + + actor := flow.Actor{ + Name: req.CommitterName, + Email: req.CommitterEmail, + } + subject := r.Context().Value(AUTH_USER_KEY).(string) + if subject != "" { + actor.Email = subject + actor.Name = subject + } + logger = logger.WithFields( "service", req.Service, "req", req, "intent", req.Intent) logger.Infof("http: release: service '%s' environment '%s' artifact id '%s': releasing artifact", req.Service, req.Environment, req.ArtifactID) - releaseID, err := flowSvc.ReleaseArtifactID(ctx, flow.Actor{ - Name: "Subject", //req.CommitterName, - Email: "Subject", //req.CommitterEmail, - }, req.Environment, req.Service, req.ArtifactID, req.Intent) + releaseID, err := flowSvc.ReleaseArtifactID(ctx, actor, req.Environment, req.Service, req.ArtifactID, req.Intent) var statusString string if err != nil { From 6c017b82f2d3c5895522e02bf73d35510fca7968 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B8rn=20S=C3=B8rensen?= Date: Tue, 7 Nov 2023 20:28:13 +0100 Subject: [PATCH 13/42] Set Auth on http.Client in root command Fixed a nil panic --- cmd/hamctl/command/root.go | 1 + 1 file changed, 1 insertion(+) diff --git a/cmd/hamctl/command/root.go b/cmd/hamctl/command/root.go index 928cc141..88f42f51 100644 --- a/cmd/hamctl/command/root.go +++ b/cmd/hamctl/command/root.go @@ -29,6 +29,7 @@ func NewRoot(version *string) (*cobra.Command, error) { var service string client := http.Client{ + Auth: &authenticator, Metadata: http.Metadata{ CLIVersion: *version, }, From 7e13bc72473eb30fbcc352dee1ba907a72c2f963 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B8rn=20S=C3=B8rensen?= Date: Tue, 7 Nov 2023 22:13:09 +0100 Subject: [PATCH 14/42] Fix req ctx update and auth strategy and add tests --- cmd/server/http/http_test.go | 223 ++++++++++++++++++++++++++++++++++- cmd/server/http/jwt.go | 22 +++- 2 files changed, 238 insertions(+), 7 deletions(-) diff --git a/cmd/server/http/http_test.go b/cmd/server/http/http_test.go index 8abe8d29..aa42d34b 100644 --- a/cmd/server/http/http_test.go +++ b/cmd/server/http/http_test.go @@ -1,14 +1,24 @@ package http import ( + "crypto/ecdsa" + "crypto/elliptic" + "crypto/rand" + "encoding/json" + "fmt" "net/http" "net/http/httptest" "testing" + "time" + "github.com/lestrrat-go/jwx/v2/jwa" + "github.com/lestrrat-go/jwx/v2/jwk" + "github.com/lestrrat-go/jwx/v2/jwt" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) -func TestAuthenticate(t *testing.T) { +func TestAuthenticate_token(t *testing.T) { tt := []struct { name string serverToken string @@ -73,3 +83,214 @@ func TestAuthenticate(t *testing.T) { }) } } + +func TestAuthenticate_jwt(t *testing.T) { + jwksServer, minter := getJwksEndpoint(t) + + issuer := "test-issuer" + audience := "test-audience" + + tt := []struct { + name string + + authorization string + status int + expectedRequestContextSubject string + }{ + { + name: "empty authorization", + authorization: "", + status: http.StatusUnauthorized, + }, + { + name: "whitespace token", + authorization: " ", + status: http.StatusUnauthorized, + }, + { + name: "non-bearer authorization", + authorization: "non-bearer-token", + status: http.StatusUnauthorized, + }, + { + name: "empty bearer authorization", + authorization: "Bearer ", + status: http.StatusUnauthorized, + }, + { + name: "whitespace bearer authorization", + authorization: "Bearer ", + status: http.StatusUnauthorized, + }, + { + name: "wrong bearer authorization", + authorization: "Bearer another-token", + status: http.StatusUnauthorized, + }, + { + name: "valid bearer authorization", + authorization: fmt.Sprintf("Bearer %s", + minter(t, principal{ + Subject: "sub", + Issuer: issuer, + Audience: audience, + IssuedAt: time.Now().Add(-1 * time.Second), + Expiration: time.Now().Add(10 * time.Second), + }), + ), + status: http.StatusOK, + expectedRequestContextSubject: "sub", + }, + { + name: "expired bearer authorization", + authorization: fmt.Sprintf("Bearer %s", + minter(t, principal{ + Subject: "sub", + Issuer: issuer, + Audience: audience, + IssuedAt: time.Now().Add(-2 * time.Second), + Expiration: time.Now().Add(-1 * time.Second), + }), + ), + status: http.StatusUnauthorized, + }, + { + name: "wrong issuer bearer authorization", + authorization: fmt.Sprintf("Bearer %s", + minter(t, principal{ + Subject: "sub", + Issuer: "wrong-issuer", + Audience: audience, + IssuedAt: time.Now().Add(-1 * time.Second), + Expiration: time.Now().Add(10 * time.Second), + }), + ), + status: http.StatusUnauthorized, + }, + { + name: "wrong audience bearer authorization", + authorization: fmt.Sprintf("Bearer %s", + minter(t, principal{ + Subject: "sub", + Issuer: issuer, + Audience: "wrong-audience", + IssuedAt: time.Now().Add(-1 * time.Second), + Expiration: time.Now().Add(10 * time.Second), + }), + ), + status: http.StatusUnauthorized, + }, + } + for _, tc := range tt { + t.Run(tc.name, func(t *testing.T) { + verifier, err := NewVerifier(jwksServer.URL, 1*time.Second, issuer, audience) + require.NoError(t, err, "failed to create verifier") + + handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + }) + req := httptest.NewRequest(http.MethodGet, "/", nil) + req.Header.Set("Authorization", tc.authorization) + w := httptest.NewRecorder() + verifier.authentication("")(handler).ServeHTTP(w, req) + + if tc.expectedRequestContextSubject != "" { + assert.Equal(t, tc.expectedRequestContextSubject, req.Context().Value(AUTH_USER_KEY)) + } else { + assert.Equal(t, nil, req.Context().Value(AUTH_USER_KEY)) + } + assert.Equal(t, tc.status, w.Result().StatusCode, "status code not as expected") + }) + } +} + +func getJwksEndpoint(t *testing.T) (*httptest.Server, func(t *testing.T, principal principal) string) { + t.Helper() + + jwkSet := jwk.NewSet() + + jwkServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + json.NewEncoder(w).Encode(jwkSet) + })) + + privateJwk, publicJwk := createSigningKey(t, jwkServer.URL) + err := jwkSet.AddKey(publicJwk) + require.NoError(t, err, "could not add jwk") + + return jwkServer, func(t *testing.T, principal principal) string { + return getSignedJwt(t, privateJwk, principal) + } +} + +// Given a JWK and a Principal returns a JWT containing the claims in the Principal and signed by the JWK. +func getSignedJwt(t *testing.T, jwk jwk.Key, principal principal) string { + t.Helper() + + signedJwt := createSignedJWTWithKey(t, &principal, jwk) + return signedJwt +} + +type principal struct { + Subject string + Issuer string + Audience string + IssuedAt time.Time + Expiration time.Time + Claims map[string]string +} + +func createSignedJWTWithKey(t *testing.T, principal *principal, privateJwkKey jwk.Key) string { + t.Helper() + + // Create a new JWT + token := jwt.New() + token.Set("sub", principal.Subject) + token.Set("iss", principal.Issuer) + token.Set("aud", principal.Audience) + token.Set("iat", principal.IssuedAt.UTC().Unix()) + token.Set("exp", principal.Expiration.UTC().Unix()) + for k, v := range principal.Claims { + token.Set(k, v) + } + + // Sign the JWT using the private key + sig, err := jwt.Sign(token, jwt.WithKey(jwa.ES256, privateJwkKey)) + require.NoError(t, err, "could not sign token") + + return string(sig) +} + +func createSigningKey(t *testing.T, issuer string) (jwk.Key, jwk.Key) { + t.Helper() + + // Create keypair + privateKey, publicKey, alg := createECDSAKeyPair(t) + + // Create a JWK from the private key + privateJwkKey, err := jwk.FromRaw(privateKey) + require.NoError(t, err, "could not create jwk key from private key") + + privateJwkKey.Set("iss", issuer) + privateJwkKey.Set("alg", jwa.ES256) + jwk.AssignKeyID(privateJwkKey) + + // Create a JWK from the public key + publicJwkKey, err := jwk.FromRaw(publicKey) + require.NoError(t, err, "could not create jwk key from public key") + + publicJwkKey.Set("iss", issuer) + publicJwkKey.Set("alg", alg) + jwk.AssignKeyID(publicJwkKey) + + return privateJwkKey, publicJwkKey +} + +func createECDSAKeyPair(t *testing.T) (interface{}, interface{}, jwa.SignatureAlgorithm) { + t.Helper() + + // Generate a new key pair + keyPair, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + require.NoError(t, err, "could not generate ecdsa key") + + return keyPair, keyPair.Public(), jwa.ES256 +} diff --git a/cmd/server/http/jwt.go b/cmd/server/http/jwt.go index 65a40ffb..db35b8c1 100644 --- a/cmd/server/http/jwt.go +++ b/cmd/server/http/jwt.go @@ -2,6 +2,7 @@ package http import ( "context" + "log" "net/http" "strings" "time" @@ -66,19 +67,27 @@ const AUTH_USER_KEY = "AUTH_USER_KEY" // // If authentication fails a 401 Unauthorized HTTP status is returned with an // ErrorResponse body. -func (v *Verifier) authentication(token string) func(http.Handler) http.Handler { +func (v *Verifier) authentication(staticAuthToken string) func(http.Handler) http.Handler { return func(h http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - bearerToken, err := ParseBearerToken(token) - if err != nil { - authorization := r.Header.Get("Authorization") + authorization := r.Header.Get("Authorization") + + if staticAuthToken != "" { + // old hamctl token auth t := strings.TrimPrefix(authorization, "Bearer ") t = strings.TrimSpace(t) - if t != token { + if t != staticAuthToken { httpinternal.Error(w, "please provide a valid authentication token", http.StatusUnauthorized) return } } else { + // jwt auth + bearerToken, err := ParseBearerToken(authorization) + if err != nil { + httpinternal.Error(w, "please provide a valid authentication token", http.StatusUnauthorized) + return + } + keySet, err := v.jwkCache.Get(context.Background(), v.jwksLocation) if err != nil { httpinternal.Error(w, "please provide a valid authentication token", http.StatusUnauthorized) @@ -103,7 +112,8 @@ func (v *Verifier) authentication(token string) func(http.Handler) http.Handler return } } - context.WithValue(r.Context(), AUTH_USER_KEY, parsedToken.Subject()) + ctx := context.WithValue(r.Context(), AUTH_USER_KEY, parsedToken.Subject()) + *r = *r.WithContext(ctx) } h.ServeHTTP(w, r) }) From 0bca149aba418d3d7f7216dd8a6766a3cbd980ff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B8rn=20S=C3=B8rensen?= Date: Tue, 7 Nov 2023 22:14:29 +0100 Subject: [PATCH 15/42] Remove unused log import --- cmd/server/http/jwt.go | 1 - 1 file changed, 1 deletion(-) diff --git a/cmd/server/http/jwt.go b/cmd/server/http/jwt.go index db35b8c1..09ac79ce 100644 --- a/cmd/server/http/jwt.go +++ b/cmd/server/http/jwt.go @@ -2,7 +2,6 @@ package http import ( "context" - "log" "net/http" "strings" "time" From b00002db1584ee7b16481d031df251c15dbba494 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B8rn=20S=C3=B8rensen?= Date: Tue, 7 Nov 2023 22:27:14 +0100 Subject: [PATCH 16/42] Log jwt subject in reqresp logger --- cmd/server/http/log.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/cmd/server/http/log.go b/cmd/server/http/log.go index d5b54be3..925ff21a 100644 --- a/cmd/server/http/log.go +++ b/cmd/server/http/log.go @@ -32,6 +32,7 @@ func reqrespLogger(h http.Handler) http.Handler { duration := time.Since(start).Nanoseconds() / 1e6 statusCode := statusWriter.statusCode requestID := getRequestID(r) + subject := r.Context().Value(AUTH_USER_KEY).(string) fields := []interface{}{ "requestId", requestID, "req", struct { @@ -40,12 +41,14 @@ func reqrespLogger(h http.Handler) http.Handler { Method string `json:"method,omitempty"` Path string `json:"path,omitempty"` Headers map[string]string `json:"headers,omitempty"` + Subject string `json:"subject,omitempty"` }{ ID: requestID, URL: r.URL.RequestURI(), Method: r.Method, Path: r.URL.Path, Headers: secureHeaders(flattenHeaders(r.Header)), + Subject: subject, }, "res", struct { StatusCode int `json:"statusCode,omitempty"` From 3afb3a8d3e9b0951b097789e57764e713eb59e29 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B8rn=20S=C3=B8rensen?= Date: Tue, 7 Nov 2023 22:46:05 +0100 Subject: [PATCH 17/42] Add logs for jwt exit points to know what happens if something fails --- cmd/server/http/http_test.go | 9 +++++++++ cmd/server/http/jwt.go | 8 ++++++++ 2 files changed, 17 insertions(+) diff --git a/cmd/server/http/http_test.go b/cmd/server/http/http_test.go index aa42d34b..c1b50d17 100644 --- a/cmd/server/http/http_test.go +++ b/cmd/server/http/http_test.go @@ -14,8 +14,10 @@ import ( "github.com/lestrrat-go/jwx/v2/jwa" "github.com/lestrrat-go/jwx/v2/jwk" "github.com/lestrrat-go/jwx/v2/jwt" + "github.com/lunarway/release-manager/internal/log" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "go.uber.org/zap/zapcore" ) func TestAuthenticate_token(t *testing.T) { @@ -85,6 +87,13 @@ func TestAuthenticate_token(t *testing.T) { } func TestAuthenticate_jwt(t *testing.T) { + log.Init(&log.Configuration{ + Level: log.Level{ + Level: zapcore.DebugLevel, + }, + Development: true, + }) + jwksServer, minter := getJwksEndpoint(t) issuer := "test-issuer" diff --git a/cmd/server/http/jwt.go b/cmd/server/http/jwt.go index 09ac79ce..1c879b96 100644 --- a/cmd/server/http/jwt.go +++ b/cmd/server/http/jwt.go @@ -9,6 +9,7 @@ import ( "github.com/lestrrat-go/jwx/v2/jwk" "github.com/lestrrat-go/jwx/v2/jwt" httpinternal "github.com/lunarway/release-manager/internal/http" + "github.com/lunarway/release-manager/internal/log" "github.com/pkg/errors" ) @@ -83,30 +84,37 @@ func (v *Verifier) authentication(staticAuthToken string) func(http.Handler) htt // jwt auth bearerToken, err := ParseBearerToken(authorization) if err != nil { + log.WithContext(r.Context()).Infof("parse bearer token failed: %v", err) httpinternal.Error(w, "please provide a valid authentication token", http.StatusUnauthorized) return } keySet, err := v.jwkCache.Get(context.Background(), v.jwksLocation) if err != nil { + log.WithContext(r.Context()).Infof("get jwk cache failed: %v", err) httpinternal.Error(w, "please provide a valid authentication token", http.StatusUnauthorized) return } parsedToken, err := v.verify(bearerToken, keySet) if err != nil { + log.WithContext(r.Context()).Infof("JWT token verification failed: %v", err) if strings.Contains(err.Error(), keyNotFoundMsg) { + log.WithContext(r.Context()).Infof("JWT token verification: refresh jwk cache and try again") freshKeys, err := v.jwkCache.Refresh(context.Background(), v.jwksLocation) if err != nil { + log.WithContext(r.Context()).Errorf("JWT token refresh failed: %v", err) httpinternal.Error(w, "please provide a valid authentication token", http.StatusUnauthorized) return } parsedToken, err = v.verify(bearerToken, freshKeys) if err != nil { + log.WithContext(r.Context()).Infof("JWT token verification second attempt failed: %v", err) httpinternal.Error(w, "please provide a valid authentication token", http.StatusUnauthorized) return } } else { + log.WithContext(r.Context()).Infof("JWT token verification failed: %v", err) httpinternal.Error(w, "please provide a valid authentication token", http.StatusUnauthorized) return } From ba8b8cff8dd07b17c266d8ab23a1cbbea802e1d2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B8rn=20S=C3=B8rensen?= Date: Wed, 8 Nov 2023 09:54:28 +0100 Subject: [PATCH 18/42] Add test and fix code to ensure both hamctl token and jwt works --- cmd/server/http/http_test.go | 78 ++++---------------------------- cmd/server/http/jwt.go | 86 ++++++++++++++++++------------------ 2 files changed, 52 insertions(+), 112 deletions(-) diff --git a/cmd/server/http/http_test.go b/cmd/server/http/http_test.go index c1b50d17..0a5117af 100644 --- a/cmd/server/http/http_test.go +++ b/cmd/server/http/http_test.go @@ -20,73 +20,7 @@ import ( "go.uber.org/zap/zapcore" ) -func TestAuthenticate_token(t *testing.T) { - tt := []struct { - name string - serverToken string - authorization string - status int - }{ - { - name: "empty authorization", - serverToken: "token", - authorization: "", - status: http.StatusUnauthorized, - }, - { - name: "whitespace token", - serverToken: "token", - authorization: " ", - status: http.StatusUnauthorized, - }, - { - name: "non-bearer authorization", - serverToken: "token", - authorization: "non-bearer-token", - status: http.StatusUnauthorized, - }, - { - name: "empty bearer authorization", - serverToken: "token", - authorization: "Bearer ", - status: http.StatusUnauthorized, - }, - { - name: "whitespace bearer authorization", - serverToken: "token", - authorization: "Bearer ", - status: http.StatusUnauthorized, - }, - { - name: "wrong bearer authorization", - serverToken: "token", - authorization: "Bearer another-token", - status: http.StatusUnauthorized, - }, - { - name: "correct bearer authorization", - serverToken: "token", - authorization: "Bearer token", - status: http.StatusOK, - }, - } - for _, tc := range tt { - t.Run(tc.name, func(t *testing.T) { - verifier := Verifier{} - handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - w.WriteHeader(http.StatusOK) - }) - req := httptest.NewRequest(http.MethodGet, "/", nil) - req.Header.Set("Authorization", tc.authorization) - w := httptest.NewRecorder() - verifier.authentication(tc.serverToken)(handler).ServeHTTP(w, req) - - assert.Equal(t, tc.status, w.Result().StatusCode, "status code not as expected") - }) - } -} - -func TestAuthenticate_jwt(t *testing.T) { +func TestAuthenticate(t *testing.T) { log.Init(&log.Configuration{ Level: log.Level{ Level: zapcore.DebugLevel, @@ -98,6 +32,7 @@ func TestAuthenticate_jwt(t *testing.T) { issuer := "test-issuer" audience := "test-audience" + serverToken := "server-token" tt := []struct { name string @@ -137,7 +72,12 @@ func TestAuthenticate_jwt(t *testing.T) { status: http.StatusUnauthorized, }, { - name: "valid bearer authorization", + name: "correct hamctl bearer authorization", + authorization: "Bearer " + serverToken, + status: http.StatusOK, + }, + { + name: "valid jwt bearer authorization", authorization: fmt.Sprintf("Bearer %s", minter(t, principal{ Subject: "sub", @@ -201,7 +141,7 @@ func TestAuthenticate_jwt(t *testing.T) { req := httptest.NewRequest(http.MethodGet, "/", nil) req.Header.Set("Authorization", tc.authorization) w := httptest.NewRecorder() - verifier.authentication("")(handler).ServeHTTP(w, req) + verifier.authentication(serverToken)(handler).ServeHTTP(w, req) if tc.expectedRequestContextSubject != "" { assert.Equal(t, tc.expectedRequestContextSubject, req.Context().Value(AUTH_USER_KEY)) diff --git a/cmd/server/http/jwt.go b/cmd/server/http/jwt.go index 1c879b96..9988d895 100644 --- a/cmd/server/http/jwt.go +++ b/cmd/server/http/jwt.go @@ -72,56 +72,56 @@ func (v *Verifier) authentication(staticAuthToken string) func(http.Handler) htt return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { authorization := r.Header.Get("Authorization") - if staticAuthToken != "" { - // old hamctl token auth - t := strings.TrimPrefix(authorization, "Bearer ") - t = strings.TrimSpace(t) - if t != staticAuthToken { - httpinternal.Error(w, "please provide a valid authentication token", http.StatusUnauthorized) - return - } - } else { - // jwt auth - bearerToken, err := ParseBearerToken(authorization) - if err != nil { - log.WithContext(r.Context()).Infof("parse bearer token failed: %v", err) - httpinternal.Error(w, "please provide a valid authentication token", http.StatusUnauthorized) - return - } + // old hamctl token auth + t := strings.TrimPrefix(authorization, "Bearer ") + t = strings.TrimSpace(t) + if t == staticAuthToken { - keySet, err := v.jwkCache.Get(context.Background(), v.jwksLocation) - if err != nil { - log.WithContext(r.Context()).Infof("get jwk cache failed: %v", err) - httpinternal.Error(w, "please provide a valid authentication token", http.StatusUnauthorized) - return - } + h.ServeHTTP(w, r) + return + } - parsedToken, err := v.verify(bearerToken, keySet) - if err != nil { - log.WithContext(r.Context()).Infof("JWT token verification failed: %v", err) - if strings.Contains(err.Error(), keyNotFoundMsg) { - log.WithContext(r.Context()).Infof("JWT token verification: refresh jwk cache and try again") - freshKeys, err := v.jwkCache.Refresh(context.Background(), v.jwksLocation) - if err != nil { - log.WithContext(r.Context()).Errorf("JWT token refresh failed: %v", err) - httpinternal.Error(w, "please provide a valid authentication token", http.StatusUnauthorized) - return - } - parsedToken, err = v.verify(bearerToken, freshKeys) - if err != nil { - log.WithContext(r.Context()).Infof("JWT token verification second attempt failed: %v", err) - httpinternal.Error(w, "please provide a valid authentication token", http.StatusUnauthorized) - return - } - } else { - log.WithContext(r.Context()).Infof("JWT token verification failed: %v", err) + // jwt auth + bearerToken, err := ParseBearerToken(authorization) + if err != nil { + log.WithContext(r.Context()).Infof("parse bearer token failed: %v", err) + httpinternal.Error(w, "please provide a valid authentication token", http.StatusUnauthorized) + return + } + + keySet, err := v.jwkCache.Get(context.Background(), v.jwksLocation) + if err != nil { + log.WithContext(r.Context()).Infof("get jwk cache failed: %v", err) + httpinternal.Error(w, "please provide a valid authentication token", http.StatusUnauthorized) + return + } + + parsedToken, err := v.verify(bearerToken, keySet) + if err != nil { + log.WithContext(r.Context()).Infof("JWT token verification failed: %v", err) + if strings.Contains(err.Error(), keyNotFoundMsg) { + log.WithContext(r.Context()).Infof("JWT token verification: refresh jwk cache and try again") + freshKeys, err := v.jwkCache.Refresh(context.Background(), v.jwksLocation) + if err != nil { + log.WithContext(r.Context()).Errorf("JWT token refresh failed: %v", err) httpinternal.Error(w, "please provide a valid authentication token", http.StatusUnauthorized) return } + parsedToken, err = v.verify(bearerToken, freshKeys) + if err != nil { + log.WithContext(r.Context()).Infof("JWT token verification second attempt failed: %v", err) + httpinternal.Error(w, "please provide a valid authentication token", http.StatusUnauthorized) + return + } + } else { + log.WithContext(r.Context()).Infof("JWT token verification failed: %v", err) + httpinternal.Error(w, "please provide a valid authentication token", http.StatusUnauthorized) + return } - ctx := context.WithValue(r.Context(), AUTH_USER_KEY, parsedToken.Subject()) - *r = *r.WithContext(ctx) } + ctx := context.WithValue(r.Context(), AUTH_USER_KEY, parsedToken.Subject()) + *r = *r.WithContext(ctx) + h.ServeHTTP(w, r) }) } From 3a325aad3f180cea25f1ab753fcedfaedafbc9a1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B8rn=20S=C3=B8rensen?= Date: Wed, 8 Nov 2023 12:21:33 +0100 Subject: [PATCH 19/42] Use custom type for context value and add helpers --- cmd/server/http/http_test.go | 6 +----- cmd/server/http/jwt.go | 20 +++++++++++++++++--- cmd/server/http/log.go | 2 +- cmd/server/http/policy.go | 6 +++--- cmd/server/http/release.go | 2 +- 5 files changed, 23 insertions(+), 13 deletions(-) diff --git a/cmd/server/http/http_test.go b/cmd/server/http/http_test.go index 0a5117af..d19a9e57 100644 --- a/cmd/server/http/http_test.go +++ b/cmd/server/http/http_test.go @@ -143,11 +143,7 @@ func TestAuthenticate(t *testing.T) { w := httptest.NewRecorder() verifier.authentication(serverToken)(handler).ServeHTTP(w, req) - if tc.expectedRequestContextSubject != "" { - assert.Equal(t, tc.expectedRequestContextSubject, req.Context().Value(AUTH_USER_KEY)) - } else { - assert.Equal(t, nil, req.Context().Value(AUTH_USER_KEY)) - } + assert.Equal(t, tc.expectedRequestContextSubject, UserFromContext(req.Context())) assert.Equal(t, tc.status, w.Result().StatusCode, "status code not as expected") }) } diff --git a/cmd/server/http/jwt.go b/cmd/server/http/jwt.go index 9988d895..d5f92088 100644 --- a/cmd/server/http/jwt.go +++ b/cmd/server/http/jwt.go @@ -28,6 +28,22 @@ type Verifier struct { jwkCache JwkCache } +type authenticatedUserContextKey struct{} + +func withAuthenticatedUser(ctx context.Context, user string) context.Context { + return context.WithValue(ctx, authenticatedUserContextKey{}, user) +} + +func UserFromContext(ctx context.Context) string { + value := ctx.Value(authenticatedUserContextKey{}) + + if value == nil { + return "" + } + + return value.(string) +} + func NewVerifier(jwksLocation string, jwkFetchTimeout time.Duration, issuer string, audience string) (*Verifier, error) { ctx := context.Background() @@ -61,8 +77,6 @@ func ParseBearerToken(token string) (string, error) { return strings.TrimSpace(jwt), nil } -const AUTH_USER_KEY = "AUTH_USER_KEY" - // authenticate authenticates the handler against a Bearer token. // // If authentication fails a 401 Unauthorized HTTP status is returned with an @@ -119,7 +133,7 @@ func (v *Verifier) authentication(staticAuthToken string) func(http.Handler) htt return } } - ctx := context.WithValue(r.Context(), AUTH_USER_KEY, parsedToken.Subject()) + ctx := withAuthenticatedUser(r.Context(), parsedToken.Subject()) *r = *r.WithContext(ctx) h.ServeHTTP(w, r) diff --git a/cmd/server/http/log.go b/cmd/server/http/log.go index 925ff21a..fc5372db 100644 --- a/cmd/server/http/log.go +++ b/cmd/server/http/log.go @@ -32,7 +32,7 @@ func reqrespLogger(h http.Handler) http.Handler { duration := time.Since(start).Nanoseconds() / 1e6 statusCode := statusWriter.statusCode requestID := getRequestID(r) - subject := r.Context().Value(AUTH_USER_KEY).(string) + subject := UserFromContext(r.Context()) fields := []interface{}{ "requestId", requestID, "req", struct { diff --git a/cmd/server/http/policy.go b/cmd/server/http/policy.go index ec429293..dd1e4083 100644 --- a/cmd/server/http/policy.go +++ b/cmd/server/http/policy.go @@ -34,7 +34,7 @@ func applyAutoReleasePolicy(payload *payload, policySvc *policyinternal.Service) Name: req.CommitterName, Email: req.CommitterEmail, } - subject := r.Context().Value(AUTH_USER_KEY).(string) + subject := UserFromContext(r.Context()) if subject != "" { actor.Email = subject actor.Name = subject @@ -99,7 +99,7 @@ func applyBranchRestrictionPolicy(payload *payload, policySvc *policyinternal.Se Name: req.CommitterName, Email: req.CommitterEmail, } - subject := r.Context().Value(AUTH_USER_KEY).(string) + subject := UserFromContext(r.Context()) if subject != "" { actor.Email = subject actor.Name = subject @@ -237,7 +237,7 @@ func deletePolicies(payload *payload, policySvc *policyinternal.Service) http.Ha Name: req.CommitterName, Email: req.CommitterEmail, } - subject := r.Context().Value(AUTH_USER_KEY).(string) + subject := UserFromContext(r.Context()) if subject != "" { actor.Email = subject actor.Name = subject diff --git a/cmd/server/http/release.go b/cmd/server/http/release.go index 3cfaf8ca..94148fb9 100644 --- a/cmd/server/http/release.go +++ b/cmd/server/http/release.go @@ -31,7 +31,7 @@ func release(payload *payload, flowSvc *flow.Service) http.HandlerFunc { Name: req.CommitterName, Email: req.CommitterEmail, } - subject := r.Context().Value(AUTH_USER_KEY).(string) + subject := UserFromContext(r.Context()) if subject != "" { actor.Email = subject actor.Name = subject From f66c4c098bdb3bad599c1a0ba441537155f5de0d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B8rn=20S=C3=B8rensen?= Date: Wed, 8 Nov 2023 12:24:15 +0100 Subject: [PATCH 20/42] Use request context --- cmd/server/http/jwt.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cmd/server/http/jwt.go b/cmd/server/http/jwt.go index d5f92088..c9310c84 100644 --- a/cmd/server/http/jwt.go +++ b/cmd/server/http/jwt.go @@ -103,7 +103,7 @@ func (v *Verifier) authentication(staticAuthToken string) func(http.Handler) htt return } - keySet, err := v.jwkCache.Get(context.Background(), v.jwksLocation) + keySet, err := v.jwkCache.Get(r.Context(), v.jwksLocation) if err != nil { log.WithContext(r.Context()).Infof("get jwk cache failed: %v", err) httpinternal.Error(w, "please provide a valid authentication token", http.StatusUnauthorized) @@ -115,7 +115,7 @@ func (v *Verifier) authentication(staticAuthToken string) func(http.Handler) htt log.WithContext(r.Context()).Infof("JWT token verification failed: %v", err) if strings.Contains(err.Error(), keyNotFoundMsg) { log.WithContext(r.Context()).Infof("JWT token verification: refresh jwk cache and try again") - freshKeys, err := v.jwkCache.Refresh(context.Background(), v.jwksLocation) + freshKeys, err := v.jwkCache.Refresh(r.Context(), v.jwksLocation) if err != nil { log.WithContext(r.Context()).Errorf("JWT token refresh failed: %v", err) httpinternal.Error(w, "please provide a valid authentication token", http.StatusUnauthorized) From 6a8963973096b5d048729090f0e35d48bfa8eacf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B8rn=20S=C3=B8rensen?= Date: Wed, 8 Nov 2023 12:25:33 +0100 Subject: [PATCH 21/42] Use command context --- cmd/hamctl/command/login.go | 2 +- internal/http/authenticator.go | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/cmd/hamctl/command/login.go b/cmd/hamctl/command/login.go index 8498ce12..10bb0a6a 100644 --- a/cmd/hamctl/command/login.go +++ b/cmd/hamctl/command/login.go @@ -11,7 +11,7 @@ func Login(authenticator http.UserAuthenticator) *cobra.Command { Short: `Log into the configured IdP`, Args: cobra.ExactArgs(0), RunE: func(c *cobra.Command, args []string) error { - return authenticator.Login() + return authenticator.Login(c.Context()) }, } } diff --git a/internal/http/authenticator.go b/internal/http/authenticator.go index c1902348..8e8a2105 100644 --- a/internal/http/authenticator.go +++ b/internal/http/authenticator.go @@ -31,8 +31,7 @@ func NewUserAuthenticator(clientID, idpURL string) UserAuthenticator { } } -func (g *UserAuthenticator) Login() error { - ctx := context.Background() +func (g *UserAuthenticator) Login(ctx context.Context) error { response, err := g.conf.DeviceAuth(ctx) if err != nil { return err From b84bcffd11f0fc1c75528fbac35924d28003e05b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B8rn=20S=C3=B8rensen?= Date: Wed, 8 Nov 2023 13:10:08 +0100 Subject: [PATCH 22/42] Add test of cache refresh handling --- cmd/server/http/http_test.go | 74 +++++++++++++++++++++++++++++++++++- 1 file changed, 73 insertions(+), 1 deletion(-) diff --git a/cmd/server/http/http_test.go b/cmd/server/http/http_test.go index d19a9e57..4af0e006 100644 --- a/cmd/server/http/http_test.go +++ b/cmd/server/http/http_test.go @@ -1,6 +1,7 @@ package http import ( + "context" "crypto/ecdsa" "crypto/elliptic" "crypto/rand" @@ -149,6 +150,77 @@ func TestAuthenticate(t *testing.T) { } } +// TestAuthenticate_withInvalidCache tests that jwk cache refresh works as +// intended and that tokens are accepted after a refresh +func TestAuthenticate_withInvalidCache(t *testing.T) { + log.Init(&log.Configuration{ + Level: log.Level{ + Level: zapcore.DebugLevel, + }, + Development: true, + }) + + testIssuer := "https://auth.dev.lunar.tech/" + testAudience := "audience" + testSubject := "subject" + + // Set up the invalid cache + _, unusedPublic1 := createSigningKey(t, testIssuer) + unusedJWKKey := jwk.NewSet() + err := unusedJWKKey.AddKey(unusedPublic1) + require.NoError(t, err, "add key to unused JWK failed") + + // Set up the valid cache + validPrivateKey, validPublicKey := createSigningKey(t, testIssuer) + validJWK := jwk.NewSet() + err = validJWK.AddKey(validPublicKey) + require.NoError(t, err, "add key to valid jwk failed") + + // test server will return the valid JWK after first call + handlerCalled := false + jwkServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if !handlerCalled { + json.NewEncoder(w).Encode(unusedJWKKey) + } else { + json.NewEncoder(w).Encode(validJWK) + } + // return valid cache second time it's called + handlerCalled = true + })) + + signedJwt := createSignedJWTWithKey(t, &principal{ + Subject: testSubject, + Issuer: testIssuer, + Audience: testAudience, + IssuedAt: time.Now().Add(-1 * time.Minute), + Expiration: time.Now().Add(time.Minute), + }, validPrivateKey) + + // Set up the cache + cache := jwk.NewCache(context.Background()) + err = cache.Register(jwkServer.URL) + require.NoError(t, err, "failed to register server in cache") + + _, err = cache.Refresh(context.Background(), jwkServer.URL) + require.NoError(t, err, "failed to refresh cache") + + authenticator, err := NewVerifier(jwkServer.URL, 10*time.Second, testIssuer, testAudience) + require.NoError(t, err, "failed to create verified") + authenticator.jwkCache = cache + + handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + }) + w := httptest.NewRecorder() + req := httptest.NewRequest(http.MethodGet, "/", nil) + req.Header.Set("Authorization", signedJwt) + + authenticator.authentication("static-token")(handler).ServeHTTP(w, req) + + assert.Equal(t, testSubject, UserFromContext(req.Context())) + assert.Equal(t, http.StatusOK, w.Result().StatusCode, "status code not as expected") +} + func getJwksEndpoint(t *testing.T) (*httptest.Server, func(t *testing.T, principal principal) string) { t.Helper() @@ -181,7 +253,7 @@ type principal struct { Audience string IssuedAt time.Time Expiration time.Time - Claims map[string]string + Claims map[string]interface{} } func createSignedJWTWithKey(t *testing.T, principal *principal, privateJwkKey jwk.Key) string { From be6439a991373f5c663c77bbf4d88fc13c2147ef Mon Sep 17 00:00:00 2001 From: Hoeg Date: Thu, 9 Nov 2023 16:16:38 +0100 Subject: [PATCH 23/42] change file permissions --- internal/http/authenticator.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/http/authenticator.go b/internal/http/authenticator.go index 8e8a2105..a66174c2 100644 --- a/internal/http/authenticator.go +++ b/internal/http/authenticator.go @@ -83,7 +83,7 @@ func storeAccessToken(token *oauth2.Token) error { } p := tokenFilePath() dir := filepath.Dir(p) - if err := os.MkdirAll(dir, 0755); err != nil { + if err := os.MkdirAll(dir, 0700); err != nil { return err } f, err := os.Create(p) From bbfbaadf940a6d126b1cf0b634b14647c81b0f2b Mon Sep 17 00:00:00 2001 From: Hoeg Date: Fri, 10 Nov 2023 06:28:54 +0100 Subject: [PATCH 24/42] moved config to args --- cmd/artifact/command/push.go | 25 +++++++++---------------- cmd/daemon/command/start.go | 24 +++++++++++------------- 2 files changed, 20 insertions(+), 29 deletions(-) diff --git a/cmd/artifact/command/push.go b/cmd/artifact/command/push.go index b4a6a099..8059d9b2 100644 --- a/cmd/artifact/command/push.go +++ b/cmd/artifact/command/push.go @@ -10,13 +10,12 @@ import ( httpinternal "github.com/lunarway/release-manager/internal/http" intslack "github.com/lunarway/release-manager/internal/slack" "github.com/nlopes/slack" - "github.com/pkg/errors" "github.com/spf13/cobra" ) func pushCommand(options *Options) *cobra.Command { releaseManagerClient := httpinternal.Client{} - + var idpURL, clientID, clientSecret string command := &cobra.Command{ Use: "push", Short: "push artifact to artifact repository", @@ -24,19 +23,6 @@ func pushCommand(options *Options) *cobra.Command { var artifactID string var err error ctx := context.Background() - - idpURL := os.Getenv("HAMCTL_OAUTH_IDP_URL") - if idpURL == "" { - return errors.New("no HAMCTL_OAUTH_IDP_URL env var set") - } - clientID := os.Getenv("HAMCTL_OAUTH_CLIENT_ID") - if clientID == "" { - return errors.New("no HAMCTL_OAUTH_CLIENT_ID env var set") - } - clientSecret := os.Getenv("HAMCTL_OAUTH_CLIENT_SECRET") - if clientID == "" { - return errors.New("no HAMCTL_OAUTH_CLIENT_SECRET env var set") - } authenticator := httpinternal.NewClientAuthenticator(clientID, clientSecret, idpURL) releaseManagerClient.Auth = &authenticator @@ -62,12 +48,19 @@ func pushCommand(options *Options) *cobra.Command { }, } command.Flags().StringVar(&releaseManagerClient.BaseURL, "http-base-url", os.Getenv("ARTIFACT_URL"), "address of the http release manager server") + command.Flags().StringVar(&idpURL, "idp-url", "", "the url of the identity provider") + command.Flags().StringVar(&clientID, "clientid", "", "client id of this application issued by the identity provider") + command.Flags().StringVar(&clientSecret, "client-secret", "", "the client secret") // errors are skipped here as the only case they can occour are if thee flag // does not exist on the command. //nolint:errcheck command.MarkFlagRequired("http-base-url") //nolint:errcheck - command.MarkFlagRequired("http-auth-token") + command.MarkFlagRequired("idp-url") + //nolint:errcheck + command.MarkFlagRequired("clientid") + //nolint:errcheck + command.MarkFlagRequired("client-secret") return command } diff --git a/cmd/daemon/command/start.go b/cmd/daemon/command/start.go index 73e09b63..a86a920c 100644 --- a/cmd/daemon/command/start.go +++ b/cmd/daemon/command/start.go @@ -21,6 +21,7 @@ import ( // 3. Detects CreateContainerConfigError, and fetches the message about the wrong config. func StartDaemon() *cobra.Command { var environment, kubeConfigPath string + var idpURL, clientID, clientSecret string var moduloCrashReportNotif float64 var logConfiguration *log.Configuration @@ -34,19 +35,6 @@ func StartDaemon() *cobra.Command { logConfiguration.ParseFromEnvironmnet() log.Init(logConfiguration) - idpURL := os.Getenv("HAMCTL_OAUTH_IDP_URL") - if idpURL == "" { - return errors.New("no HAMCTL_OAUTH_IDP_URL env var set") - } - clientID := os.Getenv("HAMCTL_OAUTH_CLIENT_ID") - if clientID == "" { - return errors.New("no HAMCTL_OAUTH_CLIENT_ID env var set") - } - clientSecret := os.Getenv("HAMCTL_OAUTH_CLIENT_SECRET") - if clientID == "" { - return errors.New("no HAMCTL_OAUTH_CLIENT_SECRET env var set") - } - authenticator := httpinternal.NewClientAuthenticator(clientID, clientSecret, idpURL) client.Auth = &authenticator @@ -118,10 +106,20 @@ func StartDaemon() *cobra.Command { command.Flags().StringVar(&environment, "environment", "", "environment where release-daemon is running") command.Flags().StringVar(&kubeConfigPath, "kubeconfig", "", "path to kubeconfig file. If not specified, then daemon is expected to run inside kubernetes") command.Flags().Float64Var(&moduloCrashReportNotif, "modulo-crash-report-notif", 5, "modulo for how often to report CrashLoopBackOff events") + command.Flags().StringVar(&idpURL, "idp-url", "", "the url of the identity provider") + command.Flags().StringVar(&clientID, "clientid", "", "client id of this application issued by the identity provider") + command.Flags().StringVar(&clientSecret, "client-secret", "", "the client secret") + // errors are skipped here as the only case they can occour are if thee flag // does not exist on the command. //nolint:errcheck command.MarkFlagRequired("environment") + //nolint:errcheck + command.MarkFlagRequired("idp-url") + //nolint:errcheck + command.MarkFlagRequired("clientid") + //nolint:errcheck + command.MarkFlagRequired("client-secret") logConfiguration = log.RegisterFlags(command) return command } From b599563a735ceae68985e520a23d0de164970102 Mon Sep 17 00:00:00 2001 From: Hoeg Date: Fri, 10 Nov 2023 08:22:52 +0100 Subject: [PATCH 25/42] removed unused ham token --- internal/http/client.go | 1 - 1 file changed, 1 deletion(-) diff --git a/internal/http/client.go b/internal/http/client.go index a7bd71f4..7bed0d90 100644 --- a/internal/http/client.go +++ b/internal/http/client.go @@ -80,7 +80,6 @@ func (c *Client) Do(method string, path string, requestBody, responseBody interf req.Header.Set("x-request-id", id.String()) } req.Header.Set("X-Cli-Version", c.Metadata.CLIVersion) - req.Header.Set("X-HAM-TOKEN", "test") resp, err := client.Do(req) if err != nil { var dnsError *net.DNSError From d871145f860746bfe566d3ab8c48134a44bb2a3f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B8rn=20S=C3=B8rensen?= Date: Sun, 12 Nov 2023 15:03:50 +0100 Subject: [PATCH 26/42] Fix client-id flag --- cmd/daemon/command/start.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmd/daemon/command/start.go b/cmd/daemon/command/start.go index a86a920c..4e24deae 100644 --- a/cmd/daemon/command/start.go +++ b/cmd/daemon/command/start.go @@ -107,7 +107,7 @@ func StartDaemon() *cobra.Command { command.Flags().StringVar(&kubeConfigPath, "kubeconfig", "", "path to kubeconfig file. If not specified, then daemon is expected to run inside kubernetes") command.Flags().Float64Var(&moduloCrashReportNotif, "modulo-crash-report-notif", 5, "modulo for how often to report CrashLoopBackOff events") command.Flags().StringVar(&idpURL, "idp-url", "", "the url of the identity provider") - command.Flags().StringVar(&clientID, "clientid", "", "client id of this application issued by the identity provider") + command.Flags().StringVar(&clientID, "client-id", "", "client id of this application issued by the identity provider") command.Flags().StringVar(&clientSecret, "client-secret", "", "the client secret") // errors are skipped here as the only case they can occour are if thee flag From d5ca5058da1ae0c521f6111c1208c77c7c4047d9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B8rn=20S=C3=B8rensen?= Date: Sun, 12 Nov 2023 15:04:42 +0100 Subject: [PATCH 27/42] Add log init in root command to support bad flags logging --- cmd/daemon/command/root.go | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/cmd/daemon/command/root.go b/cmd/daemon/command/root.go index fbe9c4e5..bf0bf1c2 100644 --- a/cmd/daemon/command/root.go +++ b/cmd/daemon/command/root.go @@ -2,10 +2,16 @@ package command import ( "github.com/spf13/cobra" + + "github.com/lunarway/release-manager/internal/log" ) // NewRoot returns a new instance of a daemon command. func NewRoot(version string) (*cobra.Command, error) { + var logConfiguration *log.Configuration + logConfiguration.ParseFromEnvironmnet() + log.Init(logConfiguration) + var command = &cobra.Command{ Use: "daemon", Short: "daemon", From 7aa459d202bd7a74de87eab065530c621da1c970 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B8rn=20S=C3=B8rensen?= Date: Sun, 12 Nov 2023 15:17:12 +0100 Subject: [PATCH 28/42] Clean up client id command args --- cmd/artifact/command/push.go | 4 ++-- cmd/daemon/command/start.go | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/cmd/artifact/command/push.go b/cmd/artifact/command/push.go index 8059d9b2..f85c35d1 100644 --- a/cmd/artifact/command/push.go +++ b/cmd/artifact/command/push.go @@ -49,7 +49,7 @@ func pushCommand(options *Options) *cobra.Command { } command.Flags().StringVar(&releaseManagerClient.BaseURL, "http-base-url", os.Getenv("ARTIFACT_URL"), "address of the http release manager server") command.Flags().StringVar(&idpURL, "idp-url", "", "the url of the identity provider") - command.Flags().StringVar(&clientID, "clientid", "", "client id of this application issued by the identity provider") + command.Flags().StringVar(&clientID, "client-id", "", "client id of this application issued by the identity provider") command.Flags().StringVar(&clientSecret, "client-secret", "", "the client secret") // errors are skipped here as the only case they can occour are if thee flag @@ -59,7 +59,7 @@ func pushCommand(options *Options) *cobra.Command { //nolint:errcheck command.MarkFlagRequired("idp-url") //nolint:errcheck - command.MarkFlagRequired("clientid") + command.MarkFlagRequired("client-id") //nolint:errcheck command.MarkFlagRequired("client-secret") return command diff --git a/cmd/daemon/command/start.go b/cmd/daemon/command/start.go index 4e24deae..4708ce02 100644 --- a/cmd/daemon/command/start.go +++ b/cmd/daemon/command/start.go @@ -117,7 +117,7 @@ func StartDaemon() *cobra.Command { //nolint:errcheck command.MarkFlagRequired("idp-url") //nolint:errcheck - command.MarkFlagRequired("clientid") + command.MarkFlagRequired("client-id") //nolint:errcheck command.MarkFlagRequired("client-secret") logConfiguration = log.RegisterFlags(command) From 40bf713c53c2e99122eb652153abc64e0e9de36c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B8rn=20S=C3=B8rensen?= Date: Sun, 12 Nov 2023 15:17:59 +0100 Subject: [PATCH 29/42] Rever logging in root.go --- cmd/daemon/command/root.go | 6 ------ 1 file changed, 6 deletions(-) diff --git a/cmd/daemon/command/root.go b/cmd/daemon/command/root.go index bf0bf1c2..fbe9c4e5 100644 --- a/cmd/daemon/command/root.go +++ b/cmd/daemon/command/root.go @@ -2,16 +2,10 @@ package command import ( "github.com/spf13/cobra" - - "github.com/lunarway/release-manager/internal/log" ) // NewRoot returns a new instance of a daemon command. func NewRoot(version string) (*cobra.Command, error) { - var logConfiguration *log.Configuration - logConfiguration.ParseFromEnvironmnet() - log.Init(logConfiguration) - var command = &cobra.Command{ Use: "daemon", Short: "daemon", From 1ade3ca09ea3ed6aeca0398d3682935ed0cad09b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B8rn=20S=C3=B8rensen?= Date: Sun, 12 Nov 2023 15:26:28 +0100 Subject: [PATCH 30/42] Accept context in NewVerifier --- cmd/server/command/start.go | 2 +- cmd/server/http/http_test.go | 4 ++-- cmd/server/http/jwt.go | 4 +--- 3 files changed, 4 insertions(+), 6 deletions(-) diff --git a/cmd/server/command/start.go b/cmd/server/command/start.go index af907eaa..8d8ccf00 100644 --- a/cmd/server/command/start.go +++ b/cmd/server/command/start.go @@ -516,7 +516,7 @@ func NewStart(startOptions *startOptions) *cobra.Command { } }() go func() { - jwtVerifier, err := http.NewVerifier(startOptions.jwtVerifier.JwksLocation, time.Duration(30)*time.Second, startOptions.jwtVerifier.Issuer, startOptions.jwtVerifier.Audience) + jwtVerifier, err := http.NewVerifier(ctx, startOptions.jwtVerifier.JwksLocation, 30*time.Second, startOptions.jwtVerifier.Issuer, startOptions.jwtVerifier.Audience) if err != nil { done <- errors.WithMessage(err, "new jwt verifier") return diff --git a/cmd/server/http/http_test.go b/cmd/server/http/http_test.go index 4af0e006..89bc1269 100644 --- a/cmd/server/http/http_test.go +++ b/cmd/server/http/http_test.go @@ -133,7 +133,7 @@ func TestAuthenticate(t *testing.T) { } for _, tc := range tt { t.Run(tc.name, func(t *testing.T) { - verifier, err := NewVerifier(jwksServer.URL, 1*time.Second, issuer, audience) + verifier, err := NewVerifier(context.Background(), jwksServer.URL, 1*time.Second, issuer, audience) require.NoError(t, err, "failed to create verifier") handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { @@ -204,7 +204,7 @@ func TestAuthenticate_withInvalidCache(t *testing.T) { _, err = cache.Refresh(context.Background(), jwkServer.URL) require.NoError(t, err, "failed to refresh cache") - authenticator, err := NewVerifier(jwkServer.URL, 10*time.Second, testIssuer, testAudience) + authenticator, err := NewVerifier(context.Background(), jwkServer.URL, 10*time.Second, testIssuer, testAudience) require.NoError(t, err, "failed to create verified") authenticator.jwkCache = cache diff --git a/cmd/server/http/jwt.go b/cmd/server/http/jwt.go index c9310c84..e11eae19 100644 --- a/cmd/server/http/jwt.go +++ b/cmd/server/http/jwt.go @@ -44,9 +44,7 @@ func UserFromContext(ctx context.Context) string { return value.(string) } -func NewVerifier(jwksLocation string, jwkFetchTimeout time.Duration, issuer string, audience string) (*Verifier, error) { - ctx := context.Background() - +func NewVerifier(ctx context.Context, jwksLocation string, jwkFetchTimeout time.Duration, issuer string, audience string) (*Verifier, error) { cache := jwk.NewCache(ctx) err := cache.Register(jwksLocation, jwk.WithMinRefreshInterval(24*time.Hour)) if err != nil { From b933451c7b6740c0a1f38d57e9a8b4399684e51b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B8rn=20S=C3=B8rensen?= Date: Sun, 12 Nov 2023 15:29:19 +0100 Subject: [PATCH 31/42] Rearrange jwt file a bit --- cmd/server/http/jwt.go | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/cmd/server/http/jwt.go b/cmd/server/http/jwt.go index e11eae19..fc3ab8fe 100644 --- a/cmd/server/http/jwt.go +++ b/cmd/server/http/jwt.go @@ -13,21 +13,6 @@ import ( "github.com/pkg/errors" ) -const keyNotFoundMsg = "failed to find key with key ID" - -type JwkCache interface { - Get(ctx context.Context, url string) (jwk.Set, error) - Refresh(ctx context.Context, url string) (jwk.Set, error) -} - -type Verifier struct { - jwksLocation string - issuer string - audience string - - jwkCache JwkCache -} - type authenticatedUserContextKey struct{} func withAuthenticatedUser(ctx context.Context, user string) context.Context { @@ -44,6 +29,21 @@ func UserFromContext(ctx context.Context) string { return value.(string) } +const keyNotFoundMsg = "failed to find key with key ID" + +type JwkCache interface { + Get(ctx context.Context, url string) (jwk.Set, error) + Refresh(ctx context.Context, url string) (jwk.Set, error) +} + +type Verifier struct { + jwksLocation string + issuer string + audience string + + jwkCache JwkCache +} + func NewVerifier(ctx context.Context, jwksLocation string, jwkFetchTimeout time.Duration, issuer string, audience string) (*Verifier, error) { cache := jwk.NewCache(ctx) err := cache.Register(jwksLocation, jwk.WithMinRefreshInterval(24*time.Hour)) From 204162636f170cd90baa6deb1dcda60652751c5a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B8rn=20S=C3=B8rensen?= Date: Sun, 12 Nov 2023 20:04:23 +0100 Subject: [PATCH 32/42] Tidy go.mod --- go.mod | 25 ++++++++++--------------- 1 file changed, 10 insertions(+), 15 deletions(-) diff --git a/go.mod b/go.mod index d5ec18c7..def9e1a3 100644 --- a/go.mod +++ b/go.mod @@ -14,6 +14,7 @@ require ( github.com/manifoldco/promptui v0.9.0 github.com/nlopes/slack v0.6.0 github.com/opentracing/opentracing-go v1.2.0 + github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8 github.com/pkg/errors v0.9.1 github.com/prometheus/client_golang v1.14.0 github.com/rabbitmq/amqp091-go v1.5.0 @@ -32,21 +33,7 @@ require ( k8s.io/client-go v0.25.4 ) -require ( - github.com/gorilla/mux v1.8.0 - github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8 -) - -require ( - github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0 // indirect - github.com/goccy/go-json v0.10.2 // indirect - github.com/lestrrat-go/blackmagic v1.0.2 // indirect - github.com/lestrrat-go/httpcc v1.0.1 // indirect - github.com/lestrrat-go/httprc v1.0.4 // indirect - github.com/lestrrat-go/iter v1.0.2 // indirect - github.com/lestrrat-go/option v1.0.1 // indirect - github.com/segmentio/asm v1.2.0 // indirect -) +require github.com/gorilla/mux v1.8.0 require ( github.com/Microsoft/go-winio v0.4.16 // indirect @@ -58,6 +45,7 @@ require ( github.com/cespare/xxhash/v2 v2.1.2 // indirect github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e // indirect github.com/davecgh/go-spew v1.1.1 // indirect + github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0 // indirect github.com/emicklei/go-restful/v3 v3.8.0 // indirect github.com/emirpasic/gods v1.12.0 // indirect github.com/go-git/gcfg v1.5.0 // indirect @@ -66,6 +54,7 @@ require ( github.com/go-openapi/jsonpointer v0.19.5 // indirect github.com/go-openapi/jsonreference v0.19.5 // indirect github.com/go-openapi/swag v0.19.14 // indirect + github.com/goccy/go-json v0.10.2 // indirect github.com/gogo/protobuf v1.3.2 // indirect github.com/golang/protobuf v1.5.3 // indirect github.com/google/gnostic v0.5.7-v3refs // indirect @@ -79,7 +68,12 @@ require ( github.com/josharian/intern v1.0.0 // indirect github.com/json-iterator/go v1.1.12 // indirect github.com/kevinburke/ssh_config v0.0.0-20201106050909-4977a11b4351 // indirect + github.com/lestrrat-go/blackmagic v1.0.2 // indirect + github.com/lestrrat-go/httpcc v1.0.1 // indirect + github.com/lestrrat-go/httprc v1.0.4 // indirect + github.com/lestrrat-go/iter v1.0.2 // indirect github.com/lestrrat-go/jwx/v2 v2.0.16 + github.com/lestrrat-go/option v1.0.1 // indirect github.com/mailru/easyjson v0.7.6 // indirect github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369 // indirect github.com/mitchellh/go-homedir v1.1.0 // indirect @@ -91,6 +85,7 @@ require ( github.com/prometheus/common v0.37.0 // indirect github.com/prometheus/procfs v0.8.0 // indirect github.com/ryszard/goskiplist v0.0.0-20150312221310-2dfbae5fcf46 // indirect + github.com/segmentio/asm v1.2.0 // indirect github.com/sergi/go-diff v1.1.0 // indirect github.com/shabbyrobe/gocovmerge v0.0.0-20180507124511-f6ea450bfb63 // indirect github.com/stretchr/objx v0.5.0 // indirect From 1df8fd9881260185eac850478a9a109c59072d87 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B8rn=20S=C3=B8rensen?= Date: Sun, 12 Nov 2023 20:12:36 +0100 Subject: [PATCH 33/42] Remove git config code --- cmd/hamctl/command/actions/config_mock.go | 67 ----------------- cmd/hamctl/command/actions/release.go | 6 -- cmd/hamctl/command/config_mock.go | 67 ----------------- cmd/hamctl/command/policy/apply.go | 6 -- cmd/hamctl/command/policy/config_mock.go | 67 ----------------- cmd/hamctl/command/rollback.go | 7 -- internal/git/config.go | 92 ----------------------- internal/git/testdata/email_missing | 2 - internal/git/testdata/name_missing | 2 - internal/git/testdata/user_set_1 | 3 - 10 files changed, 319 deletions(-) delete mode 100644 cmd/hamctl/command/actions/config_mock.go delete mode 100644 cmd/hamctl/command/config_mock.go delete mode 100644 cmd/hamctl/command/policy/config_mock.go delete mode 100644 internal/git/config.go delete mode 100644 internal/git/testdata/email_missing delete mode 100644 internal/git/testdata/name_missing delete mode 100644 internal/git/testdata/user_set_1 diff --git a/cmd/hamctl/command/actions/config_mock.go b/cmd/hamctl/command/actions/config_mock.go deleted file mode 100644 index 47346e21..00000000 --- a/cmd/hamctl/command/actions/config_mock.go +++ /dev/null @@ -1,67 +0,0 @@ -// Code generated by moq; DO NOT EDIT. -// github.com/matryer/moq - -package actions - -import ( - "github.com/lunarway/release-manager/internal/git" - "sync" -) - -// Ensure, that GitConfigAPIMock does implement GitConfigAPI. -// If this is not the case, regenerate this file with moq. -var _ GitConfigAPI = &GitConfigAPIMock{} - -// GitConfigAPIMock is a mock implementation of GitConfigAPI. -// -// func TestSomethingThatUsesGitConfigAPI(t *testing.T) { -// -// // make and configure a mocked GitConfigAPI -// mockedGitConfigAPI := &GitConfigAPIMock{ -// CommitterDetailsFunc: func() (*git.CommitterDetails, error) { -// panic("mock out the CommitterDetails method") -// }, -// } -// -// // use mockedGitConfigAPI in code that requires GitConfigAPI -// // and then make assertions. -// -// } -type GitConfigAPIMock struct { - // CommitterDetailsFunc mocks the CommitterDetails method. - CommitterDetailsFunc func() (*git.CommitterDetails, error) - - // calls tracks calls to the methods. - calls struct { - // CommitterDetails holds details about calls to the CommitterDetails method. - CommitterDetails []struct { - } - } - lockCommitterDetails sync.RWMutex -} - -// CommitterDetails calls CommitterDetailsFunc. -func (mock *GitConfigAPIMock) CommitterDetails() (*git.CommitterDetails, error) { - if mock.CommitterDetailsFunc == nil { - panic("GitConfigAPIMock.CommitterDetailsFunc: method is nil but GitConfigAPI.CommitterDetails was just called") - } - callInfo := struct { - }{} - mock.lockCommitterDetails.Lock() - mock.calls.CommitterDetails = append(mock.calls.CommitterDetails, callInfo) - mock.lockCommitterDetails.Unlock() - return mock.CommitterDetailsFunc() -} - -// CommitterDetailsCalls gets all the calls that were made to CommitterDetails. -// Check the length with: -// len(mockedGitConfigAPI.CommitterDetailsCalls()) -func (mock *GitConfigAPIMock) CommitterDetailsCalls() []struct { -} { - var calls []struct { - } - mock.lockCommitterDetails.RLock() - calls = mock.calls.CommitterDetails - mock.lockCommitterDetails.RUnlock() - return calls -} diff --git a/cmd/hamctl/command/actions/release.go b/cmd/hamctl/command/actions/release.go index ec1822aa..d230e23c 100644 --- a/cmd/hamctl/command/actions/release.go +++ b/cmd/hamctl/command/actions/release.go @@ -3,16 +3,10 @@ package actions import ( "net/http" - "github.com/lunarway/release-manager/internal/git" httpinternal "github.com/lunarway/release-manager/internal/http" "github.com/lunarway/release-manager/internal/intent" ) -//go:generate moq -rm -out config_mock.go . GitConfigAPI -type GitConfigAPI interface { - CommitterDetails() (*git.CommitterDetails, error) -} - type ReleaseResult struct { Response httpinternal.ReleaseResponse Environment string diff --git a/cmd/hamctl/command/config_mock.go b/cmd/hamctl/command/config_mock.go deleted file mode 100644 index 3437f14c..00000000 --- a/cmd/hamctl/command/config_mock.go +++ /dev/null @@ -1,67 +0,0 @@ -// Code generated by moq; DO NOT EDIT. -// github.com/matryer/moq - -package command - -import ( - "github.com/lunarway/release-manager/internal/git" - "sync" -) - -// Ensure, that GitConfigAPIMock does implement GitConfigAPI. -// If this is not the case, regenerate this file with moq. -var _ GitConfigAPI = &GitConfigAPIMock{} - -// GitConfigAPIMock is a mock implementation of GitConfigAPI. -// -// func TestSomethingThatUsesGitConfigAPI(t *testing.T) { -// -// // make and configure a mocked GitConfigAPI -// mockedGitConfigAPI := &GitConfigAPIMock{ -// CommitterDetailsFunc: func() (*git.CommitterDetails, error) { -// panic("mock out the CommitterDetails method") -// }, -// } -// -// // use mockedGitConfigAPI in code that requires GitConfigAPI -// // and then make assertions. -// -// } -type GitConfigAPIMock struct { - // CommitterDetailsFunc mocks the CommitterDetails method. - CommitterDetailsFunc func() (*git.CommitterDetails, error) - - // calls tracks calls to the methods. - calls struct { - // CommitterDetails holds details about calls to the CommitterDetails method. - CommitterDetails []struct { - } - } - lockCommitterDetails sync.RWMutex -} - -// CommitterDetails calls CommitterDetailsFunc. -func (mock *GitConfigAPIMock) CommitterDetails() (*git.CommitterDetails, error) { - if mock.CommitterDetailsFunc == nil { - panic("GitConfigAPIMock.CommitterDetailsFunc: method is nil but GitConfigAPI.CommitterDetails was just called") - } - callInfo := struct { - }{} - mock.lockCommitterDetails.Lock() - mock.calls.CommitterDetails = append(mock.calls.CommitterDetails, callInfo) - mock.lockCommitterDetails.Unlock() - return mock.CommitterDetailsFunc() -} - -// CommitterDetailsCalls gets all the calls that were made to CommitterDetails. -// Check the length with: -// len(mockedGitConfigAPI.CommitterDetailsCalls()) -func (mock *GitConfigAPIMock) CommitterDetailsCalls() []struct { -} { - var calls []struct { - } - mock.lockCommitterDetails.RLock() - calls = mock.calls.CommitterDetails - mock.lockCommitterDetails.RUnlock() - return calls -} diff --git a/cmd/hamctl/command/policy/apply.go b/cmd/hamctl/command/policy/apply.go index c637a8d4..03935985 100644 --- a/cmd/hamctl/command/policy/apply.go +++ b/cmd/hamctl/command/policy/apply.go @@ -6,16 +6,10 @@ import ( "net/http" "github.com/lunarway/release-manager/cmd/hamctl/command/completion" - "github.com/lunarway/release-manager/internal/git" httpinternal "github.com/lunarway/release-manager/internal/http" "github.com/spf13/cobra" ) -//go:generate moq -rm -out config_mock.go . GitConfigAPI -type GitConfigAPI interface { - CommitterDetails() (*git.CommitterDetails, error) -} - func NewApply(client *httpinternal.Client, service *string) *cobra.Command { var command = &cobra.Command{ Use: "apply", diff --git a/cmd/hamctl/command/policy/config_mock.go b/cmd/hamctl/command/policy/config_mock.go deleted file mode 100644 index 597c40ca..00000000 --- a/cmd/hamctl/command/policy/config_mock.go +++ /dev/null @@ -1,67 +0,0 @@ -// Code generated by moq; DO NOT EDIT. -// github.com/matryer/moq - -package policy - -import ( - "github.com/lunarway/release-manager/internal/git" - "sync" -) - -// Ensure, that GitConfigAPIMock does implement GitConfigAPI. -// If this is not the case, regenerate this file with moq. -var _ GitConfigAPI = &GitConfigAPIMock{} - -// GitConfigAPIMock is a mock implementation of GitConfigAPI. -// -// func TestSomethingThatUsesGitConfigAPI(t *testing.T) { -// -// // make and configure a mocked GitConfigAPI -// mockedGitConfigAPI := &GitConfigAPIMock{ -// CommitterDetailsFunc: func() (*git.CommitterDetails, error) { -// panic("mock out the CommitterDetails method") -// }, -// } -// -// // use mockedGitConfigAPI in code that requires GitConfigAPI -// // and then make assertions. -// -// } -type GitConfigAPIMock struct { - // CommitterDetailsFunc mocks the CommitterDetails method. - CommitterDetailsFunc func() (*git.CommitterDetails, error) - - // calls tracks calls to the methods. - calls struct { - // CommitterDetails holds details about calls to the CommitterDetails method. - CommitterDetails []struct { - } - } - lockCommitterDetails sync.RWMutex -} - -// CommitterDetails calls CommitterDetailsFunc. -func (mock *GitConfigAPIMock) CommitterDetails() (*git.CommitterDetails, error) { - if mock.CommitterDetailsFunc == nil { - panic("GitConfigAPIMock.CommitterDetailsFunc: method is nil but GitConfigAPI.CommitterDetails was just called") - } - callInfo := struct { - }{} - mock.lockCommitterDetails.Lock() - mock.calls.CommitterDetails = append(mock.calls.CommitterDetails, callInfo) - mock.lockCommitterDetails.Unlock() - return mock.CommitterDetailsFunc() -} - -// CommitterDetailsCalls gets all the calls that were made to CommitterDetails. -// Check the length with: -// len(mockedGitConfigAPI.CommitterDetailsCalls()) -func (mock *GitConfigAPIMock) CommitterDetailsCalls() []struct { -} { - var calls []struct { - } - mock.lockCommitterDetails.RLock() - calls = mock.calls.CommitterDetails - mock.lockCommitterDetails.RUnlock() - return calls -} diff --git a/cmd/hamctl/command/rollback.go b/cmd/hamctl/command/rollback.go index 0ae1ac9b..1dcbd1e3 100644 --- a/cmd/hamctl/command/rollback.go +++ b/cmd/hamctl/command/rollback.go @@ -8,19 +8,12 @@ import ( "github.com/lunarway/release-manager/cmd/hamctl/command/actions" "github.com/lunarway/release-manager/cmd/hamctl/command/completion" "github.com/lunarway/release-manager/cmd/hamctl/template" - "github.com/lunarway/release-manager/internal/git" httpinternal "github.com/lunarway/release-manager/internal/http" "github.com/lunarway/release-manager/internal/intent" "github.com/manifoldco/promptui" "github.com/spf13/cobra" ) -//go:generate moq -rm -out config_mock.go . GitConfigAPI - -type GitConfigAPI interface { - CommitterDetails() (*git.CommitterDetails, error) -} - type ReleaseArtifact interface { ReleaseArtifactID(service, environment string, artifactID string, intent intent.Intent) (actions.ReleaseResult, error) } diff --git a/internal/git/config.go b/internal/git/config.go deleted file mode 100644 index 16be4b28..00000000 --- a/internal/git/config.go +++ /dev/null @@ -1,92 +0,0 @@ -package git - -import ( - "io" - "os" - "os/exec" - "strings" - - "github.com/pkg/errors" -) - -type CommitterDetails struct { - Name string - Email string -} - -func NewCommitterDetails(name, email string) (*CommitterDetails, error) { - if name == "" { - return nil, errors.New("CommitterDetails.Name is required") - } - if email == "" { - return nil, errors.New("CommitterDetails.Email is required") - } - - return &CommitterDetails{ - Name: name, - Email: email, - }, nil -} - -type LocalGitConfigAPI struct{} - -// GitConfigAPI is an interface to interact with a git config system -// this makes it possible to extract information from the repository -// or the local user -func NewLocalGitConfigAPI() *LocalGitConfigAPI { - return &LocalGitConfigAPI{} -} - -// CommitterDetails returns name and email read for a Git configuration file. -// -// Fetching the configuration values are delegated to the git CLI and follows -// precedence rules defined by Git. -func (*LocalGitConfigAPI) CommitterDetails() (*CommitterDetails, error) { - name, err := getValue("user.name", "HAMCTL_USER_NAME") - if err != nil { - return nil, err - } - email, err := getValue("user.email", "HAMCTL_USER_EMAIL") - if err != nil { - return nil, err - } - - return NewCommitterDetails(name, email) -} - -func getValue(gitKey, envKey string) (string, error) { - v, ok := os.LookupEnv(envKey) - if ok { - return v, nil - } - v, err := getGitConfig(gitKey) - if err != nil { - return "", errors.WithMessagef(err, "Failed to get credentials with 'git config --get %s'", gitKey) - } - return v, nil -} - -// getGitConfig reads a git configuration field and returns its value as a -// string. -func getGitConfig(field string) (string, error) { - cmd := exec.Command("git", "config", "--get", field) - stdout, err := cmd.StdoutPipe() - if err != nil { - return "", errors.WithMessage(err, "get stdout pipe for command") - } - err = cmd.Start() - if err != nil { - return "", errors.WithMessage(err, "start command") - } - stdoutData, err := io.ReadAll(stdout) - if err != nil { - return "", errors.WithMessage(err, "read stdout data of command") - } - - err = cmd.Wait() - if err != nil { - return "", err - } - - return strings.TrimSpace(string(stdoutData)), nil -} diff --git a/internal/git/testdata/email_missing b/internal/git/testdata/email_missing deleted file mode 100644 index 44db5227..00000000 --- a/internal/git/testdata/email_missing +++ /dev/null @@ -1,2 +0,0 @@ -[user] - name = Missing bar diff --git a/internal/git/testdata/name_missing b/internal/git/testdata/name_missing deleted file mode 100644 index 2b73eaba..00000000 --- a/internal/git/testdata/name_missing +++ /dev/null @@ -1,2 +0,0 @@ -[user] - email = "missing@foo.com" diff --git a/internal/git/testdata/user_set_1 b/internal/git/testdata/user_set_1 deleted file mode 100644 index cc75573c..00000000 --- a/internal/git/testdata/user_set_1 +++ /dev/null @@ -1,3 +0,0 @@ -[user] - name = Foo - email = "foo@foo.com" From 7fce8c29f7bbc1e38d683713aca5fa42c43215a8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B8rn=20S=C3=B8rensen?= Date: Sun, 12 Nov 2023 20:38:35 +0100 Subject: [PATCH 34/42] Update make files with new arguments --- Makefile | 24 +++++++++++++++++++++++- cmd/artifact/Makefile | 8 ++++++-- 2 files changed, 29 insertions(+), 3 deletions(-) diff --git a/Makefile b/Makefile index 597b3a38..eba8d143 100644 --- a/Makefile +++ b/Makefile @@ -64,6 +64,11 @@ else RELEASE_MANAGER_INTEGRATION_RABBITMQ_HOST=localhost go test -count=1 ./... endif +HAMCTL_OAUTH_IDP_URL=https://idpurl +HAMCTL_OAUTH_CLIENT_ID=client-id +JWKS_URLS=https://jwksurls/v1/keys +JWT_AUDIENCE=audience +JWT_ISSUER=issuer AUTH_TOKEN=test SSH_PRIVATE_KEY=~/.ssh/github @@ -87,7 +92,10 @@ SERVER_START=./dist/server start \ --log.development t \ --config-repo ${CONFIG_REPO} \ --user-mappings '${USER_MAPPINGS}' \ - --policy-branch-restrictions '${BRANCH_RESTRICTIONS}' + --policy-branch-restrictions '${BRANCH_RESTRICTIONS}' \ + --jwks-urls '${JWKS_URLS}' \ + --jwt-audience '${JWT_AUDIENCE}' \ + --jwt-issuer '${JWT_ISSUER}' server-memory: build_server $(SERVER_START) \ @@ -102,6 +110,20 @@ server-rabbitmq: build_server --amqp-user ${AMQP_USER} \ --amqp-password ${AMQP_PASSWORD} +SERVER_URL=http://localhost:8080 +DAEMON_OAUTH_IDP_URL=https://idpurl +DAEMON_OAUTH_CLIENT_ID=client-id +DAEMON_OAUTH_CLIENT_SECRET=secret + +daemon: build_daemon + ./dist/daemon start \ + --release-manager-url '${SERVER_URL}' \ + --environment local \ + --kubeconfig '${KUBECONFIG}' \ + --idp-url '${DAEMON_OAUTH_IDP_URL}' \ + --client-id '${DAEMON_OAUTH_CLIENT_ID}' \ + --client-secret '${DAEMON_OAUTH_CLIENT_SECRET}' + artifact-init: USER_MAPPINGS="kaspernissen@gmail.com=kni@lunar.app,something@gmail.com=some@lunar.app" ./dist/artifact init --slack-token ${SLACK_TOKEN} --artifact-id "master-deed62270f-854d930ecb" --name "lunar-way-product-service" --service "product" --git-author-name "Kasper Nissen" --git-author-email "kaspernissen@gmail.com" --git-message "This is a test message" --git-committer-name "Bjørn Sørensen" --git-committer-email "test@gmail.com" --git-sha deed62270f24f1ca8cf2c19b505b2c88036e1b1c --git-branch master --url "https://bitbucket.org/LunarWay/lunar-way-product-service/commits/a05e314599a7c202724d46a009fcc0f493bce035" --ci-job-url "https://jenkins.corp.com/job/bitbucket/job/lunar-way-product-service/job/master/170/display/redirect" diff --git a/cmd/artifact/Makefile b/cmd/artifact/Makefile index 9ceaced9..bef32668 100644 --- a/cmd/artifact/Makefile +++ b/cmd/artifact/Makefile @@ -103,10 +103,14 @@ example_resources: echo "$$FLUX_KUSTOMIZATION" > examples/prod/kustomization.yaml RELEASE_MANAGER_URL=http://localhost:8080 -RELEASE_MANAGER_AUTH_TOKEN=test +OAUTH_IDP_URL=https://idpurl +OAUTH_CLIENT_ID=id +OAUTH_CLIENT_SECRET=secret test_push: example example_resources ./artifact push \ --root examples \ --http-base-url ${RELEASE_MANAGER_URL} \ - --http-auth-token ${RELEASE_MANAGER_AUTH_TOKEN} + --client-id ${OAUTH_CLIENT_ID} \ + --client-secret ${OAUTH_CLIENT_SECRET} \ + --idp-url ${OAUTH_IDP_URL} From 28c94219f475cf2ca52c6e116c07fc93ad9ac17d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B8rn=20S=C3=B8rensen?= Date: Mon, 13 Nov 2023 10:53:04 +0100 Subject: [PATCH 35/42] Add subject to log context --- cmd/server/http/jwt.go | 1 + cmd/server/http/log.go | 2 -- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/cmd/server/http/jwt.go b/cmd/server/http/jwt.go index fc3ab8fe..21b28913 100644 --- a/cmd/server/http/jwt.go +++ b/cmd/server/http/jwt.go @@ -132,6 +132,7 @@ func (v *Verifier) authentication(staticAuthToken string) func(http.Handler) htt } } ctx := withAuthenticatedUser(r.Context(), parsedToken.Subject()) + ctx = log.AddContext(ctx, "subject", parsedToken.Subject()) *r = *r.WithContext(ctx) h.ServeHTTP(w, r) diff --git a/cmd/server/http/log.go b/cmd/server/http/log.go index 5c5ce8f7..5c886954 100644 --- a/cmd/server/http/log.go +++ b/cmd/server/http/log.go @@ -38,13 +38,11 @@ func reqrespLogger(h http.Handler) http.Handler { Method string `json:"method,omitempty"` Path string `json:"path,omitempty"` Headers map[string]string `json:"headers,omitempty"` - Subject string `json:"subject,omitempty"` }{ URL: r.URL.RequestURI(), Method: r.Method, Path: r.URL.Path, Headers: secureHeaders(flattenHeaders(r.Header)), - Subject: subject, }, "res", struct { StatusCode int `json:"statusCode,omitempty"` From 223852fd5de29a91750e6e90fccecf36c9fb7e14 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B8rn=20S=C3=B8rensen?= Date: Mon, 13 Nov 2023 10:54:35 +0100 Subject: [PATCH 36/42] Remove newline --- cmd/server/http/log.go | 1 - 1 file changed, 1 deletion(-) diff --git a/cmd/server/http/log.go b/cmd/server/http/log.go index 5c886954..1fc20cd3 100644 --- a/cmd/server/http/log.go +++ b/cmd/server/http/log.go @@ -31,7 +31,6 @@ func reqrespLogger(h http.Handler) http.Handler { // request duration in miliseconds duration := time.Since(start).Nanoseconds() / 1e6 statusCode := statusWriter.statusCode - fields := []interface{}{ "req", struct { URL string `json:"url,omitempty"` From 7a4d15d069cba49fc5e4d1ccd5540c378d647b74 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B8rn=20S=C3=B8rensen?= Date: Mon, 13 Nov 2023 11:12:10 +0100 Subject: [PATCH 37/42] Remove unused jwkFetchTimeout argument --- cmd/server/command/start.go | 2 +- cmd/server/http/http_test.go | 4 ++-- cmd/server/http/jwt.go | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/cmd/server/command/start.go b/cmd/server/command/start.go index 8d8ccf00..46e13d2a 100644 --- a/cmd/server/command/start.go +++ b/cmd/server/command/start.go @@ -516,7 +516,7 @@ func NewStart(startOptions *startOptions) *cobra.Command { } }() go func() { - jwtVerifier, err := http.NewVerifier(ctx, startOptions.jwtVerifier.JwksLocation, 30*time.Second, startOptions.jwtVerifier.Issuer, startOptions.jwtVerifier.Audience) + jwtVerifier, err := http.NewVerifier(ctx, startOptions.jwtVerifier.JwksLocation, startOptions.jwtVerifier.Issuer, startOptions.jwtVerifier.Audience) if err != nil { done <- errors.WithMessage(err, "new jwt verifier") return diff --git a/cmd/server/http/http_test.go b/cmd/server/http/http_test.go index 89bc1269..30e77004 100644 --- a/cmd/server/http/http_test.go +++ b/cmd/server/http/http_test.go @@ -133,7 +133,7 @@ func TestAuthenticate(t *testing.T) { } for _, tc := range tt { t.Run(tc.name, func(t *testing.T) { - verifier, err := NewVerifier(context.Background(), jwksServer.URL, 1*time.Second, issuer, audience) + verifier, err := NewVerifier(context.Background(), jwksServer.URL, issuer, audience) require.NoError(t, err, "failed to create verifier") handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { @@ -204,7 +204,7 @@ func TestAuthenticate_withInvalidCache(t *testing.T) { _, err = cache.Refresh(context.Background(), jwkServer.URL) require.NoError(t, err, "failed to refresh cache") - authenticator, err := NewVerifier(context.Background(), jwkServer.URL, 10*time.Second, testIssuer, testAudience) + authenticator, err := NewVerifier(context.Background(), jwkServer.URL, testIssuer, testAudience) require.NoError(t, err, "failed to create verified") authenticator.jwkCache = cache diff --git a/cmd/server/http/jwt.go b/cmd/server/http/jwt.go index 21b28913..c3cac499 100644 --- a/cmd/server/http/jwt.go +++ b/cmd/server/http/jwt.go @@ -44,7 +44,7 @@ type Verifier struct { jwkCache JwkCache } -func NewVerifier(ctx context.Context, jwksLocation string, jwkFetchTimeout time.Duration, issuer string, audience string) (*Verifier, error) { +func NewVerifier(ctx context.Context, jwksLocation string, issuer string, audience string) (*Verifier, error) { cache := jwk.NewCache(ctx) err := cache.Register(jwksLocation, jwk.WithMinRefreshInterval(24*time.Hour)) if err != nil { From f6742ee10f1a98e34d767ef9581c36fa902390cb Mon Sep 17 00:00:00 2001 From: Peter Hoeg Steffensen Date: Mon, 13 Nov 2023 11:17:15 +0100 Subject: [PATCH 38/42] handle all the dots in bearer token --- cmd/server/http/http_test.go | 5 +++++ cmd/server/http/jwt.go | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/cmd/server/http/http_test.go b/cmd/server/http/http_test.go index 30e77004..0e2632a8 100644 --- a/cmd/server/http/http_test.go +++ b/cmd/server/http/http_test.go @@ -77,6 +77,11 @@ func TestAuthenticate(t *testing.T) { authorization: "Bearer " + serverToken, status: http.StatusOK, }, + { + name: "Invalid bearer token with lots of dots", + authorization: "Bearer ......................", + status: http.StatusUnauthorized, + }, { name: "valid jwt bearer authorization", authorization: fmt.Sprintf("Bearer %s", diff --git a/cmd/server/http/jwt.go b/cmd/server/http/jwt.go index c3cac499..0f667118 100644 --- a/cmd/server/http/jwt.go +++ b/cmd/server/http/jwt.go @@ -66,7 +66,7 @@ func NewVerifier(ctx context.Context, jwksLocation string, issuer string, audien func ParseBearerToken(token string) (string, error) { jwt := strings.TrimPrefix(token, "Bearer") - tokenParts := strings.Split(jwt, ".") + tokenParts := strings.SplitN(jwt, ".", 4) if len(tokenParts) != 3 { return "", errors.New("invalid token format") From 6d5469e9548a900ad0714909d44f2242935f6606 Mon Sep 17 00:00:00 2001 From: Hoeg Date: Tue, 5 Dec 2023 13:49:28 +0100 Subject: [PATCH 39/42] change to release manager folder --- internal/http/authenticator.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/http/authenticator.go b/internal/http/authenticator.go index a66174c2..68c6d084 100644 --- a/internal/http/authenticator.go +++ b/internal/http/authenticator.go @@ -57,7 +57,7 @@ func (g *UserAuthenticator) Access(ctx context.Context) (*http.Client, error) { return g.conf.Client(ctx, token), nil } -const tokenFile string = ".Config/hamctl/token.json" +const tokenFile string = ".Config/release-manager/token.json" func tokenFilePath() string { return filepath.Join(os.Getenv("HOME"), tokenFile) From bb2a8be38e97cbcfcea3851e803d27030a849dc5 Mon Sep 17 00:00:00 2001 From: Hoeg Date: Tue, 5 Dec 2023 13:52:12 +0100 Subject: [PATCH 40/42] rename struct --- cmd/hamctl/command/release_test.go | 8 ++++---- cmd/hamctl/command/rollback_test.go | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/cmd/hamctl/command/release_test.go b/cmd/hamctl/command/release_test.go index ce314e75..0b68bb61 100644 --- a/cmd/hamctl/command/release_test.go +++ b/cmd/hamctl/command/release_test.go @@ -18,9 +18,9 @@ import ( "github.com/stretchr/testify/require" ) -type NoAuthClient struct{} +type NoopAuthClient struct{} -func (NoAuthClient) Access(ctx context.Context) (*http.Client, error) { +func (NoopAuthClient) Access(ctx context.Context) (*http.Client, error) { return &http.Client{}, nil } @@ -67,7 +67,7 @@ func TestRelease(t *testing.T) { c := internalhttp.Client{ BaseURL: server.URL, - Auth: NoAuthClient{}, + Auth: NoopAuthClient{}, } releaseClient := actions.NewReleaseHttpClient(&c) @@ -201,7 +201,7 @@ func maskGUID(output []string) []string { func TestRelease_emptyEnvValue(t *testing.T) { serviceName := "service-name" - c := internalhttp.Client{Auth: NoAuthClient{}} + c := internalhttp.Client{Auth: NoopAuthClient{}} releaseClient := actions.NewReleaseHttpClient(&c) cmd := command.NewRelease(&c, &serviceName, func(f string, args ...interface{}) { diff --git a/cmd/hamctl/command/rollback_test.go b/cmd/hamctl/command/rollback_test.go index 000342cf..7831deca 100644 --- a/cmd/hamctl/command/rollback_test.go +++ b/cmd/hamctl/command/rollback_test.go @@ -69,7 +69,7 @@ func TestRollback(t *testing.T) { c := internalhttp.Client{ BaseURL: server.URL, - Auth: NoAuthClient{}, + Auth: NoopAuthClient{}, } releaseClient := actions.NewReleaseHttpClient(&c) From c2225ebdb3a63bfd003881ba97dfcd19d4808482 Mon Sep 17 00:00:00 2001 From: Hoeg Date: Tue, 5 Dec 2023 16:42:33 +0100 Subject: [PATCH 41/42] toggle static token off if empty --- cmd/server/http/jwt.go | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/cmd/server/http/jwt.go b/cmd/server/http/jwt.go index 0f667118..2fa9e876 100644 --- a/cmd/server/http/jwt.go +++ b/cmd/server/http/jwt.go @@ -84,13 +84,15 @@ func (v *Verifier) authentication(staticAuthToken string) func(http.Handler) htt return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { authorization := r.Header.Get("Authorization") - // old hamctl token auth - t := strings.TrimPrefix(authorization, "Bearer ") - t = strings.TrimSpace(t) - if t == staticAuthToken { - - h.ServeHTTP(w, r) - return + // use value as feature toggle + if staticAuthToken != "" { + // old hamctl token auth + t := strings.TrimPrefix(authorization, "Bearer ") + t = strings.TrimSpace(t) + if t == staticAuthToken { + h.ServeHTTP(w, r) + return + } } // jwt auth From 1f1535f64e00f2b0ad7cff82ccbe17fc50ab555f Mon Sep 17 00:00:00 2001 From: Hoeg Date: Tue, 5 Dec 2023 16:42:43 +0100 Subject: [PATCH 42/42] moved to function --- cmd/hamctl/command/root.go | 25 ++++++++++++++++--------- 1 file changed, 16 insertions(+), 9 deletions(-) diff --git a/cmd/hamctl/command/root.go b/cmd/hamctl/command/root.go index 88f42f51..1c97bb47 100644 --- a/cmd/hamctl/command/root.go +++ b/cmd/hamctl/command/root.go @@ -16,17 +16,11 @@ import ( // NewRoot returns a new instance of a hamctl command. func NewRoot(version *string) (*cobra.Command, error) { - idpURL := os.Getenv("HAMCTL_OAUTH_IDP_URL") - if idpURL == "" { - return nil, errors.New("no HAMCTL_OAUTH_IDP_URL env var set") - } - clientID := os.Getenv("HAMCTL_OAUTH_CLIENT_ID") - if clientID == "" { - return nil, errors.New("no HAMCTL_OAUTH_CLIENT_ID env var set") + authenticator, err := setupAuthenticator() + if err != nil { + return nil, err } - authenticator := http.NewUserAuthenticator(clientID, idpURL) - var service string client := http.Client{ Auth: &authenticator, @@ -133,3 +127,16 @@ func defaultShuttleString(shuttleLocator func() (shuttleSpec, bool), flagValue * *flagValue = t } } + +func setupAuthenticator() (http.UserAuthenticator, error) { + idpURL := os.Getenv("HAMCTL_OAUTH_IDP_URL") + if idpURL == "" { + return http.UserAuthenticator{}, errors.New("no HAMCTL_OAUTH_IDP_URL env var set") + } + clientID := os.Getenv("HAMCTL_OAUTH_CLIENT_ID") + if clientID == "" { + return http.UserAuthenticator{}, errors.New("no HAMCTL_OAUTH_CLIENT_ID env var set") + } + + return http.NewUserAuthenticator(clientID, idpURL), nil +}