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 }