diff --git a/cluster/cluster.go b/cluster/cluster.go index 3a8b41bae..26372ae72 100644 --- a/cluster/cluster.go +++ b/cluster/cluster.go @@ -1,5 +1,5 @@ // This file is part of MinIO Kubernetes Cloud -// Copyright (c) 2019 MinIO, Inc. +// 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 diff --git a/cluster/config.go b/cluster/config.go index a9cb58d22..79a11ac10 100644 --- a/cluster/config.go +++ b/cluster/config.go @@ -1,5 +1,5 @@ // This file is part of MinIO Kubernetes Cloud -// Copyright (c) 2019 MinIO, Inc. +// 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 @@ -64,13 +64,8 @@ func GetNsFromFile() string { return string(dat) } -// This operation will run only once at console startup -var namespace = GetNsFromFile() - -// Returns the namespace in which the controller is installed -func GetNs() string { - return env.Get(ConsoleNamespace, namespace) -} +// Namespace will run only once at console startup +var Namespace = GetNsFromFile() // getLatestMinIOImage returns the latest docker image for MinIO if found on the internet func getLatestMinIOImage(client HTTPClientI) (*string, error) { diff --git a/cluster/const.go b/cluster/const.go index df325da31..3946e9124 100644 --- a/cluster/const.go +++ b/cluster/const.go @@ -1,5 +1,5 @@ // This file is part of MinIO Kubernetes Cloud -// Copyright (c) 2019 MinIO, Inc. +// 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 @@ -21,5 +21,4 @@ const ( ConsoleK8SAPIServerTLSRootCA = "CONSOLE_K8S_API_SERVER_TLS_ROOT_CA" ConsoleMinioImage = "CONSOLE_MINIO_IMAGE" ConsoleMCImage = "CONSOLE_MC_IMAGE" - ConsoleNamespace = "CONSOLE_NAMESPACE" ) diff --git a/cluster/http_client.go b/cluster/http_client.go index b705810ba..c4821e00c 100644 --- a/cluster/http_client.go +++ b/cluster/http_client.go @@ -17,6 +17,7 @@ package cluster import ( + "io" "net/http" ) @@ -25,6 +26,8 @@ import ( // that are used within this project. type HTTPClientI interface { Get(url string) (resp *http.Response, err error) + Post(url, contentType string, body io.Reader) (resp *http.Response, err error) + Do(req *http.Request) (*http.Response, error) } // HTTPClient Interface implementation @@ -38,3 +41,13 @@ type HTTPClient struct { func (c *HTTPClient) Get(url string) (resp *http.Response, err error) { return c.Client.Get(url) } + +// Post implements http.Client.Post() +func (c *HTTPClient) Post(url, contentType string, body io.Reader) (resp *http.Response, err error) { + return c.Client.Post(url, contentType, body) +} + +// Do implements http.Client.Do() +func (c *HTTPClient) Do(req *http.Request) (*http.Response, error) { + return c.Client.Do(req) +} diff --git a/k8s/tools.go b/k8s/tools.go index 8120623dd..4120e0ee5 100644 --- a/k8s/tools.go +++ b/k8s/tools.go @@ -1,5 +1,5 @@ // This file is part of MinIO Kubernetes Cloud -// Copyright (c) 2019 MinIO, Inc. +// 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 diff --git a/models/license.go b/models/license.go new file mode 100644 index 000000000..0093a56c6 --- /dev/null +++ b/models/license.go @@ -0,0 +1,75 @@ +// Code generated by go-swagger; DO NOT EDIT. + +// 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 . +// + +package models + +// This file was generated by the swagger tool. +// Editing this file might prove futile when you re-run the swagger generate command + +import ( + "github.com/go-openapi/strfmt" + "github.com/go-openapi/swag" +) + +// License license +// +// swagger:model license +type License struct { + + // account id + AccountID int64 `json:"account_id,omitempty"` + + // email + Email string `json:"email,omitempty"` + + // expires at + ExpiresAt string `json:"expires_at,omitempty"` + + // organization + Organization string `json:"organization,omitempty"` + + // plan + Plan string `json:"plan,omitempty"` + + // storage capacity + StorageCapacity int64 `json:"storage_capacity,omitempty"` +} + +// Validate validates this license +func (m *License) Validate(formats strfmt.Registry) error { + return nil +} + +// MarshalBinary interface implementation +func (m *License) MarshalBinary() ([]byte, error) { + if m == nil { + return nil, nil + } + return swag.WriteJSON(m) +} + +// UnmarshalBinary interface implementation +func (m *License) UnmarshalBinary(b []byte) error { + var res License + if err := swag.ReadJSON(b, &res); err != nil { + return err + } + *m = res + return nil +} diff --git a/models/session_response.go b/models/session_response.go index 196292730..1ce6394ca 100644 --- a/models/session_response.go +++ b/models/session_response.go @@ -36,6 +36,9 @@ import ( // swagger:model sessionResponse type SessionResponse struct { + // operator + Operator bool `json:"operator,omitempty"` + // pages Pages []string `json:"pages"` diff --git a/models/subscription_validate_request.go b/models/subscription_validate_request.go new file mode 100644 index 000000000..6cb960b8c --- /dev/null +++ b/models/subscription_validate_request.go @@ -0,0 +1,66 @@ +// Code generated by go-swagger; DO NOT EDIT. + +// 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 . +// + +package models + +// This file was generated by the swagger tool. +// Editing this file might prove futile when you re-run the swagger generate command + +import ( + "github.com/go-openapi/strfmt" + "github.com/go-openapi/swag" +) + +// SubscriptionValidateRequest subscription validate request +// +// swagger:model subscriptionValidateRequest +type SubscriptionValidateRequest struct { + + // email + Email string `json:"email,omitempty"` + + // license + License string `json:"license,omitempty"` + + // password + Password string `json:"password,omitempty"` +} + +// Validate validates this subscription validate request +func (m *SubscriptionValidateRequest) Validate(formats strfmt.Registry) error { + return nil +} + +// MarshalBinary interface implementation +func (m *SubscriptionValidateRequest) MarshalBinary() ([]byte, error) { + if m == nil { + return nil, nil + } + return swag.WriteJSON(m) +} + +// UnmarshalBinary interface implementation +func (m *SubscriptionValidateRequest) UnmarshalBinary(b []byte) error { + var res SubscriptionValidateRequest + if err := swag.ReadJSON(b, &res); err != nil { + return err + } + *m = res + return nil +} diff --git a/models/tenant.go b/models/tenant.go index c9b972a51..7687d950c 100644 --- a/models/tenant.go +++ b/models/tenant.go @@ -62,6 +62,9 @@ type Tenant struct { // pools Pools []*Pool `json:"pools"` + // subnet license + SubnetLicense *License `json:"subnet_license,omitempty"` + // total size TotalSize int64 `json:"total_size,omitempty"` } @@ -74,6 +77,10 @@ func (m *Tenant) Validate(formats strfmt.Registry) error { res = append(res, err) } + if err := m.validateSubnetLicense(formats); err != nil { + res = append(res, err) + } + if len(res) > 0 { return errors.CompositeValidationError(res...) } @@ -105,6 +112,24 @@ func (m *Tenant) validatePools(formats strfmt.Registry) error { return nil } +func (m *Tenant) validateSubnetLicense(formats strfmt.Registry) error { + + if swag.IsZero(m.SubnetLicense) { // not required + return nil + } + + if m.SubnetLicense != nil { + if err := m.SubnetLicense.Validate(formats); err != nil { + if ve, ok := err.(*errors.Validation); ok { + return ve.ValidateName("subnet_license") + } + return err + } + } + + return nil +} + // MarshalBinary interface implementation func (m *Tenant) MarshalBinary() ([]byte, error) { if m == nil { diff --git a/pkg/acl/endpoints.go b/pkg/acl/endpoints.go index a610aa2f6..dea420116 100644 --- a/pkg/acl/endpoints.go +++ b/pkg/acl/endpoints.go @@ -258,6 +258,7 @@ var endpointRules = map[string]ConfigurationActionSet{ var operatorRules = map[string]ConfigurationActionSet{ tenants: tenantsActionSet, tenantsDetail: tenantsActionSet, + license: licenseActionSet, } // operatorOnly ENV variable diff --git a/pkg/acl/endpoints_test.go b/pkg/acl/endpoints_test.go index 66fbb464d..7392ea22a 100644 --- a/pkg/acl/endpoints_test.go +++ b/pkg/acl/endpoints_test.go @@ -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, }, } diff --git a/pkg/subnet/config.go b/pkg/subnet/config.go new file mode 100644 index 000000000..f033b1fe2 --- /dev/null +++ b/pkg/subnet/config.go @@ -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 . + +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") +} diff --git a/pkg/subnet/config_test.go b/pkg/subnet/config_test.go new file mode 100644 index 000000000..e1d821efb --- /dev/null +++ b/pkg/subnet/config_test.go @@ -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 . + +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) + } + }) + } +} diff --git a/pkg/subnet/const.go b/pkg/subnet/const.go new file mode 100644 index 000000000..0aa88d9de --- /dev/null +++ b/pkg/subnet/const.go @@ -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 . + +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" +) diff --git a/pkg/subnet/subnet.go b/pkg/subnet/subnet.go new file mode 100644 index 000000000..f4e627e55 --- /dev/null +++ b/pkg/subnet/subnet.go @@ -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 . +// + +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 +} diff --git a/pkg/subnet/subnet_test.go b/pkg/subnet/subnet_test.go new file mode 100644 index 000000000..6dd183ef0 --- /dev/null +++ b/pkg/subnet/subnet_test.go @@ -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 . + +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) + } + }) + } +} diff --git a/portal-ui/src/ProtectedRoutes.tsx b/portal-ui/src/ProtectedRoutes.tsx index 304aaa681..efc75bd20 100644 --- a/portal-ui/src/ProtectedRoutes.tsx +++ b/portal-ui/src/ProtectedRoutes.tsx @@ -57,7 +57,7 @@ const ProtectedRoute = ({ userLoggedIn(true); setSessionLoading(false); // check for tenants presence, that indicates we are in operator mode - if (res.pages.includes("/tenants")) { + if (res.operator) { consoleOperatorMode(true); document.title = "MinIO Operator"; } diff --git a/portal-ui/src/actions.ts b/portal-ui/src/actions.ts index fee7726ad..886711f39 100644 --- a/portal-ui/src/actions.ts +++ b/portal-ui/src/actions.ts @@ -1,5 +1,5 @@ // This file is part of MinIO Console Server -// Copyright (c) 2019 MinIO, Inc. +// 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 diff --git a/portal-ui/src/common/MinTablePaginationActions.tsx b/portal-ui/src/common/MinTablePaginationActions.tsx index c599b3fc7..ebf9aa342 100644 --- a/portal-ui/src/common/MinTablePaginationActions.tsx +++ b/portal-ui/src/common/MinTablePaginationActions.tsx @@ -1,5 +1,5 @@ // This file is part of MinIO Console Server -// Copyright (c) 2019 MinIO, Inc. +// 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 diff --git a/portal-ui/src/common/api/index.ts b/portal-ui/src/common/api/index.ts index 322e4c84a..bbb518fc7 100644 --- a/portal-ui/src/common/api/index.ts +++ b/portal-ui/src/common/api/index.ts @@ -1,5 +1,5 @@ // This file is part of MinIO Console Server -// Copyright (c) 2019 MinIO, Inc. +// 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 diff --git a/portal-ui/src/icons/DeleteIcon.tsx b/portal-ui/src/icons/DeleteIcon.tsx index 6ef75154a..6f4333575 100644 --- a/portal-ui/src/icons/DeleteIcon.tsx +++ b/portal-ui/src/icons/DeleteIcon.tsx @@ -1,5 +1,5 @@ // This file is part of MinIO Console Server -// Copyright (c) 2019 MinIO, Inc. +// 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 diff --git a/portal-ui/src/icons/PermissionIcon.tsx b/portal-ui/src/icons/PermissionIcon.tsx index 053c28f57..73d3f00f3 100644 --- a/portal-ui/src/icons/PermissionIcon.tsx +++ b/portal-ui/src/icons/PermissionIcon.tsx @@ -1,5 +1,5 @@ // This file is part of MinIO Console Server -// Copyright (c) 2019 MinIO, Inc. +// 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 diff --git a/portal-ui/src/icons/ServiceAccountIcon.tsx b/portal-ui/src/icons/ServiceAccountIcon.tsx index be8ba482e..94d447241 100644 --- a/portal-ui/src/icons/ServiceAccountIcon.tsx +++ b/portal-ui/src/icons/ServiceAccountIcon.tsx @@ -1,5 +1,5 @@ // This file is part of MinIO Console Server -// Copyright (c) 2019 MinIO, Inc. +// 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 diff --git a/portal-ui/src/index.tsx b/portal-ui/src/index.tsx index 67a34e017..588edbe7a 100644 --- a/portal-ui/src/index.tsx +++ b/portal-ui/src/index.tsx @@ -1,5 +1,5 @@ // This file is part of MinIO Console Server -// Copyright (c) 2019 MinIO, Inc. +// 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 diff --git a/portal-ui/src/reducer.ts b/portal-ui/src/reducer.ts index 39876c4c9..52a2da1bd 100644 --- a/portal-ui/src/reducer.ts +++ b/portal-ui/src/reducer.ts @@ -1,5 +1,5 @@ // This file is part of MinIO Console Server -// Copyright (c) 2019 MinIO, Inc. +// 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 diff --git a/portal-ui/src/screens/Console/Buckets/types.tsx b/portal-ui/src/screens/Console/Buckets/types.tsx index ec359bb26..8c571812a 100644 --- a/portal-ui/src/screens/Console/Buckets/types.tsx +++ b/portal-ui/src/screens/Console/Buckets/types.tsx @@ -97,6 +97,12 @@ export interface ChangePasswordRequest { new_secret_key: string; } +export interface SubscriptionActivateRequest { + license: string; + email: string; + password: string; +} + export interface IRemoteBucket { name: string; accessKey: string; diff --git a/portal-ui/src/screens/Console/Configurations/CustomForms/EditConfiguration.tsx b/portal-ui/src/screens/Console/Configurations/CustomForms/EditConfiguration.tsx index c3241c001..c27cce6f6 100644 --- a/portal-ui/src/screens/Console/Configurations/CustomForms/EditConfiguration.tsx +++ b/portal-ui/src/screens/Console/Configurations/CustomForms/EditConfiguration.tsx @@ -1,5 +1,5 @@ // This file is part of MinIO Console Server -// Copyright (c) 2019 MinIO, Inc. +// 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 diff --git a/portal-ui/src/screens/Console/Groups/AddGroup.tsx b/portal-ui/src/screens/Console/Groups/AddGroup.tsx index a56085b24..a652de685 100644 --- a/portal-ui/src/screens/Console/Groups/AddGroup.tsx +++ b/portal-ui/src/screens/Console/Groups/AddGroup.tsx @@ -1,5 +1,5 @@ // This file is part of MinIO Console Server -// Copyright (c) 2019 MinIO, Inc. +// 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 diff --git a/portal-ui/src/screens/Console/Groups/DeleteGroup.tsx b/portal-ui/src/screens/Console/Groups/DeleteGroup.tsx index 002f78b02..979ce9548 100644 --- a/portal-ui/src/screens/Console/Groups/DeleteGroup.tsx +++ b/portal-ui/src/screens/Console/Groups/DeleteGroup.tsx @@ -1,5 +1,5 @@ // This file is part of MinIO Console Server -// Copyright (c) 2019 MinIO, Inc. +// 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 diff --git a/portal-ui/src/screens/Console/Groups/Groups.tsx b/portal-ui/src/screens/Console/Groups/Groups.tsx index a942174ea..3549e6cba 100644 --- a/portal-ui/src/screens/Console/Groups/Groups.tsx +++ b/portal-ui/src/screens/Console/Groups/Groups.tsx @@ -1,5 +1,5 @@ // This file is part of MinIO Console Server -// Copyright (c) 2019 MinIO, Inc. +// 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 diff --git a/portal-ui/src/screens/Console/License/ActivationModal.tsx b/portal-ui/src/screens/Console/License/ActivationModal.tsx new file mode 100644 index 000000000..c54007386 --- /dev/null +++ b/portal-ui/src/screens/Console/License/ActivationModal.tsx @@ -0,0 +1,206 @@ +// 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 . + +import { createStyles, Theme, withStyles } from "@material-ui/core/styles"; +import { containerForHeader } from "../Common/FormComponents/common/styleLibrary"; +import React, { useState } from "react"; +import ModalWrapper from "../Common/ModalWrapper/ModalWrapper"; +import Grid from "@material-ui/core/Grid"; +import Typography from "@material-ui/core/Typography"; +import TextField from "@material-ui/core/TextField"; +import Button from "@material-ui/core/Button"; +import InputBoxWrapper from "../Common/FormComponents/InputBoxWrapper/InputBoxWrapper"; +import { SubscriptionActivateRequest } from "../Buckets/types"; +import api from "../../../common/api"; +import { LinearProgress } from "@material-ui/core"; +import ErrorBlock from "../../shared/ErrorBlock"; + +const styles = (theme: Theme) => + createStyles({ + errorBlock: { + color: "red", + }, + subnetLicenseKey: { + padding: "10px 10px 10px 0px", + borderRight: "1px solid rgba(0, 0, 0, 0.12)", + }, + subnetLoginForm: { + padding: "10px 0px 10px 10px", + }, + licenseKeyField: { + marginBottom: 20, + }, + pageTitle: { + marginBottom: 20, + }, + ...containerForHeader(theme.spacing(4)), + }); + +interface IActivationModal { + classes: any; + open: boolean; + closeModal: () => void; +} + +const ActivationModal = ({ classes, open, closeModal }: IActivationModal) => { + const [license, setLicense] = useState(""); + const [subnetPassword, setSubnetPassword] = useState(""); + const [subnetEmail, setSubnetEmail] = useState(""); + const [error, setError] = useState(""); + const [loading, setLoading] = useState(false); + + const activateProduct = () => { + if (loading) { + return; + } + setLoading(true); + let request: SubscriptionActivateRequest = { + license: license, + email: subnetEmail, + password: subnetPassword, + }; + api + .invoke("POST", "/api/v1/subscription/validate", request) + .then((res) => { + setLoading(false); + setLicense(""); + setSubnetPassword(""); + setSubnetEmail(""); + setError(""); + closeModal(); + }) + .catch((err) => { + setLoading(false); + setLicense(""); + setSubnetPassword(""); + setSubnetEmail(""); + setError(err); + }); + }; + + return open ? ( + { + setLicense(""); + setSubnetPassword(""); + setSubnetEmail(""); + setError(""); + closeModal(); + }} + aria-labelledby="alert-dialog-title" + aria-describedby="alert-dialog-description" + > + {error !== "" && ( + + + + )} + + + + + License Key + + + ) => + setLicense(event.target.value) + } + fullWidth + className={classes.licenseKeyField} + /> + activateProduct()} + disabled={loading || license.trim().length === 0} + > + Activate + + + + + + Subscription Network (SUBNET) + + + + + ) => { + setSubnetEmail(event.target.value); + }} + label="Email" + type="text" + value={subnetEmail} + /> + + + ) => { + setSubnetPassword(event.target.value); + }} + label="Password" + type="password" + value={subnetPassword} + /> + + + activateProduct()} + disabled={ + loading || + subnetEmail.trim().length === 0 || + subnetPassword.trim().length === 0 + } + > + Login + + + + + + {loading && ( + + + + )} + + ) : null; +}; + +export default withStyles(styles)(ActivationModal); diff --git a/portal-ui/src/screens/Console/License/License.tsx b/portal-ui/src/screens/Console/License/License.tsx index 57ab44cfe..a46b1e4cc 100644 --- a/portal-ui/src/screens/Console/License/License.tsx +++ b/portal-ui/src/screens/Console/License/License.tsx @@ -14,7 +14,7 @@ // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . -import React from "react"; +import React, { useEffect, useState } from "react"; import clsx from "clsx"; import Grid from "@material-ui/core/Grid"; import Paper from "@material-ui/core/Paper"; @@ -25,6 +25,18 @@ import CheckCircleIcon from "@material-ui/icons/CheckCircle"; import PageHeader from "../Common/PageHeader/PageHeader"; import { containerForHeader } from "../Common/FormComponents/common/styleLibrary"; import { planDetails, planItems, planButtons } from "./utils"; +import ActivationModal from "./ActivationModal"; +import api from "../../../common/api"; +import { LicenseInfo } from "./types"; +import { LinearProgress } from "@material-ui/core"; +import { AppState } from "../../../store"; +import { connect } from "react-redux"; + +const mapState = (state: AppState) => ({ + operatorMode: state.system.operatorMode, +}); + +const connector = connect(mapState, null); const styles = (theme: Theme) => createStyles({ @@ -137,199 +149,316 @@ const styles = (theme: Theme) => fontSize: 15, fontWeight: 700, }, + licenseButton: { + float: "right", + marginTop: 25, + marginRight: 25, + }, ...containerForHeader(theme.spacing(4)), }); -interface ILicense { +interface ILicenseProps { classes: any; + operatorMode: boolean; } -const License = ({ classes }: ILicense) => { +const License = ({ classes, operatorMode }: ILicenseProps) => { + const closeModalAndFetchLicenseInfo = () => { + setActivateProductModal(false); + fetchLicenseInfo(); + }; + const fetchLicenseInfo = () => { + setLoadingLicenseInfo(true); + api + .invoke("GET", `/api/v1/subscription/info`) + .then((res: LicenseInfo) => { + setLicenseInfo(res); + setLoadingLicenseInfo(false); + }) + .catch((err: any) => { + setLoadingLicenseInfo(false); + }); + }; + + const [activateProductModal, setActivateProductModal] = useState( + false + ); + + const [licenseInfo, setLicenseInfo] = useState(); + const [loadingLicenseInfo, setLoadingLicenseInfo] = useState(true); + + useEffect(() => { + fetchLicenseInfo(); + }, []); + + if (loadingLicenseInfo) { + return ( + + + + ); + } + console.log("operatorMode", operatorMode); return ( - - - - - - - - Upgrade to commercial license - - - - - - {planDetails.map((details: any) => { - return ( - - - {details.title} - - - {details.price} - - - {details.capacityMax || ""} - - - {details.capacityMin} - - - ); - })} + {licenseInfo ? ( + + + + + + + + Subscription Information + + Account ID: {licenseInfo.account_id} + + + Email: {licenseInfo.email} + + + Plan: {licenseInfo.plan} + + + Organization: {licenseInfo.organization} + + + Storage Capacity: {licenseInfo.storage_capacity} + + + Expiration: {licenseInfo.expires_at} - {planItems.map((item: any) => { - return ( - + + + + ) : ( + + + {operatorMode ? ( + setActivateProductModal(true)} + > + Activate Product + + ) : null} + + + + + {operatorMode ? ( + closeModalAndFetchLicenseInfo()} + /> + ) : null} + + + Upgrade to commercial license + + + + + + {planDetails.map((details: any) => { + return ( + + + {details.title} + + + {details.price} + + + {details.capacityMax || ""} + + + {details.capacityMin} + + + ); + })} + + {planItems.map((item: any) => { + return ( + + + {item.field} + + + + {item.community === "N/A" ? ( + "" + ) : item.community === "Yes" ? ( + + ) : ( + item.community + )} + + {item.communityDetail !== undefined && ( + + {item.communityDetail} + + )} + + + + {item.standard === "N/A" ? ( + "" + ) : item.standard === "Yes" ? ( + + ) : ( + item.standard + )} + + {item.standardDetail !== undefined && ( + + {item.standardDetail} + + )} + + + + {item.enterprise === "N/A" ? ( + "" + ) : item.enterprise === "Yes" ? ( + + ) : ( + item.enterprise + )} + + {item.enterpriseDetail !== undefined && ( + + {item.enterpriseDetail} + + )} + + + ); + })} + - {item.field} - - - - {item.community === "N/A" ? ( - "" - ) : item.community === "Yes" ? ( - - ) : ( - item.community - )} - - {item.communityDetail !== undefined && ( - - {item.communityDetail} + /> + {planButtons.map((button: any) => { + return ( + + + {button.text} + - )} - - - - {item.standard === "N/A" ? ( - "" - ) : item.standard === "Yes" ? ( - - ) : ( - item.standard - )} - - {item.standardDetail !== undefined && ( - - {item.standardDetail} - - )} - - - - {item.enterprise === "N/A" ? ( - "" - ) : item.enterprise === "Yes" ? ( - - ) : ( - item.enterprise - )} - - {item.enterpriseDetail !== undefined && ( - - {item.enterpriseDetail} - - )} - + ); + })} - ); - })} - - - {planButtons.map((button: any) => { - return ( - - - {button.text} - - - ); - })} + - + - - - + + + )} ); }; -export default withStyles(styles)(License); +export default connector(withStyles(styles)(License)); diff --git a/portal-ui/src/screens/Console/License/types.tsx b/portal-ui/src/screens/Console/License/types.tsx new file mode 100644 index 000000000..73b9f7c5e --- /dev/null +++ b/portal-ui/src/screens/Console/License/types.tsx @@ -0,0 +1,24 @@ +// 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 . + +export interface LicenseInfo { + account_id: number, + email: string; + expires_at: string; + plan: string; + storage_capacity: number, + organization: string; +} diff --git a/portal-ui/src/screens/Console/License/utils.ts b/portal-ui/src/screens/Console/License/utils.ts index e6f01f03f..4b49c9815 100644 --- a/portal-ui/src/screens/Console/License/utils.ts +++ b/portal-ui/src/screens/Console/License/utils.ts @@ -16,17 +16,20 @@ export const planDetails = [ { + id: 0, title: "Community", price: "Free", capacityMin: "(No minimum)", }, { + id: 1, title: "Standard", price: "$10/TB/month", capacityMax: "Up to 10PB. No additional charges for capacity over 10PB", capacityMin: "(25TB minimum)", }, { + id: 2, title: "Enterprise", price: "$20/TB/month", capacityMax: "Up to 5PB. No additional charges for capacity over 5PB", @@ -36,6 +39,7 @@ export const planDetails = [ export const planItems = [ { + id: 0, field: "License", community: "100% Open Source", communityDetail: "Apache License v2, GNU AGPL v3", @@ -45,18 +49,21 @@ export const planItems = [ enterpriseDetail: "Commercial + Open Source", }, { + id: 1, field: "Software Release", community: "Update to latest", standard: "1 Year Long Term Support", enterprise: "5 Years Long Term Support", }, { + id: 2, field: "SLA", community: "No SLA", standard: "<24 hours", enterprise: "<1 hour", }, { + id: 3, field: "Support", community: "Community:", communityDetail: "Public Slack Channel + Github Issues", @@ -66,36 +73,42 @@ export const planItems = [ enterpriseDetail: "Support via SUBNET", }, { + id: 4, field: "Security Updates & Critical Bugs", community: "Self Update", standard: "Guided Update", enterprise: "Guided Update", }, { + id: 5, field: "Panic Button", community: "N/A", standard: "1 per year", enterprise: "Unlimited", }, { + id: 6, field: "Annual Architecture Review", community: "N/A", standard: "Yes", enterprise: "Yes", }, { + id: 7, field: "Annual Performance Review", community: "N/A", standard: "Yes", enterprise: "Yes", }, { + id: 8, field: "Indemnification", community: "N/A", standard: "N/A", enterprise: "Yes", }, { + id: 9, field: "Security + Policy Review", community: "N/A", standard: "N/A", @@ -105,14 +118,17 @@ export const planItems = [ export const planButtons = [ { + id: 0, text: "Slack Community", link: "https://slack.min.io", }, { + id: 1, text: "Subscribe", link: "https://min.io/pricing", }, { + id: 2, text: "Subscribe", link: "https://min.io/pricing", }, diff --git a/portal-ui/src/screens/Console/NotificationEndopoints/AddNotificationEndpoint.tsx b/portal-ui/src/screens/Console/NotificationEndopoints/AddNotificationEndpoint.tsx new file mode 100644 index 000000000..cdae0f638 --- /dev/null +++ b/portal-ui/src/screens/Console/NotificationEndopoints/AddNotificationEndpoint.tsx @@ -0,0 +1,379 @@ +// 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 . + +import React, { useCallback, useEffect, useState } from "react"; +import get from "lodash/get"; +import { createStyles, Theme, withStyles } from "@material-ui/core/styles"; +import { Button, LinearProgress } from "@material-ui/core"; +import Grid from "@material-ui/core/Grid"; +import ModalWrapper from "../Common/ModalWrapper/ModalWrapper"; +import ConfPostgres from "../Configurations/CustomForms/ConfPostgres"; +import api from "../../../common/api"; +import { serverNeedsRestart } from "../../../actions"; +import { connect } from "react-redux"; +import ConfMySql from "../Configurations/CustomForms/ConfMySql"; +import ConfTargetGeneric from "../Configurations/ConfTargetGeneric"; +import { + notificationEndpointsFields, + notifyPostgres, + notifyMysql, + notifyKafka, + notifyAmqp, + notifyMqtt, + notifyRedis, + notifyNats, + notifyElasticsearch, + notifyWebhooks, + notifyNsq, + removeEmptyFields, +} from "../Configurations/utils"; +import { IElementValue } from "../Configurations/types"; +import { modalBasic } from "../Common/FormComponents/common/styleLibrary"; +import ErrorBlock from "../../shared/ErrorBlock"; + +const styles = (theme: Theme) => + createStyles({ + errorBlock: { + color: "red", + }, + strongText: { + fontWeight: 700, + }, + keyName: { + marginLeft: 5, + }, + buttonContainer: { + textAlign: "right", + }, + logoButton: { + height: "80px", + }, + lambdaNotif: { + backgroundColor: "#fff", + border: "#393939 1px solid", + borderRadius: 5, + width: 101, + height: 91, + display: "flex", + alignItems: "center", + justifyContent: "center", + marginBottom: 16, + cursor: "pointer", + "& img": { + maxWidth: 71, + maxHeight: 71, + }, + }, + iconContainer: { + display: "flex", + flexDirection: "row", + width: 455, + justifyContent: "space-between", + flexWrap: "wrap", + }, + nonIconContainer: { + marginBottom: 16, + "& button": { + marginRight: 16, + }, + }, + pickTitle: { + fontWeight: 600, + color: "#393939", + fontSize: 14, + marginBottom: 16, + }, + lambdaFormIndicator: { + display: "flex", + marginBottom: 40, + }, + lambdaName: { + fontSize: 18, + fontWeight: 700, + color: "#000", + marginBottom: 6, + }, + lambdaSubname: { + fontSize: 12, + color: "#000", + fontWeight: 600, + }, + lambdaIcon: { + borderRadius: 5, + border: "#393939 1px solid", + width: 53, + height: 48, + display: "flex", + justifyContent: "center", + alignItems: "center", + marginRight: 16, + "& img": { + width: 38, + }, + }, + ...modalBasic, + }); + +interface IAddNotificationEndpointProps { + open: boolean; + closeModalAndRefresh: any; + serverNeedsRestart: typeof serverNeedsRestart; + classes: any; +} + +const AddNotificationEndpoint = ({ + open, + closeModalAndRefresh, + serverNeedsRestart, + classes, +}: IAddNotificationEndpointProps) => { + //Local States + const [service, setService] = useState(""); + const [valuesArr, setValueArr] = useState([]); + const [saving, setSaving] = useState(false); + const [addError, setError] = useState(""); + + //Effects + + useEffect(() => { + if (saving) { + const payload = { + key_values: removeEmptyFields(valuesArr), + }; + api + .invoke("PUT", `/api/v1/configs/${service}`, payload) + .then((res) => { + setSaving(false); + setError(""); + serverNeedsRestart(true); + + closeModalAndRefresh(); + }) + .catch((err) => { + setSaving(false); + setError(err); + }); + } + }, [saving, serverNeedsRestart, service, valuesArr, closeModalAndRefresh]); + + //Fetch Actions + const submitForm = (event: React.FormEvent) => { + event.preventDefault(); + setSaving(true); + }; + + const onValueChange = useCallback( + (newValue) => { + setValueArr(newValue); + }, + [setValueArr] + ); + + let srvComponent = ; + switch (service) { + case notifyPostgres: { + srvComponent = ; + break; + } + case notifyMysql: { + srvComponent = ; + break; + } + default: { + const fields = get(notificationEndpointsFields, service, []); + + srvComponent = ( + + ); + } + } + + const servicesList = [ + { + actionTrigger: notifyPostgres, + targetTitle: "Postgres SQL", + logo: "/postgres.png", + }, + { + actionTrigger: notifyKafka, + targetTitle: "Kafka", + logo: "/kafka.png", + }, + { + actionTrigger: notifyAmqp, + targetTitle: "AMQP", + logo: "/amqp.png", + }, + { + actionTrigger: notifyMqtt, + targetTitle: "MQTT", + logo: "/mqtt.png", + }, + { + actionTrigger: notifyRedis, + targetTitle: "Redis", + logo: "/redis.png", + }, + { + actionTrigger: notifyNats, + targetTitle: "NATS", + logo: "/nats.png", + }, + { + actionTrigger: notifyMysql, + targetTitle: "Mysql", + logo: "/mysql.png", + }, + { + actionTrigger: notifyElasticsearch, + targetTitle: "Elastic Search", + logo: "/elasticsearch.png", + }, + { + actionTrigger: notifyWebhooks, + targetTitle: "Webhook", + logo: "", + }, + { + actionTrigger: notifyNsq, + targetTitle: "NSQ", + logo: "", + }, + ]; + + const nonLogos = servicesList.filter((elService) => elService.logo === ""); + const withLogos = servicesList.filter((elService) => elService.logo !== ""); + + const targetElement = servicesList.find( + (element) => element.actionTrigger === service + ); + + const goBack = () => { + setService(""); + }; + + return ( + + {service === "" && ( + + + Pick a supported service: + + {nonLogos.map((item) => { + return ( + { + setService(item.actionTrigger); + }} + > + {item.targetTitle.toUpperCase()} + + ); + })} + + + {withLogos.map((item) => { + return ( + { + setService(item.actionTrigger); + }} + > + + + ); + })} + + + + + + + {saving && ( + + + + )} + + )} + {service !== "" && ( + + {addError !== "" && ( + + + + )} + + + {targetElement && targetElement.logo !== "" && ( + + + + )} + + + + {targetElement ? targetElement.targetTitle : ""} + + + Add Lambda Notification Target + + + + + {srvComponent} + + + + Back + + + Save + + + + + + )} + + ); +}; + +const connector = connect(null, { serverNeedsRestart }); + +export default connector(withStyles(styles)(AddNotificationEndpoint)); diff --git a/portal-ui/src/screens/Console/Tenants/ListTenants/PoolsMultiSelector.tsx b/portal-ui/src/screens/Console/Tenants/ListTenants/PoolsMultiSelector.tsx index 6e15455e4..1e5fd3e49 100644 --- a/portal-ui/src/screens/Console/Tenants/ListTenants/PoolsMultiSelector.tsx +++ b/portal-ui/src/screens/Console/Tenants/ListTenants/PoolsMultiSelector.tsx @@ -1,5 +1,5 @@ // This file is part of MinIO Console Server -// Copyright (c) 2019 MinIO, Inc. +// 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 diff --git a/portal-ui/src/screens/Console/Tenants/ListTenants/types.ts b/portal-ui/src/screens/Console/Tenants/ListTenants/types.ts index 4c31963d8..2558424d2 100644 --- a/portal-ui/src/screens/Console/Tenants/ListTenants/types.ts +++ b/portal-ui/src/screens/Console/Tenants/ListTenants/types.ts @@ -14,6 +14,8 @@ // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . +import { LicenseInfo } from "../../License/types"; + export interface IPool { name: string; servers: number; @@ -53,6 +55,7 @@ export interface ITenant { pools: IPool[]; // computed capacity: string; + subnet_license: LicenseInfo; } export interface ITenantsResponse { diff --git a/portal-ui/src/screens/Console/Tenants/TenantDetails/TenantDetails.tsx b/portal-ui/src/screens/Console/Tenants/TenantDetails/TenantDetails.tsx index abb29cf66..0592d69e6 100644 --- a/portal-ui/src/screens/Console/Tenants/TenantDetails/TenantDetails.tsx +++ b/portal-ui/src/screens/Console/Tenants/TenantDetails/TenantDetails.tsx @@ -22,7 +22,7 @@ import { modalBasic, } from "../../Common/FormComponents/common/styleLibrary"; import Grid from "@material-ui/core/Grid"; -import { Button } from "@material-ui/core"; +import { Button, Typography } from "@material-ui/core"; import Tabs from "@material-ui/core/Tabs"; import Tab from "@material-ui/core/Tab"; import { CreateIcon } from "../../../../icons"; @@ -39,6 +39,8 @@ import UsageBarWrapper from "../../Common/UsageBarWrapper/UsageBarWrapper"; import UpdateTenantModal from "./UpdateTenantModal"; import PencilIcon from "../../Common/TableWrapper/TableActionIcons/PencilIcon"; import ErrorBlock from "../../../shared/ErrorBlock"; +import { LicenseInfo } from "../../License/types"; +import { Link } from "react-router-dom"; interface ITenantDetailsProps { classes: any; @@ -108,6 +110,9 @@ const styles = (theme: Theme) => height: 12, }, }, + noUnderLine: { + textDecoration: "none", + }, ...modalBasic, ...containerForHeader(theme.spacing(4)), }); @@ -128,6 +133,11 @@ const TenantDetails = ({ classes, match }: ITenantDetailsProps) => { const [usageError, setUsageError] = useState(""); const [usage, setUsage] = useState(0); const [updateMinioVersion, setUpdateMinioVersion] = useState(false); + const [licenseInfo, setLicenseInfo] = useState(); + const [loadingLicenseInfo, setLoadingLicenseInfo] = useState(true); + const [loadingActivateProduct, setLoadingActivateProduct] = useState( + false + ); const tenantName = match.params["tenantName"]; const tenantNamespace = match.params["tenantNamespace"]; @@ -152,6 +162,28 @@ const TenantDetails = ({ classes, match }: ITenantDetailsProps) => { } }; + const activateProduct = (namespace: string, tenant: string) => { + if (loadingActivateProduct) { + return; + } + setLoadingActivateProduct(true); + api + .invoke( + "POST", + `/api/v1/subscription/namespaces/${namespace}/tenants/${tenant}/activate`, + {} + ) + .then(() => { + setLoadingActivateProduct(false); + setError(""); + loadInfo(); + }) + .catch((err) => { + setLoadingActivateProduct(false); + setError(err); + }); + }; + const loadInfo = () => { api .invoke( @@ -210,9 +242,23 @@ const TenantDetails = ({ classes, match }: ITenantDetailsProps) => { }); }; + const fetchLicenseInfo = () => { + setLoadingLicenseInfo(true); + api + .invoke("GET", `/api/v1/subscription/info`) + .then((res: LicenseInfo) => { + setLicenseInfo(res); + setLoadingLicenseInfo(false); + }) + .catch((err: any) => { + setLoadingLicenseInfo(false); + }); + }; + useEffect(() => { loadInfo(); loadUsage(); + fetchLicenseInfo(); // eslint-disable-next-line react-hooks/exhaustive-deps }, []); @@ -310,6 +356,7 @@ const TenantDetails = ({ classes, match }: ITenantDetailsProps) => { aria-label="tenant-tabs" > + @@ -351,6 +398,79 @@ const TenantDetails = ({ classes, match }: ITenantDetailsProps) => { idField="name" /> )} + {selectedTab === 1 && ( + + + + + {tenant && tenant.subnet_license ? ( + + + Subscription Information + + Account ID: {tenant.subnet_license.account_id} + + + Email: {tenant.subnet_license.email} + + + Plan: {tenant.subnet_license.plan} + + + Organization: {tenant.subnet_license.organization} + + + Storage Capacity:{" "} + {tenant.subnet_license.storage_capacity} + + + Expiration: {tenant.subnet_license.expires_at} + + ) : ( + !loadingLicenseInfo && ( + + {!licenseInfo && ( + { + e.stopPropagation(); + }} + className={classes.noUnderLine} + > + + Activate Product + + + )} + {licenseInfo && tenant && ( + + activateProduct(tenant.namespace, tenant.name) + } + > + Attach License + + )} + + ) + )} + + + + + )} diff --git a/portal-ui/src/screens/Console/Users/AddToGroup.tsx b/portal-ui/src/screens/Console/Users/AddToGroup.tsx index 3adc6a16f..05d3d3dc1 100644 --- a/portal-ui/src/screens/Console/Users/AddToGroup.tsx +++ b/portal-ui/src/screens/Console/Users/AddToGroup.tsx @@ -1,5 +1,5 @@ // This file is part of MinIO Console Server -// Copyright (c) 2019 MinIO, Inc. +// 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 diff --git a/portal-ui/src/screens/Console/Users/AddUser.tsx b/portal-ui/src/screens/Console/Users/AddUser.tsx index 706cf5b68..bc1d94409 100644 --- a/portal-ui/src/screens/Console/Users/AddUser.tsx +++ b/portal-ui/src/screens/Console/Users/AddUser.tsx @@ -1,5 +1,5 @@ // This file is part of MinIO Console Server -// Copyright (c) 2019 MinIO, Inc. +// 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 diff --git a/portal-ui/src/screens/Console/Users/DeleteUser.tsx b/portal-ui/src/screens/Console/Users/DeleteUser.tsx index 8f9c12eee..2781b94a3 100644 --- a/portal-ui/src/screens/Console/Users/DeleteUser.tsx +++ b/portal-ui/src/screens/Console/Users/DeleteUser.tsx @@ -1,5 +1,5 @@ // This file is part of MinIO Console Server -// Copyright (c) 2019 MinIO, Inc. +// 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 diff --git a/portal-ui/src/screens/Console/Users/types.tsx b/portal-ui/src/screens/Console/Users/types.tsx index 70bb0c55c..fbe5e0461 100644 --- a/portal-ui/src/screens/Console/Users/types.tsx +++ b/portal-ui/src/screens/Console/Users/types.tsx @@ -1,5 +1,5 @@ // This file is part of MinIO Console Server -// Copyright (c) 2019 MinIO, Inc. +// 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 diff --git a/portal-ui/src/screens/Console/reducer.ts b/portal-ui/src/screens/Console/reducer.ts index 6354f9832..a8f2e2bec 100644 --- a/portal-ui/src/screens/Console/reducer.ts +++ b/portal-ui/src/screens/Console/reducer.ts @@ -23,6 +23,7 @@ export interface ConsoleState { const initialState: ConsoleState = { session: { + operator: false, status: "", pages: [], }, diff --git a/portal-ui/src/screens/Console/types.ts b/portal-ui/src/screens/Console/types.ts index 107f31283..58bdaeea1 100644 --- a/portal-ui/src/screens/Console/types.ts +++ b/portal-ui/src/screens/Console/types.ts @@ -17,4 +17,5 @@ export interface ISessionResponse { status: string; pages: string[]; + operator: boolean; } diff --git a/portal-ui/src/types.ts b/portal-ui/src/types.ts index 765262961..0b7965f2c 100644 --- a/portal-ui/src/types.ts +++ b/portal-ui/src/types.ts @@ -1,5 +1,5 @@ // This file is part of MinIO Console Server -// Copyright (c) 2019 MinIO, Inc. +// 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 diff --git a/restapi/admin_subscription.go b/restapi/admin_subscription.go new file mode 100644 index 000000000..36bc0afa8 --- /dev/null +++ b/restapi/admin_subscription.go @@ -0,0 +1,273 @@ +// 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 . +// + +package restapi + +import ( + "context" + "fmt" + "log" + "time" + + operator "github.com/minio/operator/pkg/apis/minio.min.io/v1" + + "github.com/minio/console/pkg/acl" + + "github.com/minio/console/cluster" + "github.com/minio/console/pkg/subnet" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + "github.com/go-openapi/runtime/middleware" + "github.com/minio/console/models" + "github.com/minio/console/restapi/operations" + "github.com/minio/console/restapi/operations/admin_api" +) + +func registerSubscriptionHandlers(api *operations.ConsoleAPI) { + // Validate subscription handler + api.AdminAPISubscriptionValidateHandler = admin_api.SubscriptionValidateHandlerFunc(func(params admin_api.SubscriptionValidateParams, session *models.Principal) middleware.Responder { + license, err := getSubscriptionValidateResponse(session, params.Body) + if err != nil { + return admin_api.NewSubscriptionValidateDefault(int(err.Code)).WithPayload(err) + } + return admin_api.NewSubscriptionValidateOK().WithPayload(license) + }) + // Activate license subscription for a particular tenant + api.AdminAPISubscriptionActivateHandler = admin_api.SubscriptionActivateHandlerFunc(func(params admin_api.SubscriptionActivateParams, session *models.Principal) middleware.Responder { + err := getSubscriptionActivateResponse(session, params.Namespace, params.Tenant) + if err != nil { + return admin_api.NewSubscriptionActivateDefault(int(err.Code)).WithPayload(err) + } + return admin_api.NewSubscriptionActivateNoContent() + }) + // Get subscription information handler + api.AdminAPISubscriptionInfoHandler = admin_api.SubscriptionInfoHandlerFunc(func(params admin_api.SubscriptionInfoParams, session *models.Principal) middleware.Responder { + license, err := getSubscriptionInfoResponse(session) + if err != nil { + return admin_api.NewSubscriptionInfoDefault(int(err.Code)).WithPayload(err) + } + return admin_api.NewSubscriptionInfoOK().WithPayload(license) + }) +} + +// addSubscriptionLicenseToTenant replace existing console tenant secret and adds the subnet license key +func addSubscriptionLicenseToTenant(ctx context.Context, clientSet K8sClientI, license, namespace, tenantName, secretName string) error { + // Retrieve console secret for Tenant + consoleSecret, err := clientSet.getSecret(ctx, namespace, secretName, metav1.GetOptions{}) + if err != nil { + return err + } + // Copy current console secret + dataNewSecret := consoleSecret.Data + // Add subnet license to the new console secret + dataNewSecret[ConsoleSubnetLicense] = []byte(license) + // Delete existing console secret + err = clientSet.deleteSecret(ctx, cluster.Namespace, secretName, metav1.DeleteOptions{}) + if err != nil { + return err + } + // Prepare the new Console Secret + imm := true + newConsoleSecret := &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: secretName, + Labels: map[string]string{ + operator.TenantLabel: tenantName, + }, + }, + Immutable: &imm, + Data: dataNewSecret, + } + // Create new Console secret with the subnet License + _, err = clientSet.createSecret(ctx, cluster.Namespace, newConsoleSecret, metav1.CreateOptions{}) + if err != nil { + return err + } + // restart Console pods based on label: + // v1.min.io/console: TENANT-console + err = clientSet.deletePodCollection(ctx, namespace, metav1.DeleteOptions{}, metav1.ListOptions{ + LabelSelector: fmt.Sprintf("%s=%s%s", operator.ConsoleTenantLabel, tenantName, operator.ConsoleName), + }) + if err != nil { + return err + } + return nil +} + +func getSubscriptionActivateResponse(session *models.Principal, namespace, tenant string) *models.Error { + // 20 seconds timeout + ctx, cancel := context.WithTimeout(context.Background(), 20*time.Second) + defer cancel() + opClientClientSet, err := cluster.OperatorClient(session.STSSessionToken) + if err != nil { + return prepareError(errorGeneric, nil, err) + } + clientSet, err := cluster.K8sClient(session.STSSessionToken) + if err != nil { + return prepareError(errorGeneric, nil, err) + } + opClient := &operatorClient{ + client: opClientClientSet, + } + minTenant, err := getTenant(ctx, opClient, namespace, tenant) + if err != nil { + return prepareError(err, errorGeneric) + } + // If console is not deployed for this tenant return an error + if minTenant.Spec.Console == nil { + return prepareError(errorGenericNotFound) + } + + // configure kubernetes client + k8sClient := k8sClient{ + client: clientSet, + } + // Get cluster subscription license + license, err := getSubscriptionLicense(ctx, &k8sClient, cluster.Namespace, OperatorSubnetLicenseSecretName) + if err != nil { + return prepareError(errInvalidCredentials, nil, err) + } + // add subscription license to existing console Tenant + if err = addSubscriptionLicenseToTenant(ctx, &k8sClient, license, namespace, tenant, minTenant.Spec.Console.ConsoleSecret.Name); err != nil { + return prepareError(err, errorGeneric) + } + return nil +} + +// saveSubscriptionLicense will create or replace an existing subnet license secret in the k8s cluster +func saveSubscriptionLicense(ctx context.Context, clientSet K8sClientI, license string) error { + // Delete subnet license secret if exists + err := clientSet.deleteSecret(ctx, cluster.Namespace, OperatorSubnetLicenseSecretName, metav1.DeleteOptions{}) + if err != nil { + // log the error if any and continue + log.Println(err) + } + // Save subnet license in k8s secrets + imm := true + licenseSecret := &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: OperatorSubnetLicenseSecretName, + }, + Immutable: &imm, + Data: map[string][]byte{ + ConsoleSubnetLicense: []byte(license), + }, + } + _, err = clientSet.createSecret(ctx, cluster.Namespace, licenseSecret, metav1.CreateOptions{}) + if err != nil { + return err + } + return nil +} + +// subscriptionValidate will validate the provided jwt license against the subnet public key +func subscriptionValidate(client cluster.HTTPClientI, license, email, password string) (*models.License, string, error) { + licenseInfo, rawLicense, err := subnet.ValidateLicense(client, license, email, password) + if err != nil { + return nil, "", err + } + return &models.License{ + Email: licenseInfo.Email, + AccountID: licenseInfo.AccountID, + StorageCapacity: licenseInfo.StorageCapacity, + Plan: licenseInfo.ServiceType, + ExpiresAt: licenseInfo.ExpiresAt.String(), + Organization: licenseInfo.TeamName, + }, rawLicense, nil +} + +// getSubscriptionValidateResponse +func getSubscriptionValidateResponse(session *models.Principal, params *models.SubscriptionValidateRequest) (*models.License, *models.Error) { + // 20 seconds timeout + ctx, cancel := context.WithTimeout(context.Background(), 20*time.Second) + defer cancel() + + client := &cluster.HTTPClient{ + Client: GetConsoleSTSClient(), + } + // validate license key + licenseInfo, license, err := subscriptionValidate(client, params.License, params.Email, params.Password) + if err != nil { + return nil, prepareError(errInvalidLicense, nil, err) + } + // configure kubernetes client + clientSet, err := cluster.K8sClient(session.STSSessionToken) + k8sClient := k8sClient{ + client: clientSet, + } + if err != nil { + return nil, prepareError(errorGeneric, nil, err) + } + // save license key to k8s + if err = saveSubscriptionLicense(ctx, &k8sClient, license); err != nil { + return nil, prepareError(errorGeneric, nil, err) + } + return licenseInfo, nil +} + +// getSubscriptionLicense will retrieve stored license jwt from k8s secret +func getSubscriptionLicense(ctx context.Context, clientSet K8sClientI, namespace, secretName string) (string, error) { + // retrieve license stored in k8s + licenseSecret, err := clientSet.getSecret(ctx, namespace, secretName, metav1.GetOptions{}) + if err != nil { + return "", err + } + license, ok := licenseSecret.Data[ConsoleSubnetLicense] + if !ok { + log.Println("subnet secret doesn't contain jwt license") + return "", errorGeneric + } + return string(license), nil +} + +// getSubscriptionInfoResponse returns information about the current configured subnet license for Console +func getSubscriptionInfoResponse(session *models.Principal) (*models.License, *models.Error) { + // 20 seconds timeout + ctx, cancel := context.WithTimeout(context.Background(), 20*time.Second) + defer cancel() + var licenseInfo *models.License + var license string + client := &cluster.HTTPClient{ + Client: GetConsoleSTSClient(), + } + // If Console is running in operator mode retrieve License stored in K8s secrets + if acl.GetOperatorMode() { + // configure kubernetes client + clientSet, err := cluster.K8sClient(session.STSSessionToken) + if err != nil { + return nil, prepareError(errInvalidLicense, nil, err) + } + k8sClient := k8sClient{ + client: clientSet, + } + // Get cluster subscription license + license, err = getSubscriptionLicense(ctx, &k8sClient, cluster.Namespace, OperatorSubnetLicenseSecretName) + if err != nil { + return nil, prepareError(errLicenseNotFound, nil, err) + } + } else { + // If Console is running in Tenant Admin mode retrieve license from env variable + license = GetSubnetLicense() + } + // validate license key and obtain license info + licenseInfo, _, err := subscriptionValidate(client, license, "", "") + if err != nil { + return nil, prepareError(errLicenseNotFound, nil, err) + } + return licenseInfo, nil +} diff --git a/restapi/admin_subscription_test.go b/restapi/admin_subscription_test.go new file mode 100644 index 000000000..dca0bb276 --- /dev/null +++ b/restapi/admin_subscription_test.go @@ -0,0 +1,345 @@ +// 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 . + +package restapi + +import ( + "context" + "testing" + + "errors" + + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +func Test_addSubscriptionLicenseToTenant(t *testing.T) { + k8sClient := k8sClientMock{} + type args struct { + ctx context.Context + clientSet K8sClientI + license string + namespace string + tenantName string + secretName string + } + tests := []struct { + name string + args args + wantErr bool + mockFunc func() + }{ + { + name: "error because subnet license doesnt exists", + args: args{ + ctx: context.Background(), + clientSet: k8sClient, + license: "", + namespace: "", + tenantName: "", + secretName: "subnet-license", + }, + wantErr: true, + mockFunc: func() { + k8sclientGetSecretMock = func(ctx context.Context, namespace, secretName string, opts metav1.GetOptions) (*corev1.Secret, error) { + return nil, errors.New("something went wrong") + } + }, + }, + { + name: "error because existing license could not be deleted", + args: args{ + ctx: context.Background(), + clientSet: k8sClient, + license: "", + namespace: "", + tenantName: "", + secretName: OperatorSubnetLicenseSecretName, + }, + wantErr: true, + mockFunc: func() { + k8sclientGetSecretMock = func(ctx context.Context, namespace, secretName string, opts metav1.GetOptions) (*corev1.Secret, error) { + imm := true + return &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: OperatorSubnetLicenseSecretName, + }, + Immutable: &imm, + Data: map[string][]byte{ + ConsoleSubnetLicense: []byte("eyJhbGciOiJFUzM4NCIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJsZW5pbitjMUBtaW5pby5pbyIsInRlYW1OYW1lIjoiY29uc29sZS1jdXN0b21lciIsImV4cCI6MS42Mzk5NTI2MTE2MDkxNDQ3MzJlOSwiaXNzIjoic3VibmV0QG1pbmlvLmlvIiwiY2FwYWNpdHkiOjI1LCJpYXQiOjEuNjA4NDE2NjExNjA5MTQ0NzMyZTksImFjY291bnRJZCI6MTc2LCJzZXJ2aWNlVHlwZSI6IlNUQU5EQVJEIn0.ndtf8V_FJTvhXeemVLlORyDev6RJaSPhZ2djkMVK9SvXD0srR_qlYJATPjC4NljkS71nXMGVDov5uCTuUL97x6FGQEKDruA-z24x_2Zr8kof4LfBb3HUHudCR8QvE--I"), + }, + }, nil + } + DeleteSecretMock = func(ctx context.Context, namespace string, name string, opts metav1.DeleteOptions) error { + return errors.New("something went wrong") + } + }, + }, + { + name: "error because unable to create new subnet license", + args: args{ + ctx: context.Background(), + clientSet: k8sClient, + license: "", + namespace: "", + tenantName: "", + secretName: OperatorSubnetLicenseSecretName, + }, + wantErr: true, + mockFunc: func() { + k8sclientGetSecretMock = func(ctx context.Context, namespace, secretName string, opts metav1.GetOptions) (*corev1.Secret, error) { + imm := true + return &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: OperatorSubnetLicenseSecretName, + }, + Immutable: &imm, + Data: map[string][]byte{ + ConsoleSubnetLicense: []byte("eyJhbGciOiJFUzM4NCIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJsZW5pbitjMUBtaW5pby5pbyIsInRlYW1OYW1lIjoiY29uc29sZS1jdXN0b21lciIsImV4cCI6MS42Mzk5NTI2MTE2MDkxNDQ3MzJlOSwiaXNzIjoic3VibmV0QG1pbmlvLmlvIiwiY2FwYWNpdHkiOjI1LCJpYXQiOjEuNjA4NDE2NjExNjA5MTQ0NzMyZTksImFjY291bnRJZCI6MTc2LCJzZXJ2aWNlVHlwZSI6IlNUQU5EQVJEIn0.ndtf8V_FJTvhXeemVLlORyDev6RJaSPhZ2djkMVK9SvXD0srR_qlYJATPjC4NljkS71nXMGVDov5uCTuUL97x6FGQEKDruA-z24x_2Zr8kof4LfBb3HUHudCR8QvE--I"), + }, + }, nil + } + DeleteSecretMock = func(ctx context.Context, namespace string, name string, opts metav1.DeleteOptions) error { + return nil + } + CreateSecretMock = func(ctx context.Context, namespace string, secret *corev1.Secret, opts metav1.CreateOptions) (*corev1.Secret, error) { + return nil, errors.New("something went wrong") + } + }, + }, + { + name: "error because unable to delete pod collection", + args: args{ + ctx: context.Background(), + clientSet: k8sClient, + license: "", + namespace: "", + tenantName: "", + secretName: OperatorSubnetLicenseSecretName, + }, + wantErr: true, + mockFunc: func() { + k8sclientGetSecretMock = func(ctx context.Context, namespace, secretName string, opts metav1.GetOptions) (*corev1.Secret, error) { + imm := true + return &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: OperatorSubnetLicenseSecretName, + }, + Immutable: &imm, + Data: map[string][]byte{ + ConsoleSubnetLicense: []byte("eyJhbGciOiJFUzM4NCIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJsZW5pbitjMUBtaW5pby5pbyIsInRlYW1OYW1lIjoiY29uc29sZS1jdXN0b21lciIsImV4cCI6MS42Mzk5NTI2MTE2MDkxNDQ3MzJlOSwiaXNzIjoic3VibmV0QG1pbmlvLmlvIiwiY2FwYWNpdHkiOjI1LCJpYXQiOjEuNjA4NDE2NjExNjA5MTQ0NzMyZTksImFjY291bnRJZCI6MTc2LCJzZXJ2aWNlVHlwZSI6IlNUQU5EQVJEIn0.ndtf8V_FJTvhXeemVLlORyDev6RJaSPhZ2djkMVK9SvXD0srR_qlYJATPjC4NljkS71nXMGVDov5uCTuUL97x6FGQEKDruA-z24x_2Zr8kof4LfBb3HUHudCR8QvE--I"), + }, + }, nil + } + DeleteSecretMock = func(ctx context.Context, namespace string, name string, opts metav1.DeleteOptions) error { + return nil + } + CreateSecretMock = func(ctx context.Context, namespace string, secret *corev1.Secret, opts metav1.CreateOptions) (*corev1.Secret, error) { + return nil, nil + } + DeletePodCollectionMock = func(ctx context.Context, namespace string, opts metav1.DeleteOptions, listOpts metav1.ListOptions) error { + return errors.New("something went wrong") + } + }, + }, + { + name: "subscription updated successfully", + args: args{ + ctx: context.Background(), + clientSet: k8sClient, + license: "", + namespace: "", + tenantName: "", + secretName: OperatorSubnetLicenseSecretName, + }, + wantErr: false, + mockFunc: func() { + k8sclientGetSecretMock = func(ctx context.Context, namespace, secretName string, opts metav1.GetOptions) (*corev1.Secret, error) { + imm := true + return &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: OperatorSubnetLicenseSecretName, + }, + Immutable: &imm, + Data: map[string][]byte{ + ConsoleSubnetLicense: []byte("eyJhbGciOiJFUzM4NCIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJsZW5pbitjMUBtaW5pby5pbyIsInRlYW1OYW1lIjoiY29uc29sZS1jdXN0b21lciIsImV4cCI6MS42Mzk5NTI2MTE2MDkxNDQ3MzJlOSwiaXNzIjoic3VibmV0QG1pbmlvLmlvIiwiY2FwYWNpdHkiOjI1LCJpYXQiOjEuNjA4NDE2NjExNjA5MTQ0NzMyZTksImFjY291bnRJZCI6MTc2LCJzZXJ2aWNlVHlwZSI6IlNUQU5EQVJEIn0.ndtf8V_FJTvhXeemVLlORyDev6RJaSPhZ2djkMVK9SvXD0srR_qlYJATPjC4NljkS71nXMGVDov5uCTuUL97x6FGQEKDruA-z24x_2Zr8kof4LfBb3HUHudCR8QvE--I"), + }, + }, nil + } + DeleteSecretMock = func(ctx context.Context, namespace string, name string, opts metav1.DeleteOptions) error { + return nil + } + CreateSecretMock = func(ctx context.Context, namespace string, secret *corev1.Secret, opts metav1.CreateOptions) (*corev1.Secret, error) { + return nil, nil + } + DeletePodCollectionMock = func(ctx context.Context, namespace string, opts metav1.DeleteOptions, listOpts metav1.ListOptions) error { + return nil + } + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if tt.mockFunc != nil { + tt.mockFunc() + } + if err := addSubscriptionLicenseToTenant(tt.args.ctx, tt.args.clientSet, tt.args.license, tt.args.namespace, tt.args.tenantName, tt.args.secretName); (err != nil) != tt.wantErr { + t.Errorf("addSubscriptionLicenseToTenant() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} + +func Test_saveSubscriptionLicense(t *testing.T) { + k8sClient := k8sClientMock{} + type args struct { + ctx context.Context + clientSet K8sClientI + license string + } + tests := []struct { + name string + args args + wantErr bool + mockFunc func() + }{ + { + name: "error deleting existing secret", + args: args{ + ctx: context.Background(), + clientSet: k8sClient, + license: "1111111111", + }, + mockFunc: func() { + DeleteSecretMock = func(ctx context.Context, namespace string, name string, opts metav1.DeleteOptions) error { + return nil + } + CreateSecretMock = func(ctx context.Context, namespace string, secret *corev1.Secret, opts metav1.CreateOptions) (*corev1.Secret, error) { + return nil, errors.New("something went wrong") + } + }, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if tt.mockFunc != nil { + tt.mockFunc() + } + if err := saveSubscriptionLicense(tt.args.ctx, tt.args.clientSet, tt.args.license); (err != nil) != tt.wantErr { + t.Errorf("saveSubscriptionLicense() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} + +func Test_getSubscriptionLicense(t *testing.T) { + license := "eyJhbGciOiJFUzM4NCIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJsZW5pbitjMUBtaW5pby5pbyIsInRlYW1OYW1lIjoiY29uc29sZS1jdXN0b21lciIsImV4cCI6MS42Mzk5NTI2MTE2MDkxNDQ3MzJlOSwiaXNzIjoic3VibmV0QG1pbmlvLmlvIiwiY2FwYWNpdHkiOjI1LCJpYXQiOjEuNjA4NDE2NjExNjA5MTQ0NzMyZTksImFjY291bnRJZCI6MTc2LCJzZXJ2aWNlVHlwZSI6IlNUQU5EQVJEIn0.ndtf8V_FJTvhXeemVLlORyDev6RJaSPhZ2djkMVK9SvXD0srR_qlYJATPjC4NljkS71nXMGVDov5uCTuUL97x6FGQEKDruA-z24x_2Zr8kof4LfBb3HUHudCR8QvE--I" + k8sClient := k8sClientMock{} + type args struct { + ctx context.Context + clientSet K8sClientI + namespace string + secretName string + } + tests := []struct { + name string + args args + want string + wantErr bool + mockFunc func() + }{ + { + name: "error because subscription license doesnt exists", + args: args{ + ctx: context.Background(), + clientSet: k8sClient, + namespace: "namespace", + secretName: OperatorSubnetLicenseSecretName, + }, + wantErr: true, + mockFunc: func() { + k8sclientGetSecretMock = func(ctx context.Context, namespace, secretName string, opts metav1.GetOptions) (*corev1.Secret, error) { + return nil, errors.New("something went wrong") + } + }, + }, + { + name: "error because license field doesnt exist in k8s secret", + args: args{ + ctx: context.Background(), + clientSet: k8sClient, + namespace: "namespace", + secretName: OperatorSubnetLicenseSecretName, + }, + wantErr: true, + mockFunc: func() { + k8sclientGetSecretMock = func(ctx context.Context, namespace, secretName string, opts metav1.GetOptions) (*corev1.Secret, error) { + imm := true + return &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: OperatorSubnetLicenseSecretName, + }, + Immutable: &imm, + Data: map[string][]byte{ + //ConsoleSubnetLicense: []byte(license), + }, + }, nil + } + }, + }, + { + name: "license obtained successfully", + args: args{ + ctx: context.Background(), + clientSet: k8sClient, + namespace: "namespace", + secretName: OperatorSubnetLicenseSecretName, + }, + wantErr: false, + want: license, + mockFunc: func() { + k8sclientGetSecretMock = func(ctx context.Context, namespace, secretName string, opts metav1.GetOptions) (*corev1.Secret, error) { + imm := true + return &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: OperatorSubnetLicenseSecretName, + }, + Immutable: &imm, + Data: map[string][]byte{ + ConsoleSubnetLicense: []byte(license), + }, + }, nil + } + }, + }, + } + for _, tt := range tests { + if tt.mockFunc != nil { + tt.mockFunc() + } + t.Run(tt.name, func(t *testing.T) { + got, err := getSubscriptionLicense(tt.args.ctx, tt.args.clientSet, tt.args.namespace, tt.args.secretName) + if (err != nil) != tt.wantErr { + t.Errorf("getSubscriptionLicense() error = %v, wantErr %v", err, tt.wantErr) + return + } + if got != tt.want { + t.Errorf("getSubscriptionLicense() got = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/restapi/admin_tenants.go b/restapi/admin_tenants.go index a7e63c1c0..2506bac37 100644 --- a/restapi/admin_tenants.go +++ b/restapi/admin_tenants.go @@ -335,7 +335,6 @@ func getTenantInfoResponse(session *models.Principal, params admin_api.TenantInf // 5 seconds timeout ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() - opClientClientSet, err := cluster.OperatorClient(session.STSSessionToken) if err != nil { return nil, prepareError(err) @@ -351,6 +350,29 @@ func getTenantInfoResponse(session *models.Principal, params admin_api.TenantInf } info := getTenantInfo(minTenant) + + if minTenant.Spec.Console != nil { + clientSet, err := cluster.K8sClient(session.STSSessionToken) + k8sClient := k8sClient{ + client: clientSet, + } + if err != nil { + return nil, prepareError(err) + } + // obtain current subnet license for tenant (if exists) + license, _ := getSubscriptionLicense(context.Background(), &k8sClient, params.Namespace, minTenant.Spec.Console.ConsoleSecret.Name) + if license != "" { + client := &cluster.HTTPClient{ + Client: GetConsoleSTSClient(), + } + licenseInfo, _, _ := subscriptionValidate(client, license, "", "") + // if licenseInfo is present attach it to the tenantInfo response + if licenseInfo != nil { + info.SubnetLicense = licenseInfo + } + } + } + return info, nil } @@ -511,13 +533,13 @@ func getTenantCreatedResponse(session *models.Principal, params admin_api.Create } }() - var envrionmentVariables []corev1.EnvVar + var environmentVariables []corev1.EnvVar // Check the Erasure Coding Parity for validity and pass it to Tenant if tenantReq.ErasureCodingParity > 0 { if tenantReq.ErasureCodingParity < 2 || tenantReq.ErasureCodingParity > 8 { return nil, prepareError(errorInvalidErasureCodingValue) } - envrionmentVariables = append(envrionmentVariables, corev1.EnvVar{ + environmentVariables = append(environmentVariables, corev1.EnvVar{ Name: "MINIO_STORAGE_CLASS_STANDARD", Value: fmt.Sprintf("EC:%d", tenantReq.ErasureCodingParity), }) @@ -535,7 +557,7 @@ func getTenantCreatedResponse(session *models.Principal, params admin_api.Create CredsSecret: &corev1.LocalObjectReference{ Name: secretName, }, - Env: envrionmentVariables, + Env: environmentVariables, }, } idpEnabled := false @@ -653,6 +675,19 @@ func getTenantCreatedResponse(session *models.Principal, params admin_api.Create consoleSecretName := fmt.Sprintf("%s-secret", consoleSelector) consoleAccess = RandomCharString(16) consoleSecret = RandomCharString(32) + + consoleSecretData := map[string][]byte{ + "CONSOLE_PBKDF_PASSPHRASE": []byte(RandomCharString(16)), + "CONSOLE_PBKDF_SALT": []byte(RandomCharString(8)), + "CONSOLE_ACCESS_KEY": []byte(consoleAccess), + "CONSOLE_SECRET_KEY": []byte(consoleSecret), + } + // If Subnet License is present in k8s secrets, copy that to the CONSOLE_SUBNET_LICENSE env variable + // of the console tenant + license, _ := getSubscriptionLicense(ctx, &k8sClient, cluster.Namespace, OperatorSubnetLicenseSecretName) + if license != "" { + consoleSecretData[ConsoleSubnetLicense] = []byte(license) + } imm := true instanceSecret := corev1.Secret{ ObjectMeta: metav1.ObjectMeta{ @@ -662,10 +697,7 @@ func getTenantCreatedResponse(session *models.Principal, params admin_api.Create }, }, Immutable: &imm, - Data: map[string][]byte{ - "CONSOLE_PBKDF_PASSPHRASE": []byte(RandomCharString(16)), - "CONSOLE_PBKDF_SALT": []byte(RandomCharString(8)), - }, + Data: consoleSecretData, } minInst.Spec.Console = &operator.ConsoleConfiguration{ diff --git a/restapi/admin_tenants_test.go b/restapi/admin_tenants_test.go index cc0e3c984..ba5f727b1 100644 --- a/restapi/admin_tenants_test.go +++ b/restapi/admin_tenants_test.go @@ -21,6 +21,7 @@ import ( "context" "encoding/json" "errors" + "io" "io/ioutil" "net/http" "reflect" @@ -47,6 +48,8 @@ var opClientTenantGetMock func(ctx context.Context, namespace string, tenantName var opClientTenantPatchMock func(ctx context.Context, namespace string, tenantName string, pt types.PatchType, data []byte, options metav1.PatchOptions) (*v1.Tenant, error) var opClientTenantListMock func(ctx context.Context, namespace string, opts metav1.ListOptions) (*v1.TenantList, error) var httpClientGetMock func(url string) (resp *http.Response, err error) +var httpClientPostMock func(url, contentType string, body io.Reader) (resp *http.Response, err error) +var httpClientDoMock func(req *http.Request) (*http.Response, error) var k8sclientGetSecretMock func(ctx context.Context, namespace, secretName string, opts metav1.GetOptions) (*corev1.Secret, error) var k8sclientGetServiceMock func(ctx context.Context, namespace, serviceName string, opts metav1.GetOptions) (*corev1.Service, error) @@ -75,6 +78,16 @@ func (h httpClientMock) Get(url string) (resp *http.Response, err error) { return httpClientGetMock(url) } +// mock function of post() +func (h httpClientMock) Post(url, contentType string, body io.Reader) (resp *http.Response, err error) { + return httpClientPostMock(url, contentType, body) +} + +// mock function of Do() +func (h httpClientMock) Do(req *http.Request) (*http.Response, error) { + return httpClientDoMock(req) +} + func (c k8sClientMock) getSecret(ctx context.Context, namespace, secretName string, opts metav1.GetOptions) (*corev1.Secret, error) { return k8sclientGetSecretMock(ctx, namespace, secretName, opts) } diff --git a/restapi/config.go b/restapi/config.go index fa2f0dae3..9653d66ef 100644 --- a/restapi/config.go +++ b/restapi/config.go @@ -220,6 +220,11 @@ func getSecureExpectCTHeader() string { return env.Get(ConsoleSecureExpectCTHeader, "") } +// GetSubnetLicense returns the current subnet jwt license +func GetSubnetLicense() string { + return env.Get(ConsoleSubnetLicense, "") +} + var ( // GlobalRootCAs is CA root certificates, a nil value means system certs pool will be used GlobalRootCAs *x509.CertPool diff --git a/restapi/configure_console.go b/restapi/configure_console.go index 2ac578d49..532a35c04 100644 --- a/restapi/configure_console.go +++ b/restapi/configure_console.go @@ -121,6 +121,8 @@ func configureAPI(api *operations.ConsoleAPI) http.Handler { registerServiceAccountsHandlers(api) // Register admin remote buckets registerAdminBucketRemoteHandlers(api) + // Register admin subscription handlers + registerSubscriptionHandlers(api) // Operator Console // Register tenant handlers diff --git a/restapi/consts.go b/restapi/consts.go index 9f715469b..ae3636bdf 100644 --- a/restapi/consts.go +++ b/restapi/consts.go @@ -26,6 +26,7 @@ const ( ConsolePort = "CONSOLE_PORT" ConsoleTLSHostname = "CONSOLE_TLS_HOSTNAME" ConsoleTLSPort = "CONSOLE_TLS_PORT" + ConsoleSubnetLicense = "CONSOLE_SUBNET_LICENSE" // Constants for Secure middleware ConsoleSecureAllowedHosts = "CONSOLE_SECURE_ALLOWED_HOSTS" @@ -59,3 +60,9 @@ const ( KESImageVersion = "minio/kes:v0.12.1" ConsoleImageVersion = "minio/console:v0.4.6" ) + +// K8s + +const ( + OperatorSubnetLicenseSecretName = "subnet-license" +) diff --git a/restapi/embedded_spec.go b/restapi/embedded_spec.go index 6aa3dcaef..0a79edeb7 100644 --- a/restapi/embedded_spec.go +++ b/restapi/embedded_spec.go @@ -2631,6 +2631,96 @@ func init() { } } }, + "/subscription/info": { + "get": { + "tags": [ + "AdminAPI" + ], + "summary": "Subscription info", + "operationId": "SubscriptionInfo", + "responses": { + "200": { + "description": "A successful response.", + "schema": { + "$ref": "#/definitions/license" + } + }, + "default": { + "description": "Generic error response.", + "schema": { + "$ref": "#/definitions/error" + } + } + } + } + }, + "/subscription/namespaces/{namespace}/tenants/{tenant}/activate": { + "post": { + "tags": [ + "AdminAPI" + ], + "summary": "Activate a particular tenant using the existing subscription license", + "operationId": "SubscriptionActivate", + "parameters": [ + { + "type": "string", + "name": "namespace", + "in": "path", + "required": true + }, + { + "type": "string", + "name": "tenant", + "in": "path", + "required": true + } + ], + "responses": { + "204": { + "description": "A successful response." + }, + "default": { + "description": "Generic error response.", + "schema": { + "$ref": "#/definitions/error" + } + } + } + } + }, + "/subscription/validate": { + "post": { + "tags": [ + "AdminAPI" + ], + "summary": "Validate a provided subscription license", + "operationId": "SubscriptionValidate", + "parameters": [ + { + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/subscriptionValidateRequest" + } + } + ], + "responses": { + "200": { + "description": "A successful response.", + "schema": { + "$ref": "#/definitions/license" + } + }, + "default": { + "description": "Generic error response.", + "schema": { + "$ref": "#/definitions/error" + } + } + } + } + }, "/tenants": { "get": { "tags": [ @@ -3726,6 +3816,29 @@ func init() { } } }, + "license": { + "type": "object", + "properties": { + "account_id": { + "type": "integer" + }, + "email": { + "type": "string" + }, + "expires_at": { + "type": "string" + }, + "organization": { + "type": "string" + }, + "plan": { + "type": "string" + }, + "storage_capacity": { + "type": "integer" + } + } + }, "listBucketEventsResponse": { "type": "object", "properties": { @@ -4754,6 +4867,9 @@ func init() { "sessionResponse": { "type": "object", "properties": { + "operator": { + "type": "boolean" + }, "pages": { "type": "array", "items": { @@ -4923,6 +5039,20 @@ func init() { } } }, + "subscriptionValidateRequest": { + "type": "object", + "properties": { + "email": { + "type": "string" + }, + "license": { + "type": "string" + }, + "password": { + "type": "string" + } + } + }, "tenant": { "type": "object", "properties": { @@ -4956,6 +5086,9 @@ func init() { "$ref": "#/definitions/pool" } }, + "subnet_license": { + "$ref": "#/definitions/license" + }, "total_size": { "type": "integer", "format": "int64" @@ -7792,6 +7925,96 @@ func init() { } } }, + "/subscription/info": { + "get": { + "tags": [ + "AdminAPI" + ], + "summary": "Subscription info", + "operationId": "SubscriptionInfo", + "responses": { + "200": { + "description": "A successful response.", + "schema": { + "$ref": "#/definitions/license" + } + }, + "default": { + "description": "Generic error response.", + "schema": { + "$ref": "#/definitions/error" + } + } + } + } + }, + "/subscription/namespaces/{namespace}/tenants/{tenant}/activate": { + "post": { + "tags": [ + "AdminAPI" + ], + "summary": "Activate a particular tenant using the existing subscription license", + "operationId": "SubscriptionActivate", + "parameters": [ + { + "type": "string", + "name": "namespace", + "in": "path", + "required": true + }, + { + "type": "string", + "name": "tenant", + "in": "path", + "required": true + } + ], + "responses": { + "204": { + "description": "A successful response." + }, + "default": { + "description": "Generic error response.", + "schema": { + "$ref": "#/definitions/error" + } + } + } + } + }, + "/subscription/validate": { + "post": { + "tags": [ + "AdminAPI" + ], + "summary": "Validate a provided subscription license", + "operationId": "SubscriptionValidate", + "parameters": [ + { + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/subscriptionValidateRequest" + } + } + ], + "responses": { + "200": { + "description": "A successful response.", + "schema": { + "$ref": "#/definitions/license" + } + }, + "default": { + "description": "Generic error response.", + "schema": { + "$ref": "#/definitions/error" + } + } + } + } + }, "/tenants": { "get": { "tags": [ @@ -9410,6 +9633,29 @@ func init() { } } }, + "license": { + "type": "object", + "properties": { + "account_id": { + "type": "integer" + }, + "email": { + "type": "string" + }, + "expires_at": { + "type": "string" + }, + "organization": { + "type": "string" + }, + "plan": { + "type": "string" + }, + "storage_capacity": { + "type": "integer" + } + } + }, "listBucketEventsResponse": { "type": "object", "properties": { @@ -10303,6 +10549,9 @@ func init() { "sessionResponse": { "type": "object", "properties": { + "operator": { + "type": "boolean" + }, "pages": { "type": "array", "items": { @@ -10472,6 +10721,20 @@ func init() { } } }, + "subscriptionValidateRequest": { + "type": "object", + "properties": { + "email": { + "type": "string" + }, + "license": { + "type": "string" + }, + "password": { + "type": "string" + } + } + }, "tenant": { "type": "object", "properties": { @@ -10505,6 +10768,9 @@ func init() { "$ref": "#/definitions/pool" } }, + "subnet_license": { + "$ref": "#/definitions/license" + }, "total_size": { "type": "integer", "format": "int64" diff --git a/restapi/error.go b/restapi/error.go index 88cbea63e..cd16913a6 100644 --- a/restapi/error.go +++ b/restapi/error.go @@ -32,6 +32,8 @@ var ( errInvalidEncryptionAlgorithm = errors.New("error invalid encryption algorithm") errSSENotConfigured = errors.New("error server side encryption configuration was not found") errChangePassword = errors.New("unable to update password, please check your current password") + errInvalidLicense = errors.New("invalid license key") + errLicenseNotFound = errors.New("license not found") ) // prepareError receives an error object and parse it against k8sErrors, returns the right error code paired with a generic error message @@ -95,6 +97,14 @@ func prepareError(err ...error) *models.Error { errorCode = 403 errorMessage = errChangePassword.Error() } + if errors.Is(err[0], errLicenseNotFound) { + errorCode = 404 + errorMessage = errLicenseNotFound.Error() + } + if errors.Is(err[0], errInvalidLicense) { + errorCode = 404 + errorMessage = errInvalidLicense.Error() + } if madmin.ToErrorResponse(err[0]).Code == "InvalidAccessKeyId" { errorCode = 401 errorMessage = errorGenericInvalidSession.Error() diff --git a/restapi/operations/admin_api/subscription_activate.go b/restapi/operations/admin_api/subscription_activate.go new file mode 100644 index 000000000..45c8de3df --- /dev/null +++ b/restapi/operations/admin_api/subscription_activate.go @@ -0,0 +1,90 @@ +// Code generated by go-swagger; DO NOT EDIT. + +// 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 . +// + +package admin_api + +// This file was generated by the swagger tool. +// Editing this file might prove futile when you re-run the generate command + +import ( + "net/http" + + "github.com/go-openapi/runtime/middleware" + + "github.com/minio/console/models" +) + +// SubscriptionActivateHandlerFunc turns a function with the right signature into a subscription activate handler +type SubscriptionActivateHandlerFunc func(SubscriptionActivateParams, *models.Principal) middleware.Responder + +// Handle executing the request and returning a response +func (fn SubscriptionActivateHandlerFunc) Handle(params SubscriptionActivateParams, principal *models.Principal) middleware.Responder { + return fn(params, principal) +} + +// SubscriptionActivateHandler interface for that can handle valid subscription activate params +type SubscriptionActivateHandler interface { + Handle(SubscriptionActivateParams, *models.Principal) middleware.Responder +} + +// NewSubscriptionActivate creates a new http.Handler for the subscription activate operation +func NewSubscriptionActivate(ctx *middleware.Context, handler SubscriptionActivateHandler) *SubscriptionActivate { + return &SubscriptionActivate{Context: ctx, Handler: handler} +} + +/*SubscriptionActivate swagger:route POST /subscription/namespaces/{namespace}/tenants/{tenant}/activate AdminAPI subscriptionActivate + +Activate a particular tenant using the existing subscription license + +*/ +type SubscriptionActivate struct { + Context *middleware.Context + Handler SubscriptionActivateHandler +} + +func (o *SubscriptionActivate) ServeHTTP(rw http.ResponseWriter, r *http.Request) { + route, rCtx, _ := o.Context.RouteInfo(r) + if rCtx != nil { + r = rCtx + } + var Params = NewSubscriptionActivateParams() + + uprinc, aCtx, err := o.Context.Authorize(r, route) + if err != nil { + o.Context.Respond(rw, r, route.Produces, route, err) + return + } + if aCtx != nil { + r = aCtx + } + var principal *models.Principal + if uprinc != nil { + principal = uprinc.(*models.Principal) // this is really a models.Principal, I promise + } + + if err := o.Context.BindValidRequest(r, route, &Params); err != nil { // bind params + o.Context.Respond(rw, r, route.Produces, route, err) + return + } + + res := o.Handler.Handle(Params, principal) // actually handle the request + + o.Context.Respond(rw, r, route.Produces, route, res) + +} diff --git a/restapi/operations/admin_api/subscription_activate_parameters.go b/restapi/operations/admin_api/subscription_activate_parameters.go new file mode 100644 index 000000000..cb65458b9 --- /dev/null +++ b/restapi/operations/admin_api/subscription_activate_parameters.go @@ -0,0 +1,114 @@ +// Code generated by go-swagger; DO NOT EDIT. + +// 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 . +// + +package admin_api + +// This file was generated by the swagger tool. +// Editing this file might prove futile when you re-run the swagger generate command + +import ( + "net/http" + + "github.com/go-openapi/errors" + "github.com/go-openapi/runtime/middleware" + "github.com/go-openapi/strfmt" +) + +// NewSubscriptionActivateParams creates a new SubscriptionActivateParams object +// no default values defined in spec. +func NewSubscriptionActivateParams() SubscriptionActivateParams { + + return SubscriptionActivateParams{} +} + +// SubscriptionActivateParams contains all the bound params for the subscription activate operation +// typically these are obtained from a http.Request +// +// swagger:parameters SubscriptionActivate +type SubscriptionActivateParams struct { + + // HTTP Request Object + HTTPRequest *http.Request `json:"-"` + + /* + Required: true + In: path + */ + Namespace string + /* + Required: true + In: path + */ + Tenant string +} + +// BindRequest both binds and validates a request, it assumes that complex things implement a Validatable(strfmt.Registry) error interface +// for simple values it will use straight method calls. +// +// To ensure default values, the struct must have been initialized with NewSubscriptionActivateParams() beforehand. +func (o *SubscriptionActivateParams) BindRequest(r *http.Request, route *middleware.MatchedRoute) error { + var res []error + + o.HTTPRequest = r + + rNamespace, rhkNamespace, _ := route.Params.GetOK("namespace") + if err := o.bindNamespace(rNamespace, rhkNamespace, route.Formats); err != nil { + res = append(res, err) + } + + rTenant, rhkTenant, _ := route.Params.GetOK("tenant") + if err := o.bindTenant(rTenant, rhkTenant, route.Formats); err != nil { + res = append(res, err) + } + + if len(res) > 0 { + return errors.CompositeValidationError(res...) + } + return nil +} + +// bindNamespace binds and validates parameter Namespace from path. +func (o *SubscriptionActivateParams) bindNamespace(rawData []string, hasKey bool, formats strfmt.Registry) error { + var raw string + if len(rawData) > 0 { + raw = rawData[len(rawData)-1] + } + + // Required: true + // Parameter is provided by construction from the route + + o.Namespace = raw + + return nil +} + +// bindTenant binds and validates parameter Tenant from path. +func (o *SubscriptionActivateParams) bindTenant(rawData []string, hasKey bool, formats strfmt.Registry) error { + var raw string + if len(rawData) > 0 { + raw = rawData[len(rawData)-1] + } + + // Required: true + // Parameter is provided by construction from the route + + o.Tenant = raw + + return nil +} diff --git a/restapi/operations/admin_api/subscription_activate_responses.go b/restapi/operations/admin_api/subscription_activate_responses.go new file mode 100644 index 000000000..8931907bb --- /dev/null +++ b/restapi/operations/admin_api/subscription_activate_responses.go @@ -0,0 +1,113 @@ +// Code generated by go-swagger; DO NOT EDIT. + +// 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 . +// + +package admin_api + +// This file was generated by the swagger tool. +// Editing this file might prove futile when you re-run the swagger generate command + +import ( + "net/http" + + "github.com/go-openapi/runtime" + + "github.com/minio/console/models" +) + +// SubscriptionActivateNoContentCode is the HTTP code returned for type SubscriptionActivateNoContent +const SubscriptionActivateNoContentCode int = 204 + +/*SubscriptionActivateNoContent A successful response. + +swagger:response subscriptionActivateNoContent +*/ +type SubscriptionActivateNoContent struct { +} + +// NewSubscriptionActivateNoContent creates SubscriptionActivateNoContent with default headers values +func NewSubscriptionActivateNoContent() *SubscriptionActivateNoContent { + + return &SubscriptionActivateNoContent{} +} + +// WriteResponse to the client +func (o *SubscriptionActivateNoContent) WriteResponse(rw http.ResponseWriter, producer runtime.Producer) { + + rw.Header().Del(runtime.HeaderContentType) //Remove Content-Type on empty responses + + rw.WriteHeader(204) +} + +/*SubscriptionActivateDefault Generic error response. + +swagger:response subscriptionActivateDefault +*/ +type SubscriptionActivateDefault struct { + _statusCode int + + /* + In: Body + */ + Payload *models.Error `json:"body,omitempty"` +} + +// NewSubscriptionActivateDefault creates SubscriptionActivateDefault with default headers values +func NewSubscriptionActivateDefault(code int) *SubscriptionActivateDefault { + if code <= 0 { + code = 500 + } + + return &SubscriptionActivateDefault{ + _statusCode: code, + } +} + +// WithStatusCode adds the status to the subscription activate default response +func (o *SubscriptionActivateDefault) WithStatusCode(code int) *SubscriptionActivateDefault { + o._statusCode = code + return o +} + +// SetStatusCode sets the status to the subscription activate default response +func (o *SubscriptionActivateDefault) SetStatusCode(code int) { + o._statusCode = code +} + +// WithPayload adds the payload to the subscription activate default response +func (o *SubscriptionActivateDefault) WithPayload(payload *models.Error) *SubscriptionActivateDefault { + o.Payload = payload + return o +} + +// SetPayload sets the payload to the subscription activate default response +func (o *SubscriptionActivateDefault) SetPayload(payload *models.Error) { + o.Payload = payload +} + +// WriteResponse to the client +func (o *SubscriptionActivateDefault) WriteResponse(rw http.ResponseWriter, producer runtime.Producer) { + + rw.WriteHeader(o._statusCode) + if o.Payload != nil { + payload := o.Payload + if err := producer.Produce(rw, payload); err != nil { + panic(err) // let the recovery middleware deal with this + } + } +} diff --git a/restapi/operations/admin_api/subscription_activate_urlbuilder.go b/restapi/operations/admin_api/subscription_activate_urlbuilder.go new file mode 100644 index 000000000..423043df5 --- /dev/null +++ b/restapi/operations/admin_api/subscription_activate_urlbuilder.go @@ -0,0 +1,124 @@ +// Code generated by go-swagger; DO NOT EDIT. + +// 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 . +// + +package admin_api + +// This file was generated by the swagger tool. +// Editing this file might prove futile when you re-run the generate command + +import ( + "errors" + "net/url" + golangswaggerpaths "path" + "strings" +) + +// SubscriptionActivateURL generates an URL for the subscription activate operation +type SubscriptionActivateURL struct { + Namespace string + Tenant string + + _basePath string + // avoid unkeyed usage + _ struct{} +} + +// WithBasePath sets the base path for this url builder, only required when it's different from the +// base path specified in the swagger spec. +// When the value of the base path is an empty string +func (o *SubscriptionActivateURL) WithBasePath(bp string) *SubscriptionActivateURL { + o.SetBasePath(bp) + return o +} + +// SetBasePath sets the base path for this url builder, only required when it's different from the +// base path specified in the swagger spec. +// When the value of the base path is an empty string +func (o *SubscriptionActivateURL) SetBasePath(bp string) { + o._basePath = bp +} + +// Build a url path and query string +func (o *SubscriptionActivateURL) Build() (*url.URL, error) { + var _result url.URL + + var _path = "/subscription/namespaces/{namespace}/tenants/{tenant}/activate" + + namespace := o.Namespace + if namespace != "" { + _path = strings.Replace(_path, "{namespace}", namespace, -1) + } else { + return nil, errors.New("namespace is required on SubscriptionActivateURL") + } + + tenant := o.Tenant + if tenant != "" { + _path = strings.Replace(_path, "{tenant}", tenant, -1) + } else { + return nil, errors.New("tenant is required on SubscriptionActivateURL") + } + + _basePath := o._basePath + if _basePath == "" { + _basePath = "/api/v1" + } + _result.Path = golangswaggerpaths.Join(_basePath, _path) + + return &_result, nil +} + +// Must is a helper function to panic when the url builder returns an error +func (o *SubscriptionActivateURL) Must(u *url.URL, err error) *url.URL { + if err != nil { + panic(err) + } + if u == nil { + panic("url can't be nil") + } + return u +} + +// String returns the string representation of the path with query string +func (o *SubscriptionActivateURL) String() string { + return o.Must(o.Build()).String() +} + +// BuildFull builds a full url with scheme, host, path and query string +func (o *SubscriptionActivateURL) BuildFull(scheme, host string) (*url.URL, error) { + if scheme == "" { + return nil, errors.New("scheme is required for a full url on SubscriptionActivateURL") + } + if host == "" { + return nil, errors.New("host is required for a full url on SubscriptionActivateURL") + } + + base, err := o.Build() + if err != nil { + return nil, err + } + + base.Scheme = scheme + base.Host = host + return base, nil +} + +// StringFull returns the string representation of a complete url +func (o *SubscriptionActivateURL) StringFull(scheme, host string) string { + return o.Must(o.BuildFull(scheme, host)).String() +} diff --git a/restapi/operations/admin_api/subscription_info.go b/restapi/operations/admin_api/subscription_info.go new file mode 100644 index 000000000..b894043ca --- /dev/null +++ b/restapi/operations/admin_api/subscription_info.go @@ -0,0 +1,90 @@ +// Code generated by go-swagger; DO NOT EDIT. + +// 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 . +// + +package admin_api + +// This file was generated by the swagger tool. +// Editing this file might prove futile when you re-run the generate command + +import ( + "net/http" + + "github.com/go-openapi/runtime/middleware" + + "github.com/minio/console/models" +) + +// SubscriptionInfoHandlerFunc turns a function with the right signature into a subscription info handler +type SubscriptionInfoHandlerFunc func(SubscriptionInfoParams, *models.Principal) middleware.Responder + +// Handle executing the request and returning a response +func (fn SubscriptionInfoHandlerFunc) Handle(params SubscriptionInfoParams, principal *models.Principal) middleware.Responder { + return fn(params, principal) +} + +// SubscriptionInfoHandler interface for that can handle valid subscription info params +type SubscriptionInfoHandler interface { + Handle(SubscriptionInfoParams, *models.Principal) middleware.Responder +} + +// NewSubscriptionInfo creates a new http.Handler for the subscription info operation +func NewSubscriptionInfo(ctx *middleware.Context, handler SubscriptionInfoHandler) *SubscriptionInfo { + return &SubscriptionInfo{Context: ctx, Handler: handler} +} + +/*SubscriptionInfo swagger:route GET /subscription/info AdminAPI subscriptionInfo + +Subscription info + +*/ +type SubscriptionInfo struct { + Context *middleware.Context + Handler SubscriptionInfoHandler +} + +func (o *SubscriptionInfo) ServeHTTP(rw http.ResponseWriter, r *http.Request) { + route, rCtx, _ := o.Context.RouteInfo(r) + if rCtx != nil { + r = rCtx + } + var Params = NewSubscriptionInfoParams() + + uprinc, aCtx, err := o.Context.Authorize(r, route) + if err != nil { + o.Context.Respond(rw, r, route.Produces, route, err) + return + } + if aCtx != nil { + r = aCtx + } + var principal *models.Principal + if uprinc != nil { + principal = uprinc.(*models.Principal) // this is really a models.Principal, I promise + } + + if err := o.Context.BindValidRequest(r, route, &Params); err != nil { // bind params + o.Context.Respond(rw, r, route.Produces, route, err) + return + } + + res := o.Handler.Handle(Params, principal) // actually handle the request + + o.Context.Respond(rw, r, route.Produces, route, res) + +} diff --git a/restapi/operations/admin_api/subscription_info_parameters.go b/restapi/operations/admin_api/subscription_info_parameters.go new file mode 100644 index 000000000..c79bc09be --- /dev/null +++ b/restapi/operations/admin_api/subscription_info_parameters.go @@ -0,0 +1,62 @@ +// Code generated by go-swagger; DO NOT EDIT. + +// 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 . +// + +package admin_api + +// This file was generated by the swagger tool. +// Editing this file might prove futile when you re-run the swagger generate command + +import ( + "net/http" + + "github.com/go-openapi/errors" + "github.com/go-openapi/runtime/middleware" +) + +// NewSubscriptionInfoParams creates a new SubscriptionInfoParams object +// no default values defined in spec. +func NewSubscriptionInfoParams() SubscriptionInfoParams { + + return SubscriptionInfoParams{} +} + +// SubscriptionInfoParams contains all the bound params for the subscription info operation +// typically these are obtained from a http.Request +// +// swagger:parameters SubscriptionInfo +type SubscriptionInfoParams struct { + + // HTTP Request Object + HTTPRequest *http.Request `json:"-"` +} + +// BindRequest both binds and validates a request, it assumes that complex things implement a Validatable(strfmt.Registry) error interface +// for simple values it will use straight method calls. +// +// To ensure default values, the struct must have been initialized with NewSubscriptionInfoParams() beforehand. +func (o *SubscriptionInfoParams) BindRequest(r *http.Request, route *middleware.MatchedRoute) error { + var res []error + + o.HTTPRequest = r + + if len(res) > 0 { + return errors.CompositeValidationError(res...) + } + return nil +} diff --git a/restapi/operations/admin_api/subscription_info_responses.go b/restapi/operations/admin_api/subscription_info_responses.go new file mode 100644 index 000000000..3bf477b0e --- /dev/null +++ b/restapi/operations/admin_api/subscription_info_responses.go @@ -0,0 +1,133 @@ +// Code generated by go-swagger; DO NOT EDIT. + +// 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 . +// + +package admin_api + +// This file was generated by the swagger tool. +// Editing this file might prove futile when you re-run the swagger generate command + +import ( + "net/http" + + "github.com/go-openapi/runtime" + + "github.com/minio/console/models" +) + +// SubscriptionInfoOKCode is the HTTP code returned for type SubscriptionInfoOK +const SubscriptionInfoOKCode int = 200 + +/*SubscriptionInfoOK A successful response. + +swagger:response subscriptionInfoOK +*/ +type SubscriptionInfoOK struct { + + /* + In: Body + */ + Payload *models.License `json:"body,omitempty"` +} + +// NewSubscriptionInfoOK creates SubscriptionInfoOK with default headers values +func NewSubscriptionInfoOK() *SubscriptionInfoOK { + + return &SubscriptionInfoOK{} +} + +// WithPayload adds the payload to the subscription info o k response +func (o *SubscriptionInfoOK) WithPayload(payload *models.License) *SubscriptionInfoOK { + o.Payload = payload + return o +} + +// SetPayload sets the payload to the subscription info o k response +func (o *SubscriptionInfoOK) SetPayload(payload *models.License) { + o.Payload = payload +} + +// WriteResponse to the client +func (o *SubscriptionInfoOK) WriteResponse(rw http.ResponseWriter, producer runtime.Producer) { + + rw.WriteHeader(200) + if o.Payload != nil { + payload := o.Payload + if err := producer.Produce(rw, payload); err != nil { + panic(err) // let the recovery middleware deal with this + } + } +} + +/*SubscriptionInfoDefault Generic error response. + +swagger:response subscriptionInfoDefault +*/ +type SubscriptionInfoDefault struct { + _statusCode int + + /* + In: Body + */ + Payload *models.Error `json:"body,omitempty"` +} + +// NewSubscriptionInfoDefault creates SubscriptionInfoDefault with default headers values +func NewSubscriptionInfoDefault(code int) *SubscriptionInfoDefault { + if code <= 0 { + code = 500 + } + + return &SubscriptionInfoDefault{ + _statusCode: code, + } +} + +// WithStatusCode adds the status to the subscription info default response +func (o *SubscriptionInfoDefault) WithStatusCode(code int) *SubscriptionInfoDefault { + o._statusCode = code + return o +} + +// SetStatusCode sets the status to the subscription info default response +func (o *SubscriptionInfoDefault) SetStatusCode(code int) { + o._statusCode = code +} + +// WithPayload adds the payload to the subscription info default response +func (o *SubscriptionInfoDefault) WithPayload(payload *models.Error) *SubscriptionInfoDefault { + o.Payload = payload + return o +} + +// SetPayload sets the payload to the subscription info default response +func (o *SubscriptionInfoDefault) SetPayload(payload *models.Error) { + o.Payload = payload +} + +// WriteResponse to the client +func (o *SubscriptionInfoDefault) WriteResponse(rw http.ResponseWriter, producer runtime.Producer) { + + rw.WriteHeader(o._statusCode) + if o.Payload != nil { + payload := o.Payload + if err := producer.Produce(rw, payload); err != nil { + panic(err) // let the recovery middleware deal with this + } + } +} diff --git a/restapi/operations/admin_api/subscription_info_urlbuilder.go b/restapi/operations/admin_api/subscription_info_urlbuilder.go new file mode 100644 index 000000000..e31f5d89c --- /dev/null +++ b/restapi/operations/admin_api/subscription_info_urlbuilder.go @@ -0,0 +1,104 @@ +// Code generated by go-swagger; DO NOT EDIT. + +// 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 . +// + +package admin_api + +// This file was generated by the swagger tool. +// Editing this file might prove futile when you re-run the generate command + +import ( + "errors" + "net/url" + golangswaggerpaths "path" +) + +// SubscriptionInfoURL generates an URL for the subscription info operation +type SubscriptionInfoURL struct { + _basePath string +} + +// WithBasePath sets the base path for this url builder, only required when it's different from the +// base path specified in the swagger spec. +// When the value of the base path is an empty string +func (o *SubscriptionInfoURL) WithBasePath(bp string) *SubscriptionInfoURL { + o.SetBasePath(bp) + return o +} + +// SetBasePath sets the base path for this url builder, only required when it's different from the +// base path specified in the swagger spec. +// When the value of the base path is an empty string +func (o *SubscriptionInfoURL) SetBasePath(bp string) { + o._basePath = bp +} + +// Build a url path and query string +func (o *SubscriptionInfoURL) Build() (*url.URL, error) { + var _result url.URL + + var _path = "/subscription/info" + + _basePath := o._basePath + if _basePath == "" { + _basePath = "/api/v1" + } + _result.Path = golangswaggerpaths.Join(_basePath, _path) + + return &_result, nil +} + +// Must is a helper function to panic when the url builder returns an error +func (o *SubscriptionInfoURL) Must(u *url.URL, err error) *url.URL { + if err != nil { + panic(err) + } + if u == nil { + panic("url can't be nil") + } + return u +} + +// String returns the string representation of the path with query string +func (o *SubscriptionInfoURL) String() string { + return o.Must(o.Build()).String() +} + +// BuildFull builds a full url with scheme, host, path and query string +func (o *SubscriptionInfoURL) BuildFull(scheme, host string) (*url.URL, error) { + if scheme == "" { + return nil, errors.New("scheme is required for a full url on SubscriptionInfoURL") + } + if host == "" { + return nil, errors.New("host is required for a full url on SubscriptionInfoURL") + } + + base, err := o.Build() + if err != nil { + return nil, err + } + + base.Scheme = scheme + base.Host = host + return base, nil +} + +// StringFull returns the string representation of a complete url +func (o *SubscriptionInfoURL) StringFull(scheme, host string) string { + return o.Must(o.BuildFull(scheme, host)).String() +} diff --git a/restapi/operations/admin_api/subscription_validate.go b/restapi/operations/admin_api/subscription_validate.go new file mode 100644 index 000000000..54d8ff684 --- /dev/null +++ b/restapi/operations/admin_api/subscription_validate.go @@ -0,0 +1,90 @@ +// Code generated by go-swagger; DO NOT EDIT. + +// 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 . +// + +package admin_api + +// This file was generated by the swagger tool. +// Editing this file might prove futile when you re-run the generate command + +import ( + "net/http" + + "github.com/go-openapi/runtime/middleware" + + "github.com/minio/console/models" +) + +// SubscriptionValidateHandlerFunc turns a function with the right signature into a subscription validate handler +type SubscriptionValidateHandlerFunc func(SubscriptionValidateParams, *models.Principal) middleware.Responder + +// Handle executing the request and returning a response +func (fn SubscriptionValidateHandlerFunc) Handle(params SubscriptionValidateParams, principal *models.Principal) middleware.Responder { + return fn(params, principal) +} + +// SubscriptionValidateHandler interface for that can handle valid subscription validate params +type SubscriptionValidateHandler interface { + Handle(SubscriptionValidateParams, *models.Principal) middleware.Responder +} + +// NewSubscriptionValidate creates a new http.Handler for the subscription validate operation +func NewSubscriptionValidate(ctx *middleware.Context, handler SubscriptionValidateHandler) *SubscriptionValidate { + return &SubscriptionValidate{Context: ctx, Handler: handler} +} + +/*SubscriptionValidate swagger:route POST /subscription/validate AdminAPI subscriptionValidate + +Validate a provided subscription license + +*/ +type SubscriptionValidate struct { + Context *middleware.Context + Handler SubscriptionValidateHandler +} + +func (o *SubscriptionValidate) ServeHTTP(rw http.ResponseWriter, r *http.Request) { + route, rCtx, _ := o.Context.RouteInfo(r) + if rCtx != nil { + r = rCtx + } + var Params = NewSubscriptionValidateParams() + + uprinc, aCtx, err := o.Context.Authorize(r, route) + if err != nil { + o.Context.Respond(rw, r, route.Produces, route, err) + return + } + if aCtx != nil { + r = aCtx + } + var principal *models.Principal + if uprinc != nil { + principal = uprinc.(*models.Principal) // this is really a models.Principal, I promise + } + + if err := o.Context.BindValidRequest(r, route, &Params); err != nil { // bind params + o.Context.Respond(rw, r, route.Produces, route, err) + return + } + + res := o.Handler.Handle(Params, principal) // actually handle the request + + o.Context.Respond(rw, r, route.Produces, route, res) + +} diff --git a/restapi/operations/admin_api/subscription_validate_parameters.go b/restapi/operations/admin_api/subscription_validate_parameters.go new file mode 100644 index 000000000..a6b982704 --- /dev/null +++ b/restapi/operations/admin_api/subscription_validate_parameters.go @@ -0,0 +1,94 @@ +// Code generated by go-swagger; DO NOT EDIT. + +// 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 . +// + +package admin_api + +// This file was generated by the swagger tool. +// Editing this file might prove futile when you re-run the swagger generate command + +import ( + "io" + "net/http" + + "github.com/go-openapi/errors" + "github.com/go-openapi/runtime" + "github.com/go-openapi/runtime/middleware" + + "github.com/minio/console/models" +) + +// NewSubscriptionValidateParams creates a new SubscriptionValidateParams object +// no default values defined in spec. +func NewSubscriptionValidateParams() SubscriptionValidateParams { + + return SubscriptionValidateParams{} +} + +// SubscriptionValidateParams contains all the bound params for the subscription validate operation +// typically these are obtained from a http.Request +// +// swagger:parameters SubscriptionValidate +type SubscriptionValidateParams struct { + + // HTTP Request Object + HTTPRequest *http.Request `json:"-"` + + /* + Required: true + In: body + */ + Body *models.SubscriptionValidateRequest +} + +// BindRequest both binds and validates a request, it assumes that complex things implement a Validatable(strfmt.Registry) error interface +// for simple values it will use straight method calls. +// +// To ensure default values, the struct must have been initialized with NewSubscriptionValidateParams() beforehand. +func (o *SubscriptionValidateParams) BindRequest(r *http.Request, route *middleware.MatchedRoute) error { + var res []error + + o.HTTPRequest = r + + if runtime.HasBody(r) { + defer r.Body.Close() + var body models.SubscriptionValidateRequest + if err := route.Consumer.Consume(r.Body, &body); err != nil { + if err == io.EOF { + res = append(res, errors.Required("body", "body", "")) + } else { + res = append(res, errors.NewParseError("body", "body", "", err)) + } + } else { + // validate body object + if err := body.Validate(route.Formats); err != nil { + res = append(res, err) + } + + if len(res) == 0 { + o.Body = &body + } + } + } else { + res = append(res, errors.Required("body", "body", "")) + } + if len(res) > 0 { + return errors.CompositeValidationError(res...) + } + return nil +} diff --git a/restapi/operations/admin_api/subscription_validate_responses.go b/restapi/operations/admin_api/subscription_validate_responses.go new file mode 100644 index 000000000..811aae597 --- /dev/null +++ b/restapi/operations/admin_api/subscription_validate_responses.go @@ -0,0 +1,133 @@ +// Code generated by go-swagger; DO NOT EDIT. + +// 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 . +// + +package admin_api + +// This file was generated by the swagger tool. +// Editing this file might prove futile when you re-run the swagger generate command + +import ( + "net/http" + + "github.com/go-openapi/runtime" + + "github.com/minio/console/models" +) + +// SubscriptionValidateOKCode is the HTTP code returned for type SubscriptionValidateOK +const SubscriptionValidateOKCode int = 200 + +/*SubscriptionValidateOK A successful response. + +swagger:response subscriptionValidateOK +*/ +type SubscriptionValidateOK struct { + + /* + In: Body + */ + Payload *models.License `json:"body,omitempty"` +} + +// NewSubscriptionValidateOK creates SubscriptionValidateOK with default headers values +func NewSubscriptionValidateOK() *SubscriptionValidateOK { + + return &SubscriptionValidateOK{} +} + +// WithPayload adds the payload to the subscription validate o k response +func (o *SubscriptionValidateOK) WithPayload(payload *models.License) *SubscriptionValidateOK { + o.Payload = payload + return o +} + +// SetPayload sets the payload to the subscription validate o k response +func (o *SubscriptionValidateOK) SetPayload(payload *models.License) { + o.Payload = payload +} + +// WriteResponse to the client +func (o *SubscriptionValidateOK) WriteResponse(rw http.ResponseWriter, producer runtime.Producer) { + + rw.WriteHeader(200) + if o.Payload != nil { + payload := o.Payload + if err := producer.Produce(rw, payload); err != nil { + panic(err) // let the recovery middleware deal with this + } + } +} + +/*SubscriptionValidateDefault Generic error response. + +swagger:response subscriptionValidateDefault +*/ +type SubscriptionValidateDefault struct { + _statusCode int + + /* + In: Body + */ + Payload *models.Error `json:"body,omitempty"` +} + +// NewSubscriptionValidateDefault creates SubscriptionValidateDefault with default headers values +func NewSubscriptionValidateDefault(code int) *SubscriptionValidateDefault { + if code <= 0 { + code = 500 + } + + return &SubscriptionValidateDefault{ + _statusCode: code, + } +} + +// WithStatusCode adds the status to the subscription validate default response +func (o *SubscriptionValidateDefault) WithStatusCode(code int) *SubscriptionValidateDefault { + o._statusCode = code + return o +} + +// SetStatusCode sets the status to the subscription validate default response +func (o *SubscriptionValidateDefault) SetStatusCode(code int) { + o._statusCode = code +} + +// WithPayload adds the payload to the subscription validate default response +func (o *SubscriptionValidateDefault) WithPayload(payload *models.Error) *SubscriptionValidateDefault { + o.Payload = payload + return o +} + +// SetPayload sets the payload to the subscription validate default response +func (o *SubscriptionValidateDefault) SetPayload(payload *models.Error) { + o.Payload = payload +} + +// WriteResponse to the client +func (o *SubscriptionValidateDefault) WriteResponse(rw http.ResponseWriter, producer runtime.Producer) { + + rw.WriteHeader(o._statusCode) + if o.Payload != nil { + payload := o.Payload + if err := producer.Produce(rw, payload); err != nil { + panic(err) // let the recovery middleware deal with this + } + } +} diff --git a/restapi/operations/admin_api/subscription_validate_urlbuilder.go b/restapi/operations/admin_api/subscription_validate_urlbuilder.go new file mode 100644 index 000000000..b65499fcc --- /dev/null +++ b/restapi/operations/admin_api/subscription_validate_urlbuilder.go @@ -0,0 +1,104 @@ +// Code generated by go-swagger; DO NOT EDIT. + +// 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 . +// + +package admin_api + +// This file was generated by the swagger tool. +// Editing this file might prove futile when you re-run the generate command + +import ( + "errors" + "net/url" + golangswaggerpaths "path" +) + +// SubscriptionValidateURL generates an URL for the subscription validate operation +type SubscriptionValidateURL struct { + _basePath string +} + +// WithBasePath sets the base path for this url builder, only required when it's different from the +// base path specified in the swagger spec. +// When the value of the base path is an empty string +func (o *SubscriptionValidateURL) WithBasePath(bp string) *SubscriptionValidateURL { + o.SetBasePath(bp) + return o +} + +// SetBasePath sets the base path for this url builder, only required when it's different from the +// base path specified in the swagger spec. +// When the value of the base path is an empty string +func (o *SubscriptionValidateURL) SetBasePath(bp string) { + o._basePath = bp +} + +// Build a url path and query string +func (o *SubscriptionValidateURL) Build() (*url.URL, error) { + var _result url.URL + + var _path = "/subscription/validate" + + _basePath := o._basePath + if _basePath == "" { + _basePath = "/api/v1" + } + _result.Path = golangswaggerpaths.Join(_basePath, _path) + + return &_result, nil +} + +// Must is a helper function to panic when the url builder returns an error +func (o *SubscriptionValidateURL) Must(u *url.URL, err error) *url.URL { + if err != nil { + panic(err) + } + if u == nil { + panic("url can't be nil") + } + return u +} + +// String returns the string representation of the path with query string +func (o *SubscriptionValidateURL) String() string { + return o.Must(o.Build()).String() +} + +// BuildFull builds a full url with scheme, host, path and query string +func (o *SubscriptionValidateURL) BuildFull(scheme, host string) (*url.URL, error) { + if scheme == "" { + return nil, errors.New("scheme is required for a full url on SubscriptionValidateURL") + } + if host == "" { + return nil, errors.New("host is required for a full url on SubscriptionValidateURL") + } + + base, err := o.Build() + if err != nil { + return nil, err + } + + base.Scheme = scheme + base.Host = host + return base, nil +} + +// StringFull returns the string representation of a complete url +func (o *SubscriptionValidateURL) StringFull(scheme, host string) string { + return o.Must(o.BuildFull(scheme, host)).String() +} diff --git a/restapi/operations/console_api.go b/restapi/operations/console_api.go index 2b11e3a98..ba3ab9246 100644 --- a/restapi/operations/console_api.go +++ b/restapi/operations/console_api.go @@ -289,6 +289,15 @@ func NewConsoleAPI(spec *loads.Document) *ConsoleAPI { UserAPIShareObjectHandler: user_api.ShareObjectHandlerFunc(func(params user_api.ShareObjectParams, principal *models.Principal) middleware.Responder { return middleware.NotImplemented("operation user_api.ShareObject has not yet been implemented") }), + AdminAPISubscriptionActivateHandler: admin_api.SubscriptionActivateHandlerFunc(func(params admin_api.SubscriptionActivateParams, principal *models.Principal) middleware.Responder { + return middleware.NotImplemented("operation admin_api.SubscriptionActivate has not yet been implemented") + }), + AdminAPISubscriptionInfoHandler: admin_api.SubscriptionInfoHandlerFunc(func(params admin_api.SubscriptionInfoParams, principal *models.Principal) middleware.Responder { + return middleware.NotImplemented("operation admin_api.SubscriptionInfo has not yet been implemented") + }), + AdminAPISubscriptionValidateHandler: admin_api.SubscriptionValidateHandlerFunc(func(params admin_api.SubscriptionValidateParams, principal *models.Principal) middleware.Responder { + return middleware.NotImplemented("operation admin_api.SubscriptionValidate has not yet been implemented") + }), AdminAPITenantAddPoolHandler: admin_api.TenantAddPoolHandlerFunc(func(params admin_api.TenantAddPoolParams, principal *models.Principal) middleware.Responder { return middleware.NotImplemented("operation admin_api.TenantAddPool has not yet been implemented") }), @@ -518,6 +527,12 @@ type ConsoleAPI struct { AdminAPISetPolicyMultipleHandler admin_api.SetPolicyMultipleHandler // UserAPIShareObjectHandler sets the operation handler for the share object operation UserAPIShareObjectHandler user_api.ShareObjectHandler + // AdminAPISubscriptionActivateHandler sets the operation handler for the subscription activate operation + AdminAPISubscriptionActivateHandler admin_api.SubscriptionActivateHandler + // AdminAPISubscriptionInfoHandler sets the operation handler for the subscription info operation + AdminAPISubscriptionInfoHandler admin_api.SubscriptionInfoHandler + // AdminAPISubscriptionValidateHandler sets the operation handler for the subscription validate operation + AdminAPISubscriptionValidateHandler admin_api.SubscriptionValidateHandler // AdminAPITenantAddPoolHandler sets the operation handler for the tenant add pool operation AdminAPITenantAddPoolHandler admin_api.TenantAddPoolHandler // AdminAPITenantInfoHandler sets the operation handler for the tenant info operation @@ -837,6 +852,15 @@ func (o *ConsoleAPI) Validate() error { if o.UserAPIShareObjectHandler == nil { unregistered = append(unregistered, "user_api.ShareObjectHandler") } + if o.AdminAPISubscriptionActivateHandler == nil { + unregistered = append(unregistered, "admin_api.SubscriptionActivateHandler") + } + if o.AdminAPISubscriptionInfoHandler == nil { + unregistered = append(unregistered, "admin_api.SubscriptionInfoHandler") + } + if o.AdminAPISubscriptionValidateHandler == nil { + unregistered = append(unregistered, "admin_api.SubscriptionValidateHandler") + } if o.AdminAPITenantAddPoolHandler == nil { unregistered = append(unregistered, "admin_api.TenantAddPoolHandler") } @@ -1269,6 +1293,18 @@ func (o *ConsoleAPI) initHandlerCache() { if o.handlers["POST"] == nil { o.handlers["POST"] = make(map[string]http.Handler) } + o.handlers["POST"]["/subscription/namespaces/{namespace}/tenants/{tenant}/activate"] = admin_api.NewSubscriptionActivate(o.context, o.AdminAPISubscriptionActivateHandler) + if o.handlers["GET"] == nil { + o.handlers["GET"] = make(map[string]http.Handler) + } + o.handlers["GET"]["/subscription/info"] = admin_api.NewSubscriptionInfo(o.context, o.AdminAPISubscriptionInfoHandler) + if o.handlers["POST"] == nil { + o.handlers["POST"] = make(map[string]http.Handler) + } + o.handlers["POST"]["/subscription/validate"] = admin_api.NewSubscriptionValidate(o.context, o.AdminAPISubscriptionValidateHandler) + if o.handlers["POST"] == nil { + o.handlers["POST"] = make(map[string]http.Handler) + } o.handlers["POST"]["/namespaces/{namespace}/tenants/{tenant}/pools"] = admin_api.NewTenantAddPool(o.context, o.AdminAPITenantAddPoolHandler) if o.handlers["GET"] == nil { o.handlers["GET"] = make(map[string]http.Handler) diff --git a/restapi/user_session.go b/restapi/user_session.go index c61eb64b2..a6c7f234a 100644 --- a/restapi/user_session.go +++ b/restapi/user_session.go @@ -42,8 +42,9 @@ func getSessionResponse(session *models.Principal) (*models.SessionResponse, *mo return nil, prepareError(errorGenericInvalidSession) } sessionResp := &models.SessionResponse{ - Pages: acl.GetAuthorizedEndpoints(session.Actions), - Status: models.SessionResponseStatusOk, + Pages: acl.GetAuthorizedEndpoints(session.Actions), + Status: models.SessionResponseStatusOk, + Operator: acl.GetOperatorMode(), } return sessionResp, nil } diff --git a/swagger.yml b/swagger.yml index bbf18c0cc..da779c49c 100644 --- a/swagger.yml +++ b/swagger.yml @@ -1450,6 +1450,68 @@ paths: tags: - AdminAPI + /subscription/info: + get: + summary: Subscription info + operationId: SubscriptionInfo + responses: + 200: + description: A successful response. + schema: + $ref: "#/definitions/license" + default: + description: Generic error response. + schema: + $ref: "#/definitions/error" + tags: + - AdminAPI + + /subscription/validate: + post: + summary: Validate a provided subscription license + operationId: SubscriptionValidate + parameters: + - name: body + in: body + required: true + schema: + $ref: "#/definitions/subscriptionValidateRequest" + responses: + 200: + description: A successful response. + schema: + $ref: "#/definitions/license" + default: + description: Generic error response. + schema: + $ref: "#/definitions/error" + tags: + - AdminAPI + + + /subscription/namespaces/{namespace}/tenants/{tenant}/activate: + post: + summary: Activate a particular tenant using the existing subscription license + operationId: SubscriptionActivate + parameters: + - name: namespace + in: path + required: true + type: string + - name: tenant + in: path + required: true + type: string + responses: + 204: + description: A successful response. + default: + description: Generic error response. + schema: + $ref: "#/definitions/error" + tags: + - AdminAPI + /admin/info: get: summary: Returns information about the deployment @@ -2553,6 +2615,8 @@ definitions: status: type: string enum: [ok] + operator: + type: boolean adminInfoResponse: type: object properties: @@ -2779,7 +2843,8 @@ definitions: format: int64 enable_prometheus: type: boolean - + subnet_license: + $ref: "#/definitions/license" tenantUsage: type: object properties: @@ -3717,3 +3782,29 @@ definitions: restart: description: Returns wheter server needs to restart to apply changes or not type: boolean + + + subscriptionValidateRequest: + type: object + properties: + license: + type: string + email: + type: string + password: + type: string + license: + type: object + properties: + email: + type: string + organization: + type: string + account_id: + type: integer + storage_capacity: + type: integer + plan: + type: string + expires_at: + type: string