mirror of https://github.com/minio/minio.git
				
				
				
			
		
			
				
	
	
		
			455 lines
		
	
	
		
			11 KiB
		
	
	
	
		
			Go
		
	
	
	
			
		
		
	
	
			455 lines
		
	
	
		
			11 KiB
		
	
	
	
		
			Go
		
	
	
	
| // Copyright (c) 2015-2023 MinIO, Inc.
 | |
| //
 | |
| // This file is part of MinIO Object Storage stack
 | |
| //
 | |
| // This program is free software: you can redistribute it and/or modify
 | |
| // it under the terms of the GNU Affero General Public License as published by
 | |
| // the Free Software Foundation, either version 3 of the License, or
 | |
| // (at your option) any later version.
 | |
| //
 | |
| // This program is distributed in the hope that it will be useful
 | |
| // but WITHOUT ANY WARRANTY; without even the implied warranty of
 | |
| // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 | |
| // GNU Affero General Public License for more details.
 | |
| //
 | |
| // You should have received a copy of the GNU Affero General Public License
 | |
| // along with this program.  If not, see <http://www.gnu.org/licenses/>.
 | |
| 
 | |
| package cmd
 | |
| 
 | |
| import (
 | |
| 	"bytes"
 | |
| 	"context"
 | |
| 	"errors"
 | |
| 	"fmt"
 | |
| 	"io"
 | |
| 	"os"
 | |
| 	"strings"
 | |
| 	"sync"
 | |
| 	"time"
 | |
| 
 | |
| 	"github.com/minio/madmin-go/v3"
 | |
| 	"github.com/minio/minio-go/v7"
 | |
| 	"github.com/minio/minio-go/v7/pkg/credentials"
 | |
| 	"github.com/minio/minio/internal/auth"
 | |
| 	"github.com/minio/minio/internal/logger"
 | |
| 	"github.com/pkg/sftp"
 | |
| 	"golang.org/x/crypto/ssh"
 | |
| )
 | |
| 
 | |
| type sftpDriver struct {
 | |
| 	permissions *ssh.Permissions
 | |
| 	endpoint    string
 | |
| }
 | |
| 
 | |
| //msgp:ignore sftpMetrics
 | |
| type sftpMetrics struct{}
 | |
| 
 | |
| var globalSftpMetrics sftpMetrics
 | |
| 
 | |
| func sftpTrace(s *sftp.Request, startTime time.Time, source string, user string, err error) madmin.TraceInfo {
 | |
| 	var errStr string
 | |
| 	if err != nil {
 | |
| 		errStr = err.Error()
 | |
| 	}
 | |
| 	return madmin.TraceInfo{
 | |
| 		TraceType: madmin.TraceFTP,
 | |
| 		Time:      startTime,
 | |
| 		NodeName:  globalLocalNodeName,
 | |
| 		FuncName:  fmt.Sprintf("sftp USER=%s COMMAND=%s PARAM=%s, Source=%s", user, s.Method, s.Filepath, source),
 | |
| 		Duration:  time.Since(startTime),
 | |
| 		Path:      s.Filepath,
 | |
| 		Error:     errStr,
 | |
| 	}
 | |
| }
 | |
| 
 | |
| func (m *sftpMetrics) log(s *sftp.Request, user string) func(err error) {
 | |
| 	startTime := time.Now()
 | |
| 	source := getSource(2)
 | |
| 	return func(err error) {
 | |
| 		globalTrace.Publish(sftpTrace(s, startTime, source, user, err))
 | |
| 	}
 | |
| }
 | |
| 
 | |
| // NewSFTPDriver initializes sftp.Handlers implementation of following interfaces
 | |
| //
 | |
| // - sftp.Fileread
 | |
| // - sftp.Filewrite
 | |
| // - sftp.Filelist
 | |
| // - sftp.Filecmd
 | |
| func NewSFTPDriver(perms *ssh.Permissions) sftp.Handlers {
 | |
| 	handler := &sftpDriver{endpoint: fmt.Sprintf("127.0.0.1:%s", globalMinioPort), permissions: perms}
 | |
| 	return sftp.Handlers{
 | |
| 		FileGet:  handler,
 | |
| 		FilePut:  handler,
 | |
| 		FileCmd:  handler,
 | |
| 		FileList: handler,
 | |
| 	}
 | |
| }
 | |
