github.com/minio/minio@v0.0.0-20240328213742-3f72439b8a27/cmd/ftp-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  	"crypto/subtle"
    24  	"errors"
    25  	"fmt"
    26  	"io"
    27  	"os"
    28  	"strings"
    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  	ftp "goftp.io/server/v2"
    38  )
    39  
    40  var _ ftp.Driver = &ftpDriver{}
    41  
    42  // ftpDriver implements ftpDriver to store files in minio
    43  type ftpDriver struct {
    44  	endpoint string
    45  }
    46  
    47  // NewFTPDriver implements ftp.Driver interface
    48  func NewFTPDriver() ftp.Driver {
    49  	return &ftpDriver{endpoint: fmt.Sprintf("127.0.0.1:%s", globalMinioPort)}
    50  }
    51  
    52  func buildMinioPath(p string) string {
    53  	return strings.TrimPrefix(p, SlashSeparator)
    54  }
    55  
    56  func buildMinioDir(p string) string {
    57  	v := buildMinioPath(p)
    58  	if !strings.HasSuffix(v, SlashSeparator) {
    59  		return v + SlashSeparator
    60  	}
    61  	return v
    62  }
    63  
    64  type minioFileInfo struct {
    65  	p     string
    66  	info  minio.ObjectInfo
    67  	isDir bool
    68  }
    69  
    70  func (m *minioFileInfo) Name() string {
    71  	return m.p
    72  }
    73  
    74  func (m *minioFileInfo) Size() int64 {
    75  	return m.info.Size
    76  }
    77  
    78  func (m *minioFileInfo) Mode() os.FileMode {
    79  	if m.isDir {
    80  		return os.ModeDir
    81  	}
    82  	return os.ModePerm
    83  }
    84  
    85  func (m *minioFileInfo) ModTime() time.Time {
    86  	return m.info.LastModified
    87  }
    88  
    89  func (m *minioFileInfo) IsDir() bool {
    90  	return m.isDir
    91  }
    92  
    93  func (m *minioFileInfo) Sys() interface{} {
    94  	return nil
    95  }
    96  
    97  //msgp:ignore ftpMetrics
    98  type ftpMetrics struct{}
    99  
   100  var globalFtpMetrics ftpMetrics
   101  
   102  func ftpTrace(s *ftp.Context, startTime time.Time, source, path string, err error) madmin.TraceInfo {
   103  	var errStr string
   104  	if err != nil {
   105  		errStr = err.Error()
   106  	}
   107  	return madmin.TraceInfo{
   108  		TraceType: madmin.TraceFTP,
   109  		Time:      startTime,
   110  		NodeName:  globalLocalNodeName,
   111  		FuncName:  fmt.Sprintf("ftp USER=%s COMMAND=%s PARAM=%s ISLOGIN=%t, Source=%s", s.Sess.LoginUser(), s.Cmd, s.Param, s.Sess.IsLogin(), source),
   112  		Duration:  time.Since(startTime),
   113  		Path:      path,
   114  		Error:     errStr,
   115  	}
   116  }
   117  
   118  func (m *ftpMetrics) log(s *ftp.Context, paths ...string) func(err error) {
   119  	startTime := time.Now()
   120  	source := getSource(2)
   121  	return func(err error) {
   122  		globalTrace.Publish(ftpTrace(s, startTime, source, strings.Join(paths, " "), err))
   123  	}
   124  }
   125  
   126  // Stat implements ftpDriver
   127  func (driver *ftpDriver) Stat(ctx *ftp.Context, path string) (fi os.FileInfo, err error) {
   128  	stopFn := globalFtpMetrics.log(ctx, path)
   129  	defer stopFn(err)
   130  
   131  	if path == SlashSeparator {
   132  		return &minioFileInfo{
   133  			p:     SlashSeparator,
   134  			isDir: true,
   135  		}, nil
   136  	}
   137  
   138  	bucket, object := path2BucketObject(path)
   139  	if bucket == "" {
   140  		return nil, errors.New("bucket name cannot be empty")
   141  	}
   142  
   143  	clnt, err := driver.getMinIOClient(ctx)
   144  	if err != nil {
   145  		return nil, err
   146  	}
   147  
   148  	if object == "" {
   149  		ok, err := clnt.BucketExists(context.Background(), bucket)
   150  		if err != nil {
   151  			return nil, err
   152  		}
   153  		if !ok {
   154  			return nil, os.ErrNotExist
   155  		}
   156  		return &minioFileInfo{
   157  			p:     pathClean(bucket),
   158  			info:  minio.ObjectInfo{Key: bucket},
   159  			isDir: true,
   160  		}, nil
   161  	}
   162  
   163  	objInfo, err := clnt.StatObject(context.Background(), bucket, object, minio.StatObjectOptions{})
   164  	if err != nil {
   165  		if minio.ToErrorResponse(err).Code == "NoSuchKey" {
   166  			// dummy return to satisfy LIST (stat -> list) behavior.
   167  			return &minioFileInfo{
   168  				p:     pathClean(object),
   169  				info:  minio.ObjectInfo{Key: object},
   170  				isDir: true,
   171  			}, nil
   172  		}
   173  		return nil, err
   174  	}
   175  
   176  	isDir := strings.HasSuffix(objInfo.Key, SlashSeparator)
   177  	return &minioFileInfo{
   178  		p:     pathClean(object),
   179  		info:  objInfo,
   180  		isDir: isDir,
   181  	}, nil
   182  }
   183  
   184  // ListDir implements ftpDriver
   185  func (driver *ftpDriver) ListDir(ctx *ftp.Context, path string, callback func(os.FileInfo) error) (err error) {
   186  	stopFn := globalFtpMetrics.log(ctx, path)
   187  	defer stopFn(err)
   188  
   189  	clnt, err := driver.getMinIOClient(ctx)
   190  	if err != nil {
   191  		return err
   192  	}
   193  
   194  	cctx, cancel := context.WithCancel(context.Background())
   195  	defer cancel()
   196  
   197  	bucket, prefix := path2BucketObject(path)
   198  	if bucket == "" {
   199  		buckets, err := clnt.ListBuckets(cctx)
   200  		if err != nil {
   201  			return err
   202  		}
   203  
   204  		for _, bucket := range buckets {
   205  			info := minioFileInfo{
   206  				p:     pathClean(bucket.Name),
   207  				info:  minio.ObjectInfo{Key: retainSlash(bucket.Name), LastModified: bucket.CreationDate},
   208  				isDir: true,
   209  			}
   210  			if err := callback(&info); err != nil {
   211  				return err
   212  			}
   213  		}
   214  
   215  		return nil
   216  	}
   217  
   218  	prefix = retainSlash(prefix)
   219  
   220  	for object := range clnt.ListObjects(cctx, bucket, minio.ListObjectsOptions{
   221  		Prefix:    prefix,
   222  		Recursive: false,
   223  	}) {
   224  		if object.Err != nil {
   225  			return object.Err
   226  		}
   227  
   228  		if object.Key == prefix {
   229  			continue
   230  		}
   231  
   232  		isDir := strings.HasSuffix(object.Key, SlashSeparator)
   233  		info := minioFileInfo{
   234  			p:     pathClean(strings.TrimPrefix(object.Key, prefix)),
   235  			info:  object,
   236  			isDir: isDir,
   237  		}
   238  
   239  		if err := callback(&info); err != nil {
   240  			return err
   241  		}
   242  	}
   243  
   244  	return nil
   245  }
   246  
   247  func (driver *ftpDriver) CheckPasswd(c *ftp.Context, username, password string) (ok bool, err error) {
   248  	stopFn := globalFtpMetrics.log(c, username)
   249  	defer stopFn(err)
   250  
   251  	if globalIAMSys.LDAPConfig.Enabled() {
   252  		sa, _, err := globalIAMSys.getServiceAccount(context.Background(), username)
   253  		if err != nil && !errors.Is(err, errNoSuchServiceAccount) {
   254  			return false, err
   255  		}
   256  		if errors.Is(err, errNoSuchServiceAccount) {
   257  			ldapUserDN, groupDistNames, err := globalIAMSys.LDAPConfig.Bind(username, password)
   258  			if err != nil {
   259  				return false, err
   260  			}
   261  			ldapPolicies, _ := globalIAMSys.PolicyDBGet(ldapUserDN, groupDistNames...)
   262  			return len(ldapPolicies) > 0, nil
   263  		}
   264  		return subtle.ConstantTimeCompare([]byte(sa.Credentials.SecretKey), []byte(password)) == 1, nil
   265  	}
   266  
   267  	ui, ok := globalIAMSys.GetUser(context.Background(), username)
   268  	if !ok {
   269  		return false, nil
   270  	}
   271  	return subtle.ConstantTimeCompare([]byte(ui.Credentials.SecretKey), []byte(password)) == 1, nil
   272  }
   273  
   274  func (driver *ftpDriver) getMinIOClient(ctx *ftp.Context) (*minio.Client, error) {
   275  	ui, ok := globalIAMSys.GetUser(context.Background(), ctx.Sess.LoginUser())
   276  	if !ok && !globalIAMSys.LDAPConfig.Enabled() {
   277  		return nil, errNoSuchUser
   278  	}
   279  	if !ok && globalIAMSys.LDAPConfig.Enabled() {
   280  		sa, _, err := globalIAMSys.getServiceAccount(context.Background(), ctx.Sess.LoginUser())
   281  		if err != nil && !errors.Is(err, errNoSuchServiceAccount) {
   282  			return nil, err
   283  		}
   284  
   285  		var mcreds *credentials.Credentials
   286  		if errors.Is(err, errNoSuchServiceAccount) {
   287  			targetUser, targetGroups, err := globalIAMSys.LDAPConfig.LookupUserDN(ctx.Sess.LoginUser())
   288  			if err != nil {
   289  				return nil, err
   290  			}
   291  			ldapPolicies, _ := globalIAMSys.PolicyDBGet(targetUser, targetGroups...)
   292  			if len(ldapPolicies) == 0 {
   293  				return nil, errAuthentication
   294  			}
   295  			expiryDur, err := globalIAMSys.LDAPConfig.GetExpiryDuration("")
   296  			if err != nil {
   297  				return nil, err
   298  			}
   299  			claims := make(map[string]interface{})
   300  			claims[expClaim] = UTCNow().Add(expiryDur).Unix()
   301  			claims[ldapUser] = targetUser
   302  			claims[ldapUserN] = ctx.Sess.LoginUser()
   303  
   304  			cred, err := auth.GetNewCredentialsWithMetadata(claims, globalActiveCred.SecretKey)
   305  			if err != nil {
   306  				return nil, err
   307  			}
   308  
   309  			// Set the parent of the temporary access key, this is useful
   310  			// in obtaining service accounts by this cred.
   311  			cred.ParentUser = targetUser
   312  
   313  			// Set this value to LDAP groups, LDAP user can be part
   314  			// of large number of groups
   315  			cred.Groups = targetGroups
   316  
   317  			// Set the newly generated credentials, policyName is empty on purpose
   318  			// LDAP policies are applied automatically using their ldapUser, ldapGroups
   319  			// mapping.
   320  			updatedAt, err := globalIAMSys.SetTempUser(context.Background(), cred.AccessKey, cred, "")
   321  			if err != nil {
   322  				return nil, err
   323  			}
   324  
   325  			// Call hook for site replication.
   326  			logger.LogIf(context.Background(), globalSiteReplicationSys.IAMChangeHook(context.Background(), madmin.SRIAMItem{
   327  				Type: madmin.SRIAMItemSTSAcc,
   328  				STSCredential: &madmin.SRSTSCredential{
   329  					AccessKey:    cred.AccessKey,
   330  					SecretKey:    cred.SecretKey,
   331  					SessionToken: cred.SessionToken,
   332  					ParentUser:   cred.ParentUser,
   333  				},
   334  				UpdatedAt: updatedAt,
   335  			}))
   336  
   337  			mcreds = credentials.NewStaticV4(cred.AccessKey, cred.SecretKey, cred.SessionToken)
   338  		} else {
   339  			mcreds = credentials.NewStaticV4(sa.Credentials.AccessKey, sa.Credentials.SecretKey, "")
   340  		}
   341  
   342  		return minio.New(driver.endpoint, &minio.Options{
   343  			Creds:     mcreds,
   344  			Secure:    globalIsTLS,
   345  			Transport: globalRemoteFTPClientTransport,
   346  		})
   347  	}
   348  
   349  	// ok == true - at this point
   350  
   351  	if ui.Credentials.IsTemp() {
   352  		// Temporary credentials are not allowed.
   353  		return nil, errAuthentication
   354  	}
   355  
   356  	return minio.New(driver.endpoint, &minio.Options{
   357  		Creds:     credentials.NewStaticV4(ui.Credentials.AccessKey, ui.Credentials.SecretKey, ""),
   358  		Secure:    globalIsTLS,
   359  		Transport: globalRemoteFTPClientTransport,
   360  	})
   361  }
   362  
   363  // DeleteDir implements ftpDriver
   364  func (driver *ftpDriver) DeleteDir(ctx *ftp.Context, path string) (err error) {
   365  	stopFn := globalFtpMetrics.log(ctx, path)
   366  	defer stopFn(err)
   367  
   368  	bucket, prefix := path2BucketObject(path)
   369  	if bucket == "" {
   370  		return errors.New("deleting all buckets not allowed")
   371  	}
   372  
   373  	clnt, err := driver.getMinIOClient(ctx)
   374  	if err != nil {
   375  		return err
   376  	}
   377  
   378  	cctx, cancel := context.WithCancel(context.Background())
   379  	defer cancel()
   380  
   381  	if prefix == "" {
   382  		// if all objects are not deleted yet this call may fail.
   383  		return clnt.RemoveBucket(cctx, bucket)
   384  	}
   385  
   386  	objectsCh := make(chan minio.ObjectInfo)
   387  
   388  	// Send object names that are needed to be removed to objectsCh
   389  	go func() {
   390  		defer xioutil.SafeClose(objectsCh)
   391  		opts := minio.ListObjectsOptions{
   392  			Prefix:    prefix,
   393  			Recursive: true,
   394  		}
   395  		for object := range clnt.ListObjects(cctx, bucket, opts) {
   396  			if object.Err != nil {
   397  				return
   398  			}
   399  			objectsCh <- object
   400  		}
   401  	}()
   402  
   403  	// Call RemoveObjects API
   404  	for err := range clnt.RemoveObjects(context.Background(), bucket, objectsCh, minio.RemoveObjectsOptions{}) {
   405  		if err.Err != nil {
   406  			return err.Err
   407  		}
   408  	}
   409  
   410  	return nil
   411  }
   412  
   413  // DeleteFile implements ftpDriver
   414  func (driver *ftpDriver) DeleteFile(ctx *ftp.Context, path string) (err error) {
   415  	stopFn := globalFtpMetrics.log(ctx, path)
   416  	defer stopFn(err)
   417  
   418  	bucket, object := path2BucketObject(path)
   419  	if bucket == "" {
   420  		return errors.New("bucket name cannot be empty")
   421  	}
   422  
   423  	clnt, err := driver.getMinIOClient(ctx)
   424  	if err != nil {
   425  		return err
   426  	}
   427  
   428  	return clnt.RemoveObject(context.Background(), bucket, object, minio.RemoveObjectOptions{})
   429  }
   430  
   431  // Rename implements ftpDriver
   432  func (driver *ftpDriver) Rename(ctx *ftp.Context, fromPath string, toPath string) (err error) {
   433  	stopFn := globalFtpMetrics.log(ctx, fromPath, toPath)
   434  	defer stopFn(err)
   435  
   436  	return NotImplemented{}
   437  }
   438  
   439  // MakeDir implements ftpDriver
   440  func (driver *ftpDriver) MakeDir(ctx *ftp.Context, path string) (err error) {
   441  	stopFn := globalFtpMetrics.log(ctx, path)
   442  	defer stopFn(err)
   443  
   444  	bucket, prefix := path2BucketObject(path)
   445  	if bucket == "" {
   446  		return errors.New("bucket name cannot be empty")
   447  	}
   448  
   449  	clnt, err := driver.getMinIOClient(ctx)
   450  	if err != nil {
   451  		return err
   452  	}
   453  
   454  	if prefix == "" {
   455  		return clnt.MakeBucket(context.Background(), bucket, minio.MakeBucketOptions{Region: globalSite.Region})
   456  	}
   457  
   458  	dirPath := buildMinioDir(prefix)
   459  
   460  	_, err = clnt.PutObject(context.Background(), bucket, dirPath, bytes.NewReader([]byte("")), 0,
   461  		// Always send Content-MD5 to succeed with bucket with
   462  		// locking enabled. There is no performance hit since
   463  		// this is always an empty object
   464  		minio.PutObjectOptions{SendContentMd5: true},
   465  	)
   466  	return err
   467  }
   468  
   469  // GetFile implements ftpDriver
   470  func (driver *ftpDriver) GetFile(ctx *ftp.Context, path string, offset int64) (n int64, rc io.ReadCloser, err error) {
   471  	stopFn := globalFtpMetrics.log(ctx, path)
   472  	defer stopFn(err)
   473  
   474  	bucket, object := path2BucketObject(path)
   475  	if bucket == "" {
   476  		return 0, nil, errors.New("bucket name cannot be empty")
   477  	}
   478  
   479  	clnt, err := driver.getMinIOClient(ctx)
   480  	if err != nil {
   481  		return 0, nil, err
   482  	}
   483  
   484  	opts := minio.GetObjectOptions{}
   485  	obj, err := clnt.GetObject(context.Background(), bucket, object, opts)
   486  	if err != nil {
   487  		return 0, nil, err
   488  	}
   489  	defer func() {
   490  		if err != nil && obj != nil {
   491  			obj.Close()
   492  		}
   493  	}()
   494  
   495  	_, err = obj.Seek(offset, io.SeekStart)
   496  	if err != nil {
   497  		return 0, nil, err
   498  	}
   499  
   500  	info, err := obj.Stat()
   501  	if err != nil {
   502  		return 0, nil, err
   503  	}
   504  
   505  	return info.Size - offset, obj, nil
   506  }
   507  
   508  // PutFile implements ftpDriver
   509  func (driver *ftpDriver) PutFile(ctx *ftp.Context, path string, data io.Reader, offset int64) (n int64, err error) {
   510  	stopFn := globalFtpMetrics.log(ctx, path)
   511  	defer stopFn(err)
   512  
   513  	bucket, object := path2BucketObject(path)
   514  	if bucket == "" {
   515  		return 0, errors.New("bucket name cannot be empty")
   516  	}
   517  
   518  	if offset != -1 {
   519  		// FTP - APPEND not implemented
   520  		return 0, NotImplemented{}
   521  	}
   522  
   523  	clnt, err := driver.getMinIOClient(ctx)
   524  	if err != nil {
   525  		return 0, err
   526  	}
   527  
   528  	info, err := clnt.PutObject(context.Background(), bucket, object, data, -1, minio.PutObjectOptions{
   529  		ContentType:    "application/octet-stream",
   530  		SendContentMd5: true,
   531  	})
   532  	return info.Size, err
   533  }