Product license verification in Console (#518)

Operator UI - Provide and store License key
- New License section in Operator UI will allow user to provide the
  license key via input form
- New License section in Operator UI will allow the user to fetch the
  license key using subnet credentials
-  Console backend has to verify provided license is valid -
   https://godoc.org/github.com/minio/minio/pkg/licverifier#example-package
-  Console backend has to store the license key in k8s secrets

Operator UI - Set license to tenant during provisioning
- Check if license key exists in k8s secret during tenant creation
- If License is present attach the license-key jwt to the new console
tenant via an environment variable

Operator UI - Set license for an existing tenant
- Tenant view will display information about the current status of the
  Tenant License
- If Tenant doesn't have a License then Operator-UI will allow to attach
new license by clicking the Add License button
- Console backend will extract the license from the k8s secret and save
the license-key jwt in the tenant console environment variable and
redeploy
This commit is contained in:
Lenin Alevski
2021-01-12 15:55:07 -06:00
committed by GitHub
parent fd779c2ffa
commit f3bcfc327d
69 changed files with 4293 additions and 221 deletions

View File

@@ -258,6 +258,7 @@ var endpointRules = map[string]ConfigurationActionSet{
var operatorRules = map[string]ConfigurationActionSet{
tenants: tenantsActionSet,
tenantsDetail: tenantsActionSet,
license: licenseActionSet,
}
// operatorOnly ENV variable

View File

@@ -116,7 +116,7 @@ func TestOperatorOnlyEndpoints(t *testing.T) {
"admin:*",
},
},
want: 2,
want: 3,
},
{
name: "Operator Only - all s3 endpoints",
@@ -125,7 +125,7 @@ func TestOperatorOnlyEndpoints(t *testing.T) {
"s3:*",
},
},
want: 2,
want: 3,
},
{
name: "Operator Only - all admin and s3 endpoints",
@@ -135,14 +135,14 @@ func TestOperatorOnlyEndpoints(t *testing.T) {
"s3:*",
},
},
want: 2,
want: 3,
},
{
name: "Operator Only - default endpoints",
args: args{
[]string{},
},
want: 2,
want: 3,
},
}

51
pkg/subnet/config.go Normal file
View File

@@ -0,0 +1,51 @@
// This file is part of MinIO Kubernetes Cloud
// Copyright (c) 2020 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 (
"errors"
"log"
"github.com/minio/minio/pkg/env"
"github.com/minio/minio/pkg/licverifier"
)
// GetSubnetURL
func GetSubnetURL() string {
return env.Get(ConsoleSubnetURL, "https://subnet.min.io")
}
// GetLicenseInfoFromJWT will return license metadata from a jwt string license
func GetLicenseInfoFromJWT(license string, publicKeys []string) (*licverifier.LicenseInfo, error) {
if license == "" {
return nil, errors.New("license is not present")
}
for _, publicKey := range publicKeys {
lv, err := licverifier.NewLicenseVerifier([]byte(publicKey))
if err != nil {
log.Print(err)
continue
}
licInfo, err := lv.Verify(license)
if err != nil {
log.Print(err)
continue
}
return &licInfo, nil
}
return nil, errors.New("invalid license key")
}

88
pkg/subnet/config_test.go Normal file
View File

