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 }