// Copyright (C) The Arvados Authors. All rights reserved. // // SPDX-License-Identifier: AGPL-3.0 package federation import ( "context" "errors" "fmt" "net/url" "git.arvados.org/arvados.git/lib/ctrlctx" "git.arvados.org/arvados.git/sdk/go/arvados" "git.arvados.org/arvados.git/sdk/go/arvadostest" "git.arvados.org/arvados.git/sdk/go/auth" check "gopkg.in/check.v1" ) var _ = check.Suite(&LogoutSuite{}) var emptyURL = &url.URL{} type LogoutStub struct { arvadostest.APIStub redirectLocation *url.URL } func (as *LogoutStub) CheckCalls(c *check.C, returnURL *url.URL) bool { actual := as.APIStub.Calls(as.APIStub.Logout) allOK := c.Check(actual, check.Not(check.HasLen), 0, check.Commentf("Logout stub never called")) expected := returnURL.String() for _, call := range actual { opts, ok := call.Options.(arvados.LogoutOptions) allOK = c.Check(ok, check.Equals, true, check.Commentf("call options were not LogoutOptions")) && c.Check(opts.ReturnTo, check.Equals, expected) && allOK } return allOK } func (as *LogoutStub) Logout(ctx context.Context, options arvados.LogoutOptions) (arvados.LogoutResponse, error) { as.APIStub.Logout(ctx, options) loc := as.redirectLocation.String() if loc == "" { loc = options.ReturnTo } return arvados.LogoutResponse{ RedirectLocation: loc, }, as.Error } type LogoutSuite struct { FederationSuite } func (s *LogoutSuite) badReturnURL(path string) *url.URL { return &url.URL{ Scheme: "https", Host: "example.net", Path: path, } } func (s *LogoutSuite) goodReturnURL(path string) *url.URL { u, _ := url.Parse(s.cluster.Services.Workbench2.ExternalURL.String()) u.Path = path return u } func (s *LogoutSuite) setupFederation(loginCluster string) { if loginCluster == "" { s.cluster.Login.Test.Enable = true } else { s.cluster.Login.LoginCluster = loginCluster } dbconn := ctrlctx.DBConnector{PostgreSQL: s.cluster.PostgreSQL} s.fed = New(s.ctx, s.cluster, nil, dbconn.GetDB) } func (s *LogoutSuite) setupStub(c *check.C, id string, stubURL *url.URL, stubErr error) *LogoutStub { loc, err := url.Parse(stubURL.String()) c.Check(err, check.IsNil) stub := LogoutStub{redirectLocation: loc} stub.Error = stubErr if id == s.cluster.ClusterID { s.fed.local = &stub } else { s.addDirectRemote(c, id, &stub) } return &stub } func (s *LogoutSuite) v2Token(clusterID string) string { return fmt.Sprintf("v2/%s-gj3su-12345abcde67890/abcdefghijklmnopqrstuvwxy", clusterID) } func (s *LogoutSuite) TestLocalLogoutOK(c *check.C) { s.setupFederation("") resp, err := s.fed.Logout(s.ctx, arvados.LogoutOptions{}) c.Check(err, check.IsNil) c.Check(resp.RedirectLocation, check.Equals, s.cluster.Services.Workbench2.ExternalURL.String()) } func (s *LogoutSuite) TestLocalLogoutRedirect(c *check.C) { s.setupFederation("") expURL := s.cluster.Services.Workbench1.ExternalURL opts := arvados.LogoutOptions{ReturnTo: expURL.String()} resp, err := s.fed.Logout(s.ctx, opts) c.Check(err, check.IsNil) c.Check(resp.RedirectLocation, check.Equals, expURL.String()) } func (s *LogoutSuite) TestLocalLogoutBadRequestError(c *check.C) { s.setupFederation("") returnTo := s.badReturnURL("TestLocalLogoutBadRequestError") opts := arvados.LogoutOptions{ReturnTo: returnTo.String()} _, err := s.fed.Logout(s.ctx, opts) c.Check(err, check.NotNil) } func (s *LogoutSuite) TestRemoteLogoutRedirect(c *check.C) { s.setupFederation("zhome") redirect := url.URL{Scheme: "https", Host: "example.com"} loginStub := s.setupStub(c, "zhome", &redirect, nil) returnTo := s.goodReturnURL("TestRemoteLogoutRedirect") resp, err := s.fed.Logout(s.ctx, arvados.LogoutOptions{ReturnTo: returnTo.String()}) c.Check(err, check.IsNil) c.Check(resp.RedirectLocation, check.Equals, redirect.String()) loginStub.CheckCalls(c, returnTo) } func (s *LogoutSuite) TestRemoteLogoutError(c *check.C) { s.setupFederation("zhome") expErr := errors.New("TestRemoteLogoutError expErr") loginStub := s.setupStub(c, "zhome", emptyURL, expErr) returnTo := s.goodReturnURL("TestRemoteLogoutError") _, err := s.fed.Logout(s.ctx, arvados.LogoutOptions{ReturnTo: returnTo.String()}) c.Check(err, check.Equals, expErr) loginStub.CheckCalls(c, returnTo) } func (s *LogoutSuite) TestRemoteLogoutLocalRedirect(c *check.C) { s.setupFederation("zhome") loginStub := s.setupStub(c, "zhome", emptyURL, nil) redirect := url.URL{Scheme: "https", Host: "example.com"} localStub := s.setupStub(c, "aaaaa", &redirect, nil) resp, err := s.fed.Logout(s.ctx, arvados.LogoutOptions{}) c.Check(err, check.IsNil) c.Check(resp.RedirectLocation, check.Equals, redirect.String()) // emptyURL to match the empty LogoutOptions loginStub.CheckCalls(c, emptyURL) localStub.CheckCalls(c, emptyURL) } func (s *LogoutSuite) TestRemoteLogoutLocalError(c *check.C) { s.setupFederation("zhome") expErr := errors.New("TestRemoteLogoutLocalError expErr") loginStub := s.setupStub(c, "zhome", emptyURL, nil) localStub := s.setupStub(c, "aaaaa", emptyURL, expErr) _, err := s.fed.Logout(s.ctx, arvados.LogoutOptions{}) c.Check(err, check.Equals, expErr) loginStub.CheckCalls(c, emptyURL) localStub.CheckCalls(c, emptyURL) } func (s *LogoutSuite) TestV2TokenRedirect(c *check.C) { s.setupFederation("") redirect := url.URL{Scheme: "https", Host: "example.com"} returnTo := s.goodReturnURL("TestV2TokenRedirect") localErr := errors.New("TestV2TokenRedirect error") tokenStub := s.setupStub(c, "zzzzz", &redirect, nil) s.setupStub(c, "aaaaa", emptyURL, localErr) tokens := []string{s.v2Token("zzzzz")} ctx := auth.NewContext(s.ctx, &auth.Credentials{Tokens: tokens}) resp, err := s.fed.Logout(ctx, arvados.LogoutOptions{ReturnTo: returnTo.String()}) c.Check(err, check.IsNil) c.Check(resp.RedirectLocation, check.Equals, redirect.String()) tokenStub.CheckCalls(c, returnTo) } func (s *LogoutSuite) TestV2TokenError(c *check.C) { s.setupFederation("") returnTo := s.goodReturnURL("TestV2TokenError") tokenErr := errors.New("TestV2TokenError error") tokenStub := s.setupStub(c, "zzzzz", emptyURL, tokenErr) s.setupStub(c, "aaaaa", emptyURL, nil) tokens := []string{s.v2Token("zzzzz")} ctx := auth.NewContext(s.ctx, &auth.Credentials{Tokens: tokens}) _, err := s.fed.Logout(ctx, arvados.LogoutOptions{ReturnTo: returnTo.String()}) c.Check(err, check.Equals, tokenErr) tokenStub.CheckCalls(c, returnTo) } func (s *LogoutSuite) TestV2TokenLocalRedirect(c *check.C) { s.setupFederation("") redirect := url.URL{Scheme: "https", Host: "example.com"} tokenStub := s.setupStub(c, "zzzzz", emptyURL, nil) localStub := s.setupStub(c, "aaaaa", &redirect, nil) tokens := []string{s.v2Token("zzzzz")} ctx := auth.NewContext(s.ctx, &auth.Credentials{Tokens: tokens}) resp, err := s.fed.Logout(ctx, arvados.LogoutOptions{}) c.Check(err, check.IsNil) c.Check(resp.RedirectLocation, check.Equals, redirect.String()) tokenStub.CheckCalls(c, emptyURL) localStub.CheckCalls(c, emptyURL) } func (s *LogoutSuite) TestV2TokenLocalError(c *check.C) { s.setupFederation("") tokenErr := errors.New("TestV2TokenLocalError error") tokenStub := s.setupStub(c, "zzzzz", emptyURL, nil) localStub := s.setupStub(c, "aaaaa", emptyURL, tokenErr) tokens := []string{s.v2Token("zzzzz")} ctx := auth.NewContext(s.ctx, &auth.Credentials{Tokens: tokens}) _, err := s.fed.Logout(ctx, arvados.LogoutOptions{}) c.Check(err, check.Equals, tokenErr) tokenStub.CheckCalls(c, emptyURL) localStub.CheckCalls(c, emptyURL) } func (s *LogoutSuite) TestV2LocalTokenRedirect(c *check.C) { s.setupFederation("") redirect := url.URL{Scheme: "https", Host: "example.com"} returnTo := s.goodReturnURL("TestV2LocalTokenRedirect") localStub := s.setupStub(c, "aaaaa", &redirect, nil) tokens := []string{s.v2Token("aaaaa")} ctx := auth.NewContext(s.ctx, &auth.Credentials{Tokens: tokens}) resp, err := s.fed.Logout(ctx, arvados.LogoutOptions{ReturnTo: returnTo.String()}) c.Check(err, check.IsNil) c.Check(resp.RedirectLocation, check.Equals, redirect.String()) localStub.CheckCalls(c, returnTo) } func (s *LogoutSuite) TestV2LocalTokenError(c *check.C) { s.setupFederation("") returnTo := s.goodReturnURL("TestV2LocalTokenError") tokenErr := errors.New("TestV2LocalTokenError error") localStub := s.setupStub(c, "aaaaa", emptyURL, tokenErr) tokens := []string{s.v2Token("aaaaa")} ctx := auth.NewContext(s.ctx, &auth.Credentials{Tokens: tokens}) _, err := s.fed.Logout(ctx, arvados.LogoutOptions{ReturnTo: returnTo.String()}) c.Check(err, check.Equals, tokenErr) localStub.CheckCalls(c, returnTo) }