| 
 | |
| func (f *sftpDriver) getMinIOClient() (*minio.Client, error) {
 | |
| 	ui, ok := globalIAMSys.GetUser(context.Background(), f.AccessKey())
 | |
| 	if !ok && !globalIAMSys.LDAPConfig.Enabled() {
 | |
| 		return nil, errNoSuchUser
 | |
| 	}
 | |
| 	if !ok && globalIAMSys.LDAPConfig.Enabled() {
 | |
| 		targetUser, targetGroups, err := globalIAMSys.LDAPConfig.LookupUserDN(f.AccessKey())
 | |
| 		if err != nil {
 | |
| 			return nil, err
 | |
| 		}
 | |
| 		expiryDur, err := globalIAMSys.LDAPConfig.GetExpiryDuration("")
 | |
| 		if err != nil {
 | |
| 			return nil, err
 | |
| 		}
 | |
| 		claims := make(map[string]interface{})
 | |
| 		claims[expClaim] = UTCNow().Add(expiryDur).Unix()
 | |
| 		for k, v := range f.permissions.CriticalOptions {
 | |
| 			claims[k] = v
 | |
| 		}
 | |
| 
 | |
| 		cred, err := auth.GetNewCredentialsWithMetadata(claims, globalActiveCred.SecretKey)
 | |
| 		if err != nil {
 | |
| 			return nil, err
 | |
| 		}
 | |
| 
 | |
| 		// Set the parent of the temporary access key, this is useful
 | |
| 		// in obtaining service accounts by this cred.
 | |
| 		cred.ParentUser = targetUser
 | |
| 
 | |
| 		// Set this value to LDAP groups, LDAP user can be part
 | |
| 		// of large number of groups
 | |
| 		cred.Groups = targetGroups
 | |
| 
 | |
| 		// Set the newly generated credentials, policyName is empty on purpose
 | |
| 		// LDAP policies are applied automatically using their ldapUser, ldapGroups
 | |
| 		// mapping.
 | |
| 		updatedAt, err := globalIAMSys.SetTempUser(context.Background(), cred.AccessKey, cred, "")
 | |
| 		if err != nil {
 | |
| 			return nil, err
 | |
| 		}
 | |
| 
 | |
| 		// Call hook for site replication.
 | |
| 		logger.LogIf(context.Background(), globalSiteReplicationSys.IAMChangeHook(context.Background(), madmin.SRIAMItem{
 | |
| 			Type: madmin.SRIAMItemSTSAcc,
 | |
| 			STSCredential: &madmin.SRSTSCredential{
 | |
| 				AccessKey:    cred.AccessKey,
 | |
| 				SecretKey:    cred.SecretKey,
 | |
| 				SessionToken: cred.SessionToken,
 | |
| 				ParentUser:   cred.ParentUser,
 | |
| 			},
 | |
| 			UpdatedAt: updatedAt,
 | |
| 		}))
 | |
| 
 | |
| 		return minio.New(f.endpoint, &minio.Options{
 | |
| 			Creds:     credentials.NewStaticV4(cred.AccessKey, cred.SecretKey, cred.SessionToken),
 | |
| 			Secure:    globalIsTLS,
 | |
| 			Transport: globalRemoteFTPClientTransport,
 | |
| 		})
 | |
| 	}
 | |
| 
 | |
| 	// ok == true - at this point
 | |
| 
 | |
| 	if ui.Credentials.IsTemp() {
 | |
| 		// Temporary credentials are not allowed.
 | |
| 		return nil, errAuthentication
 | |
| 	}
 | |
| 
 | |
| 	return minio.New(f.endpoint, &minio.Options{
 | |
| 		Creds:     credentials.NewStaticV4(ui.Credentials.AccessKey, ui.Credentials.SecretKey, ""),
 | |
| 		Secure:    globalIsTLS,
 | |
| 		Transport: globalRemoteFTPClientTransport,
 | |
| 	})
 | |
| }
 | |
| 
 | |