@@ -0,0 +1,88 @@
// This file is part of MinIO Kubernetes Cloud
// Copyright (c) 2020 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 (
"reflect"
"testing"
"github.com/minio/minio/pkg/licverifier"
)
func TestGetLicenseInfoFromJWT(t *testing.T) {
license := "eyJhbGciOiJFUzM4NCIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJsZW5pbitjMUBtaW5pby5pbyIsInRlYW1OYW1lIjoiY29uc29sZS1jdXN0b21lciIsImV4cCI6MS42Mzk5NTI2MTE2MDkxNDQ3MzJlOSwiaXNzIjoic3VibmV0QG1pbmlvLmlvIiwiY2FwYWNpdHkiOjI1LCJpYXQiOjEuNjA4NDE2NjExNjA5MTQ0NzMyZTksImFjY291bnRJZCI6MTc2LCJzZXJ2aWNlVHlwZSI6IlNUQU5EQVJEIn0.ndtf8V_FJTvhXeemVLlORyDev6RJaSPhZ2djkMVK9SvXD0srR_qlYJATPjC4NljkS71nXMGVDov5uCTuUL97x6FGQEKDruA-z24x_2Zr8kof4LfBb3HUHudCR8QvE--I"
publicKeys := []string{`-----BEGIN PUBLIC KEY-----
MHYwEAYHKoZIzj0CAQYFK4EEACIDYgAEbo+e1wpBY4tBq9AONKww3Kq7m6QP/TBQ
mr/cKCUyBL7rcAvg0zNq1vcSrUSGlAmY3SEDCu3GOKnjG/U4E7+p957ocWSV+mQU
9NKlTdQFGF3+aO6jbQ4hX/S5qPyF+a3z
-----END PUBLIC KEY-----`}
mockLicense, _ := GetLicenseInfoFromJWT(license, publicKeys)
type args struct {
license string
publicKeys []string
}
tests := []struct {
name string
args args
want *licverifier.LicenseInfo
wantErr bool
}{
{
name: "error because missing license",
args: args{
license: "",
publicKeys: OfflinePublicKeys,
},
wantErr: true,
},
{
name: "error because invalid license",
args: args{
license: "eyJhbGciOiJFUzM4NCIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJsZW5pbitjMUBtaW5pby5pbyIsInRlYW1OYW1lIjoiY29uc29sZS1jdXN0b21lciIsImV4cCI6MS42Mzk5NTI2MTE2MDkxNDQ3MzJlOSwiaXNzIjoic3VibmV0QG1pbmlvLmlvIiwiY2FwYWNpdHkiOjI1LCJpYXQiOjEuNjA4NDE2NjExNjA5MTQ0NzMyZTksImFjY291bnRJZCI6MTc2LCJzZXJ2aWNlVHlwZSI6IlNUQU5EQVJEIn0.ndtf8V_FJTvhXeemVLlORyDev6RJaSPhZ2djkMVK9SvXD0srR_qlYJATPjC4NljkS71nXMGVDov5uCTuUL97x6FGQEKDruA-z24x_2Zr8kof4LfBb3HUHudCR8QvE--I",
publicKeys: []string{"eaeaeae"},
},
wantErr: true,
},
{
name: "license successfully verified",
args: args{
license: "eyJhbGciOiJFUzM4NCIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJsZW5pbitjMUBtaW5pby5pbyIsInRlYW1OYW1lIjoiY29uc29sZS1jdXN0b21lciIsImV4cCI6MS42Mzk5NTI2MTE2MDkxNDQ3MzJlOSwiaXNzIjoic3VibmV0QG1pbmlvLmlvIiwiY2FwYWNpdHkiOjI1LCJpYXQiOjEuNjA4NDE2NjExNjA5MTQ0NzMyZTksImFjY291bnRJZCI6MTc2LCJzZXJ2aWNlVHlwZSI6IlNUQU5EQVJEIn0.ndtf8V_FJTvhXeemVLlORyDev6RJaSPhZ2djkMVK9SvXD0srR_qlYJATPjC4NljkS71nXMGVDov5uCTuUL97x6FGQEKDruA-z24x_2Zr8kof4LfBb3HUHudCR8QvE--I",
publicKeys: []string{`-----BEGIN PUBLIC KEY-----
MHYwEAYHKoZIzj0CAQYFK4EEACIDYgAEbo+e1wpBY4tBq9AONKww3Kq7m6QP/TBQ
mr/cKCUyBL7rcAvg0zNq1vcSrUSGlAmY3SEDCu3GOKnjG/U4E7+p957ocWSV+mQU
9NKlTdQFGF3+aO6jbQ4hX/S5qPyF+a3z
-----END PUBLIC KEY-----`},
},
wantErr: false,
want: mockLicense,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := GetLicenseInfoFromJWT(tt.args.license, tt.args.publicKeys)
if (err != nil) != tt.wantErr {
t.Errorf("GetLicenseInfoFromJWT() error = %v, wantErr %v", err, tt.wantErr)
return
}
if !reflect.DeepEqual(got, tt.want) {
t.Errorf("GetLicenseInfoFromJWT() got = %v, want %v", got, tt.want)
}
})
}
}

