share.go 9.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310
  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 dataprovider
  15. import (
  16. "encoding/json"
  17. "fmt"
  18. "net"
  19. "strings"
  20. "time"
  21. "github.com/alexedwards/argon2id"
  22. passwordvalidator "github.com/wagslane/go-password-validator"
  23. "golang.org/x/crypto/bcrypt"
  24. "github.com/drakkan/sftpgo/v2/internal/logger"
  25. "github.com/drakkan/sftpgo/v2/internal/util"
  26. )
  27. // ShareScope defines the supported share scopes
  28. type ShareScope int
  29. // Supported share scopes
  30. const (
  31. ShareScopeRead ShareScope = iota + 1
  32. ShareScopeWrite
  33. ShareScopeReadWrite
  34. )
  35. const (
  36. redactedPassword = "[**redacted**]"
  37. )
  38. // Share defines files and or directories shared with external users
  39. type Share struct {
  40. // Database unique identifier
  41. ID int64 `json:"-"`
  42. // Unique ID used to access this object
  43. ShareID string `json:"id"`
  44. Name string `json:"name"`
  45. Description string `json:"description,omitempty"`
  46. Scope ShareScope `json:"scope"`
  47. // Paths to files or directories, for ShareScopeWrite it must be exactly one directory
  48. Paths []string `json:"paths"`
  49. // Username who shared this object
  50. Username string `json:"username"`
  51. CreatedAt int64 `json:"created_at"`
  52. UpdatedAt int64 `json:"updated_at"`
  53. // 0 means never used
  54. LastUseAt int64 `json:"last_use_at,omitempty"`
  55. // ExpiresAt expiration date/time as unix timestamp in milliseconds, 0 means no expiration
  56. ExpiresAt int64 `json:"expires_at,omitempty"`
  57. // Optional password to protect the share
  58. Password string `json:"password"`
  59. // Limit the available access tokens, 0 means no limit
  60. MaxTokens int `json:"max_tokens,omitempty"`
  61. // Used tokens
  62. UsedTokens int `json:"used_tokens,omitempty"`
  63. // Limit the share availability to these IPs/CIDR networks
  64. AllowFrom []string `json:"allow_from,omitempty"`
  65. // set for restores, we don't have to validate the expiration date
  66. // otherwise we fail to restore existing shares and we have to insert
  67. // all the previous values with no modifications
  68. IsRestore bool `json:"-"`
  69. }
  70. // IsExpired returns true if the share is expired
  71. func (s *Share) IsExpired() bool {
  72. if s.ExpiresAt > 0 {
  73. return s.ExpiresAt < util.GetTimeAsMsSinceEpoch(time.Now())
  74. }
  75. return false
  76. }
  77. // GetAllowedFromAsString returns the allowed IP as comma separated string
  78. func (s *Share) GetAllowedFromAsString() string {
  79. return strings.Join(s.AllowFrom, ",")
  80. }
  81. // IsPasswordHashed returns true if the password is hashed
  82. func (s *Share) IsPasswordHashed() bool {
  83. return util.IsStringPrefixInSlice(s.Password, hashPwdPrefixes)
  84. }
  85. func (s *Share) getACopy() Share {
  86. allowFrom := make([]string, len(s.AllowFrom))
  87. copy(allowFrom, s.AllowFrom)
  88. return Share{
  89. ID: s.ID,
  90. ShareID: s.ShareID,
  91. Name: s.Name,
  92. Description: s.Description,
  93. Scope: s.Scope,
  94. Paths: s.Paths,
  95. Username: s.Username,
  96. CreatedAt: s.CreatedAt,
  97. UpdatedAt: s.UpdatedAt,
  98. LastUseAt: s.LastUseAt,
  99. ExpiresAt: s.ExpiresAt,
  100. Password: s.Password,
  101. MaxTokens: s.MaxTokens,
  102. UsedTokens: s.UsedTokens,
  103. AllowFrom: allowFrom,
  104. }
  105. }
  106. // RenderAsJSON implements the renderer interface used within plugins
  107. func (s *Share) RenderAsJSON(reload bool) ([]byte, error) {
  108. if reload {
  109. share, err := provider.shareExists(s.ShareID, s.Username)
  110. if err != nil {
  111. providerLog(logger.LevelError, "unable to reload share before rendering as json: %v", err)
  112. return nil, err
  113. }
  114. share.HideConfidentialData()
  115. return json.Marshal(share)
  116. }
  117. s.HideConfidentialData()
  118. return json.Marshal(s)
  119. }
  120. // HideConfidentialData hides share confidential data
  121. func (s *Share) HideConfidentialData() {
  122. if s.Password != "" {
  123. s.Password = redactedPassword
  124. }
  125. }
  126. // HasRedactedPassword returns true if this share has a redacted password
  127. func (s *Share) HasRedactedPassword() bool {
  128. return s.Password == redactedPassword
  129. }
  130. func (s *Share) hashPassword() error {
  131. if s.Password != "" && !util.IsStringPrefixInSlice(s.Password, internalHashPwdPrefixes) {
  132. user, err := UserExists(s.Username, "")
  133. if err != nil {
  134. return util.NewGenericError(fmt.Sprintf("unable to validate user: %v", err))
  135. }
  136. if minEntropy := user.getMinPasswordEntropy(); minEntropy > 0 {
  137. if err := passwordvalidator.Validate(s.Password, minEntropy); err != nil {
  138. return util.NewI18nError(util.NewValidationError(err.Error()), util.I18nErrorPasswordComplexity)
  139. }
  140. }
  141. if config.PasswordHashing.Algo == HashingAlgoBcrypt {
  142. hashed, err := bcrypt.GenerateFromPassword([]byte(s.Password), config.PasswordHashing.BcryptOptions.Cost)
  143. if err != nil {
  144. return err
  145. }
  146. s.Password = util.BytesToString(hashed)
  147. } else {
  148. hashed, err := argon2id.CreateHash(s.Password, argon2Params)
  149. if err != nil {
  150. return err
  151. }
  152. s.Password = hashed
  153. }
  154. }
  155. return nil
  156. }
  157. func (s *Share) validatePaths() error {
  158. var paths []string
  159. for _, p := range s.Paths {
  160. if strings.TrimSpace(p) != "" {
  161. paths = append(paths, p)
  162. }
  163. }
  164. s.Paths = paths
  165. if len(s.Paths) == 0 {
  166. return util.NewI18nError(util.NewValidationError("at least a shared path is required"), util.I18nErrorSharePathRequired)
  167. }
  168. for idx := range s.Paths {
  169. s.Paths[idx] = util.CleanPath(s.Paths[idx])
  170. }
  171. s.Paths = util.RemoveDuplicates(s.Paths, false)
  172. if s.Scope >= ShareScopeWrite && len(s.Paths) != 1 {
  173. return util.NewI18nError(util.NewValidationError("the write share scope requires exactly one path"), util.I18nErrorShareWriteScope)
  174. }
  175. // check nested paths
  176. if len(s.Paths) > 1 {
  177. for idx := range s.Paths {
  178. for innerIdx := range s.Paths {
  179. if idx == innerIdx {
  180. continue
  181. }
  182. if s.Paths[idx] == "/" || s.Paths[innerIdx] == "/" || util.IsDirOverlapped(s.Paths[idx], s.Paths[innerIdx], true, "/") {
  183. return util.NewI18nError(util.NewGenericError("shared paths cannot be nested"), util.I18nErrorShareNestedPaths)
  184. }
  185. }
  186. }
  187. }
  188. return nil
  189. }
  190. func (s *Share) validate() error {
  191. if s.ShareID == "" {
  192. return util.NewValidationError("share_id is mandatory")
  193. }
  194. if s.Name == "" {
  195. return util.NewI18nError(util.NewValidationError("name is mandatory"), util.I18nErrorNameRequired)
  196. }
  197. if s.Scope < ShareScopeRead || s.Scope > ShareScopeReadWrite {
  198. return util.NewI18nError(util.NewValidationError(fmt.Sprintf("invalid scope: %v", s.Scope)), util.I18nErrorShareScope)
  199. }
  200. if err := s.validatePaths(); err != nil {
  201. return err
  202. }
  203. if s.ExpiresAt > 0 {
  204. if !s.IsRestore && s.ExpiresAt < util.GetTimeAsMsSinceEpoch(time.Now()) {
  205. return util.NewI18nError(util.NewValidationError("expiration must be in the future"), util.I18nErrorShareExpirationPast)
  206. }
  207. } else {
  208. s.ExpiresAt = 0
  209. }
  210. if s.MaxTokens < 0 {
  211. return util.NewI18nError(util.NewValidationError("invalid max tokens"), util.I18nErrorShareMaxTokens)
  212. }
  213. if s.Username == "" {
  214. return util.NewI18nError(util.NewValidationError("username is mandatory"), util.I18nErrorUsernameRequired)
  215. }
  216. if s.HasRedactedPassword() {
  217. return util.NewValidationError("cannot save a share with a redacted password")
  218. }
  219. if err := s.hashPassword(); err != nil {
  220. return err
  221. }
  222. s.AllowFrom = util.RemoveDuplicates(s.AllowFrom, false)
  223. for _, IPMask := range s.AllowFrom {
  224. _, _, err := net.ParseCIDR(IPMask)
  225. if err != nil {
  226. return util.NewI18nError(
  227. util.NewValidationError(fmt.Sprintf("could not parse allow from entry %q : %v", IPMask, err)),
  228. util.I18nErrorInvalidIPMask,
  229. )
  230. }
  231. }
  232. return nil
  233. }
  234. // CheckCredentials verifies the share credentials if a password if set
  235. func (s *Share) CheckCredentials(password string) (bool, error) {
  236. if s.Password == "" {
  237. return true, nil
  238. }
  239. if password == "" {
  240. return false, ErrInvalidCredentials
  241. }
  242. if strings.HasPrefix(s.Password, bcryptPwdPrefix) {
  243. if err := bcrypt.CompareHashAndPassword([]byte(s.Password), []byte(password)); err != nil {
  244. return false, ErrInvalidCredentials
  245. }
  246. return true, nil
  247. }
  248. match, err := argon2id.ComparePasswordAndHash(password, s.Password)
  249. if !match || err != nil {
  250. return false, ErrInvalidCredentials
  251. }
  252. return match, err
  253. }
  254. // GetRelativePath returns the specified absolute path as relative to the share base path
  255. func (s *Share) GetRelativePath(name string) string {
  256. if len(s.Paths) == 0 {
  257. return ""
  258. }
  259. return util.CleanPath(strings.TrimPrefix(name, s.Paths[0]))
  260. }
  261. // IsUsable checks if the share is usable from the specified IP
  262. func (s *Share) IsUsable(ip string) (bool, error) {
  263. if s.MaxTokens > 0 && s.UsedTokens >= s.MaxTokens {
  264. return false, util.NewI18nError(util.NewRecordNotFoundError("max share usage exceeded"), util.I18nErrorShareUsage)
  265. }
  266. if s.ExpiresAt > 0 {
  267. if s.ExpiresAt < util.GetTimeAsMsSinceEpoch(time.Now()) {
  268. return false, util.NewI18nError(util.NewRecordNotFoundError("share expired"), util.I18nErrorShareExpired)
  269. }
  270. }
  271. if len(s.AllowFrom) == 0 {
  272. return true, nil
  273. }
  274. parsedIP := net.ParseIP(ip)
  275. if parsedIP == nil {
  276. return false, util.NewI18nError(ErrLoginNotAllowedFromIP, util.I18nErrorLoginFromIPDenied)
  277. }
  278. for _, ipMask := range s.AllowFrom {
  279. _, network, err := net.ParseCIDR(ipMask)
  280. if err != nil {
  281. continue
  282. }
  283. if network.Contains(parsedIP) {
  284. return true, nil
  285. }
  286. }
  287. return false, util.NewI18nError(ErrLoginNotAllowedFromIP, util.I18nErrorLoginFromIPDenied)
  288. }