| func (f *sftpDriver) AccessKey() string {
 | |
| 	if _, ok := f.permissions.CriticalOptions["accessKey"]; !ok {
 | |
| 		return f.permissions.CriticalOptions[ldapUserN]
 | |
| 	}
 | |
| 	return f.permissions.CriticalOptions["accessKey"]
 | |
| }
 | |
| 
 | |
| func (f *sftpDriver) Fileread(r *sftp.Request) (ra io.ReaderAt, err error) {
 | |
| 	stopFn := globalSftpMetrics.log(r, f.AccessKey())
 | |
| 	defer stopFn(err)
 | |
| 
 | |
| 	flags := r.Pflags()
 | |
| 	if !flags.Read {
 | |
| 		// sanity check
 | |
| 		return nil, os.ErrInvalid
 | |
| 	}
 | |
| 
 | |
| 	bucket, object := path2BucketObject(r.Filepath)
 | |
| 	if bucket == "" {
 | |
| 		return nil, errors.New("bucket name cannot be empty")
 | |
| 	}
 | |
| 
 | |
| 	clnt, err := f.getMinIOClient()
 | |
| 	if err != nil {
 | |
| 		return nil, err
 | |
| 	}
 | |
| 
 | |
| 	obj, err := clnt.GetObject(context.Background(), bucket, object, minio.GetObjectOptions{})
 | |
| 	if err != nil {
 | |
| 		return nil, err
 | |
| 	}
 | |
| 
 | |
| 	_, err = obj.Stat()
 | |
| 	if err != nil {
 | |
| 		return nil, err
 | |
| 	}
 | |
| 
 | |
| 	return obj, nil
 | |
| }
 | |
| 
 | |
| type writerAt struct {
 | |
| 	w  *io.PipeWriter
 | |
| 	wg *sync.WaitGroup
 | |
| }
 | |
| 
 | |
| func (w *writerAt) Close() error {
 | |
| 	err := w.w.Close()
 | |
| 	w.wg.Wait()
 | |
| 	return err
 | |
| }
 | |
| 
 | |
| func (w *writerAt) WriteAt(b []byte, offset int64) (n int, err error) {
 | |
| 	return w.w.Write(b)
 | |
| }
 | |
| 
 | |
| func (f *sftpDriver) Filewrite(r *sftp.Request) (w io.WriterAt, err error) {
 | |
| 	stopFn := globalSftpMetrics.log(r, f.AccessKey())
 | |
| 	defer stopFn(err)
 | |
| 
 | |
| 	flags := r.Pflags()
 | |
| 	if !flags.Write {
 | |
| 		// sanity check
 | |
| 		return nil, os.ErrInvalid
 | |
| 	}
 | |
| 
 | |
| 	bucket, object := path2BucketObject(r.Filepath)
 | |
| 	if bucket == "" {
 | |
| 		return nil, errors.New("bucket name cannot be empty")
 | |
| 	}
 | |
| 
 | |
| 	clnt, err := f.getMinIOClient()
 | |
| 	if err != nil {
 | |
| 		return nil, err
 | |
| 	}
 | |
| 
 | |
| 	pr, pw := io.Pipe()
 | |
| 
 | |
| 	wa := &writerAt{w: pw, wg: &sync.WaitGroup{}}
 | |
| 	wa.wg.Add(1)
 | |
| 	go func() {
 | |
| 		_, err := clnt.PutObject(r.Context(), bucket, object, pr, -1, minio.PutObjectOptions{SendContentMd5: true})
 | |
| 		pr.CloseWithError(err)
 | |
| 		wa.wg.Done()
 | |
| 	}()
 | |
| 	return wa, nil
 | |
| }
 | |
| 
 | |
