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 }