From ac2888fc4e02d289d688bb4d72a87b43c4864f6e Mon Sep 17 00:00:00 2001 From: Javier Adriel Date: Tue, 2 Aug 2022 10:41:12 -0500 Subject: [PATCH] Implement login and api key handlers (#2204) --- operatorapi/operator_subnet.go | 103 +++++++++++++++ operatorapi/operator_subnet_test.go | 188 ++++++++++++++++++++++++++++ operatorapi/subnet.go | 43 ------- 3 files changed, 291 insertions(+), 43 deletions(-) create mode 100644 operatorapi/operator_subnet.go create mode 100644 operatorapi/operator_subnet_test.go delete mode 100644 operatorapi/subnet.go diff --git a/operatorapi/operator_subnet.go b/operatorapi/operator_subnet.go new file mode 100644 index 000000000..7ce5012ae --- /dev/null +++ b/operatorapi/operator_subnet.go @@ -0,0 +1,103 @@ +// This file is part of MinIO Console Server +// Copyright (c) 2022 MinIO, Inc. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package operatorapi + +import ( + "context" + "errors" + + xhttp "github.com/minio/console/pkg/http" + "github.com/minio/console/pkg/subnet" + + "github.com/go-openapi/runtime/middleware" + "github.com/minio/console/models" + "github.com/minio/console/operatorapi/operations" + "github.com/minio/console/operatorapi/operations/operator_api" + "github.com/minio/console/restapi" +) + +func registerOperatorSubnetHandlers(api *operations.OperatorAPI) { + api.OperatorAPIOperatorSubnetLoginHandler = operator_api.OperatorSubnetLoginHandlerFunc(func(params operator_api.OperatorSubnetLoginParams, session *models.Principal) middleware.Responder { + res, err := getOperatorSubnetLoginResponse(session, params) + if err != nil { + return operator_api.NewOperatorSubnetLoginDefault(int(err.Code)).WithPayload(err) + } + return operator_api.NewOperatorSubnetLoginOK().WithPayload(res) + }) + api.OperatorAPIOperatorSubnetLoginMFAHandler = operator_api.OperatorSubnetLoginMFAHandlerFunc(func(params operator_api.OperatorSubnetLoginMFAParams, session *models.Principal) middleware.Responder { + res, err := getOperatorSubnetLoginMFAResponse(session, params) + if err != nil { + return operator_api.NewOperatorSubnetLoginMFADefault(int(err.Code)).WithPayload(err) + } + return operator_api.NewOperatorSubnetLoginMFAOK().WithPayload(res) + }) + api.OperatorAPIOperatorSubnetAPIKeyHandler = operator_api.OperatorSubnetAPIKeyHandlerFunc(func(params operator_api.OperatorSubnetAPIKeyParams, session *models.Principal) middleware.Responder { + res, err := getOperatorSubnetAPIKeyResponse(session, params) + if err != nil { + return operator_api.NewOperatorSubnetAPIKeyDefault(int(err.Code)).WithPayload(err) + } + return operator_api.NewOperatorSubnetAPIKeyOK().WithPayload(res) + }) + api.OperatorAPIOperatorSubnetRegisterAPIKeyHandler = operator_api.OperatorSubnetRegisterAPIKeyHandlerFunc(func(params operator_api.OperatorSubnetRegisterAPIKeyParams, session *models.Principal) middleware.Responder { + // TODO: Implement + return operator_api.NewOperatorSubnetRegisterAPIKeyOK() + }) +} + +func getOperatorSubnetLoginResponse(session *models.Principal, params operator_api.OperatorSubnetLoginParams) (*models.OperatorSubnetLoginResponse, *models.Error) { + ctx, cancel := context.WithCancel(params.HTTPRequest.Context()) + defer cancel() + username := params.Body.Username + password := params.Body.Password + if username == "" || password == "" { + return nil, restapi.ErrorWithContext(ctx, errors.New("empty credentials")) + } + subnetHTTPClient := &xhttp.Client{Client: restapi.GetConsoleHTTPClient()} + token, mfa, err := restapi.SubnetLogin(subnetHTTPClient, username, password) + if err != nil { + return nil, restapi.ErrorWithContext(ctx, err) + } + return &models.OperatorSubnetLoginResponse{ + AccessToken: token, + MfaToken: mfa, + }, nil +} + +func getOperatorSubnetLoginMFAResponse(session *models.Principal, params operator_api.OperatorSubnetLoginMFAParams) (*models.OperatorSubnetLoginResponse, *models.Error) { + ctx, cancel := context.WithCancel(params.HTTPRequest.Context()) + defer cancel() + subnetHTTPClient := &xhttp.Client{Client: restapi.GetConsoleHTTPClient()} + res, err := subnet.LoginWithMFA(subnetHTTPClient, *params.Body.Username, *params.Body.MfaToken, *params.Body.Otp) + if err != nil { + return nil, restapi.ErrorWithContext(ctx, err) + } + return &models.OperatorSubnetLoginResponse{ + AccessToken: res.AccessToken, + }, nil +} + +func getOperatorSubnetAPIKeyResponse(session *models.Principal, params operator_api.OperatorSubnetAPIKeyParams) (*models.OperatorSubnetAPIKey, *models.Error) { + ctx, cancel := context.WithCancel(params.HTTPRequest.Context()) + defer cancel() + subnetHTTPClient := &xhttp.Client{Client: restapi.GetConsoleHTTPClient()} + token := params.HTTPRequest.URL.Query().Get("token") + apiKey, err := subnet.GetAPIKey(subnetHTTPClient, token) + if err != nil { + return nil, restapi.ErrorWithContext(ctx, err) + } + return &models.OperatorSubnetAPIKey{APIKey: apiKey}, nil +} diff --git a/operatorapi/operator_subnet_test.go b/operatorapi/operator_subnet_test.go new file mode 100644 index 000000000..cc391c42a --- /dev/null +++ b/operatorapi/operator_subnet_test.go @@ -0,0 +1,188 @@ +// This file is part of MinIO Console Server +// Copyright (c) 2022 MinIO, Inc. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package operatorapi + +import ( + "fmt" + "net/http" + "net/http/httptest" + "net/url" + "os" + "testing" + + "github.com/minio/console/models" + "github.com/minio/console/operatorapi/operations" + "github.com/minio/console/operatorapi/operations/operator_api" + "github.com/minio/console/pkg/subnet" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/suite" +) + +type OperatorSubnetTestSuite struct { + suite.Suite + assert *assert.Assertions + loginServer *httptest.Server + loginWithError bool + loginMFAServer *httptest.Server + loginMFAWithError bool + getAPIKeyServer *httptest.Server + getAPIKeyWithError bool +} + +func (suite *OperatorSubnetTestSuite) SetupSuite() { + suite.assert = assert.New(suite.T()) + suite.loginServer = httptest.NewServer(http.HandlerFunc(suite.loginHandler)) + suite.loginMFAServer = httptest.NewServer(http.HandlerFunc(suite.loginMFAHandler)) + suite.getAPIKeyServer = httptest.NewServer(http.HandlerFunc(suite.getAPIKeyHandler)) +} + +func (suite *OperatorSubnetTestSuite) loginHandler( + w http.ResponseWriter, r *http.Request, +) { + if suite.loginWithError { + w.WriteHeader(400) + } else { + fmt.Fprintf(w, `{"mfa_required": true, "mfa_token": "mockToken"}`) + } +} + +func (suite *OperatorSubnetTestSuite) loginMFAHandler( + w http.ResponseWriter, r *http.Request, +) { + if suite.loginMFAWithError { + w.WriteHeader(400) + } else { + fmt.Fprintf(w, `{"token_info": {"access_token": "mockToken"}}`) + } +} + +func (suite *OperatorSubnetTestSuite) getAPIKeyHandler( + w http.ResponseWriter, r *http.Request, +) { + if suite.getAPIKeyWithError { + w.WriteHeader(400) + } else { + fmt.Fprintf(w, `{"api_key": "mockAPIKey"}`) + } +} + +func (suite *OperatorSubnetTestSuite) TearDownSuite() { +} + +func (suite *OperatorSubnetTestSuite) TestRegisterOperatorSubnetHanlders() { + api := &operations.OperatorAPI{} + suite.assert.Nil(api.OperatorAPIOperatorSubnetLoginHandler) + suite.assert.Nil(api.OperatorAPIOperatorSubnetLoginMFAHandler) + suite.assert.Nil(api.OperatorAPIOperatorSubnetAPIKeyHandler) + suite.assert.Nil(api.OperatorAPIOperatorSubnetRegisterAPIKeyHandler) + registerOperatorSubnetHandlers(api) + suite.assert.NotNil(api.OperatorAPIOperatorSubnetLoginHandler) + suite.assert.NotNil(api.OperatorAPIOperatorSubnetLoginMFAHandler) + suite.assert.NotNil(api.OperatorAPIOperatorSubnetAPIKeyHandler) + suite.assert.NotNil(api.OperatorAPIOperatorSubnetRegisterAPIKeyHandler) +} + +func (suite *OperatorSubnetTestSuite) TestOperatorSubnetLoginHandlerWithEmptyCredentials() { + params, api := suite.initSubnetLoginRequest("", "") + response := api.OperatorAPIOperatorSubnetLoginHandler.Handle(params, &models.Principal{}) + _, ok := response.(*operator_api.OperatorSubnetLoginDefault) + suite.assert.True(ok) +} + +func (suite *OperatorSubnetTestSuite) TestOperatorSubnetLoginHandlerWithServerError() { + params, api := suite.initSubnetLoginRequest("mockusername", "mockpassword") + suite.loginWithError = true + os.Setenv(subnet.ConsoleSubnetURL, suite.loginServer.URL) + response := api.OperatorAPIOperatorSubnetLoginHandler.Handle(params, &models.Principal{}) + _, ok := response.(*operator_api.OperatorSubnetLoginDefault) + suite.assert.True(ok) + os.Unsetenv(subnet.ConsoleSubnetURL) +} + +func (suite *OperatorSubnetTestSuite) TestOperatorSubnetLoginHandlerWithoutError() { + params, api := suite.initSubnetLoginRequest("mockusername", "mockpassword") + suite.loginWithError = false + os.Setenv(subnet.ConsoleSubnetURL, suite.loginServer.URL) + response := api.OperatorAPIOperatorSubnetLoginHandler.Handle(params, &models.Principal{}) + _, ok := response.(*operator_api.OperatorSubnetLoginOK) + suite.assert.True(ok) + os.Unsetenv(subnet.ConsoleSubnetURL) +} + +func (suite *OperatorSubnetTestSuite) initSubnetLoginRequest(username, password string) (params operator_api.OperatorSubnetLoginParams, api operations.OperatorAPI) { + registerOperatorSubnetHandlers(&api) + params.Body = &models.OperatorSubnetLoginRequest{Username: username, Password: password} + params.HTTPRequest = &http.Request{} + return params, api +} + +func (suite *OperatorSubnetTestSuite) TestOperatorSubnetLoginMFAHandlerWithServerError() { + params, api := suite.initSubnetLoginMFARequest("mockusername", "mockMFA", "mockOTP") + suite.loginMFAWithError = true + os.Setenv(subnet.ConsoleSubnetURL, suite.loginMFAServer.URL) + response := api.OperatorAPIOperatorSubnetLoginMFAHandler.Handle(params, &models.Principal{}) + _, ok := response.(*operator_api.OperatorSubnetLoginMFADefault) + suite.assert.True(ok) + os.Unsetenv(subnet.ConsoleSubnetURL) +} + +func (suite *OperatorSubnetTestSuite) TestOperatorSubnetLoginMFAHandlerWithoutError() { + params, api := suite.initSubnetLoginMFARequest("mockusername", "mockMFA", "mockOTP") + suite.loginMFAWithError = false + os.Setenv(subnet.ConsoleSubnetURL, suite.loginMFAServer.URL) + response := api.OperatorAPIOperatorSubnetLoginMFAHandler.Handle(params, &models.Principal{}) + _, ok := response.(*operator_api.OperatorSubnetLoginMFAOK) + suite.assert.True(ok) + os.Unsetenv(subnet.ConsoleSubnetURL) +} + +func (suite *OperatorSubnetTestSuite) initSubnetLoginMFARequest(username, mfa, otp string) (params operator_api.OperatorSubnetLoginMFAParams, api operations.OperatorAPI) { + registerOperatorSubnetHandlers(&api) + params.Body = &models.OperatorSubnetLoginMFARequest{Username: &username, MfaToken: &mfa, Otp: &otp} + params.HTTPRequest = &http.Request{} + return params, api +} + +func (suite *OperatorSubnetTestSuite) TestOperatorSubnetAPIKeyHandlerWithServerError() { + params, api := suite.initSubnetAPIKeyRequest() + suite.getAPIKeyWithError = true + os.Setenv(subnet.ConsoleSubnetURL, suite.getAPIKeyServer.URL) + response := api.OperatorAPIOperatorSubnetAPIKeyHandler.Handle(params, &models.Principal{}) + _, ok := response.(*operator_api.OperatorSubnetAPIKeyDefault) + suite.assert.True(ok) + os.Unsetenv(subnet.ConsoleSubnetURL) +} + +func (suite *OperatorSubnetTestSuite) TestOperatorSubnetAPIKeyHandlerWithoutError() { + params, api := suite.initSubnetAPIKeyRequest() + suite.getAPIKeyWithError = false + os.Setenv(subnet.ConsoleSubnetURL, suite.getAPIKeyServer.URL) + response := api.OperatorAPIOperatorSubnetAPIKeyHandler.Handle(params, &models.Principal{}) + _, ok := response.(*operator_api.OperatorSubnetAPIKeyOK) + suite.assert.True(ok) + os.Unsetenv(subnet.ConsoleSubnetURL) +} + +func (suite *OperatorSubnetTestSuite) initSubnetAPIKeyRequest() (params operator_api.OperatorSubnetAPIKeyParams, api operations.OperatorAPI) { + registerOperatorSubnetHandlers(&api) + params.HTTPRequest = &http.Request{URL: &url.URL{}} + return params, api +} + +func TestOperatorSubnet(t *testing.T) { + suite.Run(t, new(OperatorSubnetTestSuite)) +} diff --git a/operatorapi/subnet.go b/operatorapi/subnet.go deleted file mode 100644 index 1455564bb..000000000 --- a/operatorapi/subnet.go +++ /dev/null @@ -1,43 +0,0 @@ -// This file is part of MinIO Console Server -// Copyright (c) 2022 MinIO, Inc. -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU Affero General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU Affero General Public License for more details. -// -// You should have received a copy of the GNU Affero General Public License -// along with this program. If not, see . - -package operatorapi - -import ( - "github.com/go-openapi/runtime/middleware" - "github.com/minio/console/models" - "github.com/minio/console/operatorapi/operations" - "github.com/minio/console/operatorapi/operations/operator_api" -) - -func registerOperatorSubnetHandlers(api *operations.OperatorAPI) { - api.OperatorAPIOperatorSubnetLoginHandler = operator_api.OperatorSubnetLoginHandlerFunc(func(params operator_api.OperatorSubnetLoginParams, session *models.Principal) middleware.Responder { - // TODO: Implement - return operator_api.NewOperatorSubnetLoginOK() - }) - api.OperatorAPIOperatorSubnetLoginMFAHandler = operator_api.OperatorSubnetLoginMFAHandlerFunc(func(params operator_api.OperatorSubnetLoginMFAParams, session *models.Principal) middleware.Responder { - // TODO: Implement - return operator_api.NewOperatorSubnetLoginMFAOK() - }) - api.OperatorAPIOperatorSubnetAPIKeyHandler = operator_api.OperatorSubnetAPIKeyHandlerFunc(func(params operator_api.OperatorSubnetAPIKeyParams, session *models.Principal) middleware.Responder { - // TODO: Implement - return operator_api.NewOperatorSubnetAPIKeyOK() - }) - api.OperatorAPIOperatorSubnetRegisterAPIKeyHandler = operator_api.OperatorSubnetRegisterAPIKeyHandlerFunc(func(params operator_api.OperatorSubnetRegisterAPIKeyParams, session *models.Principal) middleware.Responder { - // TODO: Implement - return operator_api.NewOperatorSubnetRegisterAPIKeyOK() - }) -}