dataprovider.go 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642
  1. // Package dataprovider provides data access.
  2. // It abstract different data providers and exposes a common API.
  3. // Currently the supported data providers are: PostreSQL (9+), MySQL (4.1+) and SQLite 3.x
  4. package dataprovider
  5. import (
  6. "bytes"
  7. "crypto/sha1"
  8. "crypto/sha256"
  9. "crypto/sha512"
  10. "crypto/subtle"
  11. "encoding/base64"
  12. "encoding/json"
  13. "errors"
  14. "fmt"
  15. "hash"
  16. "net/http"
  17. "net/url"
  18. "os"
  19. "os/exec"
  20. "path"
  21. "path/filepath"
  22. "strconv"
  23. "strings"
  24. "time"
  25. "github.com/alexedwards/argon2id"
  26. "golang.org/x/crypto/bcrypt"
  27. "golang.org/x/crypto/pbkdf2"
  28. "golang.org/x/crypto/ssh"
  29. "github.com/drakkan/sftpgo/logger"
  30. "github.com/drakkan/sftpgo/metrics"
  31. "github.com/drakkan/sftpgo/utils"
  32. sha512crypt "github.com/nathanaelle/password"
  33. )
  34. const (
  35. // SQLiteDataProviderName name for SQLite database provider
  36. SQLiteDataProviderName = "sqlite"
  37. // PGSQLDataProviderName name for PostgreSQL database provider
  38. PGSQLDataProviderName = "postgresql"
  39. // MySQLDataProviderName name for MySQL database provider
  40. MySQLDataProviderName = "mysql"
  41. // BoltDataProviderName name for bbolt key/value store provider
  42. BoltDataProviderName = "bolt"
  43. // MemoryDataProviderName name for memory provider
  44. MemoryDataProviderName = "memory"
  45. argonPwdPrefix = "$argon2id$"
  46. bcryptPwdPrefix = "$2a$"
  47. pbkdf2SHA1Prefix = "$pbkdf2-sha1$"
  48. pbkdf2SHA256Prefix = "$pbkdf2-sha256$"
  49. pbkdf2SHA512Prefix = "$pbkdf2-sha512$"
  50. sha512cryptPwdPrefix = "$6$"
  51. manageUsersDisabledError = "please set manage_users to 1 in your configuration to enable this method"
  52. trackQuotaDisabledError = "please enable track_quota in your configuration to use this method"
  53. operationAdd = "add"
  54. operationUpdate = "update"
  55. operationDelete = "delete"
  56. )
  57. var (
  58. // SupportedProviders data provider configured in the sftpgo.conf file must match of these strings
  59. SupportedProviders = []string{SQLiteDataProviderName, PGSQLDataProviderName, MySQLDataProviderName,
  60. BoltDataProviderName, MemoryDataProviderName}
  61. // ValidPerms list that contains all the valid permissions for an user
  62. ValidPerms = []string{PermAny, PermListItems, PermDownload, PermUpload, PermOverwrite, PermRename, PermDelete,
  63. PermCreateDirs, PermCreateSymlinks, PermChmod, PermChown, PermChtimes}
  64. config Config
  65. provider Provider
  66. sqlPlaceholders []string
  67. hashPwdPrefixes = []string{argonPwdPrefix, bcryptPwdPrefix, pbkdf2SHA1Prefix, pbkdf2SHA256Prefix,
  68. pbkdf2SHA512Prefix, sha512cryptPwdPrefix}
  69. pbkdfPwdPrefixes = []string{pbkdf2SHA1Prefix, pbkdf2SHA256Prefix, pbkdf2SHA512Prefix}
  70. logSender = "dataProvider"
  71. availabilityTicker *time.Ticker
  72. availabilityTickerDone chan bool
  73. )
  74. // Actions to execute on user create, update, delete.
  75. // An external command can be executed and/or an HTTP notification can be fired
  76. type Actions struct {
  77. // Valid values are add, update, delete. Empty slice to disable
  78. ExecuteOn []string `json:"execute_on" mapstructure:"execute_on"`
  79. // Absolute path to the command to execute, empty to disable
  80. Command string `json:"command" mapstructure:"command"`
  81. // The URL to notify using an HTTP POST.
  82. // The action is added to the query string. For example <url>?action=update.
  83. // The user is sent serialized as json inside the POST body.
  84. // Empty to disable
  85. HTTPNotificationURL string `json:"http_notification_url" mapstructure:"http_notification_url"`
  86. }
  87. // Config provider configuration
  88. type Config struct {
  89. // Driver name, must be one of the SupportedProviders
  90. Driver string `json:"driver" mapstructure:"driver"`
  91. // Database name
  92. Name string `json:"name" mapstructure:"name"`
  93. // Database host
  94. Host string `json:"host" mapstructure:"host"`
  95. // Database port
  96. Port int `json:"port" mapstructure:"port"`
  97. // Database username
  98. Username string `json:"username" mapstructure:"username"`
  99. // Database password
  100. Password string `json:"password" mapstructure:"password"`
  101. // Used for drivers mysql and postgresql.
  102. // 0 disable SSL/TLS connections.
  103. // 1 require ssl.
  104. // 2 set ssl mode to verify-ca for driver postgresql and skip-verify for driver mysql.
  105. // 3 set ssl mode to verify-full for driver postgresql and preferred for driver mysql.
  106. SSLMode int `json:"sslmode" mapstructure:"sslmode"`
  107. // Custom database connection string.
  108. // If not empty this connection string will be used instead of build one using the previous parameters
  109. ConnectionString string `json:"connection_string" mapstructure:"connection_string"`
  110. // Database table for SFTP users
  111. UsersTable string `json:"users_table" mapstructure:"users_table"`
  112. // Set to 0 to disable users management, 1 to enable
  113. ManageUsers int `json:"manage_users" mapstructure:"manage_users"`
  114. // Set the preferred way to track users quota between the following choices:
  115. // 0, disable quota tracking. REST API to scan user dir and update quota will do nothing
  116. // 1, quota is updated each time a user upload or delete a file even if the user has no quota restrictions
  117. // 2, quota is updated each time a user upload or delete a file but only for users with quota restrictions.
  118. // With this configuration the "quota scan" REST API can still be used to periodically update space usage
  119. // for users without quota restrictions
  120. TrackQuota int `json:"track_quota" mapstructure:"track_quota"`
  121. // Sets the maximum number of open connections for mysql and postgresql driver.
  122. // Default 0 (unlimited)
  123. PoolSize int `json:"pool_size" mapstructure:"pool_size"`
  124. // Users' default base directory.
  125. // If no home dir is defined while adding a new user, and this value is
  126. // a valid absolute path, then the user home dir will be automatically
  127. // defined as the path obtained joining the base dir and the username
  128. UsersBaseDir string `json:"users_base_dir" mapstructure:"users_base_dir"`
  129. // Actions to execute on user add, update, delete.
  130. // Update action will not be fired for internal updates such as the last login or the user quota fields.
  131. Actions Actions `json:"actions" mapstructure:"actions"`
  132. }
  133. // ValidationError raised if input data is not valid
  134. type ValidationError struct {
  135. err string
  136. }
  137. // Validation error details
  138. func (e *ValidationError) Error() string {
  139. return fmt.Sprintf("Validation error: %s", e.err)
  140. }
  141. // MethodDisabledError raised if a method is disabled in config file.
  142. // For example, if user management is disabled, this error is raised
  143. // every time an user operation is done using the REST API
  144. type MethodDisabledError struct {
  145. err string
  146. }
  147. // Method disabled error details
  148. func (e *MethodDisabledError) Error() string {
  149. return fmt.Sprintf("Method disabled error: %s", e.err)
  150. }
  151. // RecordNotFoundError raised if a requested user is not found
  152. type RecordNotFoundError struct {
  153. err string
  154. }
  155. func (e *RecordNotFoundError) Error() string {
  156. return fmt.Sprintf("Not found: %s", e.err)
  157. }
  158. // GetProvider returns the configured provider
  159. func GetProvider() Provider {
  160. return provider
  161. }
  162. // GetQuotaTracking returns the configured mode for user's quota tracking
  163. func GetQuotaTracking() int {
  164. return config.TrackQuota
  165. }
  166. // Provider interface that data providers must implement.
  167. type Provider interface {
  168. validateUserAndPass(username string, password string) (User, error)
  169. validateUserAndPubKey(username string, pubKey string) (User, string, error)
  170. updateQuota(username string, filesAdd int, sizeAdd int64, reset bool) error
  171. getUsedQuota(username string) (int, int64, error)
  172. userExists(username string) (User, error)
  173. addUser(user User) error
  174. updateUser(user User) error
  175. deleteUser(user User) error
  176. getUsers(limit int, offset int, order string, username string) ([]User, error)
  177. dumpUsers() ([]User, error)
  178. getUserByID(ID int64) (User, error)
  179. updateLastLogin(username string) error
  180. checkAvailability() error
  181. close() error
  182. }
  183. func init() {
  184. availabilityTicker = time.NewTicker(30 * time.Second)
  185. }
  186. // Initialize the data provider.
  187. // An error is returned if the configured driver is invalid or if the data provider cannot be initialized
  188. func Initialize(cnf Config, basePath string) error {
  189. var err error
  190. config = cnf
  191. sqlPlaceholders = getSQLPlaceholders()
  192. if config.Driver == SQLiteDataProviderName {
  193. err = initializeSQLiteProvider(basePath)
  194. } else if config.Driver == PGSQLDataProviderName {
  195. err = initializePGSQLProvider()
  196. } else if config.Driver == MySQLDataProviderName {
  197. err = initializeMySQLProvider()
  198. } else if config.Driver == BoltDataProviderName {
  199. err = initializeBoltProvider(basePath)
  200. } else if config.Driver == MemoryDataProviderName {
  201. err = initializeMemoryProvider()
  202. } else {
  203. err = fmt.Errorf("unsupported data provider: %v", config.Driver)
  204. }
  205. if err == nil {
  206. startAvailabilityTimer()
  207. }
  208. return err
  209. }
  210. // CheckUserAndPass retrieves the SFTP user with the given username and password if a match is found or an error
  211. func CheckUserAndPass(p Provider, username string, password string) (User, error) {
  212. return p.validateUserAndPass(username, password)
  213. }
  214. // CheckUserAndPubKey retrieves the SFTP user with the given username and public key if a match is found or an error
  215. func CheckUserAndPubKey(p Provider, username string, pubKey string) (User, string, error) {
  216. return p.validateUserAndPubKey(username, pubKey)
  217. }
  218. // UpdateLastLogin updates the last login fields for the given SFTP user
  219. func UpdateLastLogin(p Provider, user User) error {
  220. if config.ManageUsers == 0 {
  221. return &MethodDisabledError{err: manageUsersDisabledError}
  222. }
  223. return p.updateLastLogin(user.Username)
  224. }
  225. // UpdateUserQuota updates the quota for the given SFTP user adding filesAdd and sizeAdd.
  226. // If reset is true filesAdd and sizeAdd indicates the total files and the total size instead of the difference.
  227. func UpdateUserQuota(p Provider, user User, filesAdd int, sizeAdd int64, reset bool) error {
  228. if config.TrackQuota == 0 {
  229. return &MethodDisabledError{err: trackQuotaDisabledError}
  230. } else if config.TrackQuota == 2 && !reset && !user.HasQuotaRestrictions() {
  231. return nil
  232. }
  233. if config.ManageUsers == 0 {
  234. return &MethodDisabledError{err: manageUsersDisabledError}
  235. }
  236. return p.updateQuota(user.Username, filesAdd, sizeAdd, reset)
  237. }
  238. // GetUsedQuota returns the used quota for the given SFTP user.
  239. // TrackQuota must be >=1 to enable this method
  240. func GetUsedQuota(p Provider, username string) (int, int64, error) {
  241. if config.TrackQuota == 0 {
  242. return 0, 0, &MethodDisabledError{err: trackQuotaDisabledError}
  243. }
  244. return p.getUsedQuota(username)
  245. }
  246. // UserExists checks if the given SFTP username exists, returns an error if no match is found
  247. func UserExists(p Provider, username string) (User, error) {
  248. return p.userExists(username)
  249. }
  250. // AddUser adds a new SFTP user.
  251. // ManageUsers configuration must be set to 1 to enable this method
  252. func AddUser(p Provider, user User) error {
  253. if config.ManageUsers == 0 {
  254. return &MethodDisabledError{err: manageUsersDisabledError}
  255. }
  256. err := p.addUser(user)
  257. if err == nil {
  258. go executeAction(operationAdd, user)
  259. }
  260. return err
  261. }
  262. // UpdateUser updates an existing SFTP user.
  263. // ManageUsers configuration must be set to 1 to enable this method
  264. func UpdateUser(p Provider, user User) error {
  265. if config.ManageUsers == 0 {
  266. return &MethodDisabledError{err: manageUsersDisabledError}
  267. }
  268. err := p.updateUser(user)
  269. if err == nil {
  270. go executeAction(operationUpdate, user)
  271. }
  272. return err
  273. }
  274. // DeleteUser deletes an existing SFTP user.
  275. // ManageUsers configuration must be set to 1 to enable this method
  276. func DeleteUser(p Provider, user User) error {
  277. if config.ManageUsers == 0 {
  278. return &MethodDisabledError{err: manageUsersDisabledError}
  279. }
  280. err := p.deleteUser(user)
  281. if err == nil {
  282. go executeAction(operationDelete, user)
  283. }
  284. return err
  285. }
  286. // DumpUsers returns an array with all users including their hashed password
  287. func DumpUsers(p Provider) ([]User, error) {
  288. return p.dumpUsers()
  289. }
  290. // GetUsers returns an array of users respecting limit and offset and filtered by username exact match if not empty
  291. func GetUsers(p Provider, limit int, offset int, order string, username string) ([]User, error) {
  292. return p.getUsers(limit, offset, order, username)
  293. }
  294. // GetUserByID returns the user with the given database ID if a match is found or an error
  295. func GetUserByID(p Provider, ID int64) (User, error) {
  296. return p.getUserByID(ID)
  297. }
  298. // GetProviderStatus returns an error if the provider is not available
  299. func GetProviderStatus(p Provider) error {
  300. return p.checkAvailability()
  301. }
  302. // Close releases all provider resources.
  303. // This method is used in test cases.
  304. // Closing an uninitialized provider is not supported
  305. func Close(p Provider) error {
  306. availabilityTicker.Stop()
  307. availabilityTickerDone <- true
  308. return p.close()
  309. }
  310. func buildUserHomeDir(user *User) {
  311. if len(user.HomeDir) == 0 {
  312. if len(config.UsersBaseDir) > 0 {
  313. user.HomeDir = filepath.Join(config.UsersBaseDir, user.Username)
  314. }
  315. }
  316. }
  317. func validatePermissions(user *User) error {
  318. permissions := make(map[string][]string)
  319. if _, ok := user.Permissions["/"]; !ok {
  320. return &ValidationError{err: fmt.Sprintf("Permissions for the root dir \"/\" must be set")}
  321. }
  322. for dir, perms := range user.Permissions {
  323. if len(perms) == 0 {
  324. return &ValidationError{err: fmt.Sprintf("No permissions granted for the directory: %#v", dir)}
  325. }
  326. for _, p := range perms {
  327. if !utils.IsStringInSlice(p, ValidPerms) {
  328. return &ValidationError{err: fmt.Sprintf("Invalid permission: %#v", p)}
  329. }
  330. }
  331. cleanedDir := filepath.ToSlash(path.Clean(dir))
  332. if cleanedDir != "/" {
  333. cleanedDir = strings.TrimSuffix(cleanedDir, "/")
  334. }
  335. if !path.IsAbs(cleanedDir) {
  336. return &ValidationError{err: fmt.Sprintf("Cannot set permissions for non absolute path: %#v", dir)}
  337. }
  338. if utils.IsStringInSlice(PermAny, perms) {
  339. permissions[cleanedDir] = []string{PermAny}
  340. } else {
  341. permissions[cleanedDir] = perms
  342. }
  343. }
  344. user.Permissions = permissions
  345. return nil
  346. }
  347. func validateUser(user *User) error {
  348. buildUserHomeDir(user)
  349. if len(user.Username) == 0 || len(user.HomeDir) == 0 {
  350. return &ValidationError{err: "Mandatory parameters missing"}
  351. }
  352. if len(user.Password) == 0 && len(user.PublicKeys) == 0 {
  353. return &ValidationError{err: "Please set a password or at least a public_key"}
  354. }
  355. if len(user.Permissions) == 0 {
  356. return &ValidationError{err: "Please grant some permissions to this user"}
  357. }
  358. if !filepath.IsAbs(user.HomeDir) {
  359. return &ValidationError{err: fmt.Sprintf("home_dir must be an absolute path, actual value: %v", user.HomeDir)}
  360. }
  361. if err := validatePermissions(user); err != nil {
  362. return err
  363. }
  364. if user.Status < 0 || user.Status > 1 {
  365. return &ValidationError{err: fmt.Sprintf("invalid user status: %v", user.Status)}
  366. }
  367. if len(user.Password) > 0 && !utils.IsStringPrefixInSlice(user.Password, hashPwdPrefixes) {
  368. pwd, err := argon2id.CreateHash(user.Password, argon2id.DefaultParams)
  369. if err != nil {
  370. return err
  371. }
  372. user.Password = pwd
  373. }
  374. for i, k := range user.PublicKeys {
  375. _, _, _, _, err := ssh.ParseAuthorizedKey([]byte(k))
  376. if err != nil {
  377. return &ValidationError{err: fmt.Sprintf("Could not parse key nr. %d: %s", i, err)}
  378. }
  379. }
  380. return nil
  381. }
  382. func checkLoginConditions(user User) error {
  383. if user.Status < 1 {
  384. return fmt.Errorf("user %#v is disabled", user.Username)
  385. }
  386. if user.ExpirationDate > 0 && user.ExpirationDate < utils.GetTimeAsMsSinceEpoch(time.Now()) {
  387. return fmt.Errorf("user %#v is expired, expiration timestamp: %v current timestamp: %v", user.Username,
  388. user.ExpirationDate, utils.GetTimeAsMsSinceEpoch(time.Now()))
  389. }
  390. return nil
  391. }
  392. func checkUserAndPass(user User, password string) (User, error) {
  393. err := checkLoginConditions(user)
  394. if err != nil {
  395. return user, err
  396. }
  397. if len(user.Password) == 0 {
  398. return user, errors.New("Credentials cannot be null or empty")
  399. }
  400. var match bool
  401. if strings.HasPrefix(user.Password, argonPwdPrefix) {
  402. match, err = argon2id.ComparePasswordAndHash(password, user.Password)
  403. if err != nil {
  404. providerLog(logger.LevelWarn, "error comparing password with argon hash: %v", err)
  405. return user, err
  406. }
  407. } else if strings.HasPrefix(user.Password, bcryptPwdPrefix) {
  408. if err = bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(password)); err != nil {
  409. providerLog(logger.LevelWarn, "error comparing password with bcrypt hash: %v", err)
  410. return user, err
  411. }
  412. match = true
  413. } else if utils.IsStringPrefixInSlice(user.Password, pbkdfPwdPrefixes) {
  414. match, err = comparePbkdf2PasswordAndHash(password, user.Password)
  415. if err != nil {
  416. providerLog(logger.LevelWarn, "error comparing password with pbkdf2 sha256 hash: %v", err)
  417. return user, err
  418. }
  419. } else if strings.HasPrefix(user.Password, sha512cryptPwdPrefix) {
  420. crypter, ok := sha512crypt.SHA512.CrypterFound(user.Password)
  421. if !ok {
  422. err = errors.New("cannot found matching SHA512 crypter")
  423. providerLog(logger.LevelWarn, "error comparing password with SHA512 hash: %v", err)
  424. return user, err
  425. }
  426. if !crypter.Verify([]byte(password)) {
  427. err = errors.New("password does not match")
  428. providerLog(logger.LevelWarn, "error comparing password with SHA512 hash: %v", err)
  429. return user, err
  430. }
  431. match = true
  432. }
  433. if !match {
  434. err = errors.New("Invalid credentials")
  435. }
  436. return user, err
  437. }
  438. func checkUserAndPubKey(user User, pubKey string) (User, string, error) {
  439. err := checkLoginConditions(user)
  440. if err != nil {
  441. return user, "", err
  442. }
  443. if len(user.PublicKeys) == 0 {
  444. return user, "", errors.New("Invalid credentials")
  445. }
  446. for i, k := range user.PublicKeys {
  447. storedPubKey, comment, _, _, err := ssh.ParseAuthorizedKey([]byte(k))
  448. if err != nil {
  449. providerLog(logger.LevelWarn, "error parsing stored public key %d for user %v: %v", i, user.Username, err)
  450. return user, "", err
  451. }
  452. if string(storedPubKey.Marshal()) == pubKey {
  453. fp := ssh.FingerprintSHA256(storedPubKey)
  454. return user, fp + ":" + comment, nil
  455. }
  456. }
  457. return user, "", errors.New("Invalid credentials")
  458. }
  459. func comparePbkdf2PasswordAndHash(password, hashedPassword string) (bool, error) {
  460. vals := strings.Split(hashedPassword, "$")
  461. if len(vals) != 5 {
  462. return false, fmt.Errorf("pbkdf2: hash is not in the correct format")
  463. }
  464. var hashFunc func() hash.Hash
  465. var hashSize int
  466. if strings.HasPrefix(hashedPassword, pbkdf2SHA256Prefix) {
  467. hashSize = sha256.Size
  468. hashFunc = sha256.New
  469. } else if strings.HasPrefix(hashedPassword, pbkdf2SHA512Prefix) {
  470. hashSize = sha512.Size
  471. hashFunc = sha512.New
  472. } else if strings.HasPrefix(hashedPassword, pbkdf2SHA1Prefix) {
  473. hashSize = sha1.Size
  474. hashFunc = sha1.New
  475. } else {
  476. return false, fmt.Errorf("pbkdf2: invalid or unsupported hash format %v", vals[1])
  477. }
  478. iterations, err := strconv.Atoi(vals[2])
  479. if err != nil {
  480. return false, err
  481. }
  482. salt := vals[3]
  483. expected := vals[4]
  484. df := pbkdf2.Key([]byte(password), []byte(salt), iterations, hashSize, hashFunc)
  485. buf := make([]byte, base64.StdEncoding.EncodedLen(len(df)))
  486. base64.StdEncoding.Encode(buf, df)
  487. return subtle.ConstantTimeCompare(buf, []byte(expected)) == 1, nil
  488. }
  489. func getSSLMode() string {
  490. if config.Driver == PGSQLDataProviderName {
  491. if config.SSLMode == 0 {
  492. return "disable"
  493. } else if config.SSLMode == 1 {
  494. return "require"
  495. } else if config.SSLMode == 2 {
  496. return "verify-ca"
  497. } else if config.SSLMode == 3 {
  498. return "verify-full"
  499. }
  500. } else if config.Driver == MySQLDataProviderName {
  501. if config.SSLMode == 0 {
  502. return "false"
  503. } else if config.SSLMode == 1 {
  504. return "true"
  505. } else if config.SSLMode == 2 {
  506. return "skip-verify"
  507. } else if config.SSLMode == 3 {
  508. return "preferred"
  509. }
  510. }
  511. return ""
  512. }
  513. func startAvailabilityTimer() {
  514. availabilityTickerDone = make(chan bool)
  515. checkDataprovider()
  516. go func() {
  517. for {
  518. select {
  519. case <-availabilityTickerDone:
  520. return
  521. case <-availabilityTicker.C:
  522. checkDataprovider()
  523. }
  524. }
  525. }()
  526. }
  527. func checkDataprovider() {
  528. err := provider.checkAvailability()
  529. if err != nil {
  530. providerLog(logger.LevelWarn, "check availability error: %v", err)
  531. }
  532. metrics.UpdateDataProviderAvailability(err)
  533. }
  534. func providerLog(level logger.LogLevel, format string, v ...interface{}) {
  535. logger.Log(level, logSender, "", format, v...)
  536. }
  537. // executed in a goroutine
  538. func executeAction(operation string, user User) {
  539. if !utils.IsStringInSlice(operation, config.Actions.ExecuteOn) {
  540. return
  541. }
  542. if operation != operationDelete {
  543. var err error
  544. user, err = provider.userExists(user.Username)
  545. if err != nil {
  546. providerLog(logger.LevelWarn, "unable to get the user to notify operation %#v: %v", operation, err)
  547. return
  548. }
  549. }
  550. // hide the hashed password
  551. user.Password = ""
  552. if len(config.Actions.Command) > 0 && filepath.IsAbs(config.Actions.Command) {
  553. if _, err := os.Stat(config.Actions.Command); err == nil {
  554. commandArgs := []string{operation}
  555. commandArgs = append(commandArgs, user.getNotificationFieldsAsSlice()...)
  556. command := exec.Command(config.Actions.Command, commandArgs...)
  557. err = command.Start()
  558. providerLog(logger.LevelDebug, "start command %#v with arguments: %+v, error: %v",
  559. config.Actions.Command, commandArgs, err)
  560. if err == nil {
  561. // we are in a goroutine but we don't want to block here, this way we can send the
  562. // HTTP notification, if configured, without waiting the end of the command
  563. go command.Wait()
  564. }
  565. } else {
  566. providerLog(logger.LevelWarn, "Invalid action command %#v for operation %#v: %v", config.Actions.Command, operation, err)
  567. }
  568. }
  569. if len(config.Actions.HTTPNotificationURL) > 0 {
  570. var url *url.URL
  571. url, err := url.Parse(config.Actions.HTTPNotificationURL)
  572. if err != nil {
  573. providerLog(logger.LevelWarn, "Invalid http_notification_url %#v for operation %#v: %v", config.Actions.HTTPNotificationURL,
  574. operation, err)
  575. return
  576. }
  577. q := url.Query()
  578. q.Add("action", operation)
  579. url.RawQuery = q.Encode()
  580. userAsJSON, err := json.Marshal(user)
  581. if err != nil {
  582. return
  583. }
  584. startTime := time.Now()
  585. httpClient := &http.Client{
  586. Timeout: 15 * time.Second,
  587. }
  588. resp, err := httpClient.Post(url.String(), "application/json", bytes.NewBuffer(userAsJSON))
  589. respCode := 0
  590. if err == nil {
  591. respCode = resp.StatusCode
  592. resp.Body.Close()
  593. }
  594. providerLog(logger.LevelDebug, "notified operation %#v to URL: %v status code: %v, elapsed: %v err: %v",
  595. operation, url.String(), respCode, time.Since(startTime), err)
  596. }
  597. }