github.com/minio/minio@v0.0.0-20240328213742-3f72439b8a27/cmd/sftp-server-driver.go (about)

     1  // Copyright (c) 2015-2023 MinIO, Inc.
     2  //
     3  // This file is part of MinIO Object Storage stack
     4  //
     5  // This program is free software: you can redistribute it and/or modify
     6  // it under the terms of the GNU Affero General Public License as published by
     7  // the Free Software Foundation, either version 3 of the License, or
     8  // (at your option) any later version.
     9  //
    10  // This program is distributed in the hope that it will be useful
    11  // but WITHOUT ANY WARRANTY; without even the implied warranty of
    12  // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    13  // GNU Affero General Public License for more details.
    14  //
    15  // You should have received a copy of the GNU Affero General Public License
    16  // along with this program.  If not, see <http://www.gnu.org/licenses/>.
    17  
    18  package cmd
    19  
    20  import (
    21  	"bytes"
    22  	"context"
    23  	"errors"
    24  	"fmt"
    25  	"io"
    26  	"os"
    27  	"strings"
    28  	"sync"
    29  	"time"
    30  
    31  	"github.com/minio/madmin-go/v3"
    32  	"github.com/minio/minio-go/v7"
    33  	"github.com/minio/minio-go/v7/pkg/credentials"
    34  	"github.com/minio/minio/internal/auth"
    35  	xioutil "github.com/minio/minio/internal/ioutil"
    36  	"github.com/minio/minio/internal/logger"
    37  	"github.com/pkg/sftp"
    38  	"golang.org/x/crypto/ssh"
    39  )
    40  
    41  type sftpDriver struct {
    42  	permissions *ssh.Permissions
    43  	endpoint    string
    44  }
    45  
    46  //msgp:ignore sftpMetrics
    47  type sftpMetrics struct{}
    48  
    49  var globalSftpMetrics sftpMetrics
    50  
    51  func sftpTrace(s *sftp.Request, startTime time.Time, source string, user string, err error) madmin.TraceInfo {
    52  	var errStr string
    53  	if err != nil {
    54  		errStr = err.Error()
    55  	}
    56  	return madmin.TraceInfo{
    57  		TraceType: madmin.TraceFTP,
    58  		Time:      startTime,
    59  		NodeName:  globalLocalNodeName,
    60  		FuncName:  fmt.Sprintf("sftp USER=%s COMMAND=%s PARAM=%s, Source=%s", user, s.Method, s.Filepath, source),
    61  		Duration:  time.Since(startTime),
    62  		Path:      s.Filepath,
    63  		Error:     errStr,
    64  	}
    65  }
    66  
    67  func (m *sftpMetrics) log(s *sftp.Request, user string) func(err error) {
    68  	startTime := time.Now()
    69  	source := getSource(2)
    70  	return func(err error) {
    71  		globalTrace.Publish(sftpTrace(s, startTime, source, user, err))
    72  	}
    73  }
    74  
    75  // NewSFTPDriver initializes sftp.Handlers implementation of following interfaces
    76  //
    77  // - sftp.Fileread
    78  // - sftp.Filewrite
    79  // - sftp.Filelist
    80  // - sftp.Filecmd
    81  func NewSFTPDriver(perms *ssh.Permissions) sftp.Handlers {
    82  	handler := &sftpDriver{endpoint: fmt.Sprintf("127.0.0.1:%s", globalMinioPort), permissions: perms}
    83  	return sftp.Handlers{
    84  		FileGet:  handler,
    85  		FilePut:  handler,
    86  		FileCmd:  handler,
    87  		FileList: handler,
    88  	}
    89  }
    90  
    91  func (f *sftpDriver) getMinIOClient() (*minio.Client, error) {
    92  	ui, ok := globalIAMSys.GetUser(context.Background(), f.AccessKey())
    93  	if !ok && !globalIAMSys.LDAPConfig.Enabled() {
    94  		return nil, errNoSuchUser
    95  	}
    96  	if !ok && globalIAMSys.LDAPConfig.Enabled() {
    97  		sa, _, err := globalIAMSys.getServiceAccount(context.Background(), f.AccessKey())
    98  		if err != nil && !errors.Is(err, errNoSuchServiceAccount) {
    99  			return nil, err
   100  		}
   101  		var mcreds *credentials.Credentials
   102  		if errors.Is(err, errNoSuchServiceAccount) {
   103  			targetUser, targetGroups, err := globalIAMSys.LDAPConfig.LookupUserDN(f.AccessKey())
   104  			if err != nil {
   105  				return nil, err
   106  			}
   107  			expiryDur, err := globalIAMSys.LDAPConfig.GetExpiryDuration("")
   108  			if err != nil {
   109  				return nil, err
   110  			}
   111  			claims := make(map[string]interface{})
   112  			claims[expClaim] = UTCNow().Add(expiryDur).Unix()
   113  			for k, v := range f.permissions.CriticalOptions {
   114  				claims[k] = v
   115  			}
   116  
   117  			cred, err := auth.GetNewCredentialsWithMetadata(claims, globalActiveCred.SecretKey)
   118  			if err != nil {
   119  				return nil, err
   120  			}
   121  
   122  			// Set the parent of the temporary access key, this is useful
   123  			// in obtaining service accounts by this cred.
   124  			cred.ParentUser = targetUser
   125  
   126  			// Set this value to LDAP groups, LDAP user can be part
   127  			// of large number of groups
   128  			cred.Groups = targetGroups
   129  
   130  			// Set the newly generated credentials, policyName is empty on purpose
   131  			// LDAP policies are applied automatically using their ldapUser, ldapGroups
   132  			// mapping.
   133  			updatedAt, err := globalIAMSys.SetTempUser(context.Background(), cred.AccessKey, cred, "")
   134  			if err != nil {
   135  				return nil, err
   136  			}
   137  
   138  			// Call hook for site replication.
   139  			logger.LogIf(context.Background(), globalSiteReplicationSys.IAMChangeHook(context.Background(), madmin.SRIAMItem{
   140  				Type: madmin.SRIAMItemSTSAcc,
   141  				STSCredential: &madmin.SRSTSCredential{
   142  					AccessKey:    cred.AccessKey,
   143  					SecretKey:    cred.SecretKey,
   144  					SessionToken: cred.SessionToken,
   145  					ParentUser:   cred.ParentUser,
   146  				},
   147  				UpdatedAt: updatedAt,
   148  			}))
   149  
   150  			mcreds = credentials.NewStaticV4(cred.AccessKey, cred.SecretKey, cred.SessionToken)
   151  		} else {
   152  			mcreds = credentials.NewStaticV4(sa.Credentials.AccessKey, sa.Credentials.SecretKey, "")
   153  		}
   154  
   155  		return minio.New(f.endpoint, &minio.Options{
   156  			Creds:     mcreds,
   157  			Secure:    globalIsTLS,
   158  			Transport: globalRemoteFTPClientTransport,
   159  		})
   160  	}
   161  
   162  	// ok == true - at this point
   163  
   164  	if ui.Credentials.IsTemp() {
   165  		// Temporary credentials are not allowed.
   166  		return nil, errAuthentication
   167  	}
   168  
   169  	return minio.New(f.endpoint, &minio.Options{
   170  		Creds:     credentials.NewStaticV4(ui.Credentials.AccessKey, ui.Credentials.SecretKey, ""),
   171  		Secure:    globalIsTLS,
   172  		Transport: globalRemoteFTPClientTransport,
   173  	})
   174  }
   175  
   176  func (f *sftpDriver) AccessKey() string {
   177  	if _, ok := f.permissions.CriticalOptions["accessKey"]; !ok {
   178  		return f.permissions.CriticalOptions[ldapUserN]
   179  	}
   180  	return f.permissions.CriticalOptions["accessKey"]
   181  }
   182  
   183  func (f *sftpDriver) Fileread(r *sftp.Request) (ra io.ReaderAt, err error) {
   184  	stopFn := globalSftpMetrics.log(r, f.AccessKey())
   185  	defer stopFn(err)
   186  
   187  	flags := r.Pflags()
   188  	if !flags.Read {
   189  		// sanity check
   190  		return nil, os.ErrInvalid
   191  	}
   192  
   193  	bucket, object := path2BucketObject(r.Filepath)
   194  	if bucket == "" {
   195  		return nil, errors.New("bucket name cannot be empty")
   196  	}
   197  
   198  	clnt, err := f.getMinIOClient()
   199  	if err != nil {
   200  		return nil, err
   201  	}
   202  
   203  	obj, err := clnt.GetObject(context.Background(), bucket, object, minio.GetObjectOptions{})
   204  	if err != nil {
   205  		return nil, err
   206  	}
   207  
   208  	_, err = obj.Stat()
   209  	if err != nil {
   210  		return nil, err
   211  	}
   212  
   213  	return obj, nil
   214  }
   215  
   216  // TransferError will catch network errors during transfer.
   217  // When TransferError() is called Close() will also
   218  // be called, so we do not need to Wait() here.
   219  func (w *writerAt) TransferError(err error) {
   220  	_ = w.w.CloseWithError(err)
   221  	_ = w.r.CloseWithError(err)
   222  	w.err = err
   223  }
   224  
   225  func (w *writerAt) Close() (err error) {
   226  	switch {
   227  	case len(w.buffer) > 0:
   228  		err = errors.New("some file segments were not flushed from the queue")
   229  		_ = w.w.CloseWithError(err)
   230  	case w.err != nil:
   231  		// No need to close here since both pipes were
   232  		// closing inside TransferError()
   233  		err = w.err
   234  	default:
   235  		err = w.w.Close()
   236  	}
   237  	for i := range w.buffer {
   238  		delete(w.buffer, i)
   239  	}
   240  	w.wg.Wait()
   241  	return err
   242  }
   243  
   244  type writerAt struct {
   245  	w      *io.PipeWriter
   246  	r      *io.PipeReader
   247  	wg     *sync.WaitGroup
   248  	buffer map[int64][]byte
   249  	err    error
   250  
   251  	nextOffset int64
   252  	m          sync.Mutex
   253  }
   254  
   255  func (w *writerAt) WriteAt(b []byte, offset int64) (n int, err error) {
   256  	w.m.Lock()
   257  	defer w.m.Unlock()
   258  
   259  	if w.nextOffset == offset {
   260  		n, err = w.w.Write(b)
   261  		w.nextOffset += int64(n)
   262  	} else {
   263  		w.buffer[offset] = make([]byte, len(b))
   264  		copy(w.buffer[offset], b)
   265  		n = len(b)
   266  	}
   267  
   268  again:
   269  	nextOut, ok := w.buffer[w.nextOffset]
   270  	if ok {
   271  		n, err = w.w.Write(nextOut)
   272  		delete(w.buffer, w.nextOffset)
   273  		w.nextOffset += int64(n)
   274  		if n != len(nextOut) {
   275  			return 0, fmt.Errorf("expected write size %d but wrote %d bytes", len(nextOut), n)
   276  		}
   277  		if err != nil {
   278  			return 0, err
   279  		}
   280  		goto again
   281  	}
   282  
   283  	return len(b), nil
   284  }
   285  
   286  func (f *sftpDriver) Filewrite(r *sftp.Request) (w io.WriterAt, err error) {
   287  	stopFn := globalSftpMetrics.log(r, f.AccessKey())
   288  	defer stopFn(err)
   289  
   290  	flags := r.Pflags()
   291  	if !flags.Write {
   292  		// sanity check
   293  		return nil, os.ErrInvalid
   294  	}
   295  
   296  	bucket, object := path2BucketObject(r.Filepath)
   297  	if bucket == "" {
   298  		return nil, errors.New("bucket name cannot be empty")
   299  	}
   300  
   301  	clnt, err := f.getMinIOClient()
   302  	if err != nil {
   303  		return nil, err
   304  	}
   305  	ok, err := clnt.BucketExists(r.Context(), bucket)
   306  	if err != nil {
   307  		return nil, err
   308  	}
   309  	if !ok {
   310  		return nil, os.ErrNotExist
   311  	}
   312  
   313  	pr, pw := io.Pipe()
   314  
   315  	wa := &writerAt{
   316  		buffer: make(map[int64][]byte),
   317  		w:      pw,
   318  		r:      pr,
   319  		wg:     &sync.WaitGroup{},
   320  	}
   321  	wa.wg.Add(1)
   322  	go func() {
   323  		_, err := clnt.PutObject(r.Context(), bucket, object, pr, -1, minio.PutObjectOptions{SendContentMd5: true})
   324  		pr.CloseWithError(err)
   325  		wa.wg.Done()
   326  	}()
   327  	return wa, nil
   328  }
   329  
   330  func (f *sftpDriver) Filecmd(r *sftp.Request) (err error) {
   331  	stopFn := globalSftpMetrics.log(r, f.AccessKey())
   332  	defer stopFn(err)
   333  
   334  	clnt, err := f.getMinIOClient()
   335  	if err != nil {
   336  		return err
   337  	}
   338  
   339  	switch r.Method {
   340  	case "Setstat", "Rename", "Link", "Symlink":
   341  		return NotImplemented{}
   342  
   343  	case "Rmdir":
   344  		bucket, prefix := path2BucketObject(r.Filepath)
   345  		if bucket == "" {
   346  			return errors.New("deleting all buckets not allowed")
   347  		}
   348  
   349  		cctx, cancel := context.WithCancel(context.Background())
   350  		defer cancel()
   351  
   352  		if prefix == "" {
   353  			// if all objects are not deleted yet this call may fail.
   354  			return clnt.RemoveBucket(cctx, bucket)
   355  		}
   356  
   357  		objectsCh := make(chan minio.ObjectInfo)
   358  
   359  		// Send object names that are needed to be removed to objectsCh
   360  		go func() {
   361  			defer xioutil.SafeClose(objectsCh)
   362  			opts := minio.ListObjectsOptions{
   363  				Prefix:    prefix,
   364  				Recursive: true,
   365  			}
   366  			for object := range clnt.ListObjects(cctx, bucket, opts) {
   367  				if object.Err != nil {
   368  					return
   369  				}
   370  				objectsCh <- object
   371  			}
   372  		}()
   373  
   374  		// Call RemoveObjects API
   375  		for err := range clnt.RemoveObjects(context.Background(), bucket, objectsCh, minio.RemoveObjectsOptions{}) {
   376  			if err.Err != nil {
   377  				return err.Err
   378  			}
   379  		}
   380  		return err
   381  
   382  	case "Remove":
   383  		bucket, object := path2BucketObject(r.Filepath)
   384  		if bucket == "" {
   385  			return errors.New("bucket name cannot be empty")
   386  		}
   387  
   388  		return clnt.RemoveObject(context.Background(), bucket, object, minio.RemoveObjectOptions{})
   389  
   390  	case "Mkdir":
   391  		bucket, prefix := path2BucketObject(r.Filepath)
   392  		if bucket == "" {
   393  			return errors.New("bucket name cannot be empty")
   394  		}
   395  
   396  		if prefix == "" {
   397  			return clnt.MakeBucket(context.Background(), bucket, minio.MakeBucketOptions{Region: globalSite.Region})
   398  		}
   399  
   400  		dirPath := buildMinioDir(prefix)
   401  
   402  		_, err = clnt.PutObject(context.Background(), bucket, dirPath, bytes.NewReader([]byte("")), 0,
   403  			// Always send Content-MD5 to succeed with bucket with
   404  			// locking enabled. There is no performance hit since
   405  			// this is always an empty object
   406  			minio.PutObjectOptions{SendContentMd5: true},
   407  		)
   408  		return err
   409  	}
   410  
   411  	return NotImplemented{}
   412  }
   413  
   414  type listerAt []os.FileInfo
   415  
   416  // Modeled after strings.Reader's ReadAt() implementation
   417  func (f listerAt) ListAt(ls []os.FileInfo, offset int64) (int, error) {
   418  	var n int
   419  	if offset >= int64(len(f)) {
   420  		return 0, io.EOF
   421  	}
   422  	n = copy(ls, f[offset:])
   423  	if n < len(ls) {
   424  		return n, io.EOF
   425  	}
   426  	return n, nil
   427  }
   428  
   429  func (f *sftpDriver) Filelist(r *sftp.Request) (la sftp.ListerAt, err error) {
   430  	stopFn := globalSftpMetrics.log(r, f.AccessKey())
   431  	defer stopFn(err)
   432  
   433  	clnt, err := f.getMinIOClient()
   434  	if err != nil {
   435  		return nil, err
   436  	}
   437  
   438  	switch r.Method {
   439  	case "List":
   440  		var files []os.FileInfo
   441  
   442  		bucket, prefix := path2BucketObject(r.Filepath)
   443  		if bucket == "" {
   444  			buckets, err := clnt.ListBuckets(r.Context())
   445  			if err != nil {
   446  				return nil, err
   447  			}
   448  
   449  			for _, bucket := range buckets {
   450  				files = append(files, &minioFileInfo{
   451  					p:     bucket.Name,
   452  					info:  minio.ObjectInfo{Key: bucket.Name, LastModified: bucket.CreationDate},
   453  					isDir: true,
   454  				})
   455  			}
   456  
   457  			return listerAt(files), nil
   458  		}
   459  
   460  		prefix = retainSlash(prefix)
   461  
   462  		for object := range clnt.ListObjects(r.Context(), bucket, minio.ListObjectsOptions{
   463  			Prefix:    prefix,
   464  			Recursive: false,
   465  		}) {
   466  			if object.Err != nil {
   467  				return nil, object.Err
   468  			}
   469  
   470  			if object.Key == prefix {
   471  				continue
   472  			}
   473  
   474  			isDir := strings.HasSuffix(object.Key, SlashSeparator)
   475  			files = append(files, &minioFileInfo{
   476  				p:     pathClean(strings.TrimPrefix(object.Key, prefix)),
   477  				info:  object,
   478  				isDir: isDir,
   479  			})
   480  		}
   481  
   482  		return listerAt(files), nil
   483  
   484  	case "Stat":
   485  		if r.Filepath == SlashSeparator {
   486  			return listerAt{&minioFileInfo{
   487  				p:     r.Filepath,
   488  				isDir: true,
   489  			}}, nil
   490  		}
   491  
   492  		bucket, object := path2BucketObject(r.Filepath)
   493  		if bucket == "" {
   494  			return nil, errors.New("bucket name cannot be empty")
   495  		}
   496  
   497  		if object == "" {
   498  			ok, err := clnt.BucketExists(context.Background(), bucket)
   499  			if err != nil {
   500  				return nil, err
   501  			}
   502  			if !ok {
   503  				return nil, os.ErrNotExist
   504  			}
   505  			return listerAt{&minioFileInfo{
   506  				p:     pathClean(bucket),
   507  				info:  minio.ObjectInfo{Key: bucket},
   508  				isDir: true,
   509  			}}, nil
   510  		}
   511  
   512  		objInfo, err := clnt.StatObject(context.Background(), bucket, object, minio.StatObjectOptions{})
   513  		if err != nil {
   514  			if minio.ToErrorResponse(err).Code == "NoSuchKey" {
   515  				// dummy return to satisfy LIST (stat -> list) behavior.
   516  				return listerAt{&minioFileInfo{
   517  					p:     pathClean(object),
   518  					info:  minio.ObjectInfo{Key: object},
   519  					isDir: true,
   520  				}}, nil
   521  			}
   522  			return nil, err
   523  		}
   524  
   525  		isDir := strings.HasSuffix(objInfo.Key, SlashSeparator)
   526  		return listerAt{&minioFileInfo{
   527  			p:     pathClean(object),
   528  			info:  objInfo,
   529  			isDir: isDir,
   530  		}}, nil
   531  	}
   532  
   533  	return nil, NotImplemented{}
   534  }