Subnet cluster registration (#1338)
- Removed old registration flow - Add support for new online and offline cluster registration flow - Support login accounts with mfa enabled - Registration screens Signed-off-by: Lenin Alevski <alevsk.8772@gmail.com>
This commit is contained in:
@@ -49,3 +49,15 @@ func GetLicenseInfoFromJWT(license string, publicKeys []string) (*licverifier.Li
|
||||
}
|
||||
return nil, errors.New("invalid license key")
|
||||
}
|
||||
|
||||
// MfaReq - JSON payload of the SUBNET mfa api
|
||||
type MfaReq struct {
|
||||
Username string `json:"username"`
|
||||
OTP string `json:"otp"`
|
||||
Token string `json:"token"`
|
||||
}
|
||||
|
||||
type LoginResp struct {
|
||||
AccessToken string
|
||||
MfaToken string
|
||||
}
|
||||
|
||||
@@ -29,9 +29,4 @@ JkO2PfyyAYEO/5dBlPh1Undu9WQl6J7B
|
||||
const (
|
||||
// Constants for subnet configuration
|
||||
ConsoleSubnetURL = "CONSOLE_SUBNET_URL"
|
||||
// Subnet endpoints
|
||||
publicKey = "/downloads/license-pubkey.pem"
|
||||
loginEndpoint = "/api/auth/login"
|
||||
refreshLicenseKeyEndpoint = "/api/auth/subscription/renew-license"
|
||||
licenseKeyEndpoint = "/api/auth/subscription/license-key"
|
||||
)
|
||||
|
||||
@@ -18,209 +18,94 @@
|
||||
package subnet
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"net/http"
|
||||
|
||||
"github.com/minio/console/models"
|
||||
"github.com/minio/madmin-go"
|
||||
mc "github.com/minio/mc/cmd"
|
||||
"github.com/tidwall/gjson"
|
||||
|
||||
"github.com/minio/console/cluster"
|
||||
"github.com/minio/pkg/licverifier"
|
||||
)
|
||||
|
||||
// subnetLoginRequest body request for subnet login
|
||||
type subnetLoginRequest struct {
|
||||
Username string `json:"username"`
|
||||
Password string `json:"password"`
|
||||
func LoginWithMFA(client cluster.HTTPClientI, username, mfaToken, otp string) (*LoginResp, error) {
|
||||
mfaLoginReq := MfaReq{Username: username, OTP: otp, Token: mfaToken}
|
||||
resp, err := subnetPostReq(client, subnetMFAURL(), mfaLoginReq, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
token := gjson.Get(resp, "token_info.access_token")
|
||||
if token.Exists() {
|
||||
return &LoginResp{AccessToken: token.String(), MfaToken: ""}, nil
|
||||
}
|
||||
return nil, errors.New("access token not found in response")
|
||||
}
|
||||
|
||||
// tokenInfo
|
||||
type tokenInfo struct {
|
||||
AccessToken string `json:"access_token"`
|
||||
ExpiresIn float64 `json:"expires_in"`
|
||||
TokenType string `json:"token_type"`
|
||||
}
|
||||
|
||||
// subnetLoginResponse body resonse from subnet after login
|
||||
type subnetLoginResponse struct {
|
||||
HasMembership bool `json:"has_memberships"`
|
||||
TokenInfo tokenInfo `json:"token_info"`
|
||||
}
|
||||
|
||||
// LicenseMetadata claims in subnet license
|
||||
type LicenseMetadata struct {
|
||||
Email string `json:"email"`
|
||||
Issuer string `json:"issuer"`
|
||||
TeamName string `json:"teamName"`
|
||||
ServiceType string `json:"serviceType"`
|
||||
RequestedAt string `json:"requestedAt"`
|
||||
ExpiresAt string `json:"expiresAt"`
|
||||
AccountID int64 `json:"accountId"`
|
||||
Capacity int64 `json:"capacity"`
|
||||
}
|
||||
|
||||
// subnetLicenseResponse body response returned by subnet license endpoint
|
||||
type subnetLicenseResponse struct {
|
||||
License string `json:"license"`
|
||||
Metadata LicenseMetadata `json:"metadata"`
|
||||
}
|
||||
|
||||
// subnetLoginRequest body request for subnet login
|
||||
type subnetRefreshRequest struct {
|
||||
License string `json:"license"`
|
||||
}
|
||||
|
||||
// getNewLicenseFromExistingLicense will perform license refresh based on the provided license key
|
||||
func getNewLicenseFromExistingLicense(client cluster.HTTPClientI, licenseKey string) (string, error) {
|
||||
request := subnetRefreshRequest{
|
||||
License: licenseKey,
|
||||
func Login(client cluster.HTTPClientI, username, password string) (*LoginResp, error) {
|
||||
loginReq := map[string]string{
|
||||
"username": username,
|
||||
"password": password,
|
||||
}
|
||||
// http body for login request
|
||||
payloadBytes, err := json.Marshal(request)
|
||||
respStr, err := subnetPostReq(client, subnetLoginURL(), loginReq, nil)
|
||||
if err != nil {
|
||||
return "", err
|
||||
return nil, err
|
||||
}
|
||||
subnetURL := GetSubnetURL()
|
||||
url := fmt.Sprintf("%s%s", subnetURL, refreshLicenseKeyEndpoint)
|
||||
resp, err := client.Post(url, "application/json", bytes.NewReader(payloadBytes))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
bodyBytes, err := ioutil.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
subnetLicense := &subnetLicenseResponse{}
|
||||
// Parse subnet login response
|
||||
err = json.Unmarshal(bodyBytes, subnetLicense)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return subnetLicense.License, nil
|
||||
}
|
||||
|
||||
// getLicenseFromCredentials will perform authentication against subnet using
|
||||
// user provided credentials and return the current subnet license key
|
||||
func getLicenseFromCredentials(client cluster.HTTPClientI, username, password string) (string, error) {
|
||||
request := subnetLoginRequest{
|
||||
Username: username,
|
||||
Password: password,
|
||||
}
|
||||
// http body for login request
|
||||
payloadBytes, err := json.Marshal(request)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
subnetURL := GetSubnetURL()
|
||||
url := fmt.Sprintf("%s%s", subnetURL, loginEndpoint)
|
||||
// Authenticate against subnet using email/password provided by user
|
||||
resp, err := client.Post(url, "application/json", bytes.NewReader(payloadBytes))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
bodyBytes, err := ioutil.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
subnetSession := &subnetLoginResponse{}
|
||||
// Parse subnet login response
|
||||
err = json.Unmarshal(bodyBytes, subnetSession)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
// Get license key using session token
|
||||
token := subnetSession.TokenInfo.AccessToken
|
||||
url = fmt.Sprintf("%s%s", subnetURL, licenseKeyEndpoint)
|
||||
req, err := http.NewRequest("POST", url, nil)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", token))
|
||||
resp, err = client.Do(req)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
bodyBytes, err = ioutil.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
|
||||
return "", fmt.Errorf("subnet served returned status %d code", resp.StatusCode)
|
||||
}
|
||||
userLicense := &subnetLicenseResponse{}
|
||||
// Parse subnet license response
|
||||
err = json.Unmarshal(bodyBytes, userLicense)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return userLicense.License, nil
|
||||
}
|
||||
|
||||
// downloadSubnetPublicKey will download the current subnet public key.
|
||||
func downloadSubnetPublicKey(client cluster.HTTPClientI) (string, error) {
|
||||
// Get the public key directly from Subnet
|
||||
url := fmt.Sprintf("%s%s", GetSubnetURL(), publicKey)
|
||||
resp, err := client.Get(url)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
buf := new(bytes.Buffer)
|
||||
_, err = buf.ReadFrom(resp.Body)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return buf.String(), err
|
||||
}
|
||||
|
||||
// ValidateLicense will download the current subnet public key, if the public key its not available for license
|
||||
// verification then console will fall back to verification with hardcoded public keys
|
||||
func ValidateLicense(client cluster.HTTPClientI, licenseKey, email, password string) (licInfo *licverifier.LicenseInfo, license string, err error) {
|
||||
var publicKeys []string
|
||||
if email != "" && password != "" {
|
||||
// fetch subnet license key using user credentials
|
||||
license, err = getLicenseFromCredentials(client, email, password)
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
mfaRequired := gjson.Get(respStr, "mfa_required").Bool()
|
||||
if mfaRequired {
|
||||
mfaToken := gjson.Get(respStr, "mfa_token").String()
|
||||
if mfaToken == "" {
|
||||
return nil, errors.New("missing mfa token")
|
||||
}
|
||||
} else if licenseKey != "" {
|
||||
license = licenseKey
|
||||
return &LoginResp{AccessToken: "", MfaToken: mfaToken}, nil
|
||||
}
|
||||
token := gjson.Get(respStr, "token_info.access_token")
|
||||
if token.Exists() {
|
||||
return &LoginResp{AccessToken: token.String(), MfaToken: ""}, nil
|
||||
}
|
||||
return nil, errors.New("access token not found in response")
|
||||
}
|
||||
|
||||
func GetOrganizations(client cluster.HTTPClientI, token string) ([]*models.SubnetOrganization, error) {
|
||||
headers := subnetAuthHeaders(token)
|
||||
respStr, err := subnetGetReq(client, subnetOrgsURL(), headers)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var organizations []*models.SubnetOrganization
|
||||
err = json.Unmarshal([]byte(respStr), &organizations)
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
}
|
||||
return organizations, nil
|
||||
}
|
||||
|
||||
func Register(client cluster.HTTPClientI, admInfo madmin.InfoMessage, apiKey, token, accountID string) (string, error) {
|
||||
var headers map[string]string
|
||||
regInfo := GetClusterRegInfo(admInfo)
|
||||
regURL := subnetRegisterURL()
|
||||
if apiKey != "" {
|
||||
regURL += "?api_key=" + apiKey
|
||||
} else {
|
||||
return nil, "", errors.New("invalid license")
|
||||
}
|
||||
subnetPubKey, err := downloadSubnetPublicKey(client)
|
||||
if err != nil {
|
||||
log.Print(err)
|
||||
// there was an issue getting the subnet public key
|
||||
// use hardcoded public keys instead
|
||||
publicKeys = OfflinePublicKeys
|
||||
} else {
|
||||
publicKeys = append(publicKeys, subnetPubKey)
|
||||
}
|
||||
licInfo, err = GetLicenseInfoFromJWT(license, publicKeys)
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
return licInfo, license, nil
|
||||
}
|
||||
|
||||
func RefreshLicense(client cluster.HTTPClientI, licenseKey string) (licInfo *licverifier.LicenseInfo, license string, err error) {
|
||||
if licenseKey != "" {
|
||||
license, err = getNewLicenseFromExistingLicense(client, licenseKey)
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
if accountID == "" || token == "" {
|
||||
return "", errors.New("missing accountID or authentication token")
|
||||
}
|
||||
licenseInfo, rawLicense, err := ValidateLicense(client, license, "", "")
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
return licenseInfo, rawLicense, nil
|
||||
headers = subnetAuthHeaders(token)
|
||||
regURL += "?aid=" + accountID
|
||||
}
|
||||
return nil, "", errors.New("invalid license")
|
||||
regToken, err := GenerateRegToken(regInfo)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
reqPayload := mc.ClusterRegistrationReq{Token: regToken}
|
||||
resp, err := subnetPostReq(client, regURL, reqPayload, headers)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
subnetAPIKey := gjson.Parse(resp).Get("api_key").String()
|
||||
if subnetAPIKey != "" {
|
||||
return subnetAPIKey, nil
|
||||
}
|
||||
return "", errors.New("subnet api key not found")
|
||||
}
|
||||
|
||||
@@ -1,332 +0,0 @@
|
||||
// This file is part of MinIO Kubernetes Cloud
|
||||
// Copyright (c) 2021 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 <http://www.gnu.org/licenses/>.
|
||||
|
||||
package subnet
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"errors"
|
||||
)
|
||||
|
||||
var HTTPGetMock func(url string) (resp *http.Response, err error)
|
||||
var HTTPPostMock func(url, contentType string, body io.Reader) (resp *http.Response, err error)
|
||||
var HTTPDoMock func(req *http.Request) (*http.Response, error)
|
||||
|
||||
type HTTPClientMock struct {
|
||||
Client *http.Client
|
||||
}
|
||||
|
||||
func (c *HTTPClientMock) Get(url string) (resp *http.Response, err error) {
|
||||
return HTTPGetMock(url)
|
||||
}
|
||||
|
||||
func (c *HTTPClientMock) Post(url, contentType string, body io.Reader) (resp *http.Response, err error) {
|
||||
return HTTPPostMock(url, contentType, body)
|
||||
}
|
||||
|
||||
func (c *HTTPClientMock) Do(req *http.Request) (*http.Response, error) {
|
||||
return HTTPDoMock(req)
|
||||
}
|
||||
|
||||
func Test_getLicenseFromCredentials(t *testing.T) {
|
||||
// HTTP Client mock
|
||||
clientMock := HTTPClientMock{
|
||||
Client: &http.Client{},
|
||||
}
|
||||
type args struct {
|
||||
client HTTPClientMock
|
||||
username string
|
||||
password string
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want string
|
||||
wantErr bool
|
||||
mockFunc func()
|
||||
}{
|
||||
{
|
||||
name: "error when login against subnet",
|
||||
args: args{
|
||||
client: clientMock,
|
||||
username: "invalid",
|
||||
password: "invalid",
|
||||
},
|
||||
want: "",
|
||||
wantErr: true,
|
||||
mockFunc: func() {
|
||||
HTTPPostMock = func(url, contentType string, body io.Reader) (resp *http.Response, err error) {
|
||||
return nil, errors.New("something went wrong")
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "error because of malformed subnet response",
|
||||
args: args{
|
||||
client: clientMock,
|
||||
username: "invalid",
|
||||
password: "invalid",
|
||||
},
|
||||
want: "",
|
||||
wantErr: true,
|
||||
mockFunc: func() {
|
||||
HTTPPostMock = func(url, contentType string, body io.Reader) (resp *http.Response, err error) {
|
||||
return &http.Response{Body: ioutil.NopCloser(bytes.NewReader([]byte("foo")))}, nil
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "error when obtaining license from subnet",
|
||||
args: args{
|
||||
client: clientMock,
|
||||
username: "valid",
|
||||
password: "valid",
|
||||
},
|
||||
want: "",
|
||||
wantErr: true,
|
||||
mockFunc: func() {
|
||||
HTTPPostMock = func(url, contentType string, body io.Reader) (resp *http.Response, err error) {
|
||||
// returning test jwt token
|
||||
return &http.Response{Body: ioutil.NopCloser(bytes.NewReader([]byte("{\"has_memberships\":true,\"token_info\":{\"access_token\":\"eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6Ik4wRXdOa1V5UXpORU1UUkNOekU0UmpSR1JVWkJSa1UxUmtZNE9EY3lOekZHTXpjNU1qZ3hNZyJ9.eyJodHRwczovL2lkLnN1Ym5ldC5taW4uaW8vY2xhaW1zL2dyb3VwcyI6W10sImh0dHBzOi8vaWQuc3VibmV0Lm1pbi5pby9jbGFpbXMvcm9sZXMiOltdLCJodHRwczovL2lkLnN1Ym5ldC5taW4uaW8vY2xhaW1zL2VtYWlsIjoibGVuaW4rYzFAbWluaW8uaW8iLCJpc3MiOiJodHRwczovL2lkLnN1Ym5ldC5taW4uaW8vIiwic3ViIjoiYXV0aDB8NWZjZWFlYTMyNTNhZjEwMDc3NDZkMDM0IiwiYXVkIjoiaHR0cHM6Ly9zdWJuZXQubWluLmlvL2FwaSIsImlhdCI6MTYwODQxNjE5NiwiZXhwIjoxNjExMDA4MTk2LCJhenAiOiI1WTA0eVZlejNiOFgxUFVzRHVqSmxuZXVuY3ExVjZxaiIsInNjb3BlIjoib2ZmbGluZV9hY2Nlc3MiLCJndHkiOiJwYXNzd29yZCJ9.GC8DRLT0jUEteuBZBmyMXMswLSblCr_89Gu5NcVRUzKSYAaZ5VFW4UFgo1BpiC0sePuWJ0Vykitphx7znTfZfj5B3mZbOw3ejG6kxz7nm9DuYMmySJFYnwroZ9EP02vkW7-n_-YvEg8le1wXfkJ3lTUzO3aWddS4rfQRsZ2YJJUj61GiNyEK_QNP4PrYOuzLyD1wV75NejFqfcFoj7nRkT1K2BM0-89-_f2AFDGTjov6Ig6s1s-zLC9wxcYSmubNwpCJytZmQgPqIepOr065Y6OB4n0n0B5sXguuGuzb8VAkECrHhHPz8ta926fc0jC4XxVCNKdbV1_qC3-1yY7AJA\",\"expires_in\":2592000.0,\"token_type\":\"Bearer\"}}")))}, nil
|
||||
}
|
||||
HTTPDoMock = func(req *http.Request) (*http.Response, error) {
|
||||
return nil, errors.New("something went wrong")
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "error when obtaining license from subnet because of malformed response",
|
||||
args: args{
|
||||
client: clientMock,
|
||||
username: "valid",
|
||||
password: "valid",
|
||||
},
|
||||
want: "",
|
||||
wantErr: true,
|
||||
mockFunc: func() {
|
||||
HTTPPostMock = func(url, contentType string, body io.Reader) (resp *http.Response, err error) {
|
||||
// returning test jwt token
|
||||
return &http.Response{Body: ioutil.NopCloser(bytes.NewReader([]byte("{\"has_memberships\":true,\"token_info\":{\"access_token\":\"eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6Ik4wRXdOa1V5UXpORU1UUkNOekU0UmpSR1JVWkJSa1UxUmtZNE9EY3lOekZHTXpjNU1qZ3hNZyJ9.eyJodHRwczovL2lkLnN1Ym5ldC5taW4uaW8vY2xhaW1zL2dyb3VwcyI6W10sImh0dHBzOi8vaWQuc3VibmV0Lm1pbi5pby9jbGFpbXMvcm9sZXMiOltdLCJodHRwczovL2lkLnN1Ym5ldC5taW4uaW8vY2xhaW1zL2VtYWlsIjoibGVuaW4rYzFAbWluaW8uaW8iLCJpc3MiOiJodHRwczovL2lkLnN1Ym5ldC5taW4uaW8vIiwic3ViIjoiYXV0aDB8NWZjZWFlYTMyNTNhZjEwMDc3NDZkMDM0IiwiYXVkIjoiaHR0cHM6Ly9zdWJuZXQubWluLmlvL2FwaSIsImlhdCI6MTYwODQxNjE5NiwiZXhwIjoxNjExMDA4MTk2LCJhenAiOiI1WTA0eVZlejNiOFgxUFVzRHVqSmxuZXVuY3ExVjZxaiIsInNjb3BlIjoib2ZmbGluZV9hY2Nlc3MiLCJndHkiOiJwYXNzd29yZCJ9.GC8DRLT0jUEteuBZBmyMXMswLSblCr_89Gu5NcVRUzKSYAaZ5VFW4UFgo1BpiC0sePuWJ0Vykitphx7znTfZfj5B3mZbOw3ejG6kxz7nm9DuYMmySJFYnwroZ9EP02vkW7-n_-YvEg8le1wXfkJ3lTUzO3aWddS4rfQRsZ2YJJUj61GiNyEK_QNP4PrYOuzLyD1wV75NejFqfcFoj7nRkT1K2BM0-89-_f2AFDGTjov6Ig6s1s-zLC9wxcYSmubNwpCJytZmQgPqIepOr065Y6OB4n0n0B5sXguuGuzb8VAkECrHhHPz8ta926fc0jC4XxVCNKdbV1_qC3-1yY7AJA\",\"expires_in\":2592000.0,\"token_type\":\"Bearer\"}}")))}, nil
|
||||
}
|
||||
HTTPDoMock = func(req *http.Request) (*http.Response, error) {
|
||||
return &http.Response{Body: ioutil.NopCloser(bytes.NewReader([]byte("foo")))}, nil
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "license obtained successfully",
|
||||
args: args{
|
||||
client: clientMock,
|
||||
username: "valid",
|
||||
password: "valid",
|
||||
},
|
||||
want: license,
|
||||
wantErr: false,
|
||||
mockFunc: func() {
|
||||
HTTPPostMock = func(url, contentType string, body io.Reader) (resp *http.Response, err error) {
|
||||
// returning test jwt token
|
||||
return &http.Response{
|
||||
StatusCode: 200,
|
||||
Body: ioutil.NopCloser(bytes.NewReader([]byte("{\"has_memberships\":true,\"token_info\":{\"access_token\":\"eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6Ik4wRXdOa1V5UXpORU1UUkNOekU0UmpSR1JVWkJSa1UxUmtZNE9EY3lOekZHTXpjNU1qZ3hNZyJ9.eyJodHRwczovL2lkLnN1Ym5ldC5taW4uaW8vY2xhaW1zL2dyb3VwcyI6W10sImh0dHBzOi8vaWQuc3VibmV0Lm1pbi5pby9jbGFpbXMvcm9sZXMiOltdLCJodHRwczovL2lkLnN1Ym5ldC5taW4uaW8vY2xhaW1zL2VtYWlsIjoibGVuaW4rYzFAbWluaW8uaW8iLCJpc3MiOiJodHRwczovL2lkLnN1Ym5ldC5taW4uaW8vIiwic3ViIjoiYXV0aDB8NWZjZWFlYTMyNTNhZjEwMDc3NDZkMDM0IiwiYXVkIjoiaHR0cHM6Ly9zdWJuZXQubWluLmlvL2FwaSIsImlhdCI6MTYwODQxNjE5NiwiZXhwIjoxNjExMDA4MTk2LCJhenAiOiI1WTA0eVZlejNiOFgxUFVzRHVqSmxuZXVuY3ExVjZxaiIsInNjb3BlIjoib2ZmbGluZV9hY2Nlc3MiLCJndHkiOiJwYXNzd29yZCJ9.GC8DRLT0jUEteuBZBmyMXMswLSblCr_89Gu5NcVRUzKSYAaZ5VFW4UFgo1BpiC0sePuWJ0Vykitphx7znTfZfj5B3mZbOw3ejG6kxz7nm9DuYMmySJFYnwroZ9EP02vkW7-n_-YvEg8le1wXfkJ3lTUzO3aWddS4rfQRsZ2YJJUj61GiNyEK_QNP4PrYOuzLyD1wV75NejFqfcFoj7nRkT1K2BM0-89-_f2AFDGTjov6Ig6s1s-zLC9wxcYSmubNwpCJytZmQgPqIepOr065Y6OB4n0n0B5sXguuGuzb8VAkECrHhHPz8ta926fc0jC4XxVCNKdbV1_qC3-1yY7AJA\",\"expires_in\":2592000.0,\"token_type\":\"Bearer\"}}"))),
|
||||
}, nil
|
||||
}
|
||||
HTTPDoMock = func(req *http.Request) (*http.Response, error) {
|
||||
// returning test jwt license
|
||||
return &http.Response{
|
||||
StatusCode: 200,
|
||||
Body: ioutil.NopCloser(bytes.NewReader([]byte("{\"license\":\"" + license + "\",\"metadata\":{\"email\":\"lenin+c1@minio.io\",\"issuer\":\"subnet@minio.io\",\"accountId\":176,\"teamName\":\"console-customer\",\"serviceType\":\"STANDARD\",\"capacity\":25,\"requestedAt\":\"2020-12-19T22:23:31.609144732Z\",\"expiresAt\":\"2021-12-19T22:23:31.609144732Z\"}}"))),
|
||||
}, nil
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if tt.mockFunc != nil {
|
||||
tt.mockFunc()
|
||||
}
|
||||
got, err := getLicenseFromCredentials(&tt.args.client, tt.args.username, tt.args.password)
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("getLicenseFromCredentials() error = %v, wantErr %v", err, tt.wantErr)
|
||||
return
|
||||
}
|
||||
if got != tt.want {
|
||||
t.Errorf("getLicenseFromCredentials() got = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_downloadSubnetPublicKey(t *testing.T) {
|
||||
// HTTP Client mock
|
||||
clientMock := HTTPClientMock{
|
||||
Client: &http.Client{},
|
||||
}
|
||||
type args struct {
|
||||
client HTTPClientMock
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want string
|
||||
wantErr bool
|
||||
mockFunc func()
|
||||
}{
|
||||
{
|
||||
name: "error downloading public key",
|
||||
args: args{
|
||||
client: clientMock,
|
||||
},
|
||||
mockFunc: func() {
|
||||
HTTPGetMock = func(url string) (resp *http.Response, err error) {
|
||||
return nil, errors.New("something went wrong")
|
||||
}
|
||||
},
|
||||
wantErr: true,
|
||||
want: "",
|
||||
},
|
||||
{
|
||||
name: "public key download successfully",
|
||||
args: args{
|
||||
client: clientMock,
|
||||
},
|
||||
mockFunc: func() {
|
||||
HTTPGetMock = func(url string) (resp *http.Response, err error) {
|
||||
return &http.Response{Body: ioutil.NopCloser(bytes.NewReader([]byte("foo")))}, nil
|
||||
}
|
||||
},
|
||||
wantErr: false,
|
||||
want: "foo",
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if tt.mockFunc != nil {
|
||||
tt.mockFunc()
|
||||
}
|
||||
got, err := downloadSubnetPublicKey(&tt.args.client)
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("downloadSubnetPublicKey() error = %v, wantErr %v", err, tt.wantErr)
|
||||
return
|
||||
}
|
||||
if got != tt.want {
|
||||
t.Errorf("downloadSubnetPublicKey() got = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateLicense(t *testing.T) {
|
||||
// HTTP Client mock
|
||||
clientMock := HTTPClientMock{
|
||||
Client: &http.Client{},
|
||||
}
|
||||
type args struct {
|
||||
client HTTPClientMock
|
||||
licenseKey string
|
||||
email string
|
||||
password string
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
wantLicense string
|
||||
wantErr bool
|
||||
mockFunc func()
|
||||
}{
|
||||
{
|
||||
name: "error because nor license nor user or password was provided",
|
||||
args: args{
|
||||
client: clientMock,
|
||||
licenseKey: "",
|
||||
email: "",
|
||||
password: "",
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "error because could not get license from credentials",
|
||||
args: args{
|
||||
client: clientMock,
|
||||
licenseKey: "",
|
||||
email: "email",
|
||||
password: "password",
|
||||
},
|
||||
wantErr: true,
|
||||
mockFunc: func() {
|
||||
HTTPPostMock = func(url, contentType string, body io.Reader) (resp *http.Response, err error) {
|
||||
return nil, errors.New("something went wrong")
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "error because invalid license",
|
||||
args: args{
|
||||
client: clientMock,
|
||||
licenseKey: "invalid license",
|
||||
email: "",
|
||||
password: "",
|
||||
},
|
||||
wantErr: true,
|
||||
mockFunc: func() {
|
||||
HTTPGetMock = func(url string) (resp *http.Response, err error) {
|
||||
return &http.Response{Body: ioutil.NopCloser(strings.NewReader(publicKeys[0]))}, nil
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "license validated successfully",
|
||||
args: args{
|
||||
client: clientMock,
|
||||
licenseKey: license,
|
||||
email: "",
|
||||
password: "",
|
||||
},
|
||||
wantErr: false,
|
||||
mockFunc: func() {
|
||||
HTTPGetMock = func(url string) (resp *http.Response, err error) {
|
||||
return &http.Response{Body: ioutil.NopCloser(strings.NewReader(publicKeys[0]))}, nil
|
||||
}
|
||||
},
|
||||
wantLicense: license,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if tt.mockFunc != nil {
|
||||
tt.mockFunc()
|
||||
}
|
||||
_, gotLicense, err := ValidateLicense(&tt.args.client, tt.args.licenseKey, tt.args.email, tt.args.password)
|
||||
if !tt.wantErr {
|
||||
t.Skip() // FIXME: fix all success cases
|
||||
}
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("ValidateLicense() error = %v, wantErr %v", err, tt.wantErr)
|
||||
return
|
||||
}
|
||||
if gotLicense != tt.wantLicense {
|
||||
t.Errorf("ValidateLicense() gotLicense = %v, want %v", gotLicense, tt.wantLicense)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
165
pkg/subnet/utils.go
Normal file
165
pkg/subnet/utils.go
Normal file
@@ -0,0 +1,165 @@
|
||||
// This file is part of MinIO Kubernetes Cloud
|
||||
// Copyright (c) 2021 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 <http://www.gnu.org/licenses/>.
|
||||
|
||||
package subnet
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
|
||||
"github.com/minio/console/cluster"
|
||||
"github.com/minio/madmin-go"
|
||||
mc "github.com/minio/mc/cmd"
|
||||
"github.com/minio/pkg/env"
|
||||
)
|
||||
|
||||
const (
|
||||
subnetRespBodyLimit = 1 << 20 // 1 MiB
|
||||
)
|
||||
|
||||
func subnetBaseURL() string {
|
||||
return env.Get(ConsoleSubnetURL, "https://subnet.min.io")
|
||||
}
|
||||
|
||||
func subnetRegisterURL() string {
|
||||
return subnetBaseURL() + "/api/cluster/register"
|
||||
}
|
||||
|
||||
func subnetLoginURL() string {
|
||||
return subnetBaseURL() + "/api/auth/login"
|
||||
}
|
||||
|
||||
func subnetOrgsURL() string {
|
||||
return subnetBaseURL() + "/api/auth/organizations"
|
||||
}
|
||||
|
||||
func subnetMFAURL() string {
|
||||
return subnetBaseURL() + "/api/auth/mfa-login"
|
||||
}
|
||||
|
||||
func GenerateRegToken(clusterRegInfo mc.ClusterRegistrationInfo) (string, error) {
|
||||
token, e := json.Marshal(clusterRegInfo)
|
||||
if e != nil {
|
||||
return "", e
|
||||
}
|
||||
|
||||
return base64.StdEncoding.EncodeToString(token), nil
|
||||
}
|
||||
|
||||
func subnetAuthHeaders(authToken string) map[string]string {
|
||||
return map[string]string{"Authorization": "Bearer " + authToken}
|
||||
}
|
||||
|
||||
func httpDo(client cluster.HTTPClientI, req *http.Request) (*http.Response, error) {
|
||||
//if globalSubnetProxyURL != nil {
|
||||
// client.Transport.(*http.Transport).Proxy = http.ProxyURL(globalSubnetProxyURL)
|
||||
//}
|
||||
return client.Do(req)
|
||||
}
|
||||
|
||||
func subnetReqDo(client cluster.HTTPClientI, r *http.Request, headers map[string]string) (string, error) {
|
||||
for k, v := range headers {
|
||||
r.Header.Add(k, v)
|
||||
}
|
||||
|
||||
ct := r.Header.Get("Content-Type")
|
||||
if len(ct) == 0 {
|
||||
r.Header.Add("Content-Type", "application/json")
|
||||
}
|
||||
|
||||
resp, e := httpDo(client, r)
|
||||
if e != nil {
|
||||
return "", e
|
||||
}
|
||||
|
||||
defer resp.Body.Close()
|
||||
respBytes, e := ioutil.ReadAll(io.LimitReader(resp.Body, subnetRespBodyLimit))
|
||||
if e != nil {
|
||||
return "", e
|
||||
}
|
||||
respStr := string(respBytes)
|
||||
|
||||
if resp.StatusCode == http.StatusOK {
|
||||
return respStr, nil
|
||||
}
|
||||
return respStr, fmt.Errorf("Request failed with code %d and error: %s", resp.StatusCode, respStr)
|
||||
}
|
||||
|
||||
func subnetGetReq(client cluster.HTTPClientI, reqURL string, headers map[string]string) (string, error) {
|
||||
r, e := http.NewRequest(http.MethodGet, reqURL, nil)
|
||||
if e != nil {
|
||||
return "", e
|
||||
}
|
||||
return subnetReqDo(client, r, headers)
|
||||
}
|
||||
|
||||
func subnetPostReq(client cluster.HTTPClientI, reqURL string, payload interface{}, headers map[string]string) (string, error) {
|
||||
body, e := json.Marshal(payload)
|
||||
if e != nil {
|
||||
return "", e
|
||||
}
|
||||
r, e := http.NewRequest(http.MethodPost, reqURL, bytes.NewReader(body))
|
||||
if e != nil {
|
||||
return "", e
|
||||
}
|
||||
return subnetReqDo(client, r, headers)
|
||||
}
|
||||
|
||||
func GetClusterRegInfo(admInfo madmin.InfoMessage) mc.ClusterRegistrationInfo {
|
||||
noOfPools := 1
|
||||
noOfDrives := 0
|
||||
for _, srvr := range admInfo.Servers {
|
||||
if srvr.PoolNumber > noOfPools {
|
||||
noOfPools = srvr.PoolNumber
|
||||
}
|
||||
noOfDrives += len(srvr.Disks)
|
||||
}
|
||||
|
||||
totalSpace, usedSpace := getDriveSpaceInfo(admInfo)
|
||||
|
||||
return mc.ClusterRegistrationInfo{
|
||||
DeploymentID: admInfo.DeploymentID,
|
||||
ClusterName: admInfo.DeploymentID,
|
||||
UsedCapacity: admInfo.Usage.Size,
|
||||
Info: mc.ClusterInfo{
|
||||
MinioVersion: admInfo.Servers[0].Version,
|
||||
NoOfServerPools: noOfPools,
|
||||
NoOfServers: len(admInfo.Servers),
|
||||
NoOfDrives: noOfDrives,
|
||||
TotalDriveSpace: totalSpace,
|
||||
UsedDriveSpace: usedSpace,
|
||||
NoOfBuckets: admInfo.Buckets.Count,
|
||||
NoOfObjects: admInfo.Objects.Count,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func getDriveSpaceInfo(admInfo madmin.InfoMessage) (uint64, uint64) {
|
||||
total := uint64(0)
|
||||
used := uint64(0)
|
||||
for _, srvr := range admInfo.Servers {
|
||||
for _, d := range srvr.Disks {
|
||||
total += d.TotalSpace
|
||||
used += d.UsedSpace
|
||||
}
|
||||
}
|
||||
return total, used
|
||||
}
|
||||
Reference in New Issue
Block a user