github.com/minio/console@v1.3.0/api/admin_subnet.go (about)

     1  // This file is part of MinIO Console Server
     2  // Copyright (c) 2021 MinIO, Inc.
     3  //
     4  // This program is free software: you can redistribute it and/or modify
     5  // it under the terms of the GNU Affero General Public License as published by
     6  // the Free Software Foundation, either version 3 of the License, or
     7  // (at your option) any later version.
     8  //
     9  // This program is distributed in the hope that it will be useful,
    10  // but WITHOUT ANY WARRANTY; without even the implied warranty of
    11  // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    12  // GNU Affero General Public License for more details.
    13  //
    14  // You should have received a copy of the GNU Affero General Public License
    15  // along with this program.  If not, see <http://www.gnu.org/licenses/>.
    16  //
    17  
    18  package api
    19  
    20  import (
    21  	"context"
    22  	"errors"
    23  	"fmt"
    24  	"net/http"
    25  	"net/url"
    26  	"os"
    27  
    28  	"github.com/minio/console/pkg/utils"
    29  
    30  	xhttp "github.com/minio/console/pkg/http"
    31  
    32  	"github.com/go-openapi/runtime/middleware"
    33  	"github.com/minio/console/api/operations"
    34  	subnetApi "github.com/minio/console/api/operations/subnet"
    35  	"github.com/minio/console/models"
    36  	"github.com/minio/console/pkg/subnet"
    37  	"github.com/minio/madmin-go/v3"
    38  )
    39  
    40  func registerSubnetHandlers(api *operations.ConsoleAPI) {
    41  	// Get subnet login handler
    42  	api.SubnetSubnetLoginHandler = subnetApi.SubnetLoginHandlerFunc(func(params subnetApi.SubnetLoginParams, session *models.Principal) middleware.Responder {
    43  		resp, err := GetSubnetLoginResponse(session, params)
    44  		if err != nil {
    45  			return subnetApi.NewSubnetLoginDefault(err.Code).WithPayload(err.APIError)
    46  		}
    47  		return subnetApi.NewSubnetLoginOK().WithPayload(resp)
    48  	})
    49  	// Get subnet login with MFA handler
    50  	api.SubnetSubnetLoginMFAHandler = subnetApi.SubnetLoginMFAHandlerFunc(func(params subnetApi.SubnetLoginMFAParams, session *models.Principal) middleware.Responder {
    51  		resp, err := GetSubnetLoginWithMFAResponse(session, params)
    52  		if err != nil {
    53  			return subnetApi.NewSubnetLoginMFADefault(err.Code).WithPayload(err.APIError)
    54  		}
    55  		return subnetApi.NewSubnetLoginMFAOK().WithPayload(resp)
    56  	})
    57  	// Get subnet register
    58  	api.SubnetSubnetRegisterHandler = subnetApi.SubnetRegisterHandlerFunc(func(params subnetApi.SubnetRegisterParams, session *models.Principal) middleware.Responder {
    59  		err := GetSubnetRegisterResponse(session, params)
    60  		if err != nil {
    61  			return subnetApi.NewSubnetRegisterDefault(err.Code).WithPayload(err.APIError)
    62  		}
    63  		return subnetApi.NewSubnetRegisterOK()
    64  	})
    65  	// Get subnet info
    66  	api.SubnetSubnetInfoHandler = subnetApi.SubnetInfoHandlerFunc(func(params subnetApi.SubnetInfoParams, session *models.Principal) middleware.Responder {
    67  		resp, err := GetSubnetInfoResponse(session, params)
    68  		if err != nil {
    69  			return subnetApi.NewSubnetInfoDefault(err.Code).WithPayload(err.APIError)
    70  		}
    71  		return subnetApi.NewSubnetInfoOK().WithPayload(resp)
    72  	})
    73  	// Get subnet registration token
    74  	api.SubnetSubnetRegTokenHandler = subnetApi.SubnetRegTokenHandlerFunc(func(params subnetApi.SubnetRegTokenParams, session *models.Principal) middleware.Responder {
    75  		resp, err := GetSubnetRegTokenResponse(session, params)
    76  		if err != nil {
    77  			return subnetApi.NewSubnetRegTokenDefault(err.Code).WithPayload(err.APIError)
    78  		}
    79  		return subnetApi.NewSubnetRegTokenOK().WithPayload(resp)
    80  	})
    81  
    82  	api.SubnetSubnetAPIKeyHandler = subnetApi.SubnetAPIKeyHandlerFunc(func(params subnetApi.SubnetAPIKeyParams, session *models.Principal) middleware.Responder {
    83  		resp, err := GetSubnetAPIKeyResponse(session, params)
    84  		if err != nil {
    85  			return subnetApi.NewSubnetAPIKeyDefault(err.Code).WithPayload(err.APIError)
    86  		}
    87  		return subnetApi.NewSubnetAPIKeyOK().WithPayload(resp)
    88  	})
    89  }
    90  
    91  const EnvSubnetLicense = "CONSOLE_SUBNET_LICENSE"
    92  
    93  func SubnetRegisterWithAPIKey(ctx context.Context, minioClient MinioAdmin, apiKey string) (bool, error) {
    94  	serverInfo, err := minioClient.serverInfo(ctx)
    95  	if err != nil {
    96  		return false, err
    97  	}
    98  	clientIP := utils.ClientIPFromContext(ctx)
    99  	registerResult, err := subnet.Register(GetConsoleHTTPClient("", clientIP), serverInfo, apiKey, "", "")
   100  	if err != nil {
   101  		return false, err
   102  	}
   103  	// Keep existing subnet proxy if exists
   104  	subnetKey, err := GetSubnetKeyFromMinIOConfig(ctx, minioClient)
   105  	if err != nil {
   106  		return false, err
   107  	}
   108  	configStr := fmt.Sprintf("subnet license=%s api_key=%s proxy=%s", registerResult.License, registerResult.APIKey, subnetKey.Proxy)
   109  	_, err = minioClient.setConfigKV(ctx, configStr)
   110  	if err != nil {
   111  		return false, err
   112  	}
   113  	// cluster registered correctly
   114  	return true, nil
   115  }
   116  
   117  func SubnetLogin(client xhttp.ClientI, username, password string) (string, string, error) {
   118  	tokens, err := subnet.Login(client, username, password)
   119  	if err != nil {
   120  		return "", "", err
   121  	}
   122  	if tokens.MfaToken != "" {
   123  		// user needs to complete login flow using mfa
   124  		return "", tokens.MfaToken, nil
   125  	}
   126  	if tokens.AccessToken != "" {
   127  		// register token to minio
   128  		return tokens.AccessToken, "", nil
   129  	}
   130  	return "", "", errors.New("something went wrong")
   131  }
   132  
   133  func GetSubnetLoginResponse(session *models.Principal, params subnetApi.SubnetLoginParams) (*models.SubnetLoginResponse, *CodedAPIError) {
   134  	ctx, cancel := context.WithCancel(params.HTTPRequest.Context())
   135  	defer cancel()
   136  	mAdmin, err := NewMinioAdminClient(params.HTTPRequest.Context(), session)
   137  	if err != nil {
   138  		return nil, ErrorWithContext(ctx, err)
   139  	}
   140  	return subnetLoginResponse(ctx, AdminClient{Client: mAdmin}, params)
   141  }
   142  
   143  func subnetLoginResponse(ctx context.Context, minioClient MinioAdmin, params subnetApi.SubnetLoginParams) (*models.SubnetLoginResponse, *CodedAPIError) {
   144  	subnetHTTPClient, err := GetSubnetHTTPClient(ctx, minioClient)
   145  	if err != nil {
   146  		return nil, ErrorWithContext(ctx, err)
   147  	}
   148  	apiKey := params.Body.APIKey
   149  	if apiKey != "" {
   150  		registered, err := SubnetRegisterWithAPIKey(ctx, minioClient, apiKey)
   151  		if err != nil {
   152  			return nil, ErrorWithContext(ctx, err)
   153  		}
   154  		return &models.SubnetLoginResponse{
   155  			Registered:    registered,
   156  			Organizations: []*models.SubnetOrganization{},
   157  		}, nil
   158  	}
   159  	username := params.Body.Username
   160  	password := params.Body.Password
   161  	if username != "" && password != "" {
   162  		token, mfa, err := SubnetLogin(subnetHTTPClient, username, password)
   163  		if err != nil {
   164  			return nil, ErrorWithContext(ctx, err)
   165  		}
   166  		return &models.SubnetLoginResponse{
   167  			MfaToken:      mfa,
   168  			AccessToken:   token,
   169  			Organizations: []*models.SubnetOrganization{},
   170  		}, nil
   171  	}
   172  	return nil, ErrorWithContext(ctx, ErrDefault)
   173  }
   174  
   175  type SubnetRegistration struct {
   176  	AccessToken   string
   177  	MFAToken      string
   178  	Organizations []models.SubnetOrganization
   179  }
   180  
   181  func SubnetLoginWithMFA(client xhttp.ClientI, username, mfaToken, otp string) (*models.SubnetLoginResponse, error) {
   182  	tokens, err := subnet.LoginWithMFA(client, username, mfaToken, otp)
   183  	if err != nil {
   184  		return nil, err
   185  	}
   186  	if tokens.AccessToken != "" {
   187  		organizations, errOrg := subnet.GetOrganizations(client, tokens.AccessToken)
   188  		if errOrg != nil {
   189  			return nil, errOrg
   190  		}
   191  		return &models.SubnetLoginResponse{
   192  			AccessToken:   tokens.AccessToken,
   193  			Organizations: organizations,
   194  		}, nil
   195  	}
   196  	return nil, errors.New("something went wrong")
   197  }
   198  
   199  // GetSubnetHTTPClient will return a client with proxy if configured, otherwise will return the default console http client
   200  func GetSubnetHTTPClient(ctx context.Context, minioClient MinioAdmin) (*xhttp.Client, error) {
   201  	clientIP := utils.ClientIPFromContext(ctx)
   202  	subnetHTTPClient := GetConsoleHTTPClient("", clientIP)
   203  	subnetKey, err := GetSubnetKeyFromMinIOConfig(ctx, minioClient)
   204  	if err != nil {
   205  		return nil, err
   206  	}
   207  
   208  	proxy := getSubnetProxy()
   209  	if subnetKey.Proxy != "" {
   210  		proxy = subnetKey.Proxy
   211  	}
   212  	if proxy != "" {
   213  		subnetProxyURL, err := url.Parse(proxy)
   214  		if err != nil {
   215  			return nil, err
   216  		}
   217  		subnetHTTPClient.Transport.(*ConsoleTransport).Transport.Proxy = http.ProxyURL(subnetProxyURL)
   218  	}
   219  
   220  	clientI := &xhttp.Client{
   221  		Client: subnetHTTPClient,
   222  	}
   223  	return clientI, nil
   224  }
   225  
   226  func GetSubnetLoginWithMFAResponse(session *models.Principal, params subnetApi.SubnetLoginMFAParams) (*models.SubnetLoginResponse, *CodedAPIError) {
   227  	ctx, cancel := context.WithCancel(params.HTTPRequest.Context())
   228  	defer cancel()
   229  	mAdmin, err := NewMinioAdminClient(params.HTTPRequest.Context(), session)
   230  	if err != nil {
   231  		return nil, ErrorWithContext(ctx, err)
   232  	}
   233  	minioClient := AdminClient{Client: mAdmin}
   234  	return subnetLoginWithMFAResponse(ctx, minioClient, params)
   235  }
   236  
   237  func subnetLoginWithMFAResponse(ctx context.Context, minioClient MinioAdmin, params subnetApi.SubnetLoginMFAParams) (*models.SubnetLoginResponse, *CodedAPIError) {
   238  	subnetHTTPClient, err := GetSubnetHTTPClient(ctx, minioClient)
   239  	if err != nil {
   240  		return nil, ErrorWithContext(ctx, err)
   241  	}
   242  	resp, err := SubnetLoginWithMFA(subnetHTTPClient, *params.Body.Username, *params.Body.MfaToken, *params.Body.Otp)
   243  	if err != nil {
   244  		return nil, ErrorWithContext(ctx, err)
   245  	}
   246  	return resp, nil
   247  }
   248  
   249  func GetSubnetKeyFromMinIOConfig(ctx context.Context, minioClient MinioAdmin) (*subnet.LicenseTokenConfig, error) {
   250  	buf, err := minioClient.getConfigKV(ctx, madmin.SubnetSubSys)
   251  	if err != nil {
   252  		return nil, err
   253  	}
   254  
   255  	subSysConfigs, err := madmin.ParseServerConfigOutput(string(buf))
   256  	if err != nil {
   257  		return nil, err
   258  	}
   259  
   260  	for _, scfg := range subSysConfigs {
   261  		if scfg.Target == "" {
   262  			res := subnet.LicenseTokenConfig{}
   263  			res.APIKey, _ = scfg.Lookup("api_key")
   264  			res.License, _ = scfg.Lookup("license")
   265  			res.Proxy, _ = scfg.Lookup("proxy")
   266  			return &res, nil
   267  		}
   268  	}
   269  
   270  	return nil, errors.New("unable to find subnet configuration")
   271  }
   272  
   273  func GetSubnetRegister(ctx context.Context, minioClient MinioAdmin, httpClient xhttp.ClientI, params subnetApi.SubnetRegisterParams) error {
   274  	serverInfo, err := minioClient.serverInfo(ctx)
   275  	if err != nil {
   276  		return err
   277  	}
   278  	registerResult, err := subnet.Register(httpClient, serverInfo, "", *params.Body.Token, *params.Body.AccountID)
   279  	if err != nil {
   280  		return err
   281  	}
   282  	// Keep existing subnet proxy if exists
   283  	subnetKey, err := GetSubnetKeyFromMinIOConfig(ctx, minioClient)
   284  	if err != nil {
   285  		return err
   286  	}
   287  	configStr := fmt.Sprintf("subnet license=%s api_key=%s proxy=%s", registerResult.License, registerResult.APIKey, subnetKey.Proxy)
   288  	_, err = minioClient.setConfigKV(ctx, configStr)
   289  	if err != nil {
   290  		return err
   291  	}
   292  	return nil
   293  }
   294  
   295  func GetSubnetRegisterResponse(session *models.Principal, params subnetApi.SubnetRegisterParams) *CodedAPIError {
   296  	ctx, cancel := context.WithCancel(params.HTTPRequest.Context())
   297  	defer cancel()
   298  	mAdmin, err := NewMinioAdminClient(params.HTTPRequest.Context(), session)
   299  	if err != nil {
   300  		return ErrorWithContext(ctx, err)
   301  	}
   302  	adminClient := AdminClient{Client: mAdmin}
   303  	return subnetRegisterResponse(ctx, adminClient, params)
   304  }
   305  
   306  func subnetRegisterResponse(ctx context.Context, minioClient MinioAdmin, params subnetApi.SubnetRegisterParams) *CodedAPIError {
   307  	subnetHTTPClient, err := GetSubnetHTTPClient(ctx, minioClient)
   308  	if err != nil {
   309  		return ErrorWithContext(ctx, err)
   310  	}
   311  	err = GetSubnetRegister(ctx, minioClient, subnetHTTPClient, params)
   312  	if err != nil {
   313  		return ErrorWithContext(ctx, err)
   314  	}
   315  	return nil
   316  }
   317  
   318  var ErrSubnetLicenseNotFound = errors.New("license not found")
   319  
   320  func GetSubnetInfoResponse(session *models.Principal, params subnetApi.SubnetInfoParams) (*models.License, *CodedAPIError) {
   321  	ctx, cancel := context.WithCancel(params.HTTPRequest.Context())
   322  	defer cancel()
   323  	clientIP := utils.ClientIPFromContext(ctx)
   324  	client := &xhttp.Client{
   325  		Client: GetConsoleHTTPClient("", clientIP),
   326  	}
   327  	// license gets seeded to us by MinIO
   328  	seededLicense := os.Getenv(EnvSubnetLicense)
   329  	// if it's missing, we will gracefully fallback to attempt to fetch it from MinIO
   330  	if seededLicense == "" {
   331  		mAdmin, err := NewMinioAdminClient(params.HTTPRequest.Context(), session)
   332  		if err != nil {
   333  			return nil, ErrorWithContext(ctx, err)
   334  		}
   335  		adminClient := AdminClient{Client: mAdmin}
   336  
   337  		configBytes, err := adminClient.getConfigKV(params.HTTPRequest.Context(), "subnet")
   338  		if err != nil {
   339  			return nil, ErrorWithContext(ctx, err)
   340  		}
   341  		subSysConfigs, err := madmin.ParseServerConfigOutput(string(configBytes))
   342  		if err != nil {
   343  			return nil, ErrorWithContext(ctx, err)
   344  		}
   345  		// search for licese
   346  		for _, v := range subSysConfigs {
   347  			for _, sv := range v.KV {
   348  				if sv.Key == "license" {
   349  					seededLicense = sv.Value
   350  				}
   351  			}
   352  		}
   353  	}
   354  	// still empty means not found
   355  	if seededLicense == "" {
   356  		return nil, ErrorWithContext(ctx, ErrSubnetLicenseNotFound)
   357  	}
   358  
   359  	licenseInfo, err := getLicenseInfo(*client.Client, seededLicense)
   360  	if err != nil {
   361  		return nil, ErrorWithContext(ctx, err)
   362  	}
   363  	license := &models.License{
   364  		Email:           licenseInfo.Email,
   365  		AccountID:       licenseInfo.AccountID,
   366  		StorageCapacity: licenseInfo.StorageCapacity,
   367  		Plan:            licenseInfo.Plan,
   368  		ExpiresAt:       licenseInfo.ExpiresAt.String(),
   369  		Organization:    licenseInfo.Organization,
   370  	}
   371  	return license, nil
   372  }
   373  
   374  func GetSubnetRegToken(ctx context.Context, minioClient MinioAdmin) (string, error) {
   375  	serverInfo, err := minioClient.serverInfo(ctx)
   376  	if err != nil {
   377  		return "", err
   378  	}
   379  	regInfo := subnet.GetClusterRegInfo(serverInfo)
   380  	regToken, err := subnet.GenerateRegToken(regInfo)
   381  	if err != nil {
   382  		return "", err
   383  	}
   384  	return regToken, nil
   385  }
   386  
   387  func GetSubnetRegTokenResponse(session *models.Principal, params subnetApi.SubnetRegTokenParams) (*models.SubnetRegTokenResponse, *CodedAPIError) {
   388  	ctx, cancel := context.WithCancel(params.HTTPRequest.Context())
   389  	defer cancel()
   390  	mAdmin, err := NewMinioAdminClient(params.HTTPRequest.Context(), session)
   391  	if err != nil {
   392  		return nil, ErrorWithContext(ctx, err)
   393  	}
   394  	adminClient := AdminClient{Client: mAdmin}
   395  	return subnetRegTokenResponse(ctx, adminClient)
   396  }
   397  
   398  func subnetRegTokenResponse(ctx context.Context, minioClient MinioAdmin) (*models.SubnetRegTokenResponse, *CodedAPIError) {
   399  	token, err := GetSubnetRegToken(ctx, minioClient)
   400  	if err != nil {
   401  		return nil, ErrorWithContext(ctx, err)
   402  	}
   403  	return &models.SubnetRegTokenResponse{
   404  		RegToken: token,
   405  	}, nil
   406  }
   407  
   408  func GetSubnetAPIKeyResponse(session *models.Principal, params subnetApi.SubnetAPIKeyParams) (*models.APIKey, *CodedAPIError) {
   409  	ctx, cancel := context.WithCancel(params.HTTPRequest.Context())
   410  	defer cancel()
   411  	mAdmin, err := NewMinioAdminClient(params.HTTPRequest.Context(), session)
   412  	if err != nil {
   413  		return nil, ErrorWithContext(ctx, err)
   414  	}
   415  	adminClient := AdminClient{Client: mAdmin}
   416  	return subnetAPIKeyResponse(ctx, adminClient, params)
   417  }
   418  
   419  func subnetAPIKeyResponse(ctx context.Context, minioClient MinioAdmin, params subnetApi.SubnetAPIKeyParams) (*models.APIKey, *CodedAPIError) {
   420  	subnetHTTPClient, err := GetSubnetHTTPClient(ctx, minioClient)
   421  	if err != nil {
   422  		return nil, ErrorWithContext(ctx, err)
   423  	}
   424  	token := params.HTTPRequest.URL.Query().Get("token")
   425  	apiKey, err := subnet.GetAPIKey(subnetHTTPClient, token)
   426  	if err != nil {
   427  		return nil, ErrorWithContext(ctx, err)
   428  	}
   429  	return &models.APIKey{APIKey: apiKey}, nil
   430  }