github.com/minio/console@v1.4.1/api/client.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 package api 18 19 import ( 20 "context" 21 "errors" 22 "fmt" 23 "io" 24 "path" 25 "strings" 26 "time" 27 28 "github.com/minio/minio-go/v7/pkg/replication" 29 "github.com/minio/minio-go/v7/pkg/sse" 30 xnet "github.com/minio/pkg/v3/net" 31 32 "github.com/minio/console/models" 33 "github.com/minio/console/pkg" 34 "github.com/minio/console/pkg/auth" 35 "github.com/minio/console/pkg/auth/ldap" 36 xjwt "github.com/minio/console/pkg/auth/token" 37 mc "github.com/minio/mc/cmd" 38 "github.com/minio/mc/pkg/probe" 39 "github.com/minio/minio-go/v7" 40 "github.com/minio/minio-go/v7/pkg/credentials" 41 "github.com/minio/minio-go/v7/pkg/lifecycle" 42 "github.com/minio/minio-go/v7/pkg/notification" 43 "github.com/minio/minio-go/v7/pkg/tags" 44 ) 45 46 func init() { 47 // All minio-go API operations shall be performed only once, 48 // another way to look at this is we are turning off retries. 49 minio.MaxRetry = 1 50 } 51 52 // MinioClient interface with all functions to be implemented 53 // by mock when testing, it should include all MinioClient respective api calls 54 // that are used within this project. 55 type MinioClient interface { 56 listBucketsWithContext(ctx context.Context) ([]minio.BucketInfo, error) 57 makeBucketWithContext(ctx context.Context, bucketName, location string, objectLocking bool) error 58 setBucketPolicyWithContext(ctx context.Context, bucketName, policy string) error 59 removeBucket(ctx context.Context, bucketName string) error 60 getBucketNotification(ctx context.Context, bucketName string) (config notification.Configuration, err error) 61 getBucketPolicy(ctx context.Context, bucketName string) (string, error) 62 listObjects(ctx context.Context, bucket string, opts minio.ListObjectsOptions) <-chan minio.ObjectInfo 63 getObjectRetention(ctx context.Context, bucketName, objectName, versionID string) (mode *minio.RetentionMode, retainUntilDate *time.Time, err error) 64 getObjectLegalHold(ctx context.Context, bucketName, objectName string, opts minio.GetObjectLegalHoldOptions) (status *minio.LegalHoldStatus, err error) 65 putObject(ctx context.Context, bucketName, objectName string, reader io.Reader, objectSize int64, opts minio.PutObjectOptions) (info minio.UploadInfo, err error) 66 putObjectLegalHold(ctx context.Context, bucketName, objectName string, opts minio.PutObjectLegalHoldOptions) error 67 putObjectRetention(ctx context.Context, bucketName, objectName string, opts minio.PutObjectRetentionOptions) error 68 statObject(ctx context.Context, bucketName, prefix string, opts minio.GetObjectOptions) (objectInfo minio.ObjectInfo, err error) 69 setBucketEncryption(ctx context.Context, bucketName string, config *sse.Configuration) error 70 removeBucketEncryption(ctx context.Context, bucketName string) error 71 getBucketEncryption(ctx context.Context, bucketName string) (*sse.Configuration, error) 72 putObjectTagging(ctx context.Context, bucketName, objectName string, otags *tags.Tags, opts minio.PutObjectTaggingOptions) error 73 getObjectTagging(ctx context.Context, bucketName, objectName string, opts minio.GetObjectTaggingOptions) (*tags.Tags, error) 74 setObjectLockConfig(ctx context.Context, bucketName string, mode *minio.RetentionMode, validity *uint, unit *minio.ValidityUnit) error 75 getBucketObjectLockConfig(ctx context.Context, bucketName string) (mode *minio.RetentionMode, validity *uint, unit *minio.ValidityUnit, err error) 76 getObjectLockConfig(ctx context.Context, bucketName string) (lock string, mode *minio.RetentionMode, validity *uint, unit *minio.ValidityUnit, err error) 77 getLifecycleRules(ctx context.Context, bucketName string) (lifecycle *lifecycle.Configuration, err error) 78 setBucketLifecycle(ctx context.Context, bucketName string, config *lifecycle.Configuration) error 79 copyObject(ctx context.Context, dst minio.CopyDestOptions, src minio.CopySrcOptions) (minio.UploadInfo, error) 80 GetBucketTagging(ctx context.Context, bucketName string) (*tags.Tags, error) 81 SetBucketTagging(ctx context.Context, bucketName string, tags *tags.Tags) error 82 RemoveBucketTagging(ctx context.Context, bucketName string) error 83 } 84 85 // Interface implementation 86 // 87 // Define the structure of a minIO Client and define the functions that are actually used 88 // from minIO api. 89 type minioClient struct { 90 client *minio.Client 91 } 92 93 func (c minioClient) GetBucketTagging(ctx context.Context, bucketName string) (*tags.Tags, error) { 94 return c.client.GetBucketTagging(ctx, bucketName) 95 } 96 97 func (c minioClient) SetBucketTagging(ctx context.Context, bucketName string, tags *tags.Tags) error { 98 return c.client.SetBucketTagging(ctx, bucketName, tags) 99 } 100 101 func (c minioClient) RemoveBucketTagging(ctx context.Context, bucketName string) error { 102 return c.client.RemoveBucketTagging(ctx, bucketName) 103 } 104 105 // implements minio.ListBuckets(ctx) 106 func (c minioClient) listBucketsWithContext(ctx context.Context) ([]minio.BucketInfo, error) { 107 return c.client.ListBuckets(ctx) 108 } 109 110 // implements minio.MakeBucketWithContext(ctx, bucketName, location, objectLocking) 111 func (c minioClient) makeBucketWithContext(ctx context.Context, bucketName, location string, objectLocking bool) error { 112 return c.client.MakeBucket(ctx, bucketName, minio.MakeBucketOptions{ 113 Region: location, 114 ObjectLocking: objectLocking, 115 }) 116 } 117 118 // implements minio.SetBucketPolicyWithContext(ctx, bucketName, policy) 119 func (c minioClient) setBucketPolicyWithContext(ctx context.Context, bucketName, policy string) error { 120 return c.client.SetBucketPolicy(ctx, bucketName, policy) 121 } 122 123 // implements minio.RemoveBucket(bucketName) 124 func (c minioClient) removeBucket(ctx context.Context, bucketName string) error { 125 return c.client.RemoveBucket(ctx, bucketName) 126 } 127 128 // implements minio.GetBucketNotification(bucketName) 129 func (c minioClient) getBucketNotification(ctx context.Context, bucketName string) (config notification.Configuration, err error) { 130 return c.client.GetBucketNotification(ctx, bucketName) 131 } 132 133 // implements minio.GetBucketPolicy(bucketName) 134 func (c minioClient) getBucketPolicy(ctx context.Context, bucketName string) (string, error) { 135 return c.client.GetBucketPolicy(ctx, bucketName) 136 } 137 138 // implements minio.getBucketVersioning(ctx, bucketName) 139 func (c minioClient) getBucketVersioning(ctx context.Context, bucketName string) (minio.BucketVersioningConfiguration, error) { 140 return c.client.GetBucketVersioning(ctx, bucketName) 141 } 142 143 // implements minio.getBucketVersioning(ctx, bucketName) 144 func (c minioClient) getBucketReplication(ctx context.Context, bucketName string) (replication.Config, error) { 145 return c.client.GetBucketReplication(ctx, bucketName) 146 } 147 148 // implements minio.listObjects(ctx) 149 func (c minioClient) listObjects(ctx context.Context, bucket string, opts minio.ListObjectsOptions) <-chan minio.ObjectInfo { 150 return c.client.ListObjects(ctx, bucket, opts) 151 } 152 153 func (c minioClient) getObjectRetention(ctx context.Context, bucketName, objectName, versionID string) (mode *minio.RetentionMode, retainUntilDate *time.Time, err error) { 154 return c.client.GetObjectRetention(ctx, bucketName, objectName, versionID) 155 } 156 157 func (c minioClient) getObjectLegalHold(ctx context.Context, bucketName, objectName string, opts minio.GetObjectLegalHoldOptions) (status *minio.LegalHoldStatus, err error) { 158 return c.client.GetObjectLegalHold(ctx, bucketName, objectName, opts) 159 } 160 161 func (c minioClient) putObject(ctx context.Context, bucketName, objectName string, reader io.Reader, objectSize int64, opts minio.PutObjectOptions) (info minio.UploadInfo, err error) { 162 return c.client.PutObject(ctx, bucketName, objectName, reader, objectSize, opts) 163 } 164 165 func (c minioClient) putObjectLegalHold(ctx context.Context, bucketName, objectName string, opts minio.PutObjectLegalHoldOptions) error { 166 return c.client.PutObjectLegalHold(ctx, bucketName, objectName, opts) 167 } 168 169 func (c minioClient) putObjectRetention(ctx context.Context, bucketName, objectName string, opts minio.PutObjectRetentionOptions) error { 170 return c.client.PutObjectRetention(ctx, bucketName, objectName, opts) 171 } 172 173 func (c minioClient) statObject(ctx context.Context, bucketName, prefix string, opts minio.GetObjectOptions) (objectInfo minio.ObjectInfo, err error) { 174 return c.client.StatObject(ctx, bucketName, prefix, opts) 175 } 176 177 // implements minio.SetBucketEncryption(ctx, bucketName, config) 178 func (c minioClient) setBucketEncryption(ctx context.Context, bucketName string, config *sse.Configuration) error { 179 return c.client.SetBucketEncryption(ctx, bucketName, config) 180 } 181 182 // implements minio.RemoveBucketEncryption(ctx, bucketName) 183 func (c minioClient) removeBucketEncryption(ctx context.Context, bucketName string) error { 184 return c.client.RemoveBucketEncryption(ctx, bucketName) 185 } 186 187 // implements minio.GetBucketEncryption(ctx, bucketName, config) 188 func (c minioClient) getBucketEncryption(ctx context.Context, bucketName string) (*sse.Configuration, error) { 189 return c.client.GetBucketEncryption(ctx, bucketName) 190 } 191 192 func (c minioClient) putObjectTagging(ctx context.Context, bucketName, objectName string, otags *tags.Tags, opts minio.PutObjectTaggingOptions) error { 193 return c.client.PutObjectTagging(ctx, bucketName, objectName, otags, opts) 194 } 195 196 func (c minioClient) getObjectTagging(ctx context.Context, bucketName, objectName string, opts minio.GetObjectTaggingOptions) (*tags.Tags, error) { 197 return c.client.GetObjectTagging(ctx, bucketName, objectName, opts) 198 } 199 200 func (c minioClient) setObjectLockConfig(ctx context.Context, bucketName string, mode *minio.RetentionMode, validity *uint, unit *minio.ValidityUnit) error { 201 return c.client.SetObjectLockConfig(ctx, bucketName, mode, validity, unit) 202 } 203 204 func (c minioClient) getBucketObjectLockConfig(ctx context.Context, bucketName string) (mode *minio.RetentionMode, validity *uint, unit *minio.ValidityUnit, err error) { 205 return c.client.GetBucketObjectLockConfig(ctx, bucketName) 206 } 207 208 func (c minioClient) getObjectLockConfig(ctx context.Context, bucketName string) (lock string, mode *minio.RetentionMode, validity *uint, unit *minio.ValidityUnit, err error) { 209 return c.client.GetObjectLockConfig(ctx, bucketName) 210 } 211 212 func (c minioClient) getLifecycleRules(ctx context.Context, bucketName string) (lifecycle *lifecycle.Configuration, err error) { 213 return c.client.GetBucketLifecycle(ctx, bucketName) 214 } 215 216 func (c minioClient) setBucketLifecycle(ctx context.Context, bucketName string, config *lifecycle.Configuration) error { 217 return c.client.SetBucketLifecycle(ctx, bucketName, config) 218 } 219 220 func (c minioClient) copyObject(ctx context.Context, dst minio.CopyDestOptions, src minio.CopySrcOptions) (minio.UploadInfo, error) { 221 return c.client.CopyObject(ctx, dst, src) 222 } 223 224 // MCClient interface with all functions to be implemented 225 // by mock when testing, it should include all mc/S3Client respective api calls 226 // that are used within this project. 227 type MCClient interface { 228 addNotificationConfig(ctx context.Context, arn string, events []string, prefix, suffix string, ignoreExisting bool) *probe.Error 229 removeNotificationConfig(ctx context.Context, arn string, event string, prefix string, suffix string) *probe.Error 230 watch(ctx context.Context, options mc.WatchOptions) (*mc.WatchObject, *probe.Error) 231 remove(ctx context.Context, isIncomplete, isRemoveBucket, isBypass, forceDelete bool, contentCh <-chan *mc.ClientContent) <-chan mc.RemoveResult 232 list(ctx context.Context, opts mc.ListOptions) <-chan *mc.ClientContent 233 get(ctx context.Context, opts mc.GetOptions) (io.ReadCloser, *probe.Error) 234 shareDownload(ctx context.Context, versionID string, expires time.Duration) (string, *probe.Error) 235 setVersioning(ctx context.Context, status string, excludePrefix []string, excludeFolders bool) *probe.Error 236 } 237 238 // Interface implementation 239 // 240 // Define the structure of a mc S3Client and define the functions that are actually used 241 // from mcS3client api. 242 type mcClient struct { 243 client *mc.S3Client 244 } 245 246 // implements S3Client.AddNotificationConfig() 247 func (c mcClient) addNotificationConfig(ctx context.Context, arn string, events []string, prefix, suffix string, ignoreExisting bool) *probe.Error { 248 return c.client.AddNotificationConfig(ctx, arn, events, prefix, suffix, ignoreExisting) 249 } 250 251 // implements S3Client.RemoveNotificationConfig() 252 func (c mcClient) removeNotificationConfig(ctx context.Context, arn string, event string, prefix string, suffix string) *probe.Error { 253 return c.client.RemoveNotificationConfig(ctx, arn, event, prefix, suffix) 254 } 255 256 func (c mcClient) watch(ctx context.Context, options mc.WatchOptions) (*mc.WatchObject, *probe.Error) { 257 return c.client.Watch(ctx, options) 258 } 259 260 func (c mcClient) setReplication(ctx context.Context, cfg *replication.Config, opts replication.Options) *probe.Error { 261 return c.client.SetReplication(ctx, cfg, opts) 262 } 263 264 func (c mcClient) deleteAllReplicationRules(ctx context.Context) *probe.Error { 265 return c.client.RemoveReplication(ctx) 266 } 267 268 func (c mcClient) setVersioning(ctx context.Context, status string, excludePrefix []string, excludeFolders bool) *probe.Error { 269 return c.client.SetVersion(ctx, status, excludePrefix, excludeFolders) 270 } 271 272 func (c mcClient) remove(ctx context.Context, isIncomplete, isRemoveBucket, isBypass, forceDelete bool, contentCh <-chan *mc.ClientContent) <-chan mc.RemoveResult { 273 return c.client.Remove(ctx, isIncomplete, isRemoveBucket, isBypass, forceDelete, contentCh) 274 } 275 276 func (c mcClient) list(ctx context.Context, opts mc.ListOptions) <-chan *mc.ClientContent { 277 return c.client.List(ctx, opts) 278 } 279 280 func (c mcClient) get(ctx context.Context, opts mc.GetOptions) (io.ReadCloser, *probe.Error) { 281 rd, _, err := c.client.Get(ctx, opts) 282 return rd, err 283 } 284 285 func (c mcClient) shareDownload(ctx context.Context, versionID string, expires time.Duration) (string, *probe.Error) { 286 return c.client.ShareDownload(ctx, versionID, expires) 287 } 288 289 // ConsoleCredentialsI interface with all functions to be implemented 290 // by mock when testing, it should include all needed consoleCredentials.Login api calls 291 // that are used within this project. 292 type ConsoleCredentialsI interface { 293 Get() (credentials.Value, error) 294 Expire() 295 GetAccountAccessKey() string 296 } 297 298 // Interface implementation 299 type ConsoleCredentials struct { 300 ConsoleCredentials *credentials.Credentials 301 AccountAccessKey string 302 } 303 304 func (c ConsoleCredentials) GetAccountAccessKey() string { 305 return c.AccountAccessKey 306 } 307 308 // Get implements *Login.Get() 309 func (c ConsoleCredentials) Get() (credentials.Value, error) { 310 return c.ConsoleCredentials.Get() 311 } 312 313 // Expire implements *Login.Expire() 314 func (c ConsoleCredentials) Expire() { 315 c.ConsoleCredentials.Expire() 316 } 317 318 // consoleSTSAssumeRole it's a STSAssumeRole wrapper, in general 319 // there's no need to use this struct anywhere else in the project, it's only required 320 // for passing a custom *http.Client to *credentials.STSAssumeRole 321 type consoleSTSAssumeRole struct { 322 stsAssumeRole *credentials.STSAssumeRole 323 } 324 325 func (s consoleSTSAssumeRole) Retrieve() (credentials.Value, error) { 326 return s.stsAssumeRole.Retrieve() 327 } 328 329 func (s consoleSTSAssumeRole) IsExpired() bool { 330 return s.stsAssumeRole.IsExpired() 331 } 332 333 func stsCredentials(minioURL, accessKey, secretKey, location, clientIP string) (*credentials.Credentials, error) { 334 if accessKey == "" || secretKey == "" { 335 return nil, errors.New("credentials endpoint, access and secret key are mandatory for AssumeRoleSTS") 336 } 337 opts := credentials.STSAssumeRoleOptions{ 338 AccessKey: accessKey, 339 SecretKey: secretKey, 340 Location: location, 341 DurationSeconds: int(xjwt.GetConsoleSTSDuration().Seconds()), 342 } 343 stsAssumeRole := &credentials.STSAssumeRole{ 344 Client: GetConsoleHTTPClient(clientIP), 345 STSEndpoint: minioURL, 346 Options: opts, 347 } 348 consoleSTSWrapper := consoleSTSAssumeRole{stsAssumeRole: stsAssumeRole} 349 return credentials.New(consoleSTSWrapper), nil 350 } 351 352 func NewConsoleCredentials(accessKey, secretKey, location, clientIP string) (*credentials.Credentials, error) { 353 minioURL := getMinIOServer() 354 355 // Future authentication methods can be added under this switch statement 356 switch { 357 // LDAP authentication for Console 358 case ldap.GetLDAPEnabled(): 359 { 360 creds, err := auth.GetCredentialsFromLDAP(GetConsoleHTTPClient(clientIP), minioURL, accessKey, secretKey) 361 if err != nil { 362 return nil, err 363 } 364 365 // We verify if LDAP credentials are correct and no error is returned 366 _, err = creds.Get() 367 368 if err != nil && strings.Contains(strings.ToLower(err.Error()), "not found") { 369 // We try to use STS Credentials in case LDAP credentials are incorrect. 370 stsCreds, errSTS := stsCredentials(minioURL, accessKey, secretKey, location, clientIP) 371 372 // If there is an error with STS too, then we return the original LDAP error 373 if errSTS != nil { 374 LogError("error in STS credentials for LDAP case: %v ", errSTS) 375 376 // We return LDAP result 377 return creds, nil 378 } 379 380 _, err := stsCreds.Get() 381 // There is an error with STS credentials, We return the result of LDAP as STS is not a priority in this case. 382 if err != nil { 383 return creds, nil 384 } 385 386 return stsCreds, nil 387 } 388 389 return creds, nil 390 } 391 // default authentication for Console is via STS (Security Token Service) against MinIO 392 default: 393 { 394 return stsCredentials(minioURL, accessKey, secretKey, location, clientIP) 395 } 396 } 397 } 398 399 // getConsoleCredentialsFromSession returns the *consoleCredentials.Login associated to the 400 // provided session token, this is useful for running the Expire() or IsExpired() operations 401 func getConsoleCredentialsFromSession(claims *models.Principal) *credentials.Credentials { 402 if claims == nil { 403 return credentials.NewStaticV4("", "", "") 404 } 405 return credentials.NewStaticV4(claims.STSAccessKeyID, claims.STSSecretAccessKey, claims.STSSessionToken) 406 } 407 408 // newMinioClient creates a new MinIO client based on the ConsoleCredentials extracted 409 // from the provided session token 410 func newMinioClient(claims *models.Principal, clientIP string) (*minio.Client, error) { 411 creds := getConsoleCredentialsFromSession(claims) 412 endpoint := getMinIOEndpoint() 413 secure := getMinIOEndpointIsSecure() 414 minioClient, err := minio.New(endpoint, &minio.Options{ 415 Creds: creds, 416 Secure: secure, 417 Transport: GetConsoleHTTPClient(clientIP).Transport, 418 }) 419 if err != nil { 420 return nil, err 421 } 422 // set user-agent to differentiate Console UI requests for auditing. 423 minioClient.SetAppInfo("MinIO Console", pkg.Version) 424 return minioClient, nil 425 } 426 427 // computeObjectURLWithoutEncode returns a MinIO url containing the object filename without encoding 428 func computeObjectURLWithoutEncode(bucketName, prefix string) (string, error) { 429 u, err := xnet.ParseHTTPURL(getMinIOServer()) 430 if err != nil { 431 return "", fmt.Errorf("the provided endpoint: '%s' is invalid", getMinIOServer()) 432 } 433 var p string 434 if strings.TrimSpace(bucketName) != "" { 435 p = path.Join(p, bucketName) 436 } 437 if strings.TrimSpace(prefix) != "" { 438 p = pathJoinFinalSlash(p, prefix) 439 } 440 return u.String() + "/" + p, nil 441 } 442 443 // newS3BucketClient creates a new mc S3Client to talk to the server based on a bucket 444 func newS3BucketClient(claims *models.Principal, bucketName string, prefix string, clientIP string) (*mc.S3Client, error) { 445 if claims == nil { 446 return nil, fmt.Errorf("the provided credentials are invalid") 447 } 448 // It's very important to avoid encoding the prefix since the minio client will encode the path itself 449 objectURL, err := computeObjectURLWithoutEncode(bucketName, prefix) 450 if err != nil { 451 return nil, fmt.Errorf("the provided endpoint is invalid") 452 } 453 s3Config := newS3Config(objectURL, claims.STSAccessKeyID, claims.STSSecretAccessKey, claims.STSSessionToken, clientIP) 454 client, pErr := mc.S3New(s3Config) 455 if pErr != nil { 456 return nil, pErr.Cause 457 } 458 s3Client, ok := client.(*mc.S3Client) 459 if !ok { 460 return nil, fmt.Errorf("the provided url doesn't point to a S3 server") 461 } 462 return s3Client, nil 463 } 464 465 // pathJoinFinalSlash - like path.Join() but retains trailing slashSeparator of the last element 466 func pathJoinFinalSlash(elem ...string) string { 467 if len(elem) > 0 { 468 if strings.HasSuffix(elem[len(elem)-1], SlashSeparator) { 469 return path.Join(elem...) + SlashSeparator 470 } 471 } 472 return path.Join(elem...) 473 } 474 475 // Deprecated 476 // newS3Config simply creates a new Config struct using the passed 477 // parameters. 478 func newS3Config(endpoint, accessKey, secretKey, sessionToken string, clientIP string) *mc.Config { 479 // We have a valid alias and hostConfig. We populate the/ 480 // consoleCredentials from the match found in the config file. 481 return &mc.Config{ 482 HostURL: endpoint, 483 AccessKey: accessKey, 484 SecretKey: secretKey, 485 SessionToken: sessionToken, 486 Signature: "S3v4", 487 AppName: globalAppName, 488 AppVersion: pkg.Version, 489 Insecure: isLocalIPEndpoint(endpoint), 490 Transport: &ConsoleTransport{ 491 ClientIP: clientIP, 492 Transport: GlobalTransport, 493 }, 494 } 495 }