| func (f *sftpDriver) Filecmd(r *sftp.Request) (err error) {
 | |
| 	stopFn := globalSftpMetrics.log(r, f.AccessKey())
 | |
| 	defer stopFn(err)
 | |
| 
 | |
| 	clnt, err := f.getMinIOClient()
 | |
| 	if err != nil {
 | |
| 		return err
 | |
| 	}
 | |
| 
 | |
| 	switch r.Method {
 | |
| 	case "Setstat", "Rename", "Link", "Symlink":
 | |
| 		return NotImplemented{}
 | |
| 
 | |
| 	case "Rmdir":
 | |
| 		bucket, prefix := path2BucketObject(r.Filepath)
 | |
| 		if bucket == "" {
 | |
| 			return errors.New("deleting all buckets not allowed")
 | |
| 		}
 | |
| 
 | |
| 		cctx, cancel := context.WithCancel(context.Background())
 | |
| 		defer cancel()
 | |
| 
 | |
| 		if prefix == "" {
 | |
| 			// if all objects are not deleted yet this call may fail.
 | |
| 			return clnt.RemoveBucket(cctx, bucket)
 | |
| 		}
 | |
| 
 | |
| 		objectsCh := make(chan minio.ObjectInfo)
 | |
| 
 | |
| 		// Send object names that are needed to be removed to objectsCh
 | |
| 		go func() {
 | |
| 			defer close(objectsCh)
 | |
| 			opts := minio.ListObjectsOptions{
 | |
| 				Prefix:    prefix,
 | |
| 				Recursive: true,
 | |
| 			}
 | |
| 			for object := range clnt.ListObjects(cctx, bucket, opts) {
 | |
| 				if object.Err != nil {
 | |
| 					return
 | |
| 				}
 | |
| 				objectsCh <- object
 | |
| 			}
 | |
| 		}()
 | |
| 
 | |
| 		// Call RemoveObjects API
 | |
| 		for err := range clnt.RemoveObjects(context.Background(), bucket, objectsCh, minio.RemoveObjectsOptions{}) {
 | |
| 			if err.Err != nil {
 | |
| 				return err.Err
 | |
| 			}
 | |
| 		}
 | |
| 
 | |
| 	case "Remove":
 | |
| 		bucket, object := path2BucketObject(r.Filepath)
 | |
| 		if bucket == "" {
 | |
| 			return errors.New("bucket name cannot be empty")
 | |
| 		}
 | |
| 
 | |
| 		return clnt.RemoveObject(context.Background(), bucket, object, minio.RemoveObjectOptions{})
 | |
| 
 | |
| 	case "Mkdir":
 | |
| 		bucket, prefix := path2BucketObject(r.Filepath)
 | |
| 		if bucket == "" {
 | |
| 			return errors.New("bucket name cannot be empty")
 | |
| 		}
 | |
| 
 | |
| 		if prefix == "" {
 | |
| 			return clnt.MakeBucket(context.Background(), bucket, minio.MakeBucketOptions{Region: globalSite.Region})
 | |
| 		}
 | |
| 
 | |
| 		dirPath := buildMinioDir(prefix)
 | |
| 
 | |
| 		_, err = clnt.PutObject(context.Background(), bucket, dirPath, bytes.NewReader([]byte("")), 0,
 | |
| 			// Always send Content-MD5 to succeed with bucket with
 | |
| 			// locking enabled. There is no performance hit since
 | |
| 			// this is always an empty object
 | |
| 			minio.PutObjectOptions{SendContentMd5: true},
 | |
| 		)
 | |
| 		return err
 | |
| 	}
 | |
| 
 | |
| 	return NotImplemented{}
 | |
| }
 | |
| 
 | |
| type listerAt []os.FileInfo
 | |
| 
 | |
| // Modeled after strings.Reader's ReadAt() implementation
 | |
| func (f listerAt) ListAt(ls []os.FileInfo, offset int64) (int, error) {
 | |
| 	var n int
 | |
| 	if offset >= int64(len(f)) {
 | |
| 		return 0, io.EOF
 | |
| 	}
 | |
| 	n = copy(ls, f[offset:])
 | |
| 	if n < len(ls) {
 | |
| 		return n, io.EOF
 | |
| 	}
 | |
| 	return n, nil
 | |
| }
 | |
| 
 | |
