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
331 lines
14 KiB
Go
331 lines
14 KiB
Go
// 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)
|
|
}
|
|
})
|
|
}
|
|
}
|