diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index bd286a62..9ea023b1 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -28,7 +28,7 @@ jobs: - name: Build all binaries run: make build-all - name: Run golangci-lint - uses: golangci/golangci-lint-action@v4 + uses: golangci/golangci-lint-action@v6 with: version: latest test: diff --git a/go.mod b/go.mod index 4739e567..13688cfc 100644 --- a/go.mod +++ b/go.mod @@ -4,7 +4,7 @@ go 1.22 require ( github.com/databus23/goslo.policy v0.0.0-20210929125152-81bf2876dbdb - github.com/gophercloud/gophercloud v1.14.0 + github.com/gophercloud/gophercloud/v2 v2.1.0 github.com/gorilla/mux v1.8.1 github.com/h2non/gock v1.2.0 github.com/patrickmn/go-cache v2.1.0+incompatible diff --git a/go.sum b/go.sum index f8671c04..6479238b 100644 --- a/go.sum +++ b/go.sum @@ -43,8 +43,8 @@ github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/gophercloud/gophercloud v1.14.0 h1:Bt9zQDhPrbd4qX7EILGmy+i7GP35cc+AAL2+wIJpUE8= -github.com/gophercloud/gophercloud v1.14.0/go.mod h1:aAVqcocTSXh2vYFZ1JTvx4EQmfgzxRcNupUfxZbBNDM= +github.com/gophercloud/gophercloud/v2 v2.1.0 h1:91p6c+uMckXyx39nSIYjDirDBnPVFQq0q1njLNPX+NY= +github.com/gophercloud/gophercloud/v2 v2.1.0/go.mod h1:f2hMRC7Kakbv5vM7wSGHrIPZh6JZR60GVHryJlF/K44= github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY= github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ= github.com/grafana/regexp v0.0.0-20240518133315-a468a5bfb3bc h1:GN2Lv3MGO7AS6PrRoT6yV5+wkrOpcszoIsO4+4ds248= @@ -142,30 +142,22 @@ go.uber.org/mock v0.4.0 h1:VcM4ZOtdbR4f6VXfiOpwpVJDL6lCReaZ6mw31wqh7KU= go.uber.org/mock v0.4.0/go.mod h1:a6FSlNadKUHUa9IP5Vyt1zh4fC7uAwxMutEAscFbkZc= go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= -golang.org/x/crypto v0.0.0-20220829220503-c86fa9a7ed90/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.25.0 h1:ypSNr+bnYL2YhwoMt2zPxHFmbAN1KZs/njMG3hxUp30= golang.org/x/crypto v0.25.0/go.mod h1:T+wALwcMOSE0kXgUAnPAHqTLW+XHgcELELW8VaDgm/M= golang.org/x/exp v0.0.0-20240823005443-9b4947da3948 h1:kx6Ds3MlpiUHKj7syVnbp57++8WpuKPcR5yjLBjvLEA= golang.org/x/exp v0.0.0-20240823005443-9b4947da3948/go.mod h1:akd2r19cwCdwSwWeIdzYQGa/EZZyqcOdwWiwj5L5eKQ= -golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.28.0 h1:a9JDOJc5GMUJ0+UDqmLT86WiEy7iWyIhz8gz8E4e5hE= golang.org/x/net v0.28.0/go.mod h1:yqtgsTWOOnlGLG9GFRrK3++bGOUEkNBoHZc8MEDWPNg= golang.org/x/oauth2 v0.22.0 h1:BzDx2FehcG7jJwgWLELCdmLuxk2i+x9UDpSiss2u0ZA= golang.org/x/oauth2 v0.22.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ= golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= -golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.23.0 h1:YfKFowiIMvtgl1UERQoTPPToxltDeZfbj4H7dVUCwmM= golang.org/x/sys v0.23.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= -golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.17.0 h1:XtiM5bkSOt+ewxlOE/aE/AKEHibwj/6gvWMl9Rsh0Qc= golang.org/x/text v0.17.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk= golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= -golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg= google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/main.go b/main.go index 4d6ccf13..0852efd4 100644 --- a/main.go +++ b/main.go @@ -20,9 +20,30 @@ package main import ( + "context" + "os" + "os/signal" + "syscall" + "github.com/sapcc/maia/pkg/cmd" ) func main() { - cmd.Execute() + // Create a base context + ctx := context.Background() + + // Create a context that can be canceled + ctx, cancel := context.WithCancel(ctx) + defer cancel() + + // Set up signal handling + sigChan := make(chan os.Signal, 1) + signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM) + go func() { + <-sigChan + cancel() + }() + + // Execute the root command with the context + cmd.ExecuteWithContext(ctx) } diff --git a/pkg/api/api_test.go b/pkg/api/api_test.go index d9ab839b..6778da31 100644 --- a/pkg/api/api_test.go +++ b/pkg/api/api_test.go @@ -27,7 +27,7 @@ import ( "errors" policy "github.com/databus23/goslo.policy" - "github.com/gophercloud/gophercloud/openstack/identity/v3/tokens" + "github.com/gophercloud/gophercloud/v2/openstack/identity/v3/tokens" "github.com/prometheus/client_golang/prometheus" "github.com/spf13/viper" "go.uber.org/mock/gomock" @@ -75,40 +75,40 @@ func setupTest(t *testing.T, controller *gomock.Controller) (router http.Handler func expectAuthByProjectID(keystoneMock *keystone.MockDriver) { httpReqMatcher := test.HTTPRequestMatcher{InjectHeader: projectHeader} - authCall := keystoneMock.EXPECT().AuthenticateRequest(httpReqMatcher, false).Return(projectContext, nil) - keystoneMock.EXPECT().ChildProjects(projectContext.Auth["project_id"]).Return([]string{}, nil).After(authCall) + authCall := keystoneMock.EXPECT().AuthenticateRequest(test.MatchContext(), httpReqMatcher, false).Return(projectContext, nil) + keystoneMock.EXPECT().ChildProjects(test.MatchContext(), projectContext.Auth["project_id"]).Return([]string{}, nil).After(authCall) } func expectAuthByDomainName(keystoneMock *keystone.MockDriver) { httpReqMatcher := test.HTTPRequestMatcher{InjectHeader: domainHeader} - keystoneMock.EXPECT().AuthenticateRequest(httpReqMatcher, false).Return(domainContext, nil) + keystoneMock.EXPECT().AuthenticateRequest(test.MatchContext(), httpReqMatcher, false).Return(domainContext, nil) } func expectAuthWithChildren(keystoneMock *keystone.MockDriver) { httpReqMatcher := test.HTTPRequestMatcher{InjectHeader: projectHeader} - authCall := keystoneMock.EXPECT().AuthenticateRequest(httpReqMatcher, false).Return(projectContext, nil) - keystoneMock.EXPECT().ChildProjects(projectContext.Auth["project_id"]).Return([]string{"67890"}, nil).After(authCall) + authCall := keystoneMock.EXPECT().AuthenticateRequest(test.MatchContext(), httpReqMatcher, false).Return(projectContext, nil) + keystoneMock.EXPECT().ChildProjects(test.MatchContext(), projectContext.Auth["project_id"]).Return([]string{"67890"}, nil).After(authCall) } func expectAuthByDefaults(keystoneMock *keystone.MockDriver) { httpReqMatcher := test.HTTPRequestMatcher{InjectHeader: projectHeader} - authCall := keystoneMock.EXPECT().AuthenticateRequest(httpReqMatcher, true).Return(projectContext, nil) - keystoneMock.EXPECT().UserProjects(projectContext.Auth["user_id"]).Return([]tokens.Scope{{ProjectID: projectContext.Auth["project_id"], DomainID: projectContext.Auth["project_domain_id"]}}, nil).After(authCall) + authCall := keystoneMock.EXPECT().AuthenticateRequest(test.MatchContext(), httpReqMatcher, true).Return(projectContext, nil) + keystoneMock.EXPECT().UserProjects(test.MatchContext(), projectContext.Auth["user_id"]).Return([]tokens.Scope{{ProjectID: projectContext.Auth["project_id"], DomainID: projectContext.Auth["project_domain_id"]}}, nil).After(authCall) } func expectAuthAndFail(keystoneMock *keystone.MockDriver) { httpReqMatcher := test.HTTPRequestMatcher{InjectHeader: projectHeader} - keystoneMock.EXPECT().AuthenticateRequest(httpReqMatcher, false).Return(nil, keystone.NewAuthenticationError(keystone.StatusWrongCredentials, "negativetesterror")) + keystoneMock.EXPECT().AuthenticateRequest(test.MatchContext(), httpReqMatcher, false).Return(nil, keystone.NewAuthenticationError(keystone.StatusWrongCredentials, "negativetesterror")) } func expectPlainBasicAuthAndFail(keystoneMock *keystone.MockDriver) { httpReqMatcher := test.HTTPRequestMatcher{InjectHeader: projectHeader} - keystoneMock.EXPECT().AuthenticateRequest(httpReqMatcher, true).Return(nil, keystone.NewAuthenticationError(keystone.StatusWrongCredentials, "negativetesterror")) + keystoneMock.EXPECT().AuthenticateRequest(test.MatchContext(), httpReqMatcher, true).Return(nil, keystone.NewAuthenticationError(keystone.StatusWrongCredentials, "negativetesterror")) } func expectAuthAndDenyAuthorization(keystoneMock *keystone.MockDriver) { httpReqMatcher := test.HTTPRequestMatcher{InjectHeader: projectHeader} - keystoneMock.EXPECT().AuthenticateRequest(httpReqMatcher, false).Return(projectInsufficientRolesContext, nil) + keystoneMock.EXPECT().AuthenticateRequest(test.MatchContext(), httpReqMatcher, false).Return(projectInsufficientRolesContext, nil) } // HTTP based tests diff --git a/pkg/api/server.go b/pkg/api/server.go index dcc90ba8..5fb5afbc 100644 --- a/pkg/api/server.go +++ b/pkg/api/server.go @@ -20,6 +20,7 @@ package api import ( + "context" "errors" "net/http" "net/url" @@ -45,7 +46,7 @@ var storageInstance storage.Driver var keystoneInstance keystone.Driver // Server initializes and starts the API server, hooking it up to the API router -func Server() error { +func Server(ctx context.Context) error { prometheusAPIURL := viper.GetString("maia.prometheus_url") if prometheusAPIURL == "" { panic(errors.New("prometheus endpoint not configured (maia.prometheus_url / MAIA_PROMETHEUS_URL)")) diff --git a/pkg/api/util.go b/pkg/api/util.go index e7b46fba..99b05123 100644 --- a/pkg/api/util.go +++ b/pkg/api/util.go @@ -156,8 +156,9 @@ func ReturnPromError(w http.ResponseWriter, err error, code int) { } func scopeToLabelConstraint(req *http.Request, keystoneDriver keystone.Driver) (string, []string) { //nolint:gocritic + ctx := req.Context() if projectID := req.Header.Get("X-Project-Id"); projectID != "" { - children, err := keystoneDriver.ChildProjects(projectID) + children, err := keystoneDriver.ChildProjects(ctx, projectID) if err != nil { panic(err) } @@ -251,7 +252,8 @@ func authorizeRules(w http.ResponseWriter, req *http.Request, guessScope bool, r } // 2. authenticate - context, err := keystoneInstance.AuthenticateRequest(req, guessScope) + ctx := req.Context() + policyContext, err := keystoneInstance.AuthenticateRequest(ctx, req, guessScope) if err != nil { code := err.StatusCode() httpCode := http.StatusUnauthorized @@ -297,7 +299,7 @@ func authorizeRules(w http.ResponseWriter, req *http.Request, guessScope bool, r // 3. authorize pe := policyEngine() for _, rule := range rules { - if pe.Enforce(rule, *context) { + if pe.Enforce(rule, *policyContext) { matchedRules = append(matchedRules, rule) } } diff --git a/pkg/cmd/client.go b/pkg/cmd/client.go index 60fe9401..99e22693 100644 --- a/pkg/cmd/client.go +++ b/pkg/cmd/client.go @@ -20,6 +20,7 @@ package cmd import ( + "context" "encoding/json" "errors" "fmt" @@ -31,7 +32,7 @@ import ( "text/template" "time" - "github.com/gophercloud/gophercloud" + "github.com/gophercloud/gophercloud/v2" "github.com/prometheus/common/model" "github.com/spf13/cobra" "github.com/spf13/viper" @@ -70,7 +71,7 @@ func recoverAll() { } } -func fetchToken() { +func fetchToken(ctx context.Context) { if scopedDomain != "" { auth.Scope.DomainName = scopedDomain } @@ -162,12 +163,12 @@ func fetchToken() { } // finally ... authenticate with keystone - context, url, err := keystoneInstance().Authenticate(auth) + policyContext, url, err := keystoneInstance().Authenticate(ctx, auth) if err != nil { panic(err) } // keep the token and use the URL from the catalog (unless set explicitly) - auth.TokenID = context.Auth["token"] + auth.TokenID = policyContext.Auth["token"] if maiaURL == "" { maiaURL = url } @@ -175,13 +176,14 @@ func fetchToken() { // storageInstance creates a new Prometheus driver instance lazily func storageInstance() storage.Driver { + ctx := context.Background() if storageDriver == nil { switch { case promURL != "": storageDriver = storage.NewPrometheusDriver(promURL, map[string]string{}) case auth.IdentityEndpoint != "": // authenticate and set maiaURL if missing - fetchToken() + fetchToken(ctx) storageDriver = storage.NewPrometheusDriver(maiaURL, map[string]string{"X-Auth-Token": auth.TokenID}) default: panic(errors.New("either --os-auth-url or --prometheus-url need to be specified")) diff --git a/pkg/cmd/cmd_test.go b/pkg/cmd/cmd_test.go index 87ac2ec3..6b40076e 100644 --- a/pkg/cmd/cmd_test.go +++ b/pkg/cmd/cmd_test.go @@ -20,13 +20,14 @@ package cmd import ( + "context" "fmt" "os" "testing" "time" policy "github.com/databus23/goslo.policy" - "github.com/gophercloud/gophercloud" + "github.com/gophercloud/gophercloud/v2" "go.uber.org/mock/gomock" "github.com/sapcc/maia/pkg/keystone" @@ -78,12 +79,13 @@ func setupTest(controller *gomock.Controller) (keystoneDriver *keystone.MockDriv } func expectAuth(keystoneMock *keystone.MockDriver) { - keystoneMock.EXPECT().Authenticate(gophercloud.AuthOptions{IdentityEndpoint: auth.IdentityEndpoint, Username: auth.Username, UserID: auth.UserID, + ctx := context.Background() + keystoneMock.EXPECT().Authenticate(ctx, gophercloud.AuthOptions{IdentityEndpoint: auth.IdentityEndpoint, Username: auth.Username, UserID: auth.UserID, Password: auth.Password, DomainName: auth.DomainName, Scope: auth.Scope}).Return(&policy.Context{Request: map[string]string{"username": auth.Username, "password": auth.Password, "user_domain_name": "domainname", "project_id": auth.Scope.ProjectID}, Auth: map[string]string{"project_id": auth.Scope.ProjectID}, Roles: []string{"monitoring_viewer"}}, "http://localhost:9091", nil) // call this explicitly since the mocked storage does not - fetchToken() + fetchToken(ctx) } // HTTP based tests @@ -450,6 +452,7 @@ func Test_Auth(t *testing.T) { func authentication(tokenid, authtype, username, userid, password, appcredid, appcredname, appcredsecret string) (paniced bool) { paniced = false + ctx := context.Background() defer func() { if r := recover(); r != nil { @@ -501,7 +504,7 @@ func authentication(tokenid, authtype, username, userid, password, appcredid, ap // create dummy keystone and storage mock keystoneMock := keystone.NewMockDriver(ctrl) setKeystoneInstance(keystoneMock) - keystoneMock.EXPECT().Authenticate(expectedAuth).Return(&policy.Context{ + keystoneMock.EXPECT().Authenticate(ctx, expectedAuth).Return(&policy.Context{ Request: map[string]string{ "user_id": auth.UserID, "project_id": "12345", @@ -512,7 +515,7 @@ func authentication(tokenid, authtype, username, userid, password, appcredid, ap Auth: map[string]string{"project_id": auth.Scope.ProjectID}, Roles: []string{"monitoring_viewer"}, }, "http://localhost:9091", nil) - fetchToken() + fetchToken(ctx) return paniced } diff --git a/pkg/cmd/root.go b/pkg/cmd/root.go index 95d78826..38195961 100644 --- a/pkg/cmd/root.go +++ b/pkg/cmd/root.go @@ -20,6 +20,7 @@ package cmd import ( + "context" "fmt" "os" @@ -51,8 +52,19 @@ var RootCmd = &cobra.Command{ // Execute adds all child commands to the root command sets flags appropriately. // This is called by main.main(). It only needs to happen once to the rootCmd. func Execute() { - if err := RootCmd.Execute(); err != nil { - fmt.Fprint(os.Stderr, err) + ExecuteWithContext(context.Background()) +} + +// ExecuteWithContext is similar to Execute but takes a context +func ExecuteWithContext(ctx context.Context) { + // Add the context to the root command + RootCmd.PersistentPreRun = func(cmd *cobra.Command, args []string) { + // You can use the context here if needed + cmd.SetContext(ctx) + } + + if err := RootCmd.ExecuteContext(ctx); err != nil { + fmt.Fprintln(os.Stderr, err) os.Exit(-1) } } diff --git a/pkg/cmd/serve.go b/pkg/cmd/serve.go index fa8e0a4d..8dc84bd8 100644 --- a/pkg/cmd/serve.go +++ b/pkg/cmd/serve.go @@ -35,6 +35,8 @@ var serveCmd = &cobra.Command{ Short: "Start the Maia service", Long: "Run the Maia service against a Prometheus backend collecting the metrics.", RunE: func(cmd *cobra.Command, args []string) (ret error) { + ctx := cmd.Context() + // transform panics with error params into errors defer func() { if r := recover(); r != nil { @@ -43,7 +45,7 @@ var serveCmd = &cobra.Command{ }() // just run the server - err := api.Server() + err := api.Server(ctx) if err != nil { return err } diff --git a/pkg/keystone/interface.go b/pkg/keystone/interface.go index be18cc76..dea5d64f 100644 --- a/pkg/keystone/interface.go +++ b/pkg/keystone/interface.go @@ -20,12 +20,13 @@ package keystone import ( + "context" "fmt" "net/http" policy "github.com/databus23/goslo.policy" - "github.com/gophercloud/gophercloud" - "github.com/gophercloud/gophercloud/openstack/identity/v3/tokens" + "github.com/gophercloud/gophercloud/v2" + "github.com/gophercloud/gophercloud/v2/openstack/identity/v3/tokens" "github.com/spf13/viper" ) @@ -79,17 +80,17 @@ type Driver interface { // After successful authentication, additional context information is added to the request header // In addition a Context object is returned for policy evaluation. // When guessScope is set to true, the method will try to find a suitible project when the scope is not defined (basic auth. only) - AuthenticateRequest(req *http.Request, guessScope bool) (*policy.Context, AuthenticationError) + AuthenticateRequest(ctx context.Context, req *http.Request, guessScope bool) (*policy.Context, AuthenticationError) // Authenticate authenticates a user using the provided authOptions. // It returns a context for policy evaluation and the public endpoint retrieved from the service catalog - Authenticate(options gophercloud.AuthOptions) (*policy.Context, string, AuthenticationError) + Authenticate(ctx context.Context, options gophercloud.AuthOptions) (*policy.Context, string, AuthenticationError) // ChildProjects returns the IDs of all child-projects of the project denoted by projectID - ChildProjects(projectID string) ([]string, error) + ChildProjects(ctx context.Context, projectID string) ([]string, error) // UserProjects returns the project IDs and name of all projects where the current user has a monitoring role - UserProjects(userID string) ([]tokens.Scope, error) + UserProjects(ctx context.Context, userID string) ([]tokens.Scope, error) // ServiceURL returns the service's global catalog entry // The result is empty when called from a client diff --git a/pkg/keystone/keystone.go b/pkg/keystone/keystone.go index 452f6471..eb953884 100644 --- a/pkg/keystone/keystone.go +++ b/pkg/keystone/keystone.go @@ -20,6 +20,7 @@ package keystone import ( + "context" "fmt" "net/http" @@ -31,13 +32,13 @@ import ( "time" policy "github.com/databus23/goslo.policy" - "github.com/gophercloud/gophercloud" - "github.com/gophercloud/gophercloud/openstack" - "github.com/gophercloud/gophercloud/openstack/identity/v3/projects" - "github.com/gophercloud/gophercloud/openstack/identity/v3/roles" - "github.com/gophercloud/gophercloud/openstack/identity/v3/tokens" - "github.com/gophercloud/gophercloud/openstack/identity/v3/users" - "github.com/gophercloud/gophercloud/pagination" + "github.com/gophercloud/gophercloud/v2" + "github.com/gophercloud/gophercloud/v2/openstack" + "github.com/gophercloud/gophercloud/v2/openstack/identity/v3/projects" + "github.com/gophercloud/gophercloud/v2/openstack/identity/v3/roles" + "github.com/gophercloud/gophercloud/v2/openstack/identity/v3/tokens" + "github.com/gophercloud/gophercloud/v2/openstack/identity/v3/users" + "github.com/gophercloud/gophercloud/v2/pagination" cache "github.com/patrickmn/go-cache" "github.com/spf13/viper" @@ -80,7 +81,8 @@ func (d *keystone) init() { if viper.Get("keystone.username") != nil { // force service logon to check validity early // this will set d.providerClient - _, err := d.serviceKeystoneClient() + ctx := context.Background() + _, err := d.serviceKeystoneClient(ctx) if err != nil { panic(err) } @@ -88,28 +90,28 @@ func (d *keystone) init() { } // serviceKeystoneClient creates and returns the keystone connection used by the running service -func (d *keystone) serviceKeystoneClient() (*gophercloud.ServiceClient, error) { +func (d *keystone) serviceKeystoneClient(ctx context.Context) (*gophercloud.ServiceClient, error) { d.serviceConnMutex.Lock() defer d.serviceConnMutex.Unlock() if d.providerClient == nil { util.LogInfo("Setting up identity connection to %s", viper.GetString("keystone.auth_url")) - client, err := newKeystoneClient(authOptionsFromConfig()) + client, err := newKeystoneClient(ctx, authOptionsFromConfig()) if err != nil { return nil, err } d.providerClient = client // load the list of all known domains and roles to avoid frequent API calls // changes will not be recognized at runtime - d.loadDomainsAndRoles() + d.loadDomainsAndRoles(ctx) } return d.providerClient, nil } // newKeystoneClient creates a new keystone-connection -func newKeystoneClient(authOpts gophercloud.AuthOptions) (*gophercloud.ServiceClient, error) { - provider, err := openstack.AuthenticatedClient(authOpts) +func newKeystoneClient(ctx context.Context, authOpts gophercloud.AuthOptions) (*gophercloud.ServiceClient, error) { + provider, err := openstack.AuthenticatedClient(ctx, authOpts) if err != nil { return nil, fmt.Errorf("cannot initialize OpenStack service user provider client: %w", err) } @@ -212,7 +214,7 @@ func (d *keystone) ServiceURL() string { // loadDomainsAndRoles builds an "index" for roles and domains // to avoid frequent calls to Keystone -func (d *keystone) loadDomainsAndRoles() { +func (d *keystone) loadDomainsAndRoles(ctx context.Context) { util.LogInfo("Loading/refreshing global list of domains and roles") allRoles := struct { @@ -223,7 +225,7 @@ func (d *keystone) loadDomainsAndRoles() { }{} u := d.providerClient.ServiceURL("roles") - resp, err := d.providerClient.Get(u, &allRoles, nil) + resp, err := d.providerClient.Get(ctx, u, &allRoles, nil) if err != nil { panic(err) } @@ -251,7 +253,7 @@ func (d *keystone) loadDomainsAndRoles() { d.domainNames = map[string]string{} d.domainIDs = map[string]string{} trueVal := true - err = projects.List(d.providerClient, projects.ListOpts{IsDomain: &trueVal, Enabled: &trueVal}).EachPage(func(page pagination.Page) (bool, error) { + err = projects.List(d.providerClient, projects.ListOpts{IsDomain: &trueVal, Enabled: &trueVal}).EachPage(ctx, func(ctx context.Context, page pagination.Page) (bool, error) { domains, err := projects.ExtractProjects(page) if err != nil { panic(err) @@ -308,8 +310,8 @@ func authOpts2StringKey(authOpts gophercloud.AuthOptions) string { // Authenticate authenticates a non-service user using available authOptionsFromRequest (username+password or token) // It returns the authorization context -func (d *keystone) Authenticate(authOpts gophercloud.AuthOptions) (*policy.Context, string, AuthenticationError) { - return d.authenticate(authOpts, false, false) +func (d *keystone) Authenticate(ctx context.Context, authOpts gophercloud.AuthOptions) (*policy.Context, string, AuthenticationError) { + return d.authenticate(ctx, authOpts, false, false) } // AuthenticateRequest attempts to Authenticate a user using the request header contents @@ -317,8 +319,8 @@ func (d *keystone) Authenticate(authOpts gophercloud.AuthOptions) (*policy.Conte // If no supported authOptionsFromRequest could be found, the context is nil // If the authOptionsFromRequest are invalid or the authentication provider has issues, an error is returned // When guessScope is set to true, the method will try to find a suitible project when the scope is not defined (basic auth. only) -func (d *keystone) AuthenticateRequest(r *http.Request, guessScope bool) (*policy.Context, AuthenticationError) { - authOpts, err := d.authOptionsFromRequest(r, guessScope) +func (d *keystone) AuthenticateRequest(ctx context.Context, r *http.Request, guessScope bool) (*policy.Context, AuthenticationError) { + authOpts, err := d.authOptionsFromRequest(ctx, r, guessScope) if err != nil { util.LogError(err.Error()) return nil, err @@ -327,40 +329,40 @@ func (d *keystone) AuthenticateRequest(r *http.Request, guessScope bool) (*polic // if the request does not have a keystone token, then a new token has to be requested on behalf of the client // this must not happen with the connection of the service otherwise wrong credentials will cause reauthentication // of the service user - context, _, err := d.authenticate(*authOpts, true, false) + policyContext, _, err := d.authenticate(ctx, *authOpts, true, false) if err != nil { return nil, err } // the resulting policy Context structure is copied into header fields // so that we do not have to add an extra parameter to every function. - r.Header.Set("X-User-Id", context.Auth["user_id"]) - r.Header.Set("X-User-Name", context.Auth["user_name"]) - r.Header.Set("X-User-Domain-Id", context.Auth["user_domain_id"]) - r.Header.Set("X-User-Domain-Name", context.Auth["user_domain_name"]) - r.Header.Set("X-Application-Credential-Id", context.Auth["application_credential_id"]) - r.Header.Set("X-Application-Credential-Name", context.Auth["application_credential_name"]) - r.Header.Set("X-Application-Credential-Secret", context.Auth["application_credential_secret"]) - - if context.Auth["project_id"] != "" { + r.Header.Set("X-User-Id", policyContext.Auth["user_id"]) + r.Header.Set("X-User-Name", policyContext.Auth["user_name"]) + r.Header.Set("X-User-Domain-Id", policyContext.Auth["user_domain_id"]) + r.Header.Set("X-User-Domain-Name", policyContext.Auth["user_domain_name"]) + r.Header.Set("X-Application-Credential-Id", policyContext.Auth["application_credential_id"]) + r.Header.Set("X-Application-Credential-Name", policyContext.Auth["application_credential_name"]) + r.Header.Set("X-Application-Credential-Secret", policyContext.Auth["application_credential_secret"]) + + if policyContext.Auth["project_id"] != "" { // user is scoped to project - r.Header.Set("X-Project-Id", context.Auth["project_id"]) - r.Header.Set("X-Project-Name", context.Auth["project_name"]) - r.Header.Set("X-Project-Domain-Id", context.Auth["project_domain_id"]) - r.Header.Set("X-Project-Domain-Name", context.Auth["project_domain_name"]) + r.Header.Set("X-Project-Id", policyContext.Auth["project_id"]) + r.Header.Set("X-Project-Name", policyContext.Auth["project_name"]) + r.Header.Set("X-Project-Domain-Id", policyContext.Auth["project_domain_id"]) + r.Header.Set("X-Project-Domain-Name", policyContext.Auth["project_domain_name"]) } else { // user is scoped to domain - r.Header.Set("X-Domain-Id", context.Auth["domain_id"]) - r.Header.Set("X-Domain-Name", context.Auth["domain_name"]) + r.Header.Set("X-Domain-Id", policyContext.Auth["domain_id"]) + r.Header.Set("X-Domain-Name", policyContext.Auth["domain_name"]) } // add each role as well (Add will queue up the items passed in) - for _, role := range context.Roles { + for _, role := range policyContext.Roles { r.Header.Add("X-Roles", role) } - r.Header.Set("X-Auth-Token", context.Auth["token"]) - r.Header.Set("X-Auth-Token-Expiry", context.Auth["token-expiry"]) + r.Header.Set("X-Auth-Token", policyContext.Auth["token"]) + r.Header.Set("X-Auth-Token-Expiry", policyContext.Auth["token-expiry"]) - return context, nil + return policyContext, nil } // authOptionsFromRequest retrieves authOptionsFromRequest from http request and puts them into an AuthOptions structure @@ -369,7 +371,7 @@ func (d *keystone) AuthenticateRequest(r *http.Request, guessScope bool) (*polic // user/project can either be a unique OpenStack ID or a qualified name with domain information, e.g. username"@"domain // When guessScope is set to true, the method will try to find a suitible project when the scope is not defined (basic auth. only) // Finally you can also specify the scope as URL query param -func (d *keystone) authOptionsFromRequest(r *http.Request, guessScope bool) (*gophercloud.AuthOptions, AuthenticationError) { +func (d *keystone) authOptionsFromRequest(ctx context.Context, r *http.Request, guessScope bool) (*gophercloud.AuthOptions, AuthenticationError) { ba := gophercloud.AuthOptions{ IdentityEndpoint: viper.GetString("keystone.auth_url"), AllowReauth: true, @@ -466,7 +468,7 @@ func (d *keystone) authOptionsFromRequest(r *http.Request, guessScope bool) (*go ba.Scope = &gophercloud.AuthScope{ProjectID: scopeParts[0]} case guessScope: // not defined: choose an arbitrary project where the user has access (needed for UX reasons) - if err := d.guessScope(&ba); err != nil { + if err := d.guessScope(ctx, &ba); err != nil { return nil, err } } @@ -492,17 +494,17 @@ func (d *keystone) authOptionsFromRequest(r *http.Request, guessScope bool) (*go return &ba, nil } -func (d *keystone) guessScope(ba *gophercloud.AuthOptions) AuthenticationError { +func (d *keystone) guessScope(ctx context.Context, ba *gophercloud.AuthOptions) AuthenticationError { // guess scope if it is missing userID := ba.UserID var err error if userID == "" { - userID, err = d.UserID(ba.Username, ba.DomainName) + userID, err = d.UserID(ctx, ba.Username, ba.DomainName) if err != nil { return NewAuthenticationError(StatusWrongCredentials, err.Error()) //nolint:govet } } - userprojects, err := d.UserProjects(userID) + userprojects, err := d.UserProjects(ctx, userID) if err != nil { return NewAuthenticationError(StatusNotAvailable, err.Error()) //nolint:govet } else if len(userprojects) == 0 { @@ -527,7 +529,7 @@ func (d *keystone) guessScope(ba *gophercloud.AuthOptions) AuthenticationError { // `rescope` will be set to `true` to indicate that the token passed needs to be used to create a new token // because the scope should be changed. // It returns the authorization context -func (d *keystone) authenticate(authOpts gophercloud.AuthOptions, asServiceUser, rescope bool) (*policy.Context, string, AuthenticationError) { +func (d *keystone) authenticate(ctx context.Context, authOpts gophercloud.AuthOptions, asServiceUser, rescope bool) (*policy.Context, string, AuthenticationError) { // check cache, but ignore the result if tokens are rescoped if entry, found := d.tokenCache.Get(authOpts2StringKey(authOpts)); found && (authOpts.Scope == nil || authOpts.Scope.ProjectID == entry.(*cacheEntry).context.Auth["project_id"]) { if authOpts.TokenID != "" { @@ -543,7 +545,7 @@ func (d *keystone) authenticate(authOpts gophercloud.AuthOptions, asServiceUser, if authOpts.TokenID != "" && asServiceUser && !rescope { // token passed, scope is empty since it is part of the token (no username password given) util.LogDebug("verify token") - response := tokens.Get(d.providerClient, authOpts.TokenID) + response := tokens.Get(ctx, d.providerClient, authOpts.TokenID) if response.Err != nil { // this includes 4xx responses, so after this point, we can be sure that the token is valid return nil, "", NewAuthenticationError(StatusWrongCredentials, response.Err.Error()) //nolint:govet @@ -555,7 +557,7 @@ func (d *keystone) authenticate(authOpts gophercloud.AuthOptions, asServiceUser, // detect rescoping if authOpts.Scope != nil && authOpts.Scope.ProjectID != tokenData.ProjectScope.ID { util.LogDebug("scope change detected") - return d.authenticate(authOpts, asServiceUser, true) + return d.authenticate(ctx, authOpts, asServiceUser, true) } tokenInfo, err := response.ExtractToken() if err != nil { @@ -576,7 +578,7 @@ func (d *keystone) authenticate(authOpts gophercloud.AuthOptions, asServiceUser, util.LogDebug("authenticate user %s%s with scope %+v.", authOpts.Username, authOpts.UserID, authOpts.Scope) // create new token from basic authentication credentials or token ID var tokenID string - client, err := openstack.AuthenticatedClient(authOpts) + client, err := openstack.AuthenticatedClient(ctx, authOpts) if client != nil { tokenID, err = client.GetAuthResult().ExtractTokenID() } @@ -604,7 +606,7 @@ func (d *keystone) authenticate(authOpts gophercloud.AuthOptions, asServiceUser, // recurse in order to obtain catalog entry; login in via token, to provide scope information var ce cacheEntry var authErr AuthenticationError - ce.context, ce.endpointURL, authErr = d.authenticate(gophercloud.AuthOptions{IdentityEndpoint: authOpts.IdentityEndpoint, TokenID: tokenID}, asServiceUser, false) + ce.context, ce.endpointURL, authErr = d.authenticate(ctx, gophercloud.AuthOptions{IdentityEndpoint: authOpts.IdentityEndpoint, TokenID: tokenID}, asServiceUser, false) if authErr == nil && authOpts.TokenID == "" { // cache basic / application credential authentication results in the same way as token validations util.LogDebug("Add cache entry for username %s%s for scope %+v", authOpts.UserID, authOpts.Username, authOpts.Scope) @@ -635,25 +637,25 @@ func (d *keystone) authenticate(authOpts gophercloud.AuthOptions, asServiceUser, } // authorization context - context := tokenData.ToContext() + policyContext := tokenData.ToContext() // update the cache ce := cacheEntry{ - context: &context, + context: &policyContext, endpointURL: endpointURL, } util.LogDebug("add token cache entry for token %s... for scope %+v", tokenData.Token[:1+len(tokenData.Token)/4], authOpts.Scope) d.tokenCache.Set(authOpts2StringKey(authOpts), &ce, cache.DefaultExpiration) - return &context, endpointURL, nil + return &policyContext, endpointURL, nil } -func (d *keystone) ChildProjects(projectID string) ([]string, error) { +func (d *keystone) ChildProjects(ctx context.Context, projectID string) ([]string, error) { if ce, ok := d.projectTreeCache.Get(projectID); ok { return ce.([]string), nil } - childprojects, err := d.fetchChildProjects(projectID) + childprojects, err := d.fetchChildProjects(ctx, projectID) if err != nil { util.LogError("Unable to obtain project tree of project %s: %s", projectID, err.Error) return nil, err @@ -666,11 +668,11 @@ func (d *keystone) ChildProjects(projectID string) ([]string, error) { // fetchChildProjects builds the full hierarchy of child-projects. This is used // e.g. to compute the right project_id filter expression in the PromQL queries // generated by Maia -func (d *keystone) fetchChildProjects(projectID string) ([]string, error) { +func (d *keystone) fetchChildProjects(ctx context.Context, projectID string) ([]string, error) { projectIDs := []string{} enabledVal := true // iterate of all pages returned by the list-projects API call - err := projects.List(d.providerClient, projects.ListOpts{ParentID: projectID, Enabled: &enabledVal}).EachPage(func(page pagination.Page) (bool, error) { + err := projects.List(d.providerClient, projects.ListOpts{ParentID: projectID, Enabled: &enabledVal}).EachPage(ctx, func(ctx context.Context, page pagination.Page) (bool, error) { slice, err := projects.ExtractProjects(page) if err != nil { return false, err @@ -678,7 +680,7 @@ func (d *keystone) fetchChildProjects(projectID string) ([]string, error) { for _, p := range slice { projectIDs = append(projectIDs, p.ID) // recurse - children, err := d.fetchChildProjects(p.ID) + children, err := d.fetchChildProjects(ctx, p.ID) if err != nil { return false, err } @@ -693,12 +695,12 @@ func (d *keystone) fetchChildProjects(projectID string) ([]string, error) { return projectIDs, nil } -func (d *keystone) UserProjects(userID string) ([]tokens.Scope, error) { +func (d *keystone) UserProjects(ctx context.Context, userID string) ([]tokens.Scope, error) { if up, ok := d.userProjectsCache.Get(userID); ok { return up.([]tokens.Scope), nil } - up, err := d.fetchUserProjects(userID) + up, err := d.fetchUserProjects(ctx, userID) if err != nil { util.LogError("Unable to obtain monitoring project list of user %s: %v", userID, err) return nil, err @@ -710,11 +712,11 @@ func (d *keystone) UserProjects(userID string) ([]tokens.Scope, error) { } // fetchUserProjects lists all projects (i.e. scopes) the user may access using Keystone (no cache lookup) -func (d *keystone) fetchUserProjects(userID string) ([]tokens.Scope, error) { +func (d *keystone) fetchUserProjects(ctx context.Context, userID string) ([]tokens.Scope, error) { scopes := []tokens.Scope{} effectiveVal := true // iterate of all pages returned by the list-role-assignments API call - err := roles.ListAssignments(d.providerClient, roles.ListAssignmentsOpts{UserID: userID, Effective: &effectiveVal}).EachPage(func(page pagination.Page) (bool, error) { + err := roles.ListAssignments(d.providerClient, roles.ListAssignmentsOpts{UserID: userID, Effective: &effectiveVal}).EachPage(ctx, func(ctx context.Context, page pagination.Page) (bool, error) { util.LogDebug("loading role assignment page") slice, err := roles.ExtractRoleAssignments(page) if err != nil { @@ -724,7 +726,7 @@ func (d *keystone) fetchUserProjects(userID string) ([]tokens.Scope, error) { if _, ok := d.monitoringRoles[ra.Role.ID]; ok && ra.Scope.Project.ID != "" { scope, ok := d.projectScopeCache.Get(ra.Scope.Project.ID) if !ok { - project, err := projects.Get(d.providerClient, ra.Scope.Project.ID).Extract() + project, err := projects.Get(ctx, d.providerClient, ra.Scope.Project.ID).Extract() if err != nil { return false, err } @@ -744,13 +746,13 @@ func (d *keystone) fetchUserProjects(userID string) ([]tokens.Scope, error) { return scopes, nil } -func (d *keystone) UserID(username, userDomain string) (string, error) { +func (d *keystone) UserID(ctx context.Context, username, userDomain string) (string, error) { key := username + "@" + userDomain if ce, ok := d.userIDCache.Get(key); ok { return ce.(string), nil } - id, err := d.fetchUserID(username, userDomain) + id, err := d.fetchUserID(ctx, username, userDomain) if err != nil { return "", err } @@ -761,11 +763,11 @@ func (d *keystone) UserID(username, userDomain string) (string, error) { } // fetchUserID determines the ID of a user of a given qualified name using Keystone (no cache lookup) -func (d *keystone) fetchUserID(username, userDomain string) (string, error) { +func (d *keystone) fetchUserID(ctx context.Context, username, userDomain string) (string, error) { userDomainID := d.domainIDs[userDomain] userID := "" enabled := true - err := users.List(d.providerClient, users.ListOpts{Name: username, DomainID: userDomainID, Enabled: &enabled}).EachPage(func(page pagination.Page) (bool, error) { + err := users.List(d.providerClient, users.ListOpts{Name: username, DomainID: userDomainID, Enabled: &enabled}).EachPage(ctx, func(ctx context.Context, page pagination.Page) (bool, error) { users, err := users.ExtractUsers(page) if err != nil { return false, err diff --git a/pkg/keystone/keystone_test.go b/pkg/keystone/keystone_test.go index b20eaecd..ec6eb2b7 100644 --- a/pkg/keystone/keystone_test.go +++ b/pkg/keystone/keystone_test.go @@ -15,6 +15,7 @@ package keystone import ( + "context" "net/http" "net/http/httptest" "testing" @@ -153,10 +154,12 @@ func TestChildProjects(t *testing.T) { ks := setupTest() + ctx := context.Background() + gock.New(baseURL).Get("/v3/projects").MatchParams(map[string]string{"enabled": "true", "parent_id": "p00001"}).HeaderPresent("X-Auth-Token").Reply(http.StatusOK).File("fixtures/child_projects.json").AddHeader("Content-Type", "application/json") gock.New(baseURL).Get("/v3/projects").MatchParams(map[string]string{"enabled": "true", "parent_id": "p00002"}).HeaderPresent("X-Auth-Token").Reply(http.StatusOK).BodyString("{ \"projects\": [] }").AddHeader("Content-Type", "application/json") - ids, err := ks.ChildProjects("p00001") + ids, err := ks.ChildProjects(ctx, "p00001") assert.Nil(t, err, "ChildProjects should not return error") assert.EqualValues(t, []string{"p00002"}, ids) @@ -169,15 +172,17 @@ func TestAuthenticateRequest(t *testing.T) { //nolint:dupl // the tests being ve ks := setupTest() + ctx := context.Background() + gock.New(baseURL).Post("/v3/auth/tokens").JSON(userAuthBody).Reply(http.StatusCreated).File("fixtures/user_token_create.json").AddHeader("X-Subject-Token", userToken).AddHeader("Content-Type", "application/json") gock.New(baseURL).Get("/v3/auth/tokens").Reply(http.StatusOK).File("fixtures/user_token_validate.json").AddHeader("X-Subject-Token", userToken).AddHeader("Content-Type", "application/json") req := httptest.NewRequest(http.MethodGet, "http://maia.local/federate", http.NoBody) req.SetBasicAuth("testuser@testdomain|testproject@testdomain", "testpw") - context, err := ks.AuthenticateRequest(req, false) + policyContext, err := ks.AuthenticateRequest(ctx, req, false) assert.Nil(t, err, "AuthenticateRequest should not fail") - assert.EqualValues(t, []string{"monitoring_viewer"}, context.Roles, "AuthenticateRequest should return the right roles in the context") + assert.EqualValues(t, []string{"monitoring_viewer"}, policyContext.Roles, "AuthenticateRequest should return the right roles in the context") assertDone(t) } @@ -186,16 +191,17 @@ func TestAuthenticateRequest_urlScope(t *testing.T) { //nolint:dupl // the tests defer gock.Off() ks := setupTest() + ctx := context.Background() gock.New(baseURL).Post("/v3/auth/tokens").JSON(userAuthScopeBody).Reply(http.StatusCreated).File("fixtures/user_token_create.json").AddHeader("X-Subject-Token", userToken).AddHeader("Content-Type", "application/json") gock.New(baseURL).Get("/v3/auth/tokens").Reply(http.StatusOK).File("fixtures/user_token_validate.json").AddHeader("X-Subject-Token", userToken).AddHeader("Content-Type", "application/json") req := httptest.NewRequest(http.MethodGet, "http://maia.local/testdomain/graph?project_id=p00001", http.NoBody) req.SetBasicAuth("testuser@testdomain", "testpw") - context, err := ks.AuthenticateRequest(req, false) + policyContext, err := ks.AuthenticateRequest(ctx, req, false) assert.Nil(t, err, "AuthenticateRequest should not fail") - assert.EqualValues(t, []string{"monitoring_viewer"}, context.Roles, "AuthenticateRequest should return the right roles in the context") + assert.EqualValues(t, []string{"monitoring_viewer"}, policyContext.Roles, "AuthenticateRequest should return the right roles in the context") assertDone(t) } @@ -204,15 +210,16 @@ func TestAuthenticateRequest_token(t *testing.T) { defer gock.Off() ks := setupTest() + ctx := context.Background() gock.New(baseURL).Get("/v3/auth/tokens").Reply(http.StatusOK).File("fixtures/user_token_validate.json").AddHeader("X-Subject-Token", userToken).AddHeader("Content-Type", "application/json") req := httptest.NewRequest(http.MethodGet, "http://maia.local/federate", http.NoBody) req.Header.Set("X-Auth-Token", userToken) - context, err := ks.AuthenticateRequest(req, false) + policyContext, err := ks.AuthenticateRequest(ctx, req, false) assert.Nil(t, err, "AuthenticateRequest should not fail") - assert.EqualValues(t, []string{"monitoring_viewer"}, context.Roles, "AuthenticateRequest should return the right roles in the context") + assert.EqualValues(t, []string{"monitoring_viewer"}, policyContext.Roles, "AuthenticateRequest should return the right roles in the context") assertDone(t) } @@ -221,12 +228,13 @@ func TestAuthenticateRequest_failed(t *testing.T) { defer gock.Off() ks := setupTest() + ctx := context.Background() gock.New(baseURL).Post("/v3/auth/tokens").Reply(http.StatusForbidden) req := httptest.NewRequest(http.MethodGet, "http://maia.local/federate", http.NoBody) req.SetBasicAuth("testuser@testdomain|testproject@testdomain", "testpw") - _, err := ks.AuthenticateRequest(req, false) + _, err := ks.AuthenticateRequest(ctx, req, false) assert.NotNil(t, err, "AuthenticateRequest should fail with error when Keystone responds with 4xx") @@ -237,10 +245,11 @@ func TestAuthenticateRequest_failedNoScope(t *testing.T) { defer gock.Off() ks := setupTest() + ctx := context.Background() req := httptest.NewRequest(http.MethodGet, "http://maia.local/federate", http.NoBody) req.SetBasicAuth("testuser@testdomain", "testpw") - _, err := ks.AuthenticateRequest(req, false) + _, err := ks.AuthenticateRequest(ctx, req, false) assert.NotNil(t, err, "AuthenticateRequest should fail with error when scope information is missing for /federate") @@ -251,6 +260,7 @@ func TestAuthenticateRequest_guessScope(t *testing.T) { defer gock.Off() ks := setupTest() + ctx := context.Background() gock.New(baseURL).Get("/v3/users").MatchParams(map[string]string{"domain_id": "d00001", "enabled": "true", "name": "testuser"}).HeaderPresent("X-Auth-Token").Reply(http.StatusOK).File("fixtures/testuser.json").AddHeader("Content-Type", "application/json") gock.New(baseURL).Get("/v3/role_assignments").MatchParams(map[string]string{"effective": "true", "user.id": "u00001"}).HeaderPresent("X-Auth-Token").Reply(http.StatusOK).File("fixtures/testuser_roles.json").AddHeader("Content-Type", "application/json") @@ -260,10 +270,10 @@ func TestAuthenticateRequest_guessScope(t *testing.T) { req := httptest.NewRequest(http.MethodGet, "http://maia.local/federate", http.NoBody) req.SetBasicAuth("testuser@testdomain", "testpw") - context, err := ks.AuthenticateRequest(req, true) + policyContext, err := ks.AuthenticateRequest(ctx, req, true) assert.Nil(t, err, "AuthenticateRequest should not fail") - assert.EqualValues(t, []string{"monitoring_viewer"}, context.Roles, "AuthenticateRequest should return the right roles in the context") + assert.EqualValues(t, []string{"monitoring_viewer"}, policyContext.Roles, "AuthenticateRequest should return the right roles in the context") assertDone(t) } diff --git a/pkg/test/matchers.go b/pkg/test/matchers.go new file mode 100644 index 00000000..4a3b8aff --- /dev/null +++ b/pkg/test/matchers.go @@ -0,0 +1,38 @@ +// Copyright 2024 SAP SE +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package test + +import ( + "context" + + "go.uber.org/mock/gomock" +) + +// ContextMatcher is a custom matcher for contexts +type ContextMatcher struct{} + +func (m ContextMatcher) Matches(x interface{}) bool { + _, ok := x.(context.Context) + return ok +} + +func (m ContextMatcher) String() string { + return "is a context.Context" +} + +// MatchContext returns a matcher for any context.Context +func MatchContext() gomock.Matcher { + return ContextMatcher{} +} diff --git a/pkg/ui/templates.go b/pkg/ui/templates.go index b0f9befe..5652f682 100644 --- a/pkg/ui/templates.go +++ b/pkg/ui/templates.go @@ -63,7 +63,8 @@ func ExecuteTemplate(w http.ResponseWriter, req *http.Request, name string, keys // return []string{} // }, "childProjects": func() []string { - children, err := keystoneDriver.ChildProjects(req.Header.Get("X-Project-Id")) + ctx := req.Context() + children, err := keystoneDriver.ChildProjects(ctx, req.Header.Get("X-Project-Id")) if err != nil { return []string{} } @@ -71,8 +72,9 @@ func ExecuteTemplate(w http.ResponseWriter, req *http.Request, name string, keys }, // return list of user's projects with monitoring role: name --> id "userProjects": func() map[string]string { + ctx := req.Context() result := map[string]string{} - projects, err := keystoneDriver.UserProjects(req.Header.Get("X-User-Id")) + projects, err := keystoneDriver.UserProjects(ctx, req.Header.Get("X-User-Id")) if err == nil { for _, p := range projects { result[p.ProjectName] = p.ProjectID