36
pkg/subnet/const.go Normal file
View File

@@ -0,0 +1,36 @@
// This file is part of MinIO Kubernetes Cloud
// Copyright (c) 2020 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
var (
OfflinePublicKeys = []string{
`-----BEGIN PUBLIC KEY-----
MHYwEAYHKoZIzj0CAQYFK4EEACIDYgAEaK31xujr6/rZ7ZfXZh3SlwovjC+X8wGq
qkltaKyTLRENd4w3IRktYYCRgzpDLPn/nrf7snV/ERO5qcI7fkEES34IVEr+2Uff
JkO2PfyyAYEO/5dBlPh1Undu9WQl6J7B
-----END PUBLIC KEY-----`, // https://subnet.min.io/downloads/license-pubkey.pem
}
)
const (
// Constants for subnet configuration
ConsoleSubnetURL = "CONSOLE_SUBNET_URL"
// Subnet endpoints
publicKey = "/downloads/license-pubkey.pem"
loginEndpoint = "/api/auth/login"
licenseKeyEndpoint = "/api/auth/subscription/license-key"
)

173
pkg/subnet/subnet.go Normal file
View File

@@ -0,0 +1,173 @@
// This file is part of MinIO Console Server
// Copyright (c) 2020 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/json"
"errors"
"fmt"
"io/ioutil"
"log"
"net/http"
"github.com/minio/console/cluster"
"github.com/minio/minio/pkg/licverifier"
)
// subnetLoginRequest body request for subnet login
type subnetLoginRequest struct {
Username string `json:"username"`
Password string `json:"password"`
}
// 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"`
}
// 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
}
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
}
} else if licenseKey != "" {
license = licenseKey
} 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
}

330
pkg/subnet/subnet_test.go Normal file
View File

