123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423 |
- package user
- import (
- "errors"
- "fmt"
- "github.com/ente-io/museum/pkg/repo/two_factor_recovery"
- "strings"
- cache2 "github.com/ente-io/museum/ente/cache"
- "github.com/ente-io/museum/pkg/controller/discord"
- "github.com/ente-io/museum/pkg/controller/usercache"
- "github.com/ente-io/museum/ente"
- "github.com/ente-io/museum/pkg/controller"
- "github.com/ente-io/museum/pkg/controller/family"
- "github.com/ente-io/museum/pkg/repo"
- "github.com/ente-io/museum/pkg/repo/datacleanup"
- "github.com/ente-io/museum/pkg/repo/passkey"
- storageBonusRepo "github.com/ente-io/museum/pkg/repo/storagebonus"
- "github.com/ente-io/museum/pkg/utils/billing"
- "github.com/ente-io/museum/pkg/utils/crypto"
- "github.com/ente-io/museum/pkg/utils/email"
- "github.com/ente-io/stacktrace"
- "github.com/gin-gonic/gin"
- "github.com/golang-jwt/jwt"
- "github.com/patrickmn/go-cache"
- "github.com/sirupsen/logrus"
- "github.com/spf13/viper"
- )
- // UserController exposes request handlers for all user related requests
- type UserController struct {
- UserRepo *repo.UserRepository
- TwoFactorRecoveryRepo *two_factor_recovery.Repository
- UsageRepo *repo.UsageRepository
- UserAuthRepo *repo.UserAuthRepository
- TwoFactorRepo *repo.TwoFactorRepository
- PasskeyRepo *passkey.Repository
- StorageBonusRepo *storageBonusRepo.Repository
- FileRepo *repo.FileRepository
- CollectionRepo *repo.CollectionRepository
- DataCleanupRepo *datacleanup.Repository
- CollectionCtrl *controller.CollectionController
- BillingRepo *repo.BillingRepository
- BillingController *controller.BillingController
- FamilyController *family.Controller
- DiscordController *discord.DiscordController
- MailingListsController *controller.MailingListsController
- PushController *controller.PushController
- HashingKey []byte
- SecretEncryptionKey []byte
- JwtSecret []byte
- Cache *cache.Cache // refers to the auth token cache
- HardCodedOTT HardCodedOTT
- roadmapURLPrefix string
- roadmapSSOSecret string
- UserCache *cache2.UserCache
- UserCacheController *usercache.Controller
- }
- const (
- // OTTValidityDurationInMicroSeconds is the duration for which an OTT is valid
- // (60 minutes)
- OTTValidityDurationInMicroSeconds = 60 * 60 * 1000000
- // OTTWrongAttemptLimit is the max number of wrong attempt to verify OTT (to prevent bruteforce guessing)
- // When client hits this limit, they will need to trigger new OTT.
- OTTWrongAttemptLimit = 20
- // OTTActiveCodeLimit is the max number of active OTT a user can have in
- // a time window of OTTValidityDurationInMicroSeconds duration
- OTTActiveCodeLimit = 10
- // TwoFactorValidityDurationInMicroSeconds is the duration for which an OTT is valid
- // (10 minutes)
- TwoFactorValidityDurationInMicroSeconds = 10 * 60 * 1000000
- // TokenLength is the length of the token issued to a verified user
- TokenLength = 32
- // TwoFactorSessionIDLength is the length of the twoFactorSessionID issued to a verified user
- TwoFactorSessionIDLength = 32
- // PassKeySessionIDLength is the length of the passKey sessionID issued to a verified user
- PassKeySessionIDLength = 32
- CryptoPwhashMemLimitInteractive = 67108864
- CryptoPwhashOpsLimitInteractive = 2
- TOTPIssuerORG = "ente"
- // Template and subject for the mail that we send when the user deletes
- // their account.
- AccountDeletedEmailTemplate = "account_deleted.html"
- AccountDeletedWithActiveSubscriptionEmailTemplate = "account_deleted_active_sub.html"
- AccountDeletedEmailSubject = "Your ente account has been deleted"
- )
- func NewUserController(
- userRepo *repo.UserRepository,
- usageRepo *repo.UsageRepository,
- userAuthRepo *repo.UserAuthRepository,
- twoFactorRepo *repo.TwoFactorRepository,
- twoFactorRecoveryRepo *two_factor_recovery.Repository,
- passkeyRepo *passkey.Repository,
- storageBonusRepo *storageBonusRepo.Repository,
- fileRepo *repo.FileRepository,
- collectionController *controller.CollectionController,
- collectionRepo *repo.CollectionRepository,
- dataCleanupRepository *datacleanup.Repository,
- billingRepo *repo.BillingRepository,
- secretEncryptionKeyBytes []byte,
- hashingKeyBytes []byte,
- authCache *cache.Cache,
- jwtSecretBytes []byte,
- billingController *controller.BillingController,
- familyController *family.Controller,
- discordController *discord.DiscordController,
- mailingListsController *controller.MailingListsController,
- pushController *controller.PushController,
- userCache *cache2.UserCache,
- userCacheController *usercache.Controller,
- ) *UserController {
- return &UserController{
- UserRepo: userRepo,
- UsageRepo: usageRepo,
- TwoFactorRecoveryRepo: twoFactorRecoveryRepo,
- UserAuthRepo: userAuthRepo,
- StorageBonusRepo: storageBonusRepo,
- TwoFactorRepo: twoFactorRepo,
- PasskeyRepo: passkeyRepo,
- FileRepo: fileRepo,
- CollectionCtrl: collectionController,
- CollectionRepo: collectionRepo,
- DataCleanupRepo: dataCleanupRepository,
- BillingRepo: billingRepo,
- SecretEncryptionKey: secretEncryptionKeyBytes,
- HashingKey: hashingKeyBytes,
- Cache: authCache,
- JwtSecret: jwtSecretBytes,
- BillingController: billingController,
- FamilyController: familyController,
- DiscordController: discordController,
- MailingListsController: mailingListsController,
- PushController: pushController,
- HardCodedOTT: ReadHardCodedOTTFromConfig(),
- roadmapURLPrefix: viper.GetString("roadmap.url-prefix"),
- roadmapSSOSecret: viper.GetString("roadmap.sso-secret"),
- UserCache: userCache,
- UserCacheController: userCacheController,
- }
- }
- // GetAttributes returns the key attributes for a user
- func (c *UserController) GetAttributes(userID int64) (ente.KeyAttributes, error) {
- return c.UserRepo.GetKeyAttributes(userID)
- }
- // SetAttributes sets the attributes for a user. The request will fail if key attributes are already set
- func (c *UserController) SetAttributes(userID int64, request ente.SetUserAttributesRequest) error {
- _, err := c.UserRepo.GetKeyAttributes(userID)
- if err == nil { // If there are key attributes already set
- return stacktrace.Propagate(ente.ErrPermissionDenied, "key attributes are already set")
- }
- if request.KeyAttributes.MemLimit <= 0 || request.KeyAttributes.OpsLimit <= 0 {
- // note for curious soul in the future
- _ = fmt.Sprintf("Older clients were not passing these values, so server used %d & %d as ops and memLimit",
- CryptoPwhashOpsLimitInteractive, CryptoPwhashMemLimitInteractive)
- return stacktrace.Propagate(ente.ErrBadRequest, "mem or ops limit should be > 0")
- }
- err = c.UserRepo.SetKeyAttributes(userID, request.KeyAttributes)
- if err != nil {
- return stacktrace.Propagate(err, "")
- }
- return nil
- }
- // UpdateEmailMFA updates the email MFA for a user.
- func (c *UserController) UpdateEmailMFA(context *gin.Context, userID int64, isEnabled bool) error {
- if !isEnabled {
- isSrpSetupDone, err := c.UserAuthRepo.IsSRPSetupDone(context, userID)
- if err != nil {
- return stacktrace.Propagate(err, "")
- }
- // if SRP is not setup, then we can not disable email MFA
- if !isSrpSetupDone {
- return stacktrace.Propagate(ente.NewConflictError("SRP setup incomplete"), "can not disable email MFA before SRP is setup")
- }
- }
- return c.UserAuthRepo.UpdateEmailMFA(context, userID, isEnabled)
- }
- // UpdateKeys updates the user keys on password change
- func (c *UserController) UpdateKeys(context *gin.Context, userID int64,
- request ente.UpdateKeysRequest, token string) error {
- /*
- todo: send email to the user on password change and may be keep history of old keys for X days.
- History will allow easy recovery of the account when password is changed by a bad actor
- */
- isSRPSetupDone, err := c.UserAuthRepo.IsSRPSetupDone(context, userID)
- if err != nil {
- return err
- }
- if isSRPSetupDone {
- return stacktrace.Propagate(ente.NewBadRequestWithMessage("Need to upgrade client"), "can not use old API to change password after SRP is setup")
- }
- err = c.UserRepo.UpdateKeys(userID, request)
- if err != nil {
- return stacktrace.Propagate(err, "")
- }
- err = c.UserAuthRepo.RemoveAllOtherTokens(userID, token)
- if err != nil {
- return stacktrace.Propagate(err, "")
- }
- return nil
- }
- // SetRecoveryKey sets the recovery key attributes for a user, if not already set
- func (c *UserController) SetRecoveryKey(userID int64, request ente.SetRecoveryKeyRequest) error {
- keyAttr, keyErr := c.UserRepo.GetKeyAttributes(userID)
- if keyErr != nil {
- return stacktrace.Propagate(keyErr, "User keys setup is not completed")
- }
- if keyAttr.RecoveryKeyEncryptedWithMasterKey != "" {
- return stacktrace.Propagate(errors.New("recovery key is already set"), "")
- }
- err := c.UserRepo.SetRecoveryKeyAttributes(userID, request)
- if err != nil {
- return stacktrace.Propagate(err, "")
- }
- return nil
- }
- // GetPublicKey returns the public key of a user
- func (c *UserController) GetPublicKey(email string) (string, error) {
- userID, err := c.UserRepo.GetUserIDWithEmail(email)
- if err != nil {
- return "", stacktrace.Propagate(err, "")
- }
- key, err := c.UserRepo.GetPublicKey(userID)
- if err != nil {
- return "", stacktrace.Propagate(err, "")
- }
- return key, nil
- }
- // GetRoadmapURL redirects the user to the feedback page
- func (c *UserController) GetRoadmapURL(userID int64) (string, error) {
- // If SSO is not configured, redirect the user to the plain roadmap
- if c.roadmapURLPrefix == "" || c.roadmapSSOSecret == "" {
- return "https://roadmap.ente.io", nil
- }
- user, err := c.UserRepo.Get(userID)
- if err != nil {
- return "", stacktrace.Propagate(err, "")
- }
- userData := jwt.MapClaims{
- "full_name": "",
- "email": user.Hash + "@ente.io",
- }
- token := jwt.NewWithClaims(jwt.SigningMethodHS256, userData)
- signature, err := token.SignedString([]byte(c.roadmapSSOSecret))
- if err != nil {
- return "", stacktrace.Propagate(err, "")
- }
- return c.roadmapURLPrefix + signature, nil
- }
- // GetTwoFactorStatus returns a user's two factor status
- func (c *UserController) GetTwoFactorStatus(userID int64) (bool, error) {
- isTwoFactorEnabled, err := c.UserRepo.IsTwoFactorEnabled(userID)
- if err != nil {
- return false, stacktrace.Propagate(err, "")
- }
- return isTwoFactorEnabled, nil
- }
- func (c *UserController) HandleAccountDeletion(ctx *gin.Context, userID int64, logger *logrus.Entry) (*ente.DeleteAccountResponse, error) {
- isSubscriptionCancelled, err := c.BillingController.HandleAccountDeletion(ctx, userID, logger)
- if err != nil {
- return nil, stacktrace.Propagate(err, "")
- }
- err = c.CollectionCtrl.HandleAccountDeletion(ctx, userID, logger)
- if err != nil {
- return nil, stacktrace.Propagate(err, "")
- }
- err = c.FamilyController.HandleAccountDeletion(ctx, userID, logger)
- if err != nil {
- return nil, stacktrace.Propagate(err, "")
- }
- logger.Info("remove push tokens for user")
- c.PushController.RemoveTokensForUser(userID)
- logger.Info("remove active tokens for user")
- err = c.UserAuthRepo.RemoveAllTokens(userID)
- if err != nil {
- return nil, stacktrace.Propagate(err, "")
- }
- user, err := c.UserRepo.Get(userID)
- if err != nil {
- return nil, stacktrace.Propagate(err, "")
- }
- email := user.Email
- // See also: Do not block on mailing list errors
- go func() {
- _ = c.MailingListsController.Unsubscribe(email)
- }()
- logger.Info("mark user as deleted")
- err = c.UserRepo.Delete(userID)
- if err != nil {
- return nil, stacktrace.Propagate(err, "")
- }
- logger.Info("schedule data deletion")
- err = c.DataCleanupRepo.Insert(ctx, userID)
- if err != nil {
- return nil, stacktrace.Propagate(err, "")
- }
- go c.NotifyAccountDeletion(email, isSubscriptionCancelled)
- return &ente.DeleteAccountResponse{
- IsSubscriptionCancelled: isSubscriptionCancelled,
- UserID: userID,
- }, nil
- }
- func (c *UserController) NotifyAccountDeletion(userEmail string, isSubscriptionCancelled bool) {
- template := AccountDeletedEmailTemplate
- if !isSubscriptionCancelled {
- template = AccountDeletedWithActiveSubscriptionEmailTemplate
- }
- err := email.SendTemplatedEmail([]string{userEmail}, "ente", "team@ente.io",
- AccountDeletedEmailSubject, template, nil, nil)
- if err != nil {
- logrus.WithError(err).Errorf("Failed to send the account deletion email to %s", userEmail)
- }
- }
- func (c *UserController) HandleAccountRecovery(ctx *gin.Context, req ente.RecoverAccountRequest) error {
- _, err := c.UserRepo.Get(req.UserID)
- if err == nil {
- return stacktrace.Propagate(ente.NewBadRequestError(&ente.ApiErrorParams{
- Message: "User ID is linked to undeleted account",
- }), "")
- }
- if !errors.Is(err, ente.ErrUserDeleted) {
- return stacktrace.Propagate(err, "error while getting the user")
- }
- // check if the user keyAttributes are still available
- if _, keyErr := c.UserRepo.GetKeyAttributes(req.UserID); keyErr != nil {
- return stacktrace.Propagate(keyErr, "keyAttributes missing? Account can not be recovered")
- }
- email := strings.ToLower(req.EmailID)
- encryptedEmail, err := crypto.Encrypt(email, c.SecretEncryptionKey)
- if err != nil {
- return stacktrace.Propagate(err, "")
- }
- emailHash, err := crypto.GetHash(email, c.HashingKey)
- if err != nil {
- return stacktrace.Propagate(err, "")
- }
- err = c.UserRepo.UpdateEmail(req.UserID, encryptedEmail, emailHash)
- if err != nil {
- return stacktrace.Propagate(err, "failed to update email")
- }
- err = c.DataCleanupRepo.RemoveScheduledDelete(ctx, req.UserID)
- if err != nil {
- logrus.WithError(err).Error("failed to remove scheduled delete")
- return stacktrace.Propagate(err, "")
- }
- return stacktrace.Propagate(err, "")
- }
- func (c *UserController) attachFreeSubscription(userID int64) (ente.Subscription, error) {
- subscription := billing.GetFreeSubscription(userID)
- generatedID, err := c.BillingRepo.AddSubscription(subscription)
- if err != nil {
- return subscription, stacktrace.Propagate(err, "")
- }
- subscription.ID = generatedID
- return subscription, nil
- }
- func (c *UserController) createUser(email string, source *string) (int64, ente.Subscription, error) {
- encryptedEmail, err := crypto.Encrypt(email, c.SecretEncryptionKey)
- if err != nil {
- return -1, ente.Subscription{}, stacktrace.Propagate(err, "")
- }
- emailHash, err := crypto.GetHash(email, c.HashingKey)
- if err != nil {
- return -1, ente.Subscription{}, stacktrace.Propagate(err, "")
- }
- userID, err := c.UserRepo.Create(encryptedEmail, emailHash, source)
- if err != nil {
- return -1, ente.Subscription{}, stacktrace.Propagate(err, "")
- }
- err = c.UsageRepo.Create(userID)
- if err != nil {
- return -1, ente.Subscription{}, stacktrace.Propagate(err, "failed to add entry in usage")
- }
- subscription, err := c.attachFreeSubscription(userID)
- if err != nil {
- return -1, ente.Subscription{}, stacktrace.Propagate(err, "")
- }
- // Do not block on mailing list errors
- //
- // The mailing lists are hosted on a third party (Zoho), so we do not wish
- // to fail user creation in case Zoho is having temporary issues. So we
- // perform these actions async, and ignore errors that happen with them (a
- // notification will be sent to Discord for those).
- go func() {
- _ = c.MailingListsController.Subscribe(email)
- }()
- return userID, subscription, nil
- }
|