ente/server/pkg/controller/user/user.go
2024-03-08 15:15:00 +05:30

416 lines
15 KiB
Go

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)
return stacktrace.Propagate(err, "failed to update email")
}
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
}