mirror of https://github.com/docker/compose.git
Add unit tests for login process
This commit is contained in:
parent
146dd3e639
commit
7edc6659a2
|
@ -9,7 +9,6 @@ import (
|
||||||
"math/rand"
|
"math/rand"
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/http/httputil"
|
|
||||||
"net/url"
|
"net/url"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
@ -37,6 +36,7 @@ func init() {
|
||||||
const (
|
const (
|
||||||
authorizeFormat = "https://login.microsoftonline.com/organizations/oauth2/v2.0/authorize?response_type=code&client_id=%s&redirect_uri=%s&state=%s&prompt=select_account&response_mode=query&scope=%s"
|
authorizeFormat = "https://login.microsoftonline.com/organizations/oauth2/v2.0/authorize?response_type=code&client_id=%s&redirect_uri=%s&state=%s&prompt=select_account&response_mode=query&scope=%s"
|
||||||
tokenEndpoint = "https://login.microsoftonline.com/%s/oauth2/v2.0/token"
|
tokenEndpoint = "https://login.microsoftonline.com/%s/oauth2/v2.0/token"
|
||||||
|
authorizationURL = "https://management.azure.com/tenants?api-version=2019-11-01"
|
||||||
// scopes for a multi-tenant app works for openid, email, other common scopes, but fails when trying to add a token
|
// scopes for a multi-tenant app works for openid, email, other common scopes, but fails when trying to add a token
|
||||||
// v1 scope like "https://management.azure.com/.default" for ARM access
|
// v1 scope like "https://management.azure.com/.default" for ARM access
|
||||||
scopes = "offline_access https://management.azure.com/.default"
|
scopes = "offline_access https://management.azure.com/.default"
|
||||||
|
@ -93,6 +93,8 @@ func newAzureLoginServiceFromPath(tokenStorePath string, helper apiHelper) (Azur
|
||||||
|
|
||||||
type apiHelper interface {
|
type apiHelper interface {
|
||||||
queryToken(data url.Values, tenantID string) (azureToken, error)
|
queryToken(data url.Values, tenantID string) (azureToken, error)
|
||||||
|
openAzureLoginPage(redirectURL string)
|
||||||
|
queryAuthorizationAPI(authorizationURL string, authorizationHeader string) ([]byte, int, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
type azureAPIHelper struct{}
|
type azureAPIHelper struct{}
|
||||||
|
@ -106,7 +108,7 @@ func (login AzureLoginService) Login(ctx context.Context) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
redirectURL := "http://localhost:" + strconv.Itoa(serverPort)
|
redirectURL := "http://localhost:" + strconv.Itoa(serverPort)
|
||||||
openAzureLoginPage(redirectURL)
|
login.apiHelper.openAzureLoginPage(redirectURL)
|
||||||
|
|
||||||
select {
|
select {
|
||||||
case <-ctx.Done():
|
case <-ctx.Done():
|
||||||
|
@ -132,23 +134,12 @@ func (login AzureLoginService) Login(ctx context.Context) error {
|
||||||
return errors.Wrap(err, "Access token request failed")
|
return errors.Wrap(err, "Access token request failed")
|
||||||
}
|
}
|
||||||
|
|
||||||
req, err := http.NewRequest(http.MethodGet, "https://management.azure.com/tenants?api-version=2019-11-01", nil)
|
bits, statusCode, err := login.apiHelper.queryAuthorizationAPI(authorizationURL, fmt.Sprintf("Bearer %s", token.AccessToken))
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", token.AccessToken))
|
|
||||||
res, err := http.DefaultClient.Do(req)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.Wrap(err, "login failed")
|
return errors.Wrap(err, "login failed")
|
||||||
}
|
}
|
||||||
|
|
||||||
bits, err := ioutil.ReadAll(res.Body)
|
if statusCode == 200 {
|
||||||
if err != nil {
|
|
||||||
return errors.Wrap(err, "login failed")
|
|
||||||
}
|
|
||||||
|
|
||||||
if res.StatusCode == 200 {
|
|
||||||
var tenantResult tenantResult
|
var tenantResult tenantResult
|
||||||
if err := json.Unmarshal(bits, &tenantResult); err != nil {
|
if err := json.Unmarshal(bits, &tenantResult); err != nil {
|
||||||
return errors.Wrap(err, "login failed")
|
return errors.Wrap(err, "login failed")
|
||||||
|
@ -170,12 +161,7 @@ func (login AzureLoginService) Login(ctx context.Context) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
bits, err = httputil.DumpResponse(res, true)
|
return fmt.Errorf("login failed : " + string(bits))
|
||||||
if err != nil {
|
|
||||||
return errors.Wrap(err, "login failed")
|
|
||||||
}
|
|
||||||
|
|
||||||
return fmt.Errorf("login failed: \n" + string(bits))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -199,12 +185,29 @@ func startLoginServer(queryCh chan url.Values) (int, error) {
|
||||||
return availablePort, nil
|
return availablePort, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func openAzureLoginPage(redirectURL string) {
|
func (helper azureAPIHelper) openAzureLoginPage(redirectURL string) {
|
||||||
state := randomString("", 10)
|
state := randomString("", 10)
|
||||||
authURL := fmt.Sprintf(authorizeFormat, clientID, redirectURL, state, scopes)
|
authURL := fmt.Sprintf(authorizeFormat, clientID, redirectURL, state, scopes)
|
||||||
openbrowser(authURL)
|
openbrowser(authURL)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (helper azureAPIHelper) queryAuthorizationAPI(authorizationURL string, authorizationHeader string) ([]byte, int, error) {
|
||||||
|
req, err := http.NewRequest(http.MethodGet, authorizationURL, nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, 0, err
|
||||||
|
}
|
||||||
|
req.Header.Add("Authorization", authorizationHeader)
|
||||||
|
res, err := http.DefaultClient.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return nil, 0, err
|
||||||
|
}
|
||||||
|
bits, err := ioutil.ReadAll(res.Body)
|
||||||
|
if err != nil {
|
||||||
|
return nil, 0, err
|
||||||
|
}
|
||||||
|
return bits, res.StatusCode, nil
|
||||||
|
}
|
||||||
|
|
||||||
func queryHandler(queryCh chan url.Values) func(w http.ResponseWriter, r *http.Request) {
|
func queryHandler(queryCh chan url.Values) func(w http.ResponseWriter, r *http.Request) {
|
||||||
queryHandler := func(w http.ResponseWriter, r *http.Request) {
|
queryHandler := func(w http.ResponseWriter, r *http.Request) {
|
||||||
_, hasCode := r.URL.Query()["code"]
|
_, hasCode := r.URL.Query()["code"]
|
||||||
|
|
|
@ -1,10 +1,14 @@
|
||||||
package login
|
package login
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"reflect"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
@ -19,7 +23,7 @@ import (
|
||||||
type LoginSuiteTest struct {
|
type LoginSuiteTest struct {
|
||||||
suite.Suite
|
suite.Suite
|
||||||
dir string
|
dir string
|
||||||
mockHelper MockAzureHelper
|
mockHelper *MockAzureHelper
|
||||||
azureLogin AzureLoginService
|
azureLogin AzureLoginService
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -28,8 +32,7 @@ func (suite *LoginSuiteTest) BeforeTest(suiteName, testName string) {
|
||||||
Expect(err).To(BeNil())
|
Expect(err).To(BeNil())
|
||||||
|
|
||||||
suite.dir = dir
|
suite.dir = dir
|
||||||
suite.mockHelper = MockAzureHelper{}
|
suite.mockHelper = &MockAzureHelper{}
|
||||||
//nolint copylocks
|
|
||||||
suite.azureLogin, err = newAzureLoginServiceFromPath(filepath.Join(dir, tokenStoreFilename), suite.mockHelper)
|
suite.azureLogin, err = newAzureLoginServiceFromPath(filepath.Join(dir, tokenStoreFilename), suite.mockHelper)
|
||||||
Expect(err).To(BeNil())
|
Expect(err).To(BeNil())
|
||||||
}
|
}
|
||||||
|
@ -40,12 +43,7 @@ func (suite *LoginSuiteTest) AfterTest(suiteName, testName string) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (suite *LoginSuiteTest) TestRefreshInValidToken() {
|
func (suite *LoginSuiteTest) TestRefreshInValidToken() {
|
||||||
data := url.Values{
|
data := refreshTokenData("refreshToken")
|
||||||
"grant_type": []string{"refresh_token"},
|
|
||||||
"client_id": []string{clientID},
|
|
||||||
"scope": []string{scopes},
|
|
||||||
"refresh_token": []string{"refreshToken"},
|
|
||||||
}
|
|
||||||
suite.mockHelper.On("queryToken", data, "123456").Return(azureToken{
|
suite.mockHelper.On("queryToken", data, "123456").Return(azureToken{
|
||||||
RefreshToken: "newRefreshToken",
|
RefreshToken: "newRefreshToken",
|
||||||
AccessToken: "newAccessToken",
|
AccessToken: "newAccessToken",
|
||||||
|
@ -98,6 +96,126 @@ func (suite *LoginSuiteTest) TestDoesNotRefreshValidToken() {
|
||||||
Expect(token.AccessToken).To(Equal("accessToken"))
|
Expect(token.AccessToken).To(Equal("accessToken"))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (suite *LoginSuiteTest) TestInvalidLogin() {
|
||||||
|
suite.mockHelper.On("openAzureLoginPage", mock.AnythingOfType("string")).Run(func(args mock.Arguments) {
|
||||||
|
redirectURL := args.Get(0).(string)
|
||||||
|
err := queryKeyValue(redirectURL, "error", "access denied")
|
||||||
|
Expect(err).To(BeNil())
|
||||||
|
})
|
||||||
|
|
||||||
|
//nolint copylocks
|
||||||
|
azureLogin, err := newAzureLoginServiceFromPath(filepath.Join(suite.dir, tokenStoreFilename), suite.mockHelper)
|
||||||
|
Expect(err).To(BeNil())
|
||||||
|
|
||||||
|
err = azureLogin.Login(context.TODO())
|
||||||
|
Expect(err).To(MatchError(errors.New("login failed : [access denied]")))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *LoginSuiteTest) TestValidLogin() {
|
||||||
|
var redirectURL string
|
||||||
|
suite.mockHelper.On("openAzureLoginPage", mock.AnythingOfType("string")).Run(func(args mock.Arguments) {
|
||||||
|
redirectURL = args.Get(0).(string)
|
||||||
|
err := queryKeyValue(redirectURL, "code", "123456879")
|
||||||
|
Expect(err).To(BeNil())
|
||||||
|
})
|
||||||
|
|
||||||
|
suite.mockHelper.On("queryToken", mock.MatchedBy(func(data url.Values) bool {
|
||||||
|
//Need a matcher here because the value of redirectUrl is not known until executing openAzureLoginPage
|
||||||
|
return reflect.DeepEqual(data, url.Values{
|
||||||
|
"grant_type": []string{"authorization_code"},
|
||||||
|
"client_id": []string{clientID},
|
||||||
|
"code": []string{"123456879"},
|
||||||
|
"scope": []string{scopes},
|
||||||
|
"redirect_uri": []string{redirectURL},
|
||||||
|
})
|
||||||
|
}), "organizations").Return(azureToken{
|
||||||
|
RefreshToken: "firstRefreshToken",
|
||||||
|
AccessToken: "firstAccessToken",
|
||||||
|
ExpiresIn: 3600,
|
||||||
|
Foci: "1",
|
||||||
|
}, nil)
|
||||||
|
|
||||||
|
authBody := `{"value":[{"id":"/tenants/12345a7c-c56d-43e8-9549-dd230ce8a038","tenantId":"12345a7c-c56d-43e8-9549-dd230ce8a038"}]}`
|
||||||
|
|
||||||
|
suite.mockHelper.On("queryAuthorizationAPI", authorizationURL, "Bearer firstAccessToken").Return([]byte(authBody), 200, nil)
|
||||||
|
data := refreshTokenData("firstRefreshToken")
|
||||||
|
suite.mockHelper.On("queryToken", data, "12345a7c-c56d-43e8-9549-dd230ce8a038").Return(azureToken{
|
||||||
|
RefreshToken: "newRefreshToken",
|
||||||
|
AccessToken: "newAccessToken",
|
||||||
|
ExpiresIn: 3600,
|
||||||
|
Foci: "1",
|
||||||
|
}, nil)
|
||||||
|
//nolint copylocks
|
||||||
|
azureLogin, err := newAzureLoginServiceFromPath(filepath.Join(suite.dir, tokenStoreFilename), suite.mockHelper)
|
||||||
|
Expect(err).To(BeNil())
|
||||||
|
|
||||||
|
err = azureLogin.Login(context.TODO())
|
||||||
|
Expect(err).To(BeNil())
|
||||||
|
|
||||||
|
loginToken, err := suite.azureLogin.tokenStore.readToken()
|
||||||
|
Expect(err).To(BeNil())
|
||||||
|
Expect(loginToken.Token.AccessToken).To(Equal("newAccessToken"))
|
||||||
|
Expect(loginToken.Token.RefreshToken).To(Equal("newRefreshToken"))
|
||||||
|
Expect(loginToken.Token.Expiry).To(BeTemporally(">", time.Now().Add(3500*time.Second)))
|
||||||
|
Expect(loginToken.TenantID).To(Equal("12345a7c-c56d-43e8-9549-dd230ce8a038"))
|
||||||
|
Expect(loginToken.Token.Type()).To(Equal("Bearer"))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *LoginSuiteTest) TestLoginAuthorizationFailed() {
|
||||||
|
var redirectURL string
|
||||||
|
suite.mockHelper.On("openAzureLoginPage", mock.AnythingOfType("string")).Run(func(args mock.Arguments) {
|
||||||
|
redirectURL = args.Get(0).(string)
|
||||||
|
err := queryKeyValue(redirectURL, "code", "123456879")
|
||||||
|
Expect(err).To(BeNil())
|
||||||
|
})
|
||||||
|
|
||||||
|
suite.mockHelper.On("queryToken", mock.MatchedBy(func(data url.Values) bool {
|
||||||
|
//Need a matcher here because the value of redirectUrl is not known until executing openAzureLoginPage
|
||||||
|
return reflect.DeepEqual(data, url.Values{
|
||||||
|
"grant_type": []string{"authorization_code"},
|
||||||
|
"client_id": []string{clientID},
|
||||||
|
"code": []string{"123456879"},
|
||||||
|
"scope": []string{scopes},
|
||||||
|
"redirect_uri": []string{redirectURL},
|
||||||
|
})
|
||||||
|
}), "organizations").Return(azureToken{
|
||||||
|
RefreshToken: "firstRefreshToken",
|
||||||
|
AccessToken: "firstAccessToken",
|
||||||
|
ExpiresIn: 3600,
|
||||||
|
Foci: "1",
|
||||||
|
}, nil)
|
||||||
|
|
||||||
|
authBody := `[access denied]`
|
||||||
|
|
||||||
|
suite.mockHelper.On("queryAuthorizationAPI", authorizationURL, "Bearer firstAccessToken").Return([]byte(authBody), 400, nil)
|
||||||
|
|
||||||
|
azureLogin, err := newAzureLoginServiceFromPath(filepath.Join(suite.dir, tokenStoreFilename), suite.mockHelper)
|
||||||
|
Expect(err).To(BeNil())
|
||||||
|
|
||||||
|
err = azureLogin.Login(context.TODO())
|
||||||
|
Expect(err).To(MatchError(errors.New("login failed : [access denied]")))
|
||||||
|
}
|
||||||
|
|
||||||
|
func refreshTokenData(refreshToken string) url.Values {
|
||||||
|
return url.Values{
|
||||||
|
"grant_type": []string{"refresh_token"},
|
||||||
|
"client_id": []string{clientID},
|
||||||
|
"scope": []string{scopes},
|
||||||
|
"refresh_token": []string{refreshToken},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func queryKeyValue(redirectURL string, key string, value string) error {
|
||||||
|
req, err := http.NewRequest("GET", redirectURL, nil)
|
||||||
|
Expect(err).To(BeNil())
|
||||||
|
q := req.URL.Query()
|
||||||
|
q.Add(key, value)
|
||||||
|
req.URL.RawQuery = q.Encode()
|
||||||
|
client := &http.Client{}
|
||||||
|
_, err = client.Do(req)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
func TestLoginSuite(t *testing.T) {
|
func TestLoginSuite(t *testing.T) {
|
||||||
RegisterTestingT(t)
|
RegisterTestingT(t)
|
||||||
suite.Run(t, new(LoginSuiteTest))
|
suite.Run(t, new(LoginSuiteTest))
|
||||||
|
@ -107,8 +225,16 @@ type MockAzureHelper struct {
|
||||||
mock.Mock
|
mock.Mock
|
||||||
}
|
}
|
||||||
|
|
||||||
//nolint copylocks
|
func (s *MockAzureHelper) queryToken(data url.Values, tenantID string) (token azureToken, err error) {
|
||||||
func (s MockAzureHelper) queryToken(data url.Values, tenantID string) (token azureToken, err error) {
|
|
||||||
args := s.Called(data, tenantID)
|
args := s.Called(data, tenantID)
|
||||||
return args.Get(0).(azureToken), args.Error(1)
|
return args.Get(0).(azureToken), args.Error(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *MockAzureHelper) queryAuthorizationAPI(authorizationURL string, authorizationHeader string) ([]byte, int, error) {
|
||||||
|
args := s.Called(authorizationURL, authorizationHeader)
|
||||||
|
return args.Get(0).([]byte), args.Int(1), args.Error(2)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *MockAzureHelper) openAzureLoginPage(redirectURL string) {
|
||||||
|
s.Called(redirectURL)
|
||||||
|
}
|
||||||
|
|
|
@ -28,9 +28,10 @@
|
||||||
package context
|
package context
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/docker/api/cli/cmd/context/login"
|
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
|
|
||||||
|
"github.com/docker/api/cli/cmd/context/login"
|
||||||
|
|
||||||
cliopts "github.com/docker/api/cli/options"
|
cliopts "github.com/docker/api/cli/options"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
package login
|
package login
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/spf13/cobra"
|
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
|
||||||
"github.com/docker/api/client"
|
"github.com/docker/api/client"
|
||||||
apicontext "github.com/docker/api/context"
|
apicontext "github.com/docker/api/context"
|
||||||
|
|
|
@ -29,6 +29,7 @@ package client
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
|
||||||
"github.com/docker/api/context/cloud"
|
"github.com/docker/api/context/cloud"
|
||||||
|
|
||||||
"github.com/docker/api/backend"
|
"github.com/docker/api/backend"
|
||||||
|
|
|
@ -2,8 +2,8 @@ package cloud
|
||||||
|
|
||||||
import "context"
|
import "context"
|
||||||
|
|
||||||
|
// Service cloud specific services
|
||||||
type Service interface {
|
type Service interface {
|
||||||
// Login login to cloud provider
|
// Login login to cloud provider
|
||||||
Login(ctx context.Context, params map[string]string) error
|
Login(ctx context.Context, params map[string]string) error
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -3,9 +3,10 @@ package example
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/docker/api/context/cloud"
|
|
||||||
"io"
|
"io"
|
||||||
|
|
||||||
|
"github.com/docker/api/context/cloud"
|
||||||
|
|
||||||
"github.com/docker/api/backend"
|
"github.com/docker/api/backend"
|
||||||
"github.com/docker/api/compose"
|
"github.com/docker/api/compose"
|
||||||
"github.com/docker/api/containers"
|
"github.com/docker/api/containers"
|
||||||
|
|
|
@ -2,9 +2,10 @@ package moby
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"github.com/docker/api/context/cloud"
|
|
||||||
"io"
|
"io"
|
||||||
|
|
||||||
|
"github.com/docker/api/context/cloud"
|
||||||
|
|
||||||
"github.com/docker/docker/api/types"
|
"github.com/docker/docker/api/types"
|
||||||
"github.com/docker/docker/api/types/container"
|
"github.com/docker/docker/api/types/container"
|
||||||
"github.com/docker/docker/client"
|
"github.com/docker/docker/client"
|
||||||
|
|
Loading…
Reference in New Issue