mirror of
https://github.com/minio/minio.git
synced 2024-12-27 07:35:55 -05:00
506 lines
12 KiB
Go
506 lines
12 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"
|
||
|
"crypto/subtle"
|
||
|
"errors"
|
||
|
"fmt"
|
||
|
"io"
|
||
|
"os"
|
||
|
"strings"
|
||
|
"time"
|
||
|
|
||
|
"github.com/minio/madmin-go/v2"
|
||
|
"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"
|
||
|
ftp "goftp.io/server/v2"
|
||
|
)
|
||
|
|
||
|
var _ ftp.Driver = &ftpDriver{}
|
||
|
|
||
|
// ftpDriver implements ftpDriver to store files in minio
|
||
|
type ftpDriver struct {
|
||
|
endpoint string
|
||
|
}
|
||
|
|
||
|
// NewFTPDriver implements ftp.Driver interface
|
||
|
func NewFTPDriver() ftp.Driver {
|
||
|
return &ftpDriver{endpoint: fmt.Sprintf("127.0.0.1:%s", globalMinioPort)}
|
||
|
}
|
||
|
|
||
|
func buildMinioPath(p string) string {
|
||
|
return strings.TrimPrefix(p, SlashSeparator)
|
||
|
}
|
||
|
|
||
|
func buildMinioDir(p string) string {
|
||
|
v := buildMinioPath(p)
|
||
|
if !strings.HasSuffix(v, SlashSeparator) {
|
||
|
return v + SlashSeparator
|
||
|
}
|
||
|
return v
|
||
|
}
|
||
|
|
||
|
type minioFileInfo struct {
|
||
|
p string
|
||
|
info minio.ObjectInfo
|
||
|
isDir bool
|
||
|
}
|
||
|
|
||
|
func (m *minioFileInfo) Name() string {
|
||
|
return m.p
|
||
|
}
|
||
|
|
||
|
func (m *minioFileInfo) Size() int64 {
|
||
|
return m.info.Size
|
||
|
}
|
||
|
|
||
|
func (m *minioFileInfo) Mode() os.FileMode {
|
||
|
if m.isDir {
|
||
|
return os.ModeDir
|
||
|
}
|
||
|
return os.ModePerm
|
||
|
}
|
||
|
|
||
|
func (m *minioFileInfo) ModTime() time.Time {
|
||
|
return m.info.LastModified
|
||
|
}
|
||
|
|
||
|
func (m *minioFileInfo) IsDir() bool {
|
||
|
return m.isDir
|
||
|
}
|
||
|
|
||
|
func (m *minioFileInfo) Sys() interface{} {
|
||
|
return nil
|
||
|
}
|
||
|
|
||
|
//msgp:ignore ftpMetrics
|
||
|
type ftpMetrics struct{}
|
||
|
|
||
|
var globalFtpMetrics ftpMetrics
|
||
|
|
||
|
func ftpTrace(s *ftp.Context, startTime time.Time, source, path 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("ftp USER=%s COMMAND=%s PARAM=%s ISLOGIN=%t, Source=%s", s.Sess.LoginUser(), s.Cmd, s.Param, s.Sess.IsLogin(), source),
|
||
|
Duration: time.Since(startTime),
|
||
|
Path: path,
|
||
|
Error: errStr,
|
||
|
}
|
||
|
}
|
||
|
|
||
|
func (m *ftpMetrics) log(s *ftp.Context, paths ...string) func(err error) {
|
||
|
startTime := time.Now()
|
||
|
source := getSource(2)
|
||
|
return func(err error) {
|
||
|
globalTrace.Publish(ftpTrace(s, startTime, source, strings.Join(paths, " "), err))
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// Stat implements ftpDriver
|
||
|
func (driver *ftpDriver) Stat(ctx *ftp.Context, path string) (fi os.FileInfo, err error) {
|
||
|
stopFn := globalFtpMetrics.log(ctx, path)
|
||
|
defer stopFn(err)
|
||
|
|
||
|
if path == SlashSeparator {
|
||
|
return &minioFileInfo{
|
||
|
p: SlashSeparator,
|
||
|
isDir: true,
|
||
|
}, nil
|
||
|
}
|
||
|
|
||
|
bucket, object := path2BucketObject(path)
|
||
|
if bucket == "" {
|
||
|
return nil, errors.New("bucket name cannot be empty")
|
||
|
}
|
||
|
|
||
|
clnt, err := driver.getMinIOClient(ctx)
|
||
|
if err != nil {
|
||
|
return nil, err
|
||
|
}
|
||
|
|
||
|
if object == "" {
|
||
|
ok, err := clnt.BucketExists(context.Background(), bucket)
|
||
|
if err != nil {
|
||
|
return nil, err
|
||
|
}
|
||
|
if !ok {
|
||
|
return nil, os.ErrNotExist
|
||
|
}
|
||
|
return &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 &minioFileInfo{
|
||
|
p: pathClean(object),
|
||
|
info: minio.ObjectInfo{Key: object},
|
||
|
isDir: true,
|
||
|
}, nil
|
||
|
}
|
||
|
return nil, err
|
||
|
}
|
||
|
|
||
|
isDir := strings.HasSuffix(objInfo.Key, SlashSeparator)
|
||
|
return &minioFileInfo{
|
||
|
p: pathClean(object),
|
||
|
info: objInfo,
|
||
|
isDir: isDir,
|
||
|
}, nil
|
||
|
}
|
||
|
|
||
|
// ListDir implements ftpDriver
|
||
|
func (driver *ftpDriver) ListDir(ctx *ftp.Context, path string, callback func(os.FileInfo) error) (err error) {
|
||
|
stopFn := globalFtpMetrics.log(ctx, path)
|
||
|
defer stopFn(err)
|
||
|
|
||
|
clnt, err := driver.getMinIOClient(ctx)
|
||
|
if err != nil {
|
||
|
return err
|
||
|
}
|
||
|
|
||
|
cctx, cancel := context.WithCancel(context.Background())
|
||
|
defer cancel()
|
||
|
|
||
|
bucket, prefix := path2BucketObject(path)
|
||
|
if bucket == "" {
|
||
|
buckets, err := clnt.ListBuckets(cctx)
|
||
|
if err != nil {
|
||
|
return err
|
||
|
}
|
||
|
|
||
|
for _, bucket := range buckets {
|
||
|
info := minioFileInfo{
|
||
|
p: pathClean(bucket.Name),
|
||
|
info: minio.ObjectInfo{Key: retainSlash(bucket.Name), LastModified: bucket.CreationDate},
|
||
|
isDir: true,
|
||
|
}
|
||
|
if err := callback(&info); err != nil {
|
||
|
return err
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return nil
|
||
|
}
|
||
|
|
||
|
prefix = retainSlash(prefix)
|
||
|
|
||
|
for object := range clnt.ListObjects(cctx, bucket, minio.ListObjectsOptions{
|
||
|
Prefix: prefix,
|
||
|
Recursive: false,
|
||
|
}) {
|
||
|
if object.Err != nil {
|
||
|
return object.Err
|
||
|
}
|
||
|
|
||
|
if object.Key == prefix {
|
||
|
continue
|
||
|
}
|
||
|
|
||
|
isDir := strings.HasSuffix(object.Key, SlashSeparator)
|
||
|
info := minioFileInfo{
|
||
|
p: pathClean(strings.TrimPrefix(object.Key, prefix)),
|
||
|
info: object,
|
||
|
isDir: isDir,
|
||
|
}
|
||
|
|
||
|
if err := callback(&info); err != nil {
|
||
|
return err
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return nil
|
||
|
}
|
||
|
|
||
|
func (driver *ftpDriver) CheckPasswd(c *ftp.Context, username, password string) (ok bool, err error) {
|
||
|
stopFn := globalFtpMetrics.log(c, username)
|
||
|
defer stopFn(err)
|
||
|
|
||
|
if globalIAMSys.LDAPConfig.Enabled() {
|
||
|
ldapUserDN, groupDistNames, err := globalIAMSys.LDAPConfig.Bind(username, password)
|
||
|
if err != nil {
|
||
|
return false, err
|
||
|
}
|
||
|
ldapPolicies, _ := globalIAMSys.PolicyDBGet(ldapUserDN, false, groupDistNames...)
|
||
|
if len(ldapPolicies) == 0 {
|
||
|
// no policy associated reject it.
|
||
|
return false, nil
|
||
|
}
|
||
|
return true, nil
|
||
|
}
|
||
|
|
||
|
ui, ok := globalIAMSys.GetUser(context.Background(), username)
|
||
|
if !ok {
|
||
|
return false, nil
|
||
|
}
|
||
|
return subtle.ConstantTimeCompare([]byte(ui.Credentials.SecretKey), []byte(password)) == 1, nil
|
||
|
}
|
||
|
|
||
|
func (driver *ftpDriver) getMinIOClient(ctx *ftp.Context) (*minio.Client, error) {
|
||
|
ui, ok := globalIAMSys.GetUser(context.Background(), ctx.Sess.LoginUser())
|
||
|
if !ok && !globalIAMSys.LDAPConfig.Enabled() {
|
||
|
return nil, errNoSuchUser
|
||
|
}
|
||
|
if !ok && globalIAMSys.LDAPConfig.Enabled() {
|
||
|
targetUser, targetGroups, err := globalIAMSys.LDAPConfig.LookupUserDN(ctx.Sess.LoginUser())
|
||
|
if err != nil {
|
||
|
return nil, err
|
||
|
}
|
||
|
ldapPolicies, _ := globalIAMSys.PolicyDBGet(targetUser, false, targetGroups...)
|
||
|
if len(ldapPolicies) == 0 {
|
||
|
return nil, errAuthentication
|
||
|
}
|
||
|
expiryDur, err := globalIAMSys.LDAPConfig.GetExpiryDuration("")
|
||
|
if err != nil {
|
||
|
return nil, err
|
||
|
}
|
||
|
claims := make(map[string]interface{})
|
||
|
claims[expClaim] = UTCNow().Add(expiryDur).Unix()
|
||
|
claims[ldapUser] = targetUser
|
||
|
claims[ldapUserN] = ctx.Sess.LoginUser()
|
||
|
|
||
|
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(driver.endpoint, &minio.Options{
|
||
|
Creds: credentials.NewStaticV4(cred.AccessKey, cred.SecretKey, cred.SessionToken),
|
||
|
Secure: globalIsTLS,
|
||
|
Transport: globalRemoteTargetTransport,
|
||
|
})
|
||
|
}
|
||
|
|
||
|
// ok == true - at this point
|
||
|
|
||
|
if ui.Credentials.IsTemp() {
|
||
|
// Temporary credentials are not allowed.
|
||
|
return nil, errAuthentication
|
||
|
}
|
||
|
|
||
|
return minio.New(driver.endpoint, &minio.Options{
|
||
|
Creds: credentials.NewStaticV4(ui.Credentials.AccessKey, ui.Credentials.SecretKey, ""),
|
||
|
Secure: globalIsTLS,
|
||
|
Transport: globalRemoteTargetTransport,
|
||
|
})
|
||
|
}
|
||
|
|
||
|
// DeleteDir implements ftpDriver
|
||
|
func (driver *ftpDriver) DeleteDir(ctx *ftp.Context, path string) (err error) {
|
||
|
stopFn := globalFtpMetrics.log(ctx, path)
|
||
|
defer stopFn(err)
|
||
|
|
||
|
bucket, prefix := path2BucketObject(path)
|
||
|
if bucket == "" {
|
||
|
return errors.New("deleting all buckets not allowed")
|
||
|
}
|
||
|
|
||
|
clnt, err := driver.getMinIOClient(ctx)
|
||
|
if err != nil {
|
||
|
return err
|
||
|
}
|
||
|
|
||
|
cctx, cancel := context.WithCancel(context.Background())
|
||
|
defer cancel()
|
||
|
|
||
|
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
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return nil
|
||
|
}
|
||
|
|
||
|
// DeleteFile implements ftpDriver
|
||
|
func (driver *ftpDriver) DeleteFile(ctx *ftp.Context, path string) (err error) {
|
||
|
stopFn := globalFtpMetrics.log(ctx, path)
|
||
|
defer stopFn(err)
|
||
|
|
||
|
bucket, object := path2BucketObject(path)
|
||
|
if bucket == "" {
|
||
|
return errors.New("bucket name cannot be empty")
|
||
|
}
|
||
|
|
||
|
clnt, err := driver.getMinIOClient(ctx)
|
||
|
if err != nil {
|
||
|
return err
|
||
|
}
|
||
|
|
||
|
return clnt.RemoveObject(context.Background(), bucket, object, minio.RemoveObjectOptions{})
|
||
|
}
|
||
|
|
||
|
// Rename implements ftpDriver
|
||
|
func (driver *ftpDriver) Rename(ctx *ftp.Context, fromPath string, toPath string) (err error) {
|
||
|
stopFn := globalFtpMetrics.log(ctx, fromPath, toPath)
|
||
|
defer stopFn(err)
|
||
|
|
||
|
return NotImplemented{}
|
||
|
}
|
||
|
|
||
|
// MakeDir implements ftpDriver
|
||
|
func (driver *ftpDriver) MakeDir(ctx *ftp.Context, path string) (err error) {
|
||
|
stopFn := globalFtpMetrics.log(ctx, path)
|
||
|
defer stopFn(err)
|
||
|
|
||
|
bucket, prefix := path2BucketObject(path)
|
||
|
if bucket == "" {
|
||
|
return errors.New("bucket name cannot be empty")
|
||
|
}
|
||
|
|
||
|
clnt, err := driver.getMinIOClient(ctx)
|
||
|
if err != nil {
|
||
|
return err
|
||
|
}
|
||
|
|
||
|
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
|
||
|
}
|
||
|
|
||
|
// GetFile implements ftpDriver
|
||
|
func (driver *ftpDriver) GetFile(ctx *ftp.Context, path string, offset int64) (n int64, rc io.ReadCloser, err error) {
|
||
|
stopFn := globalFtpMetrics.log(ctx, path)
|
||
|
defer stopFn(err)
|
||
|
|
||
|
bucket, object := path2BucketObject(path)
|
||
|
if bucket == "" {
|
||
|
return 0, nil, errors.New("bucket name cannot be empty")
|
||
|
}
|
||
|
|
||
|
clnt, err := driver.getMinIOClient(ctx)
|
||
|
if err != nil {
|
||
|
return 0, nil, err
|
||
|
}
|
||
|
|
||
|
opts := minio.GetObjectOptions{}
|
||
|
obj, err := clnt.GetObject(context.Background(), bucket, object, opts)
|
||
|
if err != nil {
|
||
|
return 0, nil, err
|
||
|
}
|
||
|
defer func() {
|
||
|
if err != nil && obj != nil {
|
||
|
obj.Close()
|
||
|
}
|
||
|
}()
|
||
|
|
||
|
_, err = obj.Seek(offset, io.SeekStart)
|
||
|
if err != nil {
|
||
|
return 0, nil, err
|
||
|
}
|
||
|
|
||
|
info, err := obj.Stat()
|
||
|
if err != nil {
|
||
|
return 0, nil, err
|
||
|
}
|
||
|
|
||
|
return info.Size - offset, obj, nil
|
||
|
}
|
||
|
|
||
|
// PutFile implements ftpDriver
|
||
|
func (driver *ftpDriver) PutFile(ctx *ftp.Context, path string, data io.Reader, offset int64) (n int64, err error) {
|
||
|
stopFn := globalFtpMetrics.log(ctx, path)
|
||
|
defer stopFn(err)
|
||
|
|
||
|
bucket, object := path2BucketObject(path)
|
||
|
if bucket == "" {
|
||
|
return 0, errors.New("bucket name cannot be empty")
|
||
|
}
|
||
|
|
||
|
if offset != -1 {
|
||
|
// FTP - APPEND not implemented
|
||
|
return 0, NotImplemented{}
|
||
|
}
|
||
|
|
||
|
clnt, err := driver.getMinIOClient(ctx)
|
||
|
if err != nil {
|
||
|
return 0, err
|
||
|
}
|
||
|
|
||
|
info, err := clnt.PutObject(context.Background(), bucket, object, data, -1, minio.PutObjectOptions{
|
||
|
ContentType: "application/octet-stream",
|
||
|
SendContentMd5: true,
|
||
|
})
|
||
|
return info.Size, err
|
||
|
}
|