public_collection.go 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338
  1. package controller
  2. import (
  3. "context"
  4. "errors"
  5. "fmt"
  6. "github.com/ente-io/museum/ente"
  7. enteJWT "github.com/ente-io/museum/ente/jwt"
  8. emailCtrl "github.com/ente-io/museum/pkg/controller/email"
  9. "github.com/ente-io/museum/pkg/repo"
  10. "github.com/ente-io/museum/pkg/utils/auth"
  11. "github.com/ente-io/museum/pkg/utils/email"
  12. "github.com/ente-io/museum/pkg/utils/time"
  13. "github.com/ente-io/stacktrace"
  14. "github.com/gin-gonic/gin"
  15. "github.com/golang-jwt/jwt"
  16. "github.com/lithammer/shortuuid/v3"
  17. "github.com/sirupsen/logrus"
  18. )
  19. var AllowedReasons = map[string]string{
  20. "COPYRIGHT": "Copyright Infringement",
  21. "MALICIOUS_CONTENT": "Malicious Content",
  22. }
  23. const (
  24. AccessTokenLength = 8
  25. // AutoDisableAbuseThreshold indicates minimum number of abuse reports post which the access token is
  26. // automatically disabled
  27. AutoDisableAbuseThreshold = 3
  28. // DeviceLimitThreshold represents number of unique devices which can access a shared collection. (ip + user agent)
  29. // is treated as unique device
  30. DeviceLimitThreshold = 50
  31. DeviceLimitThresholdMultiplier = 10
  32. DeviceLimitWarningThreshold = 2000
  33. AbuseAlertSubject = "[Alert] Abuse report received against your album on ente"
  34. AbuseAlertTeamSubject = "Abuse report received"
  35. AbuseLimitExceededSubject = "[Alert] Too many abuse reports received against your album on ente"
  36. AbuseAlertTemplate = "report_alert.html"
  37. AbuseLimitExceededTemplate = "report_limit_exceeded_alert.html"
  38. )
  39. // PublicCollectionController controls share collection operations
  40. type PublicCollectionController struct {
  41. FileController *FileController
  42. EmailNotificationCtrl *emailCtrl.EmailNotificationController
  43. PublicCollectionRepo *repo.PublicCollectionRepository
  44. CollectionRepo *repo.CollectionRepository
  45. UserRepo *repo.UserRepository
  46. JwtSecret []byte
  47. }
  48. func (c *PublicCollectionController) CreateAccessToken(ctx context.Context, req ente.CreatePublicAccessTokenRequest) (ente.PublicURL, error) {
  49. accessToken := shortuuid.New()[0:AccessTokenLength]
  50. err := c.PublicCollectionRepo.Insert(ctx, req.CollectionID, accessToken, req.ValidTill, req.DeviceLimit, req.EnableCollect)
  51. if err != nil {
  52. if errors.Is(err, ente.ErrActiveLinkAlreadyExists) {
  53. collectionToPubUrlMap, err2 := c.PublicCollectionRepo.GetCollectionToActivePublicURLMap(ctx, []int64{req.CollectionID})
  54. if err2 != nil {
  55. return ente.PublicURL{}, stacktrace.Propagate(err2, "")
  56. }
  57. if publicUrls, ok := collectionToPubUrlMap[req.CollectionID]; ok {
  58. if len(publicUrls) > 0 {
  59. return publicUrls[0], nil
  60. }
  61. }
  62. // ideally we should never reach here
  63. return ente.PublicURL{}, stacktrace.NewError("Unexpected state")
  64. } else {
  65. return ente.PublicURL{}, stacktrace.Propagate(err, "")
  66. }
  67. }
  68. response := ente.PublicURL{
  69. URL: c.PublicCollectionRepo.GetAlbumUrl(accessToken),
  70. ValidTill: req.ValidTill,
  71. DeviceLimit: req.DeviceLimit,
  72. EnableDownload: true,
  73. EnableCollect: req.EnableCollect,
  74. PasswordEnabled: false,
  75. }
  76. return response, nil
  77. }
  78. func (c *PublicCollectionController) CreateFile(ctx *gin.Context, file ente.File, app ente.App) (ente.File, error) {
  79. collection, err := c.GetPublicCollection(ctx, true)
  80. if err != nil {
  81. return ente.File{}, stacktrace.Propagate(err, "")
  82. }
  83. collectionOwnerID := collection.Owner.ID
  84. // Do not let any update happen via public Url
  85. file.ID = 0
  86. file.OwnerID = collectionOwnerID
  87. file.UpdationTime = time.Microseconds()
  88. file.IsDeleted = false
  89. createdFile, err := c.FileController.Create(ctx, collectionOwnerID, file, ctx.Request.UserAgent(), app)
  90. if err != nil {
  91. return ente.File{}, stacktrace.Propagate(err, "")
  92. }
  93. // Note: Stop sending email notification for public collection till
  94. // we add in-app setting to enable/disable email notifications
  95. //go c.EmailNotificationCtrl.OnFilesCollected(file.OwnerID)
  96. return createdFile, nil
  97. }
  98. // Disable all public accessTokens generated for the given cID till date.
  99. func (c *PublicCollectionController) Disable(ctx context.Context, cID int64) error {
  100. err := c.PublicCollectionRepo.DisableSharing(ctx, cID)
  101. return stacktrace.Propagate(err, "")
  102. }
  103. func (c *PublicCollectionController) UpdateSharedUrl(ctx context.Context, req ente.UpdatePublicAccessTokenRequest) (ente.PublicURL, error) {
  104. publicCollectionToken, err := c.PublicCollectionRepo.GetActivePublicCollectionToken(ctx, req.CollectionID)
  105. if err != nil {
  106. return ente.PublicURL{}, err
  107. }
  108. if req.ValidTill != nil {
  109. publicCollectionToken.ValidTill = *req.ValidTill
  110. }
  111. if req.DeviceLimit != nil {
  112. publicCollectionToken.DeviceLimit = *req.DeviceLimit
  113. }
  114. if req.PassHash != nil && req.Nonce != nil && req.OpsLimit != nil && req.MemLimit != nil {
  115. publicCollectionToken.PassHash = req.PassHash
  116. publicCollectionToken.Nonce = req.Nonce
  117. publicCollectionToken.OpsLimit = req.OpsLimit
  118. publicCollectionToken.MemLimit = req.MemLimit
  119. } else if req.DisablePassword != nil && *req.DisablePassword {
  120. publicCollectionToken.PassHash = nil
  121. publicCollectionToken.Nonce = nil
  122. publicCollectionToken.OpsLimit = nil
  123. publicCollectionToken.MemLimit = nil
  124. }
  125. if req.EnableDownload != nil {
  126. publicCollectionToken.EnableDownload = *req.EnableDownload
  127. }
  128. if req.EnableCollect != nil {
  129. publicCollectionToken.EnableCollect = *req.EnableCollect
  130. }
  131. err = c.PublicCollectionRepo.UpdatePublicCollectionToken(ctx, publicCollectionToken)
  132. if err != nil {
  133. return ente.PublicURL{}, stacktrace.Propagate(err, "")
  134. }
  135. return ente.PublicURL{
  136. URL: c.PublicCollectionRepo.GetAlbumUrl(publicCollectionToken.Token),
  137. DeviceLimit: publicCollectionToken.DeviceLimit,
  138. ValidTill: publicCollectionToken.ValidTill,
  139. EnableDownload: publicCollectionToken.EnableDownload,
  140. EnableCollect: publicCollectionToken.EnableCollect,
  141. PasswordEnabled: publicCollectionToken.PassHash != nil && *publicCollectionToken.PassHash != "",
  142. Nonce: publicCollectionToken.Nonce,
  143. MemLimit: publicCollectionToken.MemLimit,
  144. OpsLimit: publicCollectionToken.OpsLimit,
  145. }, nil
  146. }
  147. // VerifyPassword verifies if the user has provided correct pw hash. If yes, it returns a signed jwt token which can be
  148. // used by the client to pass in other requests for public collection.
  149. // Having a separate endpoint for password validation allows us to easily rate-limit the attempts for brute-force
  150. // attack for guessing password.
  151. func (c *PublicCollectionController) VerifyPassword(ctx *gin.Context, req ente.VerifyPasswordRequest) (*ente.VerifyPasswordResponse, error) {
  152. accessContext := auth.MustGetPublicAccessContext(ctx)
  153. publicCollectionToken, err := c.PublicCollectionRepo.GetActivePublicCollectionToken(ctx, accessContext.CollectionID)
  154. if err != nil {
  155. return nil, stacktrace.Propagate(err, "failed to get public collection info")
  156. }
  157. if publicCollectionToken.PassHash == nil || *publicCollectionToken.PassHash == "" {
  158. return nil, stacktrace.Propagate(ente.ErrBadRequest, "password is not configured for the link")
  159. }
  160. if req.PassHash != *publicCollectionToken.PassHash {
  161. return nil, stacktrace.Propagate(ente.ErrInvalidPassword, "incorrect password for link")
  162. }
  163. token := jwt.NewWithClaims(jwt.SigningMethodHS256, &enteJWT.PublicAlbumPasswordClaim{
  164. PassHash: req.PassHash,
  165. ExpiryTime: time.NDaysFromNow(365),
  166. })
  167. // Sign and get the complete encoded token as a string using the secret
  168. tokenString, err := token.SignedString(c.JwtSecret)
  169. if err != nil {
  170. return nil, stacktrace.Propagate(err, "")
  171. }
  172. return &ente.VerifyPasswordResponse{
  173. JWTToken: tokenString,
  174. }, nil
  175. }
  176. func (c *PublicCollectionController) ValidateJWTToken(ctx *gin.Context, jwtToken string, passwordHash string) error {
  177. token, err := jwt.ParseWithClaims(jwtToken, &enteJWT.PublicAlbumPasswordClaim{}, func(token *jwt.Token) (interface{}, error) {
  178. if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
  179. return stacktrace.Propagate(fmt.Errorf("unexpected signing method: %v", token.Header["alg"]), ""), nil
  180. }
  181. return c.JwtSecret, nil
  182. })
  183. if err != nil {
  184. return stacktrace.Propagate(err, "JWT parsed failed")
  185. }
  186. claims, ok := token.Claims.(*enteJWT.PublicAlbumPasswordClaim)
  187. if !ok {
  188. return stacktrace.Propagate(errors.New("no claim in jwt token"), "")
  189. }
  190. if token.Valid && claims.PassHash == passwordHash {
  191. return nil
  192. }
  193. return ente.ErrInvalidPassword
  194. }
  195. // ReportAbuse captures abuse report for a publicly shared collection.
  196. // It will also disable the accessToken for the collection if total abuse reports for the said collection
  197. // reaches AutoDisableAbuseThreshold
  198. func (c *PublicCollectionController) ReportAbuse(ctx *gin.Context, req ente.AbuseReportRequest) error {
  199. accessContext := auth.MustGetPublicAccessContext(ctx)
  200. readableReason, found := AllowedReasons[req.Reason]
  201. if !found {
  202. return stacktrace.Propagate(ente.ErrBadRequest, fmt.Sprintf("unexpected reason %s", req.Reason))
  203. }
  204. logrus.WithField("collectionID", accessContext.CollectionID).Error("CRITICAL: received abuse report")
  205. err := c.PublicCollectionRepo.RecordAbuseReport(ctx, accessContext, req.URL, req.Reason, req.Details)
  206. if err != nil {
  207. return stacktrace.Propagate(err, "")
  208. }
  209. count, err := c.PublicCollectionRepo.GetAbuseReportCount(ctx, accessContext)
  210. if err != nil {
  211. return stacktrace.Propagate(err, "")
  212. }
  213. c.onAbuseReportReceived(accessContext.CollectionID, req, readableReason, count)
  214. if count >= AutoDisableAbuseThreshold {
  215. logrus.WithFields(logrus.Fields{
  216. "collectionID": accessContext.CollectionID,
  217. }).Warn("disabling accessTokens for shared collection due to multiple abuse reports")
  218. return stacktrace.Propagate(c.Disable(ctx, accessContext.CollectionID), "")
  219. }
  220. return nil
  221. }
  222. func (c *PublicCollectionController) onAbuseReportReceived(collectionID int64, report ente.AbuseReportRequest, readableReason string, abuseCount int64) {
  223. collection, err := c.CollectionRepo.Get(collectionID)
  224. if err != nil {
  225. logrus.Error("Could not get collection for abuse report")
  226. return
  227. }
  228. user, err := c.UserRepo.Get(collection.Owner.ID)
  229. if err != nil {
  230. logrus.Error("Could not get owner for abuse report")
  231. return
  232. }
  233. comment := report.Details.Comment
  234. if comment == "" {
  235. comment = "None"
  236. }
  237. err = email.SendTemplatedEmail([]string{user.Email}, "abuse@ente.io", "abuse@ente.io", AbuseAlertSubject, AbuseAlertTemplate, map[string]interface{}{
  238. "AlbumLink": report.URL,
  239. "Reason": readableReason,
  240. "Comments": comment,
  241. }, nil)
  242. if err != nil {
  243. logrus.Error("Error sending abuse notification ", err)
  244. }
  245. if abuseCount >= AutoDisableAbuseThreshold {
  246. err = email.SendTemplatedEmail([]string{user.Email}, "abuse@ente.io", "abuse@ente.io", AbuseLimitExceededSubject, AbuseLimitExceededTemplate, nil, nil)
  247. if err != nil {
  248. logrus.Error("Error sending abuse limit exceeded notification ", err)
  249. }
  250. }
  251. err = email.SendTemplatedEmail([]string{"team@ente.io"}, "abuse@ente.io", "abuse@ente.io", AbuseAlertTeamSubject, AbuseAlertTemplate, map[string]interface{}{
  252. "AlbumLink": report.URL,
  253. "Reason": readableReason,
  254. "Comments": comment,
  255. }, nil)
  256. if err != nil {
  257. logrus.Error("Error notifying team about abuse ", err)
  258. }
  259. }
  260. func (c *PublicCollectionController) HandleAccountDeletion(ctx context.Context, userID int64, logger *logrus.Entry) error {
  261. logger.Info("updating public collection on account deletion")
  262. collectionIDs, err := c.PublicCollectionRepo.GetActivePublicTokenForUser(ctx, userID)
  263. if err != nil {
  264. return stacktrace.Propagate(err, "")
  265. }
  266. logger.WithField("cIDs", collectionIDs).Info("disable public tokens due to account deletion")
  267. for _, collectionID := range collectionIDs {
  268. err = c.Disable(ctx, collectionID)
  269. if err != nil {
  270. return stacktrace.Propagate(err, "")
  271. }
  272. }
  273. return nil
  274. }
  275. // GetPublicCollection will return collection info for a public url.
  276. // is mustAllowCollect is set to true but the underlying collection doesn't allow uploading
  277. func (c *PublicCollectionController) GetPublicCollection(ctx *gin.Context, mustAllowCollect bool) (ente.Collection, error) {
  278. accessContext := auth.MustGetPublicAccessContext(ctx)
  279. collection, err := c.CollectionRepo.Get(accessContext.CollectionID)
  280. if err != nil {
  281. return ente.Collection{}, stacktrace.Propagate(err, "")
  282. }
  283. if collection.IsDeleted {
  284. return ente.Collection{}, stacktrace.Propagate(ente.ErrNotFound, "collection is deleted")
  285. }
  286. // hide redundant/private information
  287. collection.Sharees = nil
  288. collection.MagicMetadata = nil
  289. publicURLsWithLimitedInfo := make([]ente.PublicURL, 0)
  290. for _, publicUrl := range collection.PublicURLs {
  291. publicURLsWithLimitedInfo = append(publicURLsWithLimitedInfo, ente.PublicURL{
  292. EnableDownload: publicUrl.EnableDownload,
  293. EnableCollect: publicUrl.EnableCollect,
  294. PasswordEnabled: publicUrl.PasswordEnabled,
  295. Nonce: publicUrl.Nonce,
  296. MemLimit: publicUrl.MemLimit,
  297. OpsLimit: publicUrl.OpsLimit,
  298. })
  299. }
  300. collection.PublicURLs = publicURLsWithLimitedInfo
  301. if mustAllowCollect {
  302. if len(publicURLsWithLimitedInfo) != 1 {
  303. errorMsg := fmt.Sprintf("Unexpected number of public urls: %d", len(publicURLsWithLimitedInfo))
  304. return ente.Collection{}, stacktrace.Propagate(ente.NewInternalError(errorMsg), "")
  305. }
  306. if !publicURLsWithLimitedInfo[0].EnableCollect {
  307. return ente.Collection{}, stacktrace.Propagate(&ente.ErrPublicCollectDisabled, "")
  308. }
  309. }
  310. return collection, nil
  311. }