dataretention.go 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507
  1. // Copyright (C) 2019 Nicola Murino
  2. //
  3. // This program is free software: you can redistribute it and/or modify
  4. // it under the terms of the GNU Affero General Public License as published
  5. // by the Free Software Foundation, version 3.
  6. //
  7. // This program is distributed in the hope that it will be useful,
  8. // but WITHOUT ANY WARRANTY; without even the implied warranty of
  9. // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  10. // GNU Affero General Public License for more details.
  11. //
  12. // You should have received a copy of the GNU Affero General Public License
  13. // along with this program. If not, see <https://www.gnu.org/licenses/>.
  14. package common
  15. import (
  16. "bytes"
  17. "context"
  18. "encoding/json"
  19. "errors"
  20. "fmt"
  21. "io"
  22. "net/http"
  23. "net/url"
  24. "os"
  25. "os/exec"
  26. "path"
  27. "path/filepath"
  28. "strings"
  29. "sync"
  30. "time"
  31. "github.com/wneessen/go-mail"
  32. "github.com/drakkan/sftpgo/v2/internal/command"
  33. "github.com/drakkan/sftpgo/v2/internal/dataprovider"
  34. "github.com/drakkan/sftpgo/v2/internal/httpclient"
  35. "github.com/drakkan/sftpgo/v2/internal/logger"
  36. "github.com/drakkan/sftpgo/v2/internal/smtp"
  37. "github.com/drakkan/sftpgo/v2/internal/util"
  38. "github.com/drakkan/sftpgo/v2/internal/vfs"
  39. )
  40. // RetentionCheckNotification defines the supported notification methods for a retention check result
  41. type RetentionCheckNotification = string
  42. // Supported notification methods
  43. const (
  44. // notify results using the defined "data_retention_hook"
  45. RetentionCheckNotificationHook = "Hook"
  46. // notify results by email
  47. RetentionCheckNotificationEmail = "Email"
  48. )
  49. var (
  50. // RetentionChecks is the list of active retention checks
  51. RetentionChecks ActiveRetentionChecks
  52. )
  53. // ActiveRetentionChecks holds the active retention checks
  54. type ActiveRetentionChecks struct {
  55. sync.RWMutex
  56. Checks []RetentionCheck
  57. }
  58. // Get returns the active retention checks
  59. func (c *ActiveRetentionChecks) Get(role string) []RetentionCheck {
  60. c.RLock()
  61. defer c.RUnlock()
  62. checks := make([]RetentionCheck, 0, len(c.Checks))
  63. for _, check := range c.Checks {
  64. if role == "" || role == check.Role {
  65. foldersCopy := make([]dataprovider.FolderRetention, len(check.Folders))
  66. copy(foldersCopy, check.Folders)
  67. notificationsCopy := make([]string, len(check.Notifications))
  68. copy(notificationsCopy, check.Notifications)
  69. checks = append(checks, RetentionCheck{
  70. Username: check.Username,
  71. StartTime: check.StartTime,
  72. Notifications: notificationsCopy,
  73. Email: check.Email,
  74. Folders: foldersCopy,
  75. })
  76. }
  77. }
  78. return checks
  79. }
  80. // Add a new retention check, returns nil if a retention check for the given
  81. // username is already active. The returned result can be used to start the check
  82. func (c *ActiveRetentionChecks) Add(check RetentionCheck, user *dataprovider.User) *RetentionCheck {
  83. c.Lock()
  84. defer c.Unlock()
  85. for _, val := range c.Checks {
  86. if val.Username == user.Username {
  87. return nil
  88. }
  89. }
  90. // we silently ignore file patterns
  91. user.Filters.FilePatterns = nil
  92. conn := NewBaseConnection("", "", "", "", *user)
  93. conn.SetProtocol(ProtocolDataRetention)
  94. conn.ID = fmt.Sprintf("data_retention_%v", user.Username)
  95. check.Username = user.Username
  96. check.Role = user.Role
  97. check.StartTime = util.GetTimeAsMsSinceEpoch(time.Now())
  98. check.conn = conn
  99. check.updateUserPermissions()
  100. c.Checks = append(c.Checks, check)
  101. return &check
  102. }
  103. // remove a user from the ones with active retention checks
  104. // and returns true if the user is removed
  105. func (c *ActiveRetentionChecks) remove(username string) bool {
  106. c.Lock()
  107. defer c.Unlock()
  108. for idx, check := range c.Checks {
  109. if check.Username == username {
  110. lastIdx := len(c.Checks) - 1
  111. c.Checks[idx] = c.Checks[lastIdx]
  112. c.Checks = c.Checks[:lastIdx]
  113. return true
  114. }
  115. }
  116. return false
  117. }
  118. type folderRetentionCheckResult struct {
  119. Path string `json:"path"`
  120. Retention int `json:"retention"`
  121. DeletedFiles int `json:"deleted_files"`
  122. DeletedSize int64 `json:"deleted_size"`
  123. Elapsed time.Duration `json:"-"`
  124. Info string `json:"info,omitempty"`
  125. Error string `json:"error,omitempty"`
  126. }
  127. // RetentionCheck defines an active retention check
  128. type RetentionCheck struct {
  129. // Username to which the retention check refers
  130. Username string `json:"username"`
  131. // retention check start time as unix timestamp in milliseconds
  132. StartTime int64 `json:"start_time"`
  133. // affected folders
  134. Folders []dataprovider.FolderRetention `json:"folders"`
  135. // how cleanup results will be notified
  136. Notifications []RetentionCheckNotification `json:"notifications,omitempty"`
  137. // email to use if the notification method is set to email
  138. Email string `json:"email,omitempty"`
  139. Role string `json:"-"`
  140. // Cleanup results
  141. results []folderRetentionCheckResult `json:"-"`
  142. conn *BaseConnection
  143. }
  144. // Validate returns an error if the specified folders are not valid
  145. func (c *RetentionCheck) Validate() error {
  146. folderPaths := make(map[string]bool)
  147. nothingToDo := true
  148. for idx := range c.Folders {
  149. f := &c.Folders[idx]
  150. if err := f.Validate(); err != nil {
  151. return err
  152. }
  153. if f.Retention > 0 {
  154. nothingToDo = false
  155. }
  156. if _, ok := folderPaths[f.Path]; ok {
  157. return util.NewValidationError(fmt.Sprintf("duplicated folder path %q", f.Path))
  158. }
  159. folderPaths[f.Path] = true
  160. }
  161. if nothingToDo {
  162. return util.NewValidationError("nothing to delete!")
  163. }
  164. for _, notification := range c.Notifications {
  165. switch notification {
  166. case RetentionCheckNotificationEmail:
  167. if !smtp.IsEnabled() {
  168. return util.NewValidationError("in order to notify results via email you must configure an SMTP server")
  169. }
  170. if c.Email == "" {
  171. return util.NewValidationError("in order to notify results via email you must add a valid email address to your profile")
  172. }
  173. case RetentionCheckNotificationHook:
  174. if Config.DataRetentionHook == "" {
  175. return util.NewValidationError("in order to notify results via hook you must define a data_retention_hook")
  176. }
  177. default:
  178. return util.NewValidationError(fmt.Sprintf("invalid notification %q", notification))
  179. }
  180. }
  181. return nil
  182. }
  183. func (c *RetentionCheck) updateUserPermissions() {
  184. for _, folder := range c.Folders {
  185. if folder.IgnoreUserPermissions {
  186. c.conn.User.Permissions[folder.Path] = []string{dataprovider.PermAny}
  187. }
  188. }
  189. }
  190. func (c *RetentionCheck) getFolderRetention(folderPath string) (dataprovider.FolderRetention, error) {
  191. dirsForPath := util.GetDirsForVirtualPath(folderPath)
  192. for _, dirPath := range dirsForPath {
  193. for _, folder := range c.Folders {
  194. if folder.Path == dirPath {
  195. return folder, nil
  196. }
  197. }
  198. }
  199. return dataprovider.FolderRetention{}, fmt.Errorf("unable to find folder retention for %q", folderPath)
  200. }
  201. func (c *RetentionCheck) removeFile(virtualPath string, info os.FileInfo) error {
  202. fs, fsPath, err := c.conn.GetFsAndResolvedPath(virtualPath)
  203. if err != nil {
  204. return err
  205. }
  206. return c.conn.RemoveFile(fs, fsPath, virtualPath, info)
  207. }
  208. func (c *RetentionCheck) hasCleanupPerms(folderPath string) bool {
  209. if !c.conn.User.HasPerm(dataprovider.PermListItems, folderPath) {
  210. return false
  211. }
  212. if !c.conn.User.HasAnyPerm([]string{dataprovider.PermDelete, dataprovider.PermDeleteFiles}, folderPath) {
  213. return false
  214. }
  215. return true
  216. }
  217. func (c *RetentionCheck) cleanupFolder(folderPath string, recursion int) error {
  218. startTime := time.Now()
  219. result := folderRetentionCheckResult{
  220. Path: folderPath,
  221. }
  222. defer func() {
  223. c.results = append(c.results, result)
  224. }()
  225. if recursion >= util.MaxRecursion {
  226. result.Elapsed = time.Since(startTime)
  227. result.Info = "data retention check skipped: recursion too deep"
  228. c.conn.Log(logger.LevelError, "data retention check skipped, recursion too depth for %q: %d",
  229. folderPath, recursion)
  230. return util.ErrRecursionTooDeep
  231. }
  232. recursion++
  233. if !c.hasCleanupPerms(folderPath) {
  234. result.Elapsed = time.Since(startTime)
  235. result.Info = "data retention check skipped: no permissions"
  236. c.conn.Log(logger.LevelInfo, "user %q does not have permissions to check retention on %q, retention check skipped",
  237. c.conn.User.Username, folderPath)
  238. return nil
  239. }
  240. folderRetention, err := c.getFolderRetention(folderPath)
  241. if err != nil {
  242. result.Elapsed = time.Since(startTime)
  243. result.Error = "unable to get folder retention"
  244. c.conn.Log(logger.LevelError, "unable to get folder retention for path %q", folderPath)
  245. return err
  246. }
  247. result.Retention = folderRetention.Retention
  248. if folderRetention.Retention == 0 {
  249. result.Elapsed = time.Since(startTime)
  250. result.Info = "data retention check skipped: retention is set to 0"
  251. c.conn.Log(logger.LevelDebug, "retention check skipped for folder %q, retention is set to 0", folderPath)
  252. return nil
  253. }
  254. c.conn.Log(logger.LevelDebug, "start retention check for folder %q, retention: %v hours, delete empty dirs? %v, ignore user perms? %v",
  255. folderPath, folderRetention.Retention, folderRetention.DeleteEmptyDirs, folderRetention.IgnoreUserPermissions)
  256. lister, err := c.conn.ListDir(folderPath)
  257. if err != nil {
  258. result.Elapsed = time.Since(startTime)
  259. if err == c.conn.GetNotExistError() {
  260. result.Info = "data retention check skipped, folder does not exist"
  261. c.conn.Log(logger.LevelDebug, "folder %q does not exist, retention check skipped", folderPath)
  262. return nil
  263. }
  264. result.Error = fmt.Sprintf("unable to get lister for directory %q", folderPath)
  265. c.conn.Log(logger.LevelError, result.Error)
  266. return err
  267. }
  268. defer lister.Close()
  269. for {
  270. files, err := lister.Next(vfs.ListerBatchSize)
  271. finished := errors.Is(err, io.EOF)
  272. if err := lister.convertError(err); err != nil {
  273. result.Elapsed = time.Since(startTime)
  274. result.Error = fmt.Sprintf("unable to list directory %q", folderPath)
  275. c.conn.Log(logger.LevelError, "unable to list dir %q: %v", folderPath, err)
  276. return err
  277. }
  278. for _, info := range files {
  279. virtualPath := path.Join(folderPath, info.Name())
  280. if info.IsDir() {
  281. if err := c.cleanupFolder(virtualPath, recursion); err != nil {
  282. result.Elapsed = time.Since(startTime)
  283. result.Error = fmt.Sprintf("unable to check folder: %v", err)
  284. c.conn.Log(logger.LevelError, "unable to cleanup folder %q: %v", virtualPath, err)
  285. return err
  286. }
  287. } else {
  288. retentionTime := info.ModTime().Add(time.Duration(folderRetention.Retention) * time.Hour)
  289. if retentionTime.Before(time.Now()) {
  290. if err := c.removeFile(virtualPath, info); err != nil {
  291. result.Elapsed = time.Since(startTime)
  292. result.Error = fmt.Sprintf("unable to remove file %q: %v", virtualPath, err)
  293. c.conn.Log(logger.LevelError, "unable to remove file %q, retention %v: %v",
  294. virtualPath, retentionTime, err)
  295. return err
  296. }
  297. c.conn.Log(logger.LevelDebug, "removed file %q, modification time: %v, retention: %v hours, retention time: %v",
  298. virtualPath, info.ModTime(), folderRetention.Retention, retentionTime)
  299. result.DeletedFiles++
  300. result.DeletedSize += info.Size()
  301. }
  302. }
  303. }
  304. if finished {
  305. break
  306. }
  307. }
  308. lister.Close()
  309. c.checkEmptyDirRemoval(folderPath, folderRetention.DeleteEmptyDirs)
  310. result.Elapsed = time.Since(startTime)
  311. c.conn.Log(logger.LevelDebug, "retention check completed for folder %q, deleted files: %v, deleted size: %v bytes",
  312. folderPath, result.DeletedFiles, result.DeletedSize)
  313. return nil
  314. }
  315. func (c *RetentionCheck) checkEmptyDirRemoval(folderPath string, checkVal bool) {
  316. if folderPath == "/" || !checkVal {
  317. return
  318. }
  319. for _, folder := range c.Folders {
  320. if folderPath == folder.Path {
  321. return
  322. }
  323. }
  324. if c.conn.User.HasAnyPerm([]string{
  325. dataprovider.PermDelete,
  326. dataprovider.PermDeleteDirs,
  327. }, path.Dir(folderPath),
  328. ) {
  329. lister, err := c.conn.ListDir(folderPath)
  330. if err == nil {
  331. files, err := lister.Next(1)
  332. lister.Close()
  333. if len(files) == 0 && errors.Is(err, io.EOF) {
  334. err = c.conn.RemoveDir(folderPath)
  335. c.conn.Log(logger.LevelDebug, "tried to remove empty dir %q, error: %v", folderPath, err)
  336. }
  337. }
  338. }
  339. }
  340. // Start starts the retention check
  341. func (c *RetentionCheck) Start() error {
  342. c.conn.Log(logger.LevelInfo, "retention check started")
  343. defer RetentionChecks.remove(c.conn.User.Username)
  344. defer c.conn.CloseFS() //nolint:errcheck
  345. startTime := time.Now()
  346. for _, folder := range c.Folders {
  347. if folder.Retention > 0 {
  348. if err := c.cleanupFolder(folder.Path, 0); err != nil {
  349. c.conn.Log(logger.LevelError, "retention check failed, unable to cleanup folder %q", folder.Path)
  350. c.sendNotifications(time.Since(startTime), err)
  351. return err
  352. }
  353. }
  354. }
  355. c.conn.Log(logger.LevelInfo, "retention check completed")
  356. c.sendNotifications(time.Since(startTime), nil)
  357. return nil
  358. }
  359. func (c *RetentionCheck) sendNotifications(elapsed time.Duration, err error) {
  360. for _, notification := range c.Notifications {
  361. switch notification {
  362. case RetentionCheckNotificationEmail:
  363. c.sendEmailNotification(err) //nolint:errcheck
  364. case RetentionCheckNotificationHook:
  365. c.sendHookNotification(elapsed, err) //nolint:errcheck
  366. }
  367. }
  368. }
  369. func (c *RetentionCheck) sendEmailNotification(errCheck error) error {
  370. params := EventParams{}
  371. if len(c.results) > 0 || errCheck != nil {
  372. params.retentionChecks = append(params.retentionChecks, executedRetentionCheck{
  373. Username: c.conn.User.Username,
  374. ActionName: "Retention check",
  375. Results: c.results,
  376. })
  377. }
  378. var files []*mail.File
  379. f, err := params.getRetentionReportsAsMailAttachment()
  380. if err != nil {
  381. c.conn.Log(logger.LevelError, "unable to get retention report as mail attachment: %v", err)
  382. return err
  383. }
  384. f.Name = "retention-report.zip"
  385. files = append(files, f)
  386. startTime := time.Now()
  387. var subject string
  388. if errCheck == nil {
  389. subject = fmt.Sprintf("Successful retention check for user %q", c.conn.User.Username)
  390. } else {
  391. subject = fmt.Sprintf("Retention check failed for user %q", c.conn.User.Username)
  392. }
  393. body := "Further details attached."
  394. err = smtp.SendEmail([]string{c.Email}, nil, subject, body, smtp.EmailContentTypeTextPlain, files...)
  395. if err != nil {
  396. c.conn.Log(logger.LevelError, "unable to notify retention check result via email: %v, elapsed: %s", err,
  397. time.Since(startTime))
  398. return err
  399. }
  400. c.conn.Log(logger.LevelInfo, "retention check result successfully notified via email, elapsed: %s", time.Since(startTime))
  401. return nil
  402. }
  403. func (c *RetentionCheck) sendHookNotification(elapsed time.Duration, errCheck error) error {
  404. startNewHook()
  405. defer hookEnded()
  406. data := make(map[string]any)
  407. totalDeletedFiles := 0
  408. totalDeletedSize := int64(0)
  409. for _, result := range c.results {
  410. totalDeletedFiles += result.DeletedFiles
  411. totalDeletedSize += result.DeletedSize
  412. }
  413. data["username"] = c.conn.User.Username
  414. data["start_time"] = c.StartTime
  415. data["elapsed"] = elapsed.Milliseconds()
  416. if errCheck == nil {
  417. data["status"] = 1
  418. } else {
  419. data["status"] = 0
  420. }
  421. data["total_deleted_files"] = totalDeletedFiles
  422. data["total_deleted_size"] = totalDeletedSize
  423. data["details"] = c.results
  424. jsonData, _ := json.Marshal(data)
  425. startTime := time.Now()
  426. if strings.HasPrefix(Config.DataRetentionHook, "http") {
  427. var url *url.URL
  428. url, err := url.Parse(Config.DataRetentionHook)
  429. if err != nil {
  430. c.conn.Log(logger.LevelError, "invalid data retention hook %q: %v", Config.DataRetentionHook, err)
  431. return err
  432. }
  433. respCode := 0
  434. resp, err := httpclient.RetryablePost(url.String(), "application/json", bytes.NewBuffer(jsonData))
  435. if err == nil {
  436. respCode = resp.StatusCode
  437. resp.Body.Close()
  438. if respCode != http.StatusOK {
  439. err = errUnexpectedHTTResponse
  440. }
  441. }
  442. c.conn.Log(logger.LevelDebug, "notified result to URL: %q, status code: %v, elapsed: %v err: %v",
  443. url.Redacted(), respCode, time.Since(startTime), err)
  444. return err
  445. }
  446. if !filepath.IsAbs(Config.DataRetentionHook) {
  447. err := fmt.Errorf("invalid data retention hook %q", Config.DataRetentionHook)
  448. c.conn.Log(logger.LevelError, "%v", err)
  449. return err
  450. }
  451. timeout, env, args := command.GetConfig(Config.DataRetentionHook, command.HookDataRetention)
  452. ctx, cancel := context.WithTimeout(context.Background(), timeout)
  453. defer cancel()
  454. cmd := exec.CommandContext(ctx, Config.DataRetentionHook, args...)
  455. cmd.Env = append(env,
  456. fmt.Sprintf("SFTPGO_DATA_RETENTION_RESULT=%s", string(jsonData)))
  457. err := cmd.Run()
  458. c.conn.Log(logger.LevelDebug, "notified result using command: %q, elapsed: %s err: %v",
  459. Config.DataRetentionHook, time.Since(startTime), err)
  460. return err
  461. }