@@ -0,0 +1,330 @@
// This file is part of MinIO Kubernetes Cloud
// Copyright (c) 2020 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"
"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: "eyJhbGciOiJFUzM4NCIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJsZW5pbitjMUBtaW5pby5pbyIsInRlYW1OYW1lIjoiY29uc29sZS1jdXN0b21lciIsImV4cCI6MS42Mzk5NTI2MTE2MDkxNDQ3MzJlOSwiaXNzIjoic3VibmV0QG1pbmlvLmlvIiwiY2FwYWNpdHkiOjI1LCJpYXQiOjEuNjA4NDE2NjExNjA5MTQ0NzMyZTksImFjY291bnRJZCI6MTc2LCJzZXJ2aWNlVHlwZSI6IlNUQU5EQVJEIn0.ndtf8V_FJTvhXeemVLlORyDev6RJaSPhZ2djkMVK9SvXD0srR_qlYJATPjC4NljkS71nXMGVDov5uCTuUL97x6FGQEKDruA-z24x_2Zr8kof4LfBb3HUHudCR8QvE--I",
wantErr: false,
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) {
// returning test jwt license
return &http.Response{Body: ioutil.NopCloser(bytes.NewReader([]byte("{\"license\":\"eyJhbGciOiJFUzM4NCIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJsZW5pbitjMUBtaW5pby5pbyIsInRlYW1OYW1lIjoiY29uc29sZS1jdXN0b21lciIsImV4cCI6MS42Mzk5NTI2MTE2MDkxNDQ3MzJlOSwiaXNzIjoic3VibmV0QG1pbmlvLmlvIiwiY2FwYWNpdHkiOjI1LCJpYXQiOjEuNjA4NDE2NjExNjA5MTQ0NzMyZTksImFjY291bnRJZCI6MTc2LCJzZXJ2aWNlVHlwZSI6IlNUQU5EQVJEIn0.ndtf8V_FJTvhXeemVLlORyDev6RJaSPhZ2djkMVK9SvXD0srR_qlYJATPjC4NljkS71nXMGVDov5uCTuUL97x6FGQEKDruA-z24x_2Zr8kof4LfBb3HUHudCR8QvE--I\",\"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(bytes.NewReader([]byte(`-----BEGIN PUBLIC KEY-----
MHYwEAYHKoZIzj0CAQYFK4EEACIDYgAEbo+e1wpBY4tBq9AONKww3Kq7m6QP/TBQ
mr/cKCUyBL7rcAvg0zNq1vcSrUSGlAmY3SEDCu3GOKnjG/U4E7+p957ocWSV+mQU
9NKlTdQFGF3+aO6jbQ4hX/S5qPyF+a3z
-----END PUBLIC KEY-----`)))}, nil
}
},
},
{
name: "license validated successfully",
args: args{
client: clientMock,
licenseKey: "eyJhbGciOiJFUzM4NCIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJsZW5pbitjMUBtaW5pby5pbyIsInRlYW1OYW1lIjoiY29uc29sZS1jdXN0b21lciIsImV4cCI6MS42Mzk5NTI2MTE2MDkxNDQ3MzJlOSwiaXNzIjoic3VibmV0QG1pbmlvLmlvIiwiY2FwYWNpdHkiOjI1LCJpYXQiOjEuNjA4NDE2NjExNjA5MTQ0NzMyZTksImFjY291bnRJZCI6MTc2LCJzZXJ2aWNlVHlwZSI6IlNUQU5EQVJEIn0.ndtf8V_FJTvhXeemVLlORyDev6RJaSPhZ2djkMVK9SvXD0srR_qlYJATPjC4NljkS71nXMGVDov5uCTuUL97x6FGQEKDruA-z24x_2Zr8kof4LfBb3HUHudCR8QvE--I",
email: "",
password: "",
},
wantErr: false,
mockFunc: func() {
HTTPGetMock = func(url string) (resp *http.Response, err error) {
return &http.Response{Body: ioutil.NopCloser(bytes.NewReader([]byte(`-----BEGIN PUBLIC KEY-----
MHYwEAYHKoZIzj0CAQYFK4EEACIDYgAEbo+e1wpBY4tBq9AONKww3Kq7m6QP/TBQ
mr/cKCUyBL7rcAvg0zNq1vcSrUSGlAmY3SEDCu3GOKnjG/U4E7+p957ocWSV+mQU
9NKlTdQFGF3+aO6jbQ4hX/S5qPyF+a3z
-----END PUBLIC KEY-----`)))}, nil
}
},
wantLicense: "eyJhbGciOiJFUzM4NCIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJsZW5pbitjMUBtaW5pby5pbyIsInRlYW1OYW1lIjoiY29uc29sZS1jdXN0b21lciIsImV4cCI6MS42Mzk5NTI2MTE2MDkxNDQ3MzJlOSwiaXNzIjoic3VibmV0QG1pbmlvLmlvIiwiY2FwYWNpdHkiOjI1LCJpYXQiOjEuNjA4NDE2NjExNjA5MTQ0NzMyZTksImFjY291bnRJZCI6MTc2LCJzZXJ2aWNlVHlwZSI6IlNUQU5EQVJEIn0.ndtf8V_FJTvhXeemVLlORyDev6RJaSPhZ2djkMVK9SvXD0srR_qlYJATPjC4NljkS71nXMGVDov5uCTuUL97x6FGQEKDruA-z24x_2Zr8kof4LfBb3HUHudCR8QvE--I",
},
}
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 (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)
}
})
}
}