github.com/minio/console@v1.4.1/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  	subnetKey, err := GetSubnetKeyFromMinIOConfig(ctx, minioClient)
   203  	if err != nil {
   204  		return nil, err
   205  	}
   206  
   207  	proxy := getSubnetProxy()
   208  	if subnetKey.Proxy != "" {
   209  		proxy = subnetKey.Proxy
   210  	}
   211  
   212  	tr := GlobalTransport.Clone()
   213  	if proxy != "" {
   214  		u, err := url.Parse(proxy)
   215  		if err != nil {
   216  			return nil, err
   217  		}
   218  		tr.Proxy = http.ProxyURL(u)
   219  	}
   220  
   221  	return &xhttp.Client{
   222  		Client: &http.Client{
   223  			Transport: &ConsoleTransport{
   224  				Transport: tr,
   225  				ClientIP:  clientIP,
   226  			},
   227  		},
   228  	}, nil
   229  }
   230  
   231  func GetSubnetLoginWithMFAResponse(session *models.Principal, params subnetApi.SubnetLoginMFAParams) (*models.SubnetLoginResponse, *CodedAPIError) {
   232  	ctx, cancel := context.WithCancel(params.HTTPRequest.Context())
   233  	defer cancel()
   234  	mAdmin, err := NewMinioAdminClient(params.HTTPRequest.Context(), session)
   235  	if err != nil {
   236  		return nil, ErrorWithContext(ctx, err)
   237  	}
   238  	minioClient := AdminClient{Client: mAdmin}
   239  	return subnetLoginWithMFAResponse(ctx, minioClient, params)
   240  }
   241  
   242  func subnetLoginWithMFAResponse(ctx context.Context, minioClient MinioAdmin, params subnetApi.SubnetLoginMFAParams) (*models.SubnetLoginResponse, *CodedAPIError) {
   243  	subnetHTTPClient, err := GetSubnetHTTPClient(ctx, minioClient)
   244  	if err != nil {
   245  		return nil, ErrorWithContext(ctx, err)
   246  	}
   247  	resp, err := SubnetLoginWithMFA(subnetHTTPClient, *params.Body.Username, *params.Body.MfaToken, *params.Body.Otp)
   248  	if err != nil {
   249  		return nil, ErrorWithContext(ctx, err)
   250  	}
   251  	return resp, nil
   252  }
   253  
   254  func GetSubnetKeyFromMinIOConfig(ctx context.Context, minioClient MinioAdmin) (*subnet.LicenseTokenConfig, error) {
   255  	buf, err := minioClient.getConfigKV(ctx, madmin.SubnetSubSys)
   256  	if err != nil {
   257  		return nil, err
   258  	}
   259  
   260  	subSysConfigs, err := madmin.ParseServerConfigOutput(string(buf))
   261  	if err != nil {
   262  		return nil, err
   263  	}
   264  
   265  	for _, scfg := range subSysConfigs {
   266  		if scfg.Target == "" {
   267  			res := subnet.LicenseTokenConfig{}
   268  			res.APIKey, _ = scfg.Lookup("api_key")
   269  			res.License, _ = scfg.Lookup("license")
   270  			res.Proxy, _ = scfg.Lookup("proxy")
   271  			return &res, nil
   272  		}
   273  	}
   274  
   275  	return nil, errors.New("unable to find subnet configuration")
   276  }
   277  
   278  func GetSubnetRegister(ctx context.Context, minioClient MinioAdmin, httpClient xhttp.ClientI, params subnetApi.SubnetRegisterParams) error {
   279  	serverInfo, err := minioClient.serverInfo(ctx)
   280  	if err != nil {
   281  		return err
   282  	}
   283  	registerResult, err := subnet.Register(httpClient, serverInfo, "", *params.Body.Token, *params.Body.AccountID)
   284  	if err != nil {
   285  		return err
   286  	}
   287  	// Keep existing subnet proxy if exists
   288  	subnetKey, err := GetSubnetKeyFromMinIOConfig(ctx, minioClient)
   289  	if err != nil {
   290  		return err
   291  	}
   292  	configStr := fmt.Sprintf("subnet license=%s api_key=%s proxy=%s", registerResult.License, registerResult.APIKey, subnetKey.Proxy)
   293  	_, err = minioClient.setConfigKV(ctx, configStr)
   294  	if err != nil {
   295  		return err
   296  	}
   297  	return nil
   298  }
   299  
   300  func GetSubnetRegisterResponse(session *models.Principal, params subnetApi.SubnetRegisterParams) *CodedAPIError {
   301  	ctx, cancel := context.WithCancel(params.HTTPRequest.Context())
   302  	defer cancel()
   303  	mAdmin, err := NewMinioAdminClient(params.HTTPRequest.Context(), session)
   304  	if err != nil {
   305  		return ErrorWithContext(ctx, err)
   306  	}
   307  	adminClient := AdminClient{Client: mAdmin}
   308  	return subnetRegisterResponse(ctx, adminClient, params)
   309  }
   310  
   311  func subnetRegisterResponse(ctx context.Context, minioClient MinioAdmin, params subnetApi.SubnetRegisterParams) *CodedAPIError {
   312  	subnetHTTPClient, err := GetSubnetHTTPClient(ctx, minioClient)
   313  	if err != nil {
   314  		return ErrorWithContext(ctx, err)
   315  	}
   316  	err = GetSubnetRegister(ctx, minioClient, subnetHTTPClient, params)
   317  	if err != nil {
   318  		return ErrorWithContext(ctx, err)
   319  	}
   320  	return nil
   321  }
   322  
   323  var ErrSubnetLicenseNotFound = errors.New("license not found")
   324  
   325  func GetSubnetInfoResponse(session *models.Principal, params subnetApi.SubnetInfoParams) (*models.License, *CodedAPIError) {
   326  	ctx, cancel := context.WithCancel(params.HTTPRequest.Context())
   327  	defer cancel()
   328  	clientIP := utils.ClientIPFromContext(ctx)
   329  	client := &xhttp.Client{
   330  		Client: GetConsoleHTTPClient(clientIP),
   331  	}
   332  	// license gets seeded to us by MinIO
   333  	seededLicense := os.Getenv(EnvSubnetLicense)
   334  	// if it's missing, we will gracefully fallback to attempt to fetch it from MinIO
   335  	if seededLicense == "" {
   336  		mAdmin, err := NewMinioAdminClient(params.HTTPRequest.Context(), session)
   337  		if err != nil {
   338  			return nil, ErrorWithContext(ctx, err)
   339  		}
   340  		adminClient := AdminClient{Client: mAdmin}
   341  
   342  		configBytes, err := adminClient.getConfigKV(params.HTTPRequest.Context(), "subnet")
   343  		if err != nil {
   344  			return nil, ErrorWithContext(ctx, err)
   345  		}
   346  		subSysConfigs, err := madmin.ParseServerConfigOutput(string(configBytes))
   347  		if err != nil {
   348  			return nil, ErrorWithContext(ctx, err)
   349  		}
   350  		// search for licese
   351  		for _, v := range subSysConfigs {
   352  			for _, sv := range v.KV {
   353  				if sv.Key == "license" {
   354  					seededLicense = sv.Value
   355  				}
   356  			}
   357  		}
   358  	}
   359  	// still empty means not found
   360  	if seededLicense == "" {
   361  		return nil, ErrorWithContext(ctx, ErrSubnetLicenseNotFound)
   362  	}
   363  
   364  	licenseInfo, err := getLicenseInfo(*client.Client, seededLicense)
   365  	if err != nil {
   366  		return nil, ErrorWithContext(ctx, err)
   367  	}
   368  	license := &models.License{
   369  		Email:           licenseInfo.Email,
   370  		AccountID:       licenseInfo.AccountID,
   371  		StorageCapacity: licenseInfo.StorageCapacity,
   372  		Plan:            licenseInfo.Plan,
   373  		ExpiresAt:       licenseInfo.ExpiresAt.String(),
   374  		Organization:    licenseInfo.Organization,
   375  	}
   376  	return license, nil
   377  }
   378  
   379  func GetSubnetRegToken(ctx context.Context, minioClient MinioAdmin) (string, error) {
   380  	serverInfo, err := minioClient.serverInfo(ctx)
   381  	if err != nil {
   382  		return "", err
   383  	}
   384  	regInfo := subnet.GetClusterRegInfo(serverInfo)
   385  	regToken, err := subnet.GenerateRegToken(regInfo)
   386  	if err != nil {
   387  		return "", err
   388  	}
   389  	return regToken, nil
   390  }
   391  
   392  func GetSubnetRegTokenResponse(session *models.Principal, params subnetApi.SubnetRegTokenParams) (*models.SubnetRegTokenResponse, *CodedAPIError) {
   393  	ctx, cancel := context.WithCancel(params.HTTPRequest.Context())
   394  	defer cancel()
   395  	mAdmin, err := NewMinioAdminClient(params.HTTPRequest.Context(), session)
   396  	if err != nil {
   397  		return nil, ErrorWithContext(ctx, err)
   398  	}
   399  	adminClient := AdminClient{Client: mAdmin}
   400  	return subnetRegTokenResponse(ctx, adminClient)
   401  }
   402  
   403  func subnetRegTokenResponse(ctx context.Context, minioClient MinioAdmin) (*models.SubnetRegTokenResponse, *CodedAPIError) {
   404  	token, err := GetSubnetRegToken(ctx, minioClient)
   405  	if err != nil {
   406  		return nil, ErrorWithContext(ctx, err)
   407  	}
   408  	return &models.SubnetRegTokenResponse{
   409  		RegToken: token,
   410  	}, nil
   411  }
   412  
   413  func GetSubnetAPIKeyResponse(session *models.Principal, params subnetApi.SubnetAPIKeyParams) (*models.APIKey, *CodedAPIError) {
   414  	ctx, cancel := context.WithCancel(params.HTTPRequest.Context())
   415  	defer cancel()
   416  	mAdmin, err := NewMinioAdminClient(params.HTTPRequest.Context(), session)
   417  	if err != nil {
   418  		return nil, ErrorWithContext(ctx, err)
   419  	}
   420  	adminClient := AdminClient{Client: mAdmin}
   421  	return subnetAPIKeyResponse(ctx, adminClient, params)
   422  }
   423  
   424  func subnetAPIKeyResponse(ctx context.Context, minioClient MinioAdmin, params subnetApi.SubnetAPIKeyParams) (*models.APIKey, *CodedAPIError) {
   425  	subnetHTTPClient, err := GetSubnetHTTPClient(ctx, minioClient)
   426  	if err != nil {
   427  		return nil, ErrorWithContext(ctx, err)
   428  	}
   429  	token := params.HTTPRequest.URL.Query().Get("token")
   430  	apiKey, err := subnet.GetAPIKey(subnetHTTPClient, token)
   431  	if err != nil {
   432  		return nil, ErrorWithContext(ctx, err)
   433  	}
   434  	return &models.APIKey{APIKey: apiKey}, nil
   435  }