| func (f *sftpDriver) Filelist(r *sftp.Request) (la sftp.ListerAt, err error) {
 | |
| 	stopFn := globalSftpMetrics.log(r, f.AccessKey())
 | |
| 	defer stopFn(err)
 | |
| 
 | |
| 	clnt, err := f.getMinIOClient()
 | |
| 	if err != nil {
 | |
| 		return nil, err
 | |
| 	}
 | |
| 
 | |
| 	switch r.Method {
 | |
| 	case "List":
 | |
| 		var files []os.FileInfo
 | |
| 
 | |
| 		bucket, prefix := path2BucketObject(r.Filepath)
 | |
| 		if bucket == "" {
 | |
| 			buckets, err := clnt.ListBuckets(r.Context())
 | |
| 			if err != nil {
 | |
| 				return nil, err
 | |
| 			}
 | |
| 
 | |
| 			for _, bucket := range buckets {
 | |
| 				files = append(files, &minioFileInfo{
 | |
| 					p:     bucket.Name,
 | |
| 					info:  minio.ObjectInfo{Key: bucket.Name, LastModified: bucket.CreationDate},
 | |
| 					isDir: true,
 | |
| 				})
 | |
| 			}
 | |
| 
 | |
| 			return listerAt(files), nil
 | |
| 		}
 | |
| 
 | |
| 		prefix = retainSlash(prefix)
 | |
| 
 | |
| 		for object := range clnt.ListObjects(r.Context(), bucket, minio.ListObjectsOptions{
 | |
| 			Prefix:    prefix,
 | |
| 			Recursive: false,
 | |
| 		}) {
 | |
| 			if object.Err != nil {
 | |
| 				return nil, object.Err
 | |
| 			}
 | |
| 
 | |
| 			if object.Key == prefix {
 | |
| 				continue
 | |
| 			}
 | |
| 
 | |
| 			isDir := strings.HasSuffix(object.Key, SlashSeparator)
 | |
| 			files = append(files, &minioFileInfo{
 | |
| 				p:     pathClean(strings.TrimPrefix(object.Key, prefix)),
 | |
| 				info:  object,
 | |
| 				isDir: isDir,
 | |
| 			})
 | |
| 		}
 | |
| 
 | |
| 		return listerAt(files), nil
 | |
| 
 | |
| 	case "Stat":
 | |
| 		if r.Filepath == SlashSeparator {
 | |
| 			return listerAt{&minioFileInfo{
 | |
| 				p:     r.Filepath,
 | |
| 				isDir: true,
 | |
| 			}}, nil
 | |
| 		}
 | |
| 
 | |
| 		bucket, object := path2BucketObject(r.Filepath)
 | |
| 		if bucket == "" {
 | |
| 			return nil, errors.New("bucket name cannot be empty")
 | |
| 		}
 | |
| 
 | |
| 		if object == "" {
 | |
| 			ok, err := clnt.BucketExists(context.Background(), bucket)
 | |
| 			if err != nil {
 | |
| 				return nil, err
 | |
| 			}
 | |
| 			if !ok {
 | |
| 				return nil, os.ErrNotExist
 | |
| 			}
 | |
| 			return listerAt{&minioFileInfo{
 | |
| 				p:     pathClean(bucket),
 | |
| 				info:  minio.ObjectInfo{Key: bucket},
 | |
| 				isDir: true,
 | |
| 			}}, nil
 | |
| 		}
 | |
| 
 | |
| 		objInfo, err := clnt.StatObject(context.Background(), bucket, object, minio.StatObjectOptions{})
 | |
| 		if err != nil {
 | |
| 			if minio.ToErrorResponse(err).Code == "NoSuchKey" {
 | |
| 				// dummy return to satisfy LIST (stat -> list) behavior.
 | |
| 				return listerAt{&minioFileInfo{
 | |
| 					p:     pathClean(object),
 | |
| 					info:  minio.ObjectInfo{Key: object},
 | |
| 					isDir: true,
 | |
| 				}}, nil
 | |
| 			}
 | |
| 			return nil, err
 | |
| 		}
 | |
| 
 | |
| 		isDir := strings.HasSuffix(objInfo.Key, SlashSeparator)
 | |
| 		return listerAt{&minioFileInfo{
 | |
| 			p:     pathClean(object),
 | |
| 			info:  objInfo,
 | |
| 			isDir: isDir,
 | |
| 		}}, nil
 | |
| 	}
 | |
| 
 | |
| 	return nil, NotImplemented{}
 | |
| }
 |