Renamed bridge to general users and keep bridge only for bridge stuff

This commit is contained in:
Michal Horejsek
2020-05-21 14:37:15 +02:00
parent 4e2ab9b389
commit 4d2baa6b85
30 changed files with 720 additions and 661 deletions

View File

@ -6,6 +6,7 @@ Changelog [format](http://keepachangelog.com/en/1.0.0/)
## unreleased ## unreleased
### Changed ### Changed
* GODT-386 renamed bridge to general users and keep bridge only for bridge stuff
* GODT-308 better user error message when request is canceled * GODT-308 better user error message when request is canceled
* GODT-312 validate recipient emails in send before asking for their public keys * GODT-312 validate recipient emails in send before asking for their public keys

View File

@ -165,9 +165,11 @@ test: gofiles
./internal/frontend/autoconfig/... \ ./internal/frontend/autoconfig/... \
./internal/frontend/cli/... \ ./internal/frontend/cli/... \
./internal/imap/... \ ./internal/imap/... \
./internal/metrics/... \
./internal/preferences/... \ ./internal/preferences/... \
./internal/smtp/... \ ./internal/smtp/... \
./internal/store/... \ ./internal/store/... \
./internal/users/... \
./pkg/... ./pkg/...
bench: bench:
@ -179,7 +181,7 @@ coverage: test
go tool cover -html=/tmp/coverage.out -o=coverage.html go tool cover -html=/tmp/coverage.out -o=coverage.html
mocks: mocks:
mockgen --package mocks github.com/ProtonMail/proton-bridge/internal/bridge Configer,PreferenceProvider,PanicHandler,ClientManager,CredentialsStorer > internal/bridge/mocks/mocks.go mockgen --package mocks github.com/ProtonMail/proton-bridge/internal/users Configer,PreferenceProvider,PanicHandler,ClientManager,CredentialsStorer > internal/users/mocks/mocks.go
mockgen --package mocks github.com/ProtonMail/proton-bridge/internal/store PanicHandler,ClientManager,BridgeUser > internal/store/mocks/mocks.go mockgen --package mocks github.com/ProtonMail/proton-bridge/internal/store PanicHandler,ClientManager,BridgeUser > internal/store/mocks/mocks.go
mockgen --package mocks github.com/ProtonMail/proton-bridge/pkg/listener Listener > internal/store/mocks/utils_mocks.go mockgen --package mocks github.com/ProtonMail/proton-bridge/pkg/listener Listener > internal/store/mocks/utils_mocks.go
mockgen --package mocks github.com/ProtonMail/proton-bridge/pkg/pmapi Client > pkg/pmapi/mocks/mocks.go mockgen --package mocks github.com/ProtonMail/proton-bridge/pkg/pmapi Client > pkg/pmapi/mocks/mocks.go

View File

@ -47,12 +47,12 @@ import (
"github.com/ProtonMail/proton-bridge/internal/api" "github.com/ProtonMail/proton-bridge/internal/api"
"github.com/ProtonMail/proton-bridge/internal/bridge" "github.com/ProtonMail/proton-bridge/internal/bridge"
"github.com/ProtonMail/proton-bridge/internal/bridge/credentials"
"github.com/ProtonMail/proton-bridge/internal/events" "github.com/ProtonMail/proton-bridge/internal/events"
"github.com/ProtonMail/proton-bridge/internal/frontend" "github.com/ProtonMail/proton-bridge/internal/frontend"
"github.com/ProtonMail/proton-bridge/internal/imap" "github.com/ProtonMail/proton-bridge/internal/imap"
"github.com/ProtonMail/proton-bridge/internal/preferences" "github.com/ProtonMail/proton-bridge/internal/preferences"
"github.com/ProtonMail/proton-bridge/internal/smtp" "github.com/ProtonMail/proton-bridge/internal/smtp"
"github.com/ProtonMail/proton-bridge/internal/users/credentials"
"github.com/ProtonMail/proton-bridge/pkg/args" "github.com/ProtonMail/proton-bridge/pkg/args"
"github.com/ProtonMail/proton-bridge/pkg/config" "github.com/ProtonMail/proton-bridge/pkg/config"
"github.com/ProtonMail/proton-bridge/pkg/constants" "github.com/ProtonMail/proton-bridge/pkg/constants"
@ -261,7 +261,7 @@ func run(context *cli.Context) (contextError error) { // nolint[funlen]
eventListener := listener.New() eventListener := listener.New()
events.SetupEvents(eventListener) events.SetupEvents(eventListener)
credentialsStore, credentialsError := credentials.NewStore() credentialsStore, credentialsError := credentials.NewStore("bridge")
if credentialsError != nil { if credentialsError != nil {
log.Error("Could not get credentials store: ", credentialsError) log.Error("Could not get credentials store: ", credentialsError)
} }

View File

@ -15,57 +15,24 @@
// You should have received a copy of the GNU General Public License // You should have received a copy of the GNU General Public License
// along with ProtonMail Bridge. If not, see <https://www.gnu.org/licenses/>. // along with ProtonMail Bridge. If not, see <https://www.gnu.org/licenses/>.
// Package bridge provides core business logic providing API over credentials store and PM API. // Package bridge provides core functionality of Bridge app.
package bridge package bridge
import ( import (
"strconv" "github.com/ProtonMail/proton-bridge/internal/users"
"strings"
"sync"
"time"
"github.com/ProtonMail/proton-bridge/internal/events"
"github.com/ProtonMail/proton-bridge/internal/metrics"
"github.com/ProtonMail/proton-bridge/internal/preferences"
"github.com/ProtonMail/proton-bridge/internal/store"
"github.com/ProtonMail/proton-bridge/pkg/listener" "github.com/ProtonMail/proton-bridge/pkg/listener"
"github.com/ProtonMail/proton-bridge/pkg/pmapi"
imapBackend "github.com/emersion/go-imap/backend"
"github.com/hashicorp/go-multierror"
"github.com/pkg/errors"
logrus "github.com/sirupsen/logrus" logrus "github.com/sirupsen/logrus"
) )
var ( var (
log = logrus.WithField("pkg", "bridge") //nolint[gochecknoglobals] log = logrus.WithField("pkg", "bridge") //nolint[gochecknoglobals]
isApplicationOutdated = false //nolint[gochecknoglobals]
) )
// Bridge is a struct handling users.
type Bridge struct { type Bridge struct {
config Configer *users.Users
pref PreferenceProvider
panicHandler PanicHandler
events listener.Listener
clientManager ClientManager
credStorer CredentialsStorer
storeCache *store.Cache
// users is a list of accounts that have been added to bridge. clientManager users.ClientManager
// They are stored sorted in the credentials store in the order
// that they were added to bridge chronologically.
// People are used to that and so we preserve that ordering here.
users []*User
// idleUpdates is a channel which the imap backend listens to and which it uses
// to send idle updates to the mail client (eg thunderbird).
// The user stores should send idle updates on this channel.
idleUpdates chan imapBackend.Update
lock sync.RWMutex
// stopAll can be closed to stop all goroutines from looping (watchBridgeOutdated, watchAPIAuths, heartbeat etc).
stopAll chan struct{}
userAgentClientName string userAgentClientName string
userAgentClientVersion string userAgentClientVersion string
@ -73,444 +40,19 @@ type Bridge struct {
} }
func New( func New(
config Configer, config users.Configer,
pref PreferenceProvider, pref users.PreferenceProvider,
panicHandler PanicHandler, panicHandler users.PanicHandler,
eventListener listener.Listener, eventListener listener.Listener,
clientManager ClientManager, clientManager users.ClientManager,
credStorer CredentialsStorer, credStorer users.CredentialsStorer,
) *Bridge { ) *Bridge {
log.Trace("Creating new bridge") u := users.New(config, pref, panicHandler, eventListener, clientManager, credStorer)
return &Bridge{
Users: u,
b := &Bridge{
config: config,
pref: pref,
panicHandler: panicHandler,
events: eventListener,
clientManager: clientManager, clientManager: clientManager,
credStorer: credStorer,
storeCache: store.NewCache(config.GetIMAPCachePath()),
idleUpdates: make(chan imapBackend.Update),
lock: sync.RWMutex{},
stopAll: make(chan struct{}),
} }
// Allow DoH before starting bridge if the user has previously set this setting.
// This allows us to start even if protonmail is blocked.
if pref.GetBool(preferences.AllowProxyKey) {
b.AllowProxy()
}
go func() {
defer panicHandler.HandlePanic()
b.watchBridgeOutdated()
}()
go func() {
defer panicHandler.HandlePanic()
b.watchAPIAuths()
}()
go b.heartbeat()
if b.credStorer == nil {
log.Error("Bridge has no credentials store")
} else if err := b.loadUsersFromCredentialsStore(); err != nil {
log.WithError(err).Error("Could not load all users from credentials store")
}
if pref.GetBool(preferences.FirstStartKey) {
b.SendMetric(metrics.New(metrics.Setup, metrics.FirstStart, metrics.Label(config.GetVersion())))
}
return b
}
// heartbeat sends a heartbeat signal once a day.
func (b *Bridge) heartbeat() {
ticker := time.NewTicker(1 * time.Minute)
for {
select {
case <-ticker.C:
next, err := strconv.ParseInt(b.pref.Get(preferences.NextHeartbeatKey), 10, 64)
if err != nil {
continue
}
nextTime := time.Unix(next, 0)
if time.Now().After(nextTime) {
b.SendMetric(metrics.New(metrics.Heartbeat, metrics.Daily, metrics.NoLabel))
nextTime = nextTime.Add(24 * time.Hour)
b.pref.Set(preferences.NextHeartbeatKey, strconv.FormatInt(nextTime.Unix(), 10))
}
case <-b.stopAll:
return
}
}
}
func (b *Bridge) loadUsersFromCredentialsStore() (err error) {
b.lock.Lock()
defer b.lock.Unlock()
userIDs, err := b.credStorer.List()
if err != nil {
return
}
for _, userID := range userIDs {
l := log.WithField("user", userID)
user, newUserErr := newUser(b.panicHandler, userID, b.events, b.credStorer, b.clientManager, b.storeCache, b.config.GetDBDir())
if newUserErr != nil {
l.WithField("user", userID).WithError(newUserErr).Warn("Could not load user, skipping")
continue
}
b.users = append(b.users, user)
if initUserErr := user.init(b.idleUpdates); initUserErr != nil {
l.WithField("user", userID).WithError(initUserErr).Warn("Could not initialise user")
}
}
return err
}
func (b *Bridge) watchBridgeOutdated() {
ch := make(chan string)
b.events.Add(events.UpgradeApplicationEvent, ch)
for {
select {
case <-ch:
isApplicationOutdated = true
b.closeAllConnections()
case <-b.stopAll:
return
}
}
}
// watchAPIAuths receives auths from the client manager and sends them to the appropriate user.
func (b *Bridge) watchAPIAuths() {
for {
select {
case auth := <-b.clientManager.GetAuthUpdateChannel():
log.Debug("Bridge received auth from ClientManager")
user, ok := b.hasUser(auth.UserID)
if !ok {
log.WithField("userID", auth.UserID).Info("User not available for auth update")
continue
}
if auth.Auth != nil {
user.updateAuthToken(auth.Auth)
} else if err := user.logout(); err != nil {
log.WithError(err).
WithField("userID", auth.UserID).
Error("User logout failed while watching API auths")
}
case <-b.stopAll:
return
}
}
}
func (b *Bridge) closeAllConnections() {
for _, user := range b.users {
user.closeAllConnections()
}
}
// Login authenticates a user.
// The login flow:
// * Authenticate user:
// client, auth, err := bridge.Authenticate(username, password)
//
// * In case user `auth.HasTwoFactor()`, ask for it and fully authenticate the user.
// auth2FA, err := client.Auth2FA(twoFactorCode)
//
// * In case user `auth.HasMailboxPassword()`, ask for it, otherwise use `password`
// and then finish the login procedure.
// user, err := bridge.FinishLogin(client, auth, mailboxPassword)
func (b *Bridge) Login(username, password string) (authClient pmapi.Client, auth *pmapi.Auth, err error) {
b.crashBandicoot(username)
// We need to use anonymous client because we don't yet have userID and so can't save auth tokens yet.
authClient = b.clientManager.GetAnonymousClient()
authInfo, err := authClient.AuthInfo(username)
if err != nil {
log.WithField("username", username).WithError(err).Error("Could not get auth info for user")
return
}
if auth, err = authClient.Auth(username, password, authInfo); err != nil {
log.WithField("username", username).WithError(err).Error("Could not get auth for user")
return
}
return
}
// FinishLogin finishes the login procedure and adds the user into the credentials store.
// See `Login` for more details of the login flow.
func (b *Bridge) FinishLogin(authClient pmapi.Client, auth *pmapi.Auth, mbPassword string) (user *User, err error) { //nolint[funlen]
defer func() {
if err == pmapi.ErrUpgradeApplication {
b.events.Emit(events.UpgradeApplicationEvent, "")
}
if err != nil {
log.WithError(err).Debug("Login not finished; removing auth session")
if delAuthErr := authClient.DeleteAuth(); delAuthErr != nil {
log.WithError(delAuthErr).Error("Failed to clear login session after unlock")
}
}
// The anonymous client will be removed from list and authentication will not be deleted.
authClient.Logout()
}()
apiUser, hashedPassword, err := getAPIUser(authClient, auth, mbPassword)
if err != nil {
log.WithError(err).Error("Failed to get API user")
return
}
var ok bool
if user, ok = b.hasUser(apiUser.ID); ok {
if err = b.connectExistingUser(user, auth, hashedPassword); err != nil {
log.WithError(err).Error("Failed to connect existing user")
return
}
} else {
if err = b.addNewUser(apiUser, auth, hashedPassword); err != nil {
log.WithError(err).Error("Failed to add new user")
return
}
}
b.events.Emit(events.UserRefreshEvent, apiUser.ID)
return b.GetUser(apiUser.ID)
}
// connectExistingUser connects an existing bridge user to the bridge.
func (b *Bridge) connectExistingUser(user *User, auth *pmapi.Auth, hashedPassword string) (err error) {
if user.IsConnected() {
return errors.New("user is already connected")
}
// Update the user's password in the cred store in case they changed it.
if err = b.credStorer.UpdatePassword(user.ID(), hashedPassword); err != nil {
return errors.Wrap(err, "failed to update password of user in credentials store")
}
client := b.clientManager.GetClient(user.ID())
if auth, err = client.AuthRefresh(auth.GenToken()); err != nil {
return errors.Wrap(err, "failed to refresh auth token of new client")
}
if err = b.credStorer.UpdateToken(user.ID(), auth.GenToken()); err != nil {
return errors.Wrap(err, "failed to update token of user in credentials store")
}
if err = user.init(b.idleUpdates); err != nil {
return errors.Wrap(err, "failed to initialise user")
}
return
}
// addNewUser adds a new bridge user to the bridge.
func (b *Bridge) addNewUser(user *pmapi.User, auth *pmapi.Auth, hashedPassword string) (err error) {
b.lock.Lock()
defer b.lock.Unlock()
client := b.clientManager.GetClient(user.ID)
if auth, err = client.AuthRefresh(auth.GenToken()); err != nil {
return errors.Wrap(err, "failed to refresh token in new client")
}
if user, err = client.CurrentUser(); err != nil {
return errors.Wrap(err, "failed to update API user")
}
activeEmails := client.Addresses().ActiveEmails()
if _, err = b.credStorer.Add(user.ID, user.Name, auth.GenToken(), hashedPassword, activeEmails); err != nil {
return errors.Wrap(err, "failed to add user to credentials store")
}
bridgeUser, err := newUser(b.panicHandler, user.ID, b.events, b.credStorer, b.clientManager, b.storeCache, b.config.GetDBDir())
if err != nil {
return errors.Wrap(err, "failed to create user")
}
// The user needs to be part of the users list in order for it to receive an auth during initialisation.
b.users = append(b.users, bridgeUser)
if err = bridgeUser.init(b.idleUpdates); err != nil {
b.users = b.users[:len(b.users)-1]
return errors.Wrap(err, "failed to initialise user")
}
b.SendMetric(metrics.New(metrics.Setup, metrics.NewUser, metrics.NoLabel))
return err
}
func getAPIUser(client pmapi.Client, auth *pmapi.Auth, mbPassword string) (user *pmapi.User, hashedPassword string, err error) {
hashedPassword, err = pmapi.HashMailboxPassword(mbPassword, auth.KeySalt)
if err != nil {
log.WithError(err).Error("Could not hash mailbox password")
return
}
// We unlock the user's PGP key here to detect if the user's mailbox password is wrong.
if _, err = client.Unlock(hashedPassword); err != nil {
log.WithError(err).Error("Wrong mailbox password")
return
}
if user, err = client.CurrentUser(); err != nil {
log.WithError(err).Error("Could not load API user")
return
}
return
}
// GetUsers returns all added users into keychain (even logged out users).
func (b *Bridge) GetUsers() []*User {
b.lock.RLock()
defer b.lock.RUnlock()
return b.users
}
// GetUser returns a user by `query` which is compared to users' ID, username or any attached e-mail address.
func (b *Bridge) GetUser(query string) (*User, error) {
b.crashBandicoot(query)
b.lock.RLock()
defer b.lock.RUnlock()
for _, user := range b.users {
if strings.EqualFold(user.ID(), query) || strings.EqualFold(user.Username(), query) {
return user, nil
}
for _, address := range user.GetAddresses() {
if strings.EqualFold(address, query) {
return user, nil
}
}
}
return nil, errors.New("user " + query + " not found")
}
// ClearData closes all connections (to release db files and so on) and clears all data.
func (b *Bridge) ClearData() error {
var result *multierror.Error
for _, user := range b.users {
if err := user.Logout(); err != nil {
result = multierror.Append(result, err)
}
if err := user.closeStore(); err != nil {
result = multierror.Append(result, err)
}
}
if err := b.config.ClearData(); err != nil {
result = multierror.Append(result, err)
}
return result.ErrorOrNil()
}
// DeleteUser deletes user completely; it logs user out from the API, stops any
// active connection, deletes from credentials store and removes from the Bridge struct.
func (b *Bridge) DeleteUser(userID string, clearStore bool) error {
b.lock.Lock()
defer b.lock.Unlock()
log := log.WithField("user", userID)
for idx, user := range b.users {
if user.ID() == userID {
if err := user.Logout(); err != nil {
log.WithError(err).Error("Cannot logout user")
// We can try to continue to remove the user.
// Token will still be valid, but will expire eventually.
}
if err := user.closeStore(); err != nil {
log.WithError(err).Error("Failed to close user store")
}
if clearStore {
// Clear cache after closing connections (done in logout).
if err := user.clearStore(); err != nil {
log.WithError(err).Error("Failed to clear user")
}
}
if err := b.credStorer.Delete(userID); err != nil {
log.WithError(err).Error("Cannot remove user")
return err
}
b.users = append(b.users[:idx], b.users[idx+1:]...)
return nil
}
}
return errors.New("user " + userID + " not found")
}
// ReportBug reports a new bug from the user.
func (b *Bridge) ReportBug(osType, osVersion, description, accountName, address, emailClient string) error {
c := b.clientManager.GetAnonymousClient()
defer c.Logout()
title := "[Bridge] Bug"
if err := c.ReportBugWithEmailClient(
osType,
osVersion,
title,
description,
accountName,
address,
emailClient,
); err != nil {
log.Error("Reporting bug failed: ", err)
return err
}
log.Info("Bug successfully reported")
return nil
}
// SendMetric sends a metric. We don't want to return any errors, only log them.
func (b *Bridge) SendMetric(m metrics.Metric) {
c := b.clientManager.GetAnonymousClient()
defer c.Logout()
cat, act, lab := m.Get()
if err := c.SendSimpleMetric(string(cat), string(act), string(lab)); err != nil {
log.Error("Sending metric failed: ", err)
}
log.WithFields(logrus.Fields{
"cat": cat,
"act": act,
"lab": lab,
}).Debug("Metric successfully sent")
} }
// GetCurrentClient returns currently connected client (e.g. Thunderbird). // GetCurrentClient returns currently connected client (e.g. Thunderbird).
@ -541,53 +83,26 @@ func (b *Bridge) updateUserAgent() {
b.clientManager.SetUserAgent(b.userAgentClientName, b.userAgentClientVersion, b.userAgentOS) b.clientManager.SetUserAgent(b.userAgentClientName, b.userAgentClientVersion, b.userAgentOS)
} }
// GetIMAPUpdatesChannel sets the channel on which idle events should be sent. // ReportBug reports a new bug from the user.
func (b *Bridge) GetIMAPUpdatesChannel() chan imapBackend.Update { func (b *Bridge) ReportBug(osType, osVersion, description, accountName, address, emailClient string) error {
if b.idleUpdates == nil { c := b.clientManager.GetAnonymousClient()
log.Warn("Bridge updates channel is nil") defer c.Logout()
title := "[Bridge] Bug"
if err := c.ReportBugWithEmailClient(
osType,
osVersion,
title,
description,
accountName,
address,
emailClient,
); err != nil {
log.Error("Reporting bug failed: ", err)
return err
} }
return b.idleUpdates log.Info("Bug successfully reported")
}
// AllowProxy instructs bridge to use DoH to access an API proxy if necessary. return nil
// It also needs to work before bridge is initialised (because we may need to use the proxy at startup).
func (b *Bridge) AllowProxy() {
b.clientManager.AllowProxy()
}
// DisallowProxy instructs bridge to not use DoH to access an API proxy if necessary.
// It also needs to work before bridge is initialised (because we may need to use the proxy at startup).
func (b *Bridge) DisallowProxy() {
b.clientManager.DisallowProxy()
}
// CheckConnection returns whether there is an internet connection.
// This should use the connection manager when it is eventually implemented.
func (b *Bridge) CheckConnection() error {
return b.clientManager.CheckConnection()
}
// StopWatchers stops all bridge goroutines.
func (b *Bridge) StopWatchers() {
close(b.stopAll)
}
// hasUser returns whether the bridge currently has a user with ID `id`.
func (b *Bridge) hasUser(id string) (user *User, ok bool) {
for _, u := range b.users {
if u.ID() == id {
user, ok = u, true
return
}
}
return
}
// "Easter egg" for testing purposes.
func (b *Bridge) crashBandicoot(username string) {
if username == "crash@bandicoot" {
panic("Your wish is my command… I crash!")
}
} }

View File

@ -19,6 +19,7 @@ package imap
import ( import (
"github.com/ProtonMail/proton-bridge/internal/bridge" "github.com/ProtonMail/proton-bridge/internal/bridge"
"github.com/ProtonMail/proton-bridge/internal/users"
"github.com/ProtonMail/proton-bridge/pkg/pmapi" "github.com/ProtonMail/proton-bridge/pkg/pmapi"
) )
@ -67,10 +68,10 @@ func (b *bridgeWrap) GetUser(query string) (bridgeUser, error) {
} }
type bridgeUserWrap struct { type bridgeUserWrap struct {
*bridge.User *users.User
} }
func newBridgeUserWrap(bridgeUser *bridge.User) *bridgeUserWrap { func newBridgeUserWrap(bridgeUser *users.User) *bridgeUserWrap {
return &bridgeUserWrap{User: bridgeUser} return &bridgeUserWrap{User: bridgeUser}
} }

View File

@ -19,6 +19,7 @@ package smtp
import ( import (
"github.com/ProtonMail/proton-bridge/internal/bridge" "github.com/ProtonMail/proton-bridge/internal/bridge"
"github.com/ProtonMail/proton-bridge/internal/users"
"github.com/ProtonMail/proton-bridge/pkg/pmapi" "github.com/ProtonMail/proton-bridge/pkg/pmapi"
) )
@ -54,10 +55,10 @@ func (b *bridgeWrap) GetUser(query string) (bridgeUser, error) {
} }
type bridgeUserWrap struct { type bridgeUserWrap struct {
*bridge.User *users.User
} }
func newBridgeUserWrap(bridgeUser *bridge.User) *bridgeUserWrap { func newBridgeUserWrap(bridgeUser *users.User) *bridgeUserWrap {
return &bridgeUserWrap{User: bridgeUser} return &bridgeUserWrap{User: bridgeUser}
} }

View File

@ -22,8 +22,8 @@ import (
"testing" "testing"
pmcrypto "github.com/ProtonMail/gopenpgp/crypto" pmcrypto "github.com/ProtonMail/gopenpgp/crypto"
"github.com/ProtonMail/proton-bridge/internal/bridge"
"github.com/ProtonMail/proton-bridge/internal/events" "github.com/ProtonMail/proton-bridge/internal/events"
"github.com/ProtonMail/proton-bridge/internal/users"
"github.com/ProtonMail/proton-bridge/pkg/listener" "github.com/ProtonMail/proton-bridge/pkg/listener"
"github.com/ProtonMail/proton-bridge/pkg/pmapi" "github.com/ProtonMail/proton-bridge/pkg/pmapi"
"github.com/golang/mock/gomock" "github.com/golang/mock/gomock"
@ -32,14 +32,14 @@ import (
type mocks struct { type mocks struct {
t *testing.T t *testing.T
eventListener *bridge.MockListener eventListener *users.MockListener
} }
func initMocks(t *testing.T) mocks { func initMocks(t *testing.T) mocks {
mockCtrl := gomock.NewController(t) mockCtrl := gomock.NewController(t)
return mocks{ return mocks{
t: t, t: t,
eventListener: bridge.NewMockListener(mockCtrl), eventListener: users.NewMockListener(mockCtrl),
} }
} }

View File

@ -33,7 +33,7 @@ import (
const sep = "\x00" const sep = "\x00"
var ( var (
log = logrus.WithField("pkg", "bridge") //nolint[gochecknoglobals] log = logrus.WithField("pkg", "credentials") //nolint[gochecknoglobals]
ErrWrongFormat = errors.New("backend/creds: malformed password") ErrWrongFormat = errors.New("backend/creds: malformed password")
) )

View File

@ -36,8 +36,8 @@ type Store struct {
} }
// NewStore creates a new encrypted credentials store. // NewStore creates a new encrypted credentials store.
func NewStore() (*Store, error) { func NewStore(appName string) (*Store, error) {
secrets, err := keychain.NewAccess("bridge") secrets, err := keychain.NewAccess(appName)
return &Store{ return &Store{
secrets: secrets, secrets: secrets,
}, err }, err

View File

@ -1,8 +1,8 @@
// Code generated by MockGen. DO NOT EDIT. // Code generated by MockGen. DO NOT EDIT.
// Source: ./listener/listener.go // Source: ./listener/listener.go
// Package bridge is a generated GoMock package. // Package users is a generated GoMock package.
package bridge package users
import ( import (
reflect "reflect" reflect "reflect"

View File

@ -1,5 +1,5 @@
// Code generated by MockGen. DO NOT EDIT. // Code generated by MockGen. DO NOT EDIT.
// Source: github.com/ProtonMail/proton-bridge/internal/bridge (interfaces: Configer,PreferenceProvider,PanicHandler,ClientManager,CredentialsStorer) // Source: github.com/ProtonMail/proton-bridge/internal/users (interfaces: Configer,PreferenceProvider,PanicHandler,ClientManager,CredentialsStorer)
// Package mocks is a generated GoMock package. // Package mocks is a generated GoMock package.
package mocks package mocks
@ -7,7 +7,7 @@ package mocks
import ( import (
reflect "reflect" reflect "reflect"
credentials "github.com/ProtonMail/proton-bridge/internal/bridge/credentials" credentials "github.com/ProtonMail/proton-bridge/internal/users/credentials"
pmapi "github.com/ProtonMail/proton-bridge/pkg/pmapi" pmapi "github.com/ProtonMail/proton-bridge/pkg/pmapi"
gomock "github.com/golang/mock/gomock" gomock "github.com/golang/mock/gomock"
) )

View File

@ -15,10 +15,10 @@
// You should have received a copy of the GNU General Public License // You should have received a copy of the GNU General Public License
// along with ProtonMail Bridge. If not, see <https://www.gnu.org/licenses/>. // along with ProtonMail Bridge. If not, see <https://www.gnu.org/licenses/>.
package bridge package users
import ( import (
"github.com/ProtonMail/proton-bridge/internal/bridge/credentials" "github.com/ProtonMail/proton-bridge/internal/users/credentials"
"github.com/ProtonMail/proton-bridge/pkg/pmapi" "github.com/ProtonMail/proton-bridge/pkg/pmapi"
) )

View File

@ -15,7 +15,7 @@
// You should have received a copy of the GNU General Public License // You should have received a copy of the GNU General Public License
// along with ProtonMail Bridge. If not, see <https://www.gnu.org/licenses/>. // along with ProtonMail Bridge. If not, see <https://www.gnu.org/licenses/>.
package bridge package users
import ( import (
"fmt" "fmt"
@ -24,9 +24,9 @@ import (
"strings" "strings"
"sync" "sync"
"github.com/ProtonMail/proton-bridge/internal/bridge/credentials"
"github.com/ProtonMail/proton-bridge/internal/events" "github.com/ProtonMail/proton-bridge/internal/events"
"github.com/ProtonMail/proton-bridge/internal/store" "github.com/ProtonMail/proton-bridge/internal/store"
"github.com/ProtonMail/proton-bridge/internal/users/credentials"
"github.com/ProtonMail/proton-bridge/pkg/listener" "github.com/ProtonMail/proton-bridge/pkg/listener"
"github.com/ProtonMail/proton-bridge/pkg/pmapi" "github.com/ProtonMail/proton-bridge/pkg/pmapi"
imapBackend "github.com/emersion/go-imap/backend" imapBackend "github.com/emersion/go-imap/backend"
@ -34,8 +34,8 @@ import (
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
) )
// ErrLoggedOutUser is sent to IMAP and SMTP if user exists, password is OK but user is logged out from bridge. // ErrLoggedOutUser is sent to IMAP and SMTP if user exists, password is OK but user is logged out from the app.
var ErrLoggedOutUser = errors.New("bridge account is logged out, use bridge to login again") var ErrLoggedOutUser = errors.New("account is logged out, use the app to login again")
// User is a struct on top of API client and credentials store. // User is a struct on top of API client and credentials store.
type User struct { type User struct {
@ -61,7 +61,7 @@ type User struct {
wasKeyringUnlocked bool wasKeyringUnlocked bool
} }
// newUser creates a new bridge user. // newUser creates a new user.
func newUser( func newUser(
panicHandler PanicHandler, panicHandler PanicHandler,
userID string, userID string,
@ -98,7 +98,7 @@ func (u *User) client() pmapi.Client {
return u.clientManager.GetClient(u.userID) return u.clientManager.GetClient(u.userID)
} }
// init initialises a bridge user. This includes reloading its credentials from the credentials store // init initialises a user. This includes reloading its credentials from the credentials store
// (such as when logging out and back in, you need to reload the credentials because the new credentials will // (such as when logging out and back in, you need to reload the credentials because the new credentials will
// have the apitoken and password), authorising the user against the api, loading the user store (creating a new one // have the apitoken and password), authorising the user against the api, loading the user store (creating a new one
// if necessary), and setting the imap idle updates channel (used to send imap idle updates to the imap backend if // if necessary), and setting the imap idle updates channel (used to send imap idle updates to the imap backend if
@ -119,7 +119,7 @@ func (u *User) init(idleUpdates chan imapBackend.Update) (err error) {
u.creds = creds u.creds = creds
// Try to authorise the user if they aren't already authorised. // Try to authorise the user if they aren't already authorised.
// Note: we still allow users to set up bridge if the internet is off. // Note: we still allow users to set up accounts if the internet is off.
if authErr := u.authorizeIfNecessary(false); authErr != nil { if authErr := u.authorizeIfNecessary(false); authErr != nil {
switch errors.Cause(authErr) { switch errors.Cause(authErr) {
case pmapi.ErrAPINotReachable, pmapi.ErrUpgradeApplication, ErrLoggedOutUser: case pmapi.ErrAPINotReachable, pmapi.ErrUpgradeApplication, ErrLoggedOutUser:
@ -245,7 +245,7 @@ func (u *User) authorizeAndUnlock() (err error) {
} }
func (u *User) updateAuthToken(auth *pmapi.Auth) { func (u *User) updateAuthToken(auth *pmapi.Auth) {
u.log.Debug("User received auth from bridge") u.log.Debug("User received auth")
if err := u.credStorer.UpdateToken(u.userID, auth.GenToken()); err != nil { if err := u.credStorer.UpdateToken(u.userID, auth.GenToken()); err != nil {
u.log.WithError(err).Error("Failed to update refresh token in credentials store") u.log.WithError(err).Error("Failed to update refresh token in credentials store")
@ -495,7 +495,7 @@ func (u *User) SwitchAddressMode() (err error) {
} }
// logout is the same as Logout, but for internal purposes (logged out from // logout is the same as Logout, but for internal purposes (logged out from
// the server) which emits LogoutEvent to notify other parts of the Bridge. // the server) which emits LogoutEvent to notify other parts of the app.
func (u *User) logout() error { func (u *User) logout() error {
u.lock.Lock() u.lock.Lock()
wasConnected := u.creds.IsConnected() wasConnected := u.creds.IsConnected()

View File

@ -15,7 +15,7 @@
// You should have received a copy of the GNU General Public License // You should have received a copy of the GNU General Public License
// along with ProtonMail Bridge. If not, see <https://www.gnu.org/licenses/>. // along with ProtonMail Bridge. If not, see <https://www.gnu.org/licenses/>.
package bridge package users
import ( import (
"testing" "testing"
@ -213,7 +213,7 @@ func TestCheckBridgeLoginLoggedOut(t *testing.T) {
err = user.CheckBridgeLogin(testCredentialsDisconnected.BridgePassword) err = user.CheckBridgeLogin(testCredentialsDisconnected.BridgePassword)
waitForEvents() waitForEvents()
assert.Equal(t, "bridge account is logged out, use bridge to login again", err.Error()) assert.Equal(t, ErrLoggedOutUser, err)
} }
func TestCheckBridgeLoginBadPassword(t *testing.T) { func TestCheckBridgeLoginBadPassword(t *testing.T) {

View File

@ -15,14 +15,14 @@
// You should have received a copy of the GNU General Public License // You should have received a copy of the GNU General Public License
// along with ProtonMail Bridge. If not, see <https://www.gnu.org/licenses/>. // along with ProtonMail Bridge. If not, see <https://www.gnu.org/licenses/>.
package bridge package users
import ( import (
"errors" "errors"
"testing" "testing"
"github.com/ProtonMail/proton-bridge/internal/bridge/credentials"
"github.com/ProtonMail/proton-bridge/internal/events" "github.com/ProtonMail/proton-bridge/internal/events"
"github.com/ProtonMail/proton-bridge/internal/users/credentials"
"github.com/ProtonMail/proton-bridge/pkg/pmapi" "github.com/ProtonMail/proton-bridge/pkg/pmapi"
gomock "github.com/golang/mock/gomock" gomock "github.com/golang/mock/gomock"
a "github.com/stretchr/testify/assert" a "github.com/stretchr/testify/assert"
@ -38,7 +38,7 @@ func TestNewUserNoCredentialsStore(t *testing.T) {
a.Error(t, err) a.Error(t, err)
} }
func TestNewUserBridgeOutdated(t *testing.T) { func TestNewUserAppOutdated(t *testing.T) {
m := initMocks(t) m := initMocks(t)
defer m.ctrl.Finish() defer m.ctrl.Finish()

View File

@ -15,7 +15,7 @@
// You should have received a copy of the GNU General Public License // You should have received a copy of the GNU General Public License
// along with ProtonMail Bridge. If not, see <https://www.gnu.org/licenses/>. // along with ProtonMail Bridge. If not, see <https://www.gnu.org/licenses/>.
package bridge package users
import ( import (
"testing" "testing"

537
internal/users/users.go Normal file
View File

@ -0,0 +1,537 @@
// Copyright (c) 2020 Proton Technologies AG
//
// This file is part of ProtonMail Bridge.
//
// ProtonMail Bridge is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// ProtonMail Bridge is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with ProtonMail Bridge. If not, see <https://www.gnu.org/licenses/>.
// Package users provides core business logic providing API over credentials store and PM API.
package users
import (
"strconv"
"strings"
"sync"
"time"
"github.com/ProtonMail/proton-bridge/internal/events"
"github.com/ProtonMail/proton-bridge/internal/metrics"
"github.com/ProtonMail/proton-bridge/internal/preferences"
"github.com/ProtonMail/proton-bridge/internal/store"
"github.com/ProtonMail/proton-bridge/pkg/listener"
"github.com/ProtonMail/proton-bridge/pkg/pmapi"
imapBackend "github.com/emersion/go-imap/backend"
"github.com/hashicorp/go-multierror"
"github.com/pkg/errors"
logrus "github.com/sirupsen/logrus"
)
var (
log = logrus.WithField("pkg", "users") //nolint[gochecknoglobals]
isApplicationOutdated = false //nolint[gochecknoglobals]
)
// Users is a struct handling users.
type Users struct {
config Configer
pref PreferenceProvider
panicHandler PanicHandler
events listener.Listener
clientManager ClientManager
credStorer CredentialsStorer
storeCache *store.Cache
// users is a list of accounts that have been added to the app.
// They are stored sorted in the credentials store in the order
// that they were added to the app chronologically.
// People are used to that and so we preserve that ordering here.
users []*User
// idleUpdates is a channel which the imap backend listens to and which it uses
// to send idle updates to the mail client (eg thunderbird).
// The user stores should send idle updates on this channel.
idleUpdates chan imapBackend.Update
lock sync.RWMutex
// stopAll can be closed to stop all goroutines from looping (watchAppOutdated, watchAPIAuths, heartbeat etc).
stopAll chan struct{}
}
func New(
config Configer,
pref PreferenceProvider,
panicHandler PanicHandler,
eventListener listener.Listener,
clientManager ClientManager,
credStorer CredentialsStorer,
) *Users {
log.Trace("Creating new users")
u := &Users{
config: config,
pref: pref,
panicHandler: panicHandler,
events: eventListener,
clientManager: clientManager,
credStorer: credStorer,
storeCache: store.NewCache(config.GetIMAPCachePath()),
idleUpdates: make(chan imapBackend.Update),
lock: sync.RWMutex{},
stopAll: make(chan struct{}),
}
// Allow DoH before starting the app if the user has previously set this setting.
// This allows us to start even if protonmail is blocked.
if pref.GetBool(preferences.AllowProxyKey) {
u.AllowProxy()
}
go func() {
defer panicHandler.HandlePanic()
u.watchAppOutdated()
}()
go func() {
defer panicHandler.HandlePanic()
u.watchAPIAuths()
}()
go u.heartbeat()
if u.credStorer == nil {
log.Error("No credentials store is available")
} else if err := u.loadUsersFromCredentialsStore(); err != nil {
log.WithError(err).Error("Could not load all users from credentials store")
}
if pref.GetBool(preferences.FirstStartKey) {
u.SendMetric(metrics.New(metrics.Setup, metrics.FirstStart, metrics.Label(config.GetVersion())))
}
return u
}
// heartbeat sends a heartbeat signal once a day.
func (u *Users) heartbeat() {
ticker := time.NewTicker(1 * time.Minute)
for {
select {
case <-ticker.C:
next, err := strconv.ParseInt(u.pref.Get(preferences.NextHeartbeatKey), 10, 64)
if err != nil {
continue
}
nextTime := time.Unix(next, 0)
if time.Now().After(nextTime) {
u.SendMetric(metrics.New(metrics.Heartbeat, metrics.Daily, metrics.NoLabel))
nextTime = nextTime.Add(24 * time.Hour)
u.pref.Set(preferences.NextHeartbeatKey, strconv.FormatInt(nextTime.Unix(), 10))
}
case <-u.stopAll:
return
}
}
}
func (u *Users) loadUsersFromCredentialsStore() (err error) {
u.lock.Lock()
defer u.lock.Unlock()
userIDs, err := u.credStorer.List()
if err != nil {
return
}
for _, userID := range userIDs {
l := log.WithField("user", userID)
user, newUserErr := newUser(u.panicHandler, userID, u.events, u.credStorer, u.clientManager, u.storeCache, u.config.GetDBDir())
if newUserErr != nil {
l.WithField("user", userID).WithError(newUserErr).Warn("Could not load user, skipping")
continue
}
u.users = append(u.users, user)
if initUserErr := user.init(u.idleUpdates); initUserErr != nil {
l.WithField("user", userID).WithError(initUserErr).Warn("Could not initialise user")
}
}
return err
}
func (u *Users) watchAppOutdated() {
ch := make(chan string)
u.events.Add(events.UpgradeApplicationEvent, ch)
for {
select {
case <-ch:
isApplicationOutdated = true
u.closeAllConnections()
case <-u.stopAll:
return
}
}
}
// watchAPIAuths receives auths from the client manager and sends them to the appropriate user.
func (u *Users) watchAPIAuths() {
for {
select {
case auth := <-u.clientManager.GetAuthUpdateChannel():
log.Debug("Users received auth from ClientManager")
user, ok := u.hasUser(auth.UserID)
if !ok {
log.WithField("userID", auth.UserID).Info("User not available for auth update")
continue
}
if auth.Auth != nil {
user.updateAuthToken(auth.Auth)
} else if err := user.logout(); err != nil {
log.WithError(err).
WithField("userID", auth.UserID).
Error("User logout failed while watching API auths")
}
case <-u.stopAll:
return
}
}
}
func (u *Users) closeAllConnections() {
for _, user := range u.users {
user.closeAllConnections()
}
}
// Login authenticates a user.
// The login flow:
// * Authenticate user:
// client, auth, err := users.Authenticate(username, password)
//
// * In case user `auth.HasTwoFactor()`, ask for it and fully authenticate the user.
// auth2FA, err := client.Auth2FA(twoFactorCode)
//
// * In case user `auth.HasMailboxPassword()`, ask for it, otherwise use `password`
// and then finish the login procedure.
// user, err := users.FinishLogin(client, auth, mailboxPassword)
func (u *Users) Login(username, password string) (authClient pmapi.Client, auth *pmapi.Auth, err error) {
u.crashBandicoot(username)
// We need to use anonymous client because we don't yet have userID and so can't save auth tokens yet.
authClient = u.clientManager.GetAnonymousClient()
authInfo, err := authClient.AuthInfo(username)
if err != nil {
log.WithField("username", username).WithError(err).Error("Could not get auth info for user")
return
}
if auth, err = authClient.Auth(username, password, authInfo); err != nil {
log.WithField("username", username).WithError(err).Error("Could not get auth for user")
return
}
return
}
// FinishLogin finishes the login procedure and adds the user into the credentials store.
// See `Login` for more details of the login flow.
func (u *Users) FinishLogin(authClient pmapi.Client, auth *pmapi.Auth, mbPassword string) (user *User, err error) { //nolint[funlen]
defer func() {
if err == pmapi.ErrUpgradeApplication {
u.events.Emit(events.UpgradeApplicationEvent, "")
}
if err != nil {
log.WithError(err).Debug("Login not finished; removing auth session")
if delAuthErr := authClient.DeleteAuth(); delAuthErr != nil {
log.WithError(delAuthErr).Error("Failed to clear login session after unlock")
}
}
// The anonymous client will be removed from list and authentication will not be deleted.
authClient.Logout()
}()
apiUser, hashedPassword, err := getAPIUser(authClient, auth, mbPassword)
if err != nil {
log.WithError(err).Error("Failed to get API user")
return
}
var ok bool
if user, ok = u.hasUser(apiUser.ID); ok {
if err = u.connectExistingUser(user, auth, hashedPassword); err != nil {
log.WithError(err).Error("Failed to connect existing user")
return
}
} else {
if err = u.addNewUser(apiUser, auth, hashedPassword); err != nil {
log.WithError(err).Error("Failed to add new user")
return
}
}
u.events.Emit(events.UserRefreshEvent, apiUser.ID)
return u.GetUser(apiUser.ID)
}
// connectExistingUser connects an existing user.
func (u *Users) connectExistingUser(user *User, auth *pmapi.Auth, hashedPassword string) (err error) {
if user.IsConnected() {
return errors.New("user is already connected")
}
// Update the user's password in the cred store in case they changed it.
if err = u.credStorer.UpdatePassword(user.ID(), hashedPassword); err != nil {
return errors.Wrap(err, "failed to update password of user in credentials store")
}
client := u.clientManager.GetClient(user.ID())
if auth, err = client.AuthRefresh(auth.GenToken()); err != nil {
return errors.Wrap(err, "failed to refresh auth token of new client")
}
if err = u.credStorer.UpdateToken(user.ID(), auth.GenToken()); err != nil {
return errors.Wrap(err, "failed to update token of user in credentials store")
}
if err = user.init(u.idleUpdates); err != nil {
return errors.Wrap(err, "failed to initialise user")
}
return
}
// addNewUser adds a new user.
func (u *Users) addNewUser(apiUser *pmapi.User, auth *pmapi.Auth, hashedPassword string) (err error) {
u.lock.Lock()
defer u.lock.Unlock()
client := u.clientManager.GetClient(apiUser.ID)
if auth, err = client.AuthRefresh(auth.GenToken()); err != nil {
return errors.Wrap(err, "failed to refresh token in new client")
}
if apiUser, err = client.CurrentUser(); err != nil {
return errors.Wrap(err, "failed to update API user")
}
activeEmails := client.Addresses().ActiveEmails()
if _, err = u.credStorer.Add(apiUser.ID, apiUser.Name, auth.GenToken(), hashedPassword, activeEmails); err != nil {
return errors.Wrap(err, "failed to add user to credentials store")
}
user, err := newUser(u.panicHandler, apiUser.ID, u.events, u.credStorer, u.clientManager, u.storeCache, u.config.GetDBDir())
if err != nil {
return errors.Wrap(err, "failed to create user")
}
// The user needs to be part of the users list in order for it to receive an auth during initialisation.
u.users = append(u.users, user)
if err = user.init(u.idleUpdates); err != nil {
u.users = u.users[:len(u.users)-1]
return errors.Wrap(err, "failed to initialise user")
}
u.SendMetric(metrics.New(metrics.Setup, metrics.NewUser, metrics.NoLabel))
return err
}
func getAPIUser(client pmapi.Client, auth *pmapi.Auth, mbPassword string) (user *pmapi.User, hashedPassword string, err error) {
hashedPassword, err = pmapi.HashMailboxPassword(mbPassword, auth.KeySalt)
if err != nil {
log.WithError(err).Error("Could not hash mailbox password")
return
}
// We unlock the user's PGP key here to detect if the user's mailbox password is wrong.
if _, err = client.Unlock(hashedPassword); err != nil {
log.WithError(err).Error("Wrong mailbox password")
return
}
if user, err = client.CurrentUser(); err != nil {
log.WithError(err).Error("Could not load API user")
return
}
return
}
// GetUsers returns all added users into keychain (even logged out users).
func (u *Users) GetUsers() []*User {
u.lock.RLock()
defer u.lock.RUnlock()
return u.users
}
// GetUser returns a user by `query` which is compared to users' ID, username or any attached e-mail address.
func (u *Users) GetUser(query string) (*User, error) {
u.crashBandicoot(query)
u.lock.RLock()
defer u.lock.RUnlock()
for _, user := range u.users {
if strings.EqualFold(user.ID(), query) || strings.EqualFold(user.Username(), query) {
return user, nil
}
for _, address := range user.GetAddresses() {
if strings.EqualFold(address, query) {
return user, nil
}
}
}
return nil, errors.New("user " + query + " not found")
}
// ClearData closes all connections (to release db files and so on) and clears all data.
func (u *Users) ClearData() error {
var result *multierror.Error
for _, user := range u.users {
if err := user.Logout(); err != nil {
result = multierror.Append(result, err)
}
if err := user.closeStore(); err != nil {
result = multierror.Append(result, err)
}
}
if err := u.config.ClearData(); err != nil {
result = multierror.Append(result, err)
}
return result.ErrorOrNil()
}
// DeleteUser deletes user completely; it logs user out from the API, stops any
// active connection, deletes from credentials store and removes from the Bridge struct.
func (u *Users) DeleteUser(userID string, clearStore bool) error {
u.lock.Lock()
defer u.lock.Unlock()
log := log.WithField("user", userID)
for idx, user := range u.users {
if user.ID() == userID {
if err := user.Logout(); err != nil {
log.WithError(err).Error("Cannot logout user")
// We can try to continue to remove the user.
// Token will still be valid, but will expire eventually.
}
if err := user.closeStore(); err != nil {
log.WithError(err).Error("Failed to close user store")
}
if clearStore {
// Clear cache after closing connections (done in logout).
if err := user.clearStore(); err != nil {
log.WithError(err).Error("Failed to clear user")
}
}
if err := u.credStorer.Delete(userID); err != nil {
log.WithError(err).Error("Cannot remove user")
return err
}
u.users = append(u.users[:idx], u.users[idx+1:]...)
return nil
}
}
return errors.New("user " + userID + " not found")
}
// SendMetric sends a metric. We don't want to return any errors, only log them.
func (u *Users) SendMetric(m metrics.Metric) {
c := u.clientManager.GetAnonymousClient()
defer c.Logout()
cat, act, lab := m.Get()
if err := c.SendSimpleMetric(string(cat), string(act), string(lab)); err != nil {
log.Error("Sending metric failed: ", err)
}
log.WithFields(logrus.Fields{
"cat": cat,
"act": act,
"lab": lab,
}).Debug("Metric successfully sent")
}
// GetIMAPUpdatesChannel sets the channel on which idle events should be sent.
func (u *Users) GetIMAPUpdatesChannel() chan imapBackend.Update {
if u.idleUpdates == nil {
log.Warn("IMAP updates channel is nil")
}
return u.idleUpdates
}
// AllowProxy instructs the app to use DoH to access an API proxy if necessary.
// It also needs to work before the app is initialised (because we may need to use the proxy at startup).
func (u *Users) AllowProxy() {
u.clientManager.AllowProxy()
}
// DisallowProxy instructs the app to not use DoH to access an API proxy if necessary.
// It also needs to work before the app is initialised (because we may need to use the proxy at startup).
func (u *Users) DisallowProxy() {
u.clientManager.DisallowProxy()
}
// CheckConnection returns whether there is an internet connection.
// This should use the connection manager when it is eventually implemented.
func (u *Users) CheckConnection() error {
return u.clientManager.CheckConnection()
}
// StopWatchers stops all goroutines.
func (u *Users) StopWatchers() {
close(u.stopAll)
}
// hasUser returns whether the struct currently has a user with ID `id`.
func (u *Users) hasUser(id string) (user *User, ok bool) {
for _, u := range u.users {
if u.ID() == id {
user, ok = u, true
return
}
}
return
}
// "Easter egg" for testing purposes.
func (u *Users) crashBandicoot(username string) {
if username == "crash@bandicoot" {
panic("Your wish is my command… I crash!")
}
}

View File

@ -15,7 +15,7 @@
// You should have received a copy of the GNU General Public License // You should have received a copy of the GNU General Public License
// along with ProtonMail Bridge. If not, see <https://www.gnu.org/licenses/>. // along with ProtonMail Bridge. If not, see <https://www.gnu.org/licenses/>.
package bridge package users
import ( import (
"errors" "errors"
@ -33,7 +33,7 @@ func TestGetNoUser(t *testing.T) {
m.clientManager.EXPECT().GetClient("user").Return(m.pmapiClient).MinTimes(1) m.clientManager.EXPECT().GetClient("user").Return(m.pmapiClient).MinTimes(1)
m.clientManager.EXPECT().GetClient("users").Return(m.pmapiClient).MinTimes(1) m.clientManager.EXPECT().GetClient("users").Return(m.pmapiClient).MinTimes(1)
checkBridgeGetUser(t, m, "nouser", -1, "user nouser not found") checkUsersGetUser(t, m, "nouser", -1, "user nouser not found")
} }
func TestGetUserByID(t *testing.T) { func TestGetUserByID(t *testing.T) {
@ -43,8 +43,8 @@ func TestGetUserByID(t *testing.T) {
m.clientManager.EXPECT().GetClient("user").Return(m.pmapiClient).MinTimes(1) m.clientManager.EXPECT().GetClient("user").Return(m.pmapiClient).MinTimes(1)
m.clientManager.EXPECT().GetClient("users").Return(m.pmapiClient).MinTimes(1) m.clientManager.EXPECT().GetClient("users").Return(m.pmapiClient).MinTimes(1)
checkBridgeGetUser(t, m, "user", 0, "") checkUsersGetUser(t, m, "user", 0, "")
checkBridgeGetUser(t, m, "users", 1, "") checkUsersGetUser(t, m, "users", 1, "")
} }
func TestGetUserByName(t *testing.T) { func TestGetUserByName(t *testing.T) {
@ -54,8 +54,8 @@ func TestGetUserByName(t *testing.T) {
m.clientManager.EXPECT().GetClient("user").Return(m.pmapiClient).MinTimes(1) m.clientManager.EXPECT().GetClient("user").Return(m.pmapiClient).MinTimes(1)
m.clientManager.EXPECT().GetClient("users").Return(m.pmapiClient).MinTimes(1) m.clientManager.EXPECT().GetClient("users").Return(m.pmapiClient).MinTimes(1)
checkBridgeGetUser(t, m, "username", 0, "") checkUsersGetUser(t, m, "username", 0, "")
checkBridgeGetUser(t, m, "usersname", 1, "") checkUsersGetUser(t, m, "usersname", 1, "")
} }
func TestGetUserByEmail(t *testing.T) { func TestGetUserByEmail(t *testing.T) {
@ -65,10 +65,10 @@ func TestGetUserByEmail(t *testing.T) {
m.clientManager.EXPECT().GetClient("user").Return(m.pmapiClient).MinTimes(1) m.clientManager.EXPECT().GetClient("user").Return(m.pmapiClient).MinTimes(1)
m.clientManager.EXPECT().GetClient("users").Return(m.pmapiClient).MinTimes(1) m.clientManager.EXPECT().GetClient("users").Return(m.pmapiClient).MinTimes(1)
checkBridgeGetUser(t, m, "user@pm.me", 0, "") checkUsersGetUser(t, m, "user@pm.me", 0, "")
checkBridgeGetUser(t, m, "users@pm.me", 1, "") checkUsersGetUser(t, m, "users@pm.me", 1, "")
checkBridgeGetUser(t, m, "anotheruser@pm.me", 1, "") checkUsersGetUser(t, m, "anotheruser@pm.me", 1, "")
checkBridgeGetUser(t, m, "alsouser@pm.me", 1, "") checkUsersGetUser(t, m, "alsouser@pm.me", 1, "")
} }
func TestDeleteUser(t *testing.T) { func TestDeleteUser(t *testing.T) {
@ -78,8 +78,8 @@ func TestDeleteUser(t *testing.T) {
m.clientManager.EXPECT().GetClient("user").Return(m.pmapiClient).MinTimes(1) m.clientManager.EXPECT().GetClient("user").Return(m.pmapiClient).MinTimes(1)
m.clientManager.EXPECT().GetClient("users").Return(m.pmapiClient).MinTimes(1) m.clientManager.EXPECT().GetClient("users").Return(m.pmapiClient).MinTimes(1)
bridge := testNewBridgeWithUsers(t, m) users := testNewUsersWithUsers(t, m)
defer cleanUpBridgeUserData(bridge) defer cleanUpUsersData(users)
gomock.InOrder( gomock.InOrder(
m.pmapiClient.EXPECT().Logout().Return(), m.pmapiClient.EXPECT().Logout().Return(),
@ -90,9 +90,9 @@ func TestDeleteUser(t *testing.T) {
m.eventListener.EXPECT().Emit(events.CloseConnectionEvent, "user@pm.me") m.eventListener.EXPECT().Emit(events.CloseConnectionEvent, "user@pm.me")
err := bridge.DeleteUser("user", true) err := users.DeleteUser("user", true)
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, 1, len(bridge.users)) assert.Equal(t, 1, len(users.users))
} }
// Even when logout fails, delete is done. // Even when logout fails, delete is done.
@ -103,8 +103,8 @@ func TestDeleteUserWithFailingLogout(t *testing.T) {
m.clientManager.EXPECT().GetClient("user").Return(m.pmapiClient).MinTimes(1) m.clientManager.EXPECT().GetClient("user").Return(m.pmapiClient).MinTimes(1)
m.clientManager.EXPECT().GetClient("users").Return(m.pmapiClient).MinTimes(1) m.clientManager.EXPECT().GetClient("users").Return(m.pmapiClient).MinTimes(1)
bridge := testNewBridgeWithUsers(t, m) users := testNewUsersWithUsers(t, m)
defer cleanUpBridgeUserData(bridge) defer cleanUpUsersData(users)
gomock.InOrder( gomock.InOrder(
m.pmapiClient.EXPECT().Logout().Return(), m.pmapiClient.EXPECT().Logout().Return(),
@ -116,16 +116,16 @@ func TestDeleteUserWithFailingLogout(t *testing.T) {
m.eventListener.EXPECT().Emit(events.CloseConnectionEvent, "user@pm.me") m.eventListener.EXPECT().Emit(events.CloseConnectionEvent, "user@pm.me")
err := bridge.DeleteUser("user", true) err := users.DeleteUser("user", true)
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, 1, len(bridge.users)) assert.Equal(t, 1, len(users.users))
} }
func checkBridgeGetUser(t *testing.T, m mocks, query string, index int, expectedError string) { func checkUsersGetUser(t *testing.T, m mocks, query string, index int, expectedError string) {
bridge := testNewBridgeWithUsers(t, m) users := testNewUsersWithUsers(t, m)
defer cleanUpBridgeUserData(bridge) defer cleanUpUsersData(users)
user, err := bridge.GetUser(query) user, err := users.GetUser(query)
waitForEvents() waitForEvents()
if expectedError != "" { if expectedError != "" {
@ -136,7 +136,7 @@ func checkBridgeGetUser(t *testing.T, m mocks, query string, index int, expected
var expectedUser *User var expectedUser *User
if index >= 0 { if index >= 0 {
expectedUser = bridge.users[index] expectedUser = users.users[index]
} }
assert.Equal(m.t, expectedUser, user) assert.Equal(m.t, expectedUser, user)

View File

@ -15,27 +15,27 @@
// You should have received a copy of the GNU General Public License // You should have received a copy of the GNU General Public License
// along with ProtonMail Bridge. If not, see <https://www.gnu.org/licenses/>. // along with ProtonMail Bridge. If not, see <https://www.gnu.org/licenses/>.
package bridge package users
import ( import (
"errors" "errors"
"testing" "testing"
"github.com/ProtonMail/proton-bridge/internal/bridge/credentials"
"github.com/ProtonMail/proton-bridge/internal/events" "github.com/ProtonMail/proton-bridge/internal/events"
"github.com/ProtonMail/proton-bridge/internal/metrics" "github.com/ProtonMail/proton-bridge/internal/metrics"
"github.com/ProtonMail/proton-bridge/internal/users/credentials"
"github.com/ProtonMail/proton-bridge/pkg/pmapi" "github.com/ProtonMail/proton-bridge/pkg/pmapi"
gomock "github.com/golang/mock/gomock" gomock "github.com/golang/mock/gomock"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
) )
func TestBridgeFinishLoginBadMailboxPassword(t *testing.T) { func TestUsersFinishLoginBadMailboxPassword(t *testing.T) {
m := initMocks(t) m := initMocks(t)
defer m.ctrl.Finish() defer m.ctrl.Finish()
err := errors.New("bad password") err := errors.New("bad password")
gomock.InOrder( gomock.InOrder(
// Init bridge with no user from keychain. // Init users with no user from keychain.
m.credentialsStore.EXPECT().List().Return([]string{}, nil), m.credentialsStore.EXPECT().List().Return([]string{}, nil),
// Set up mocks for FinishLogin. // Set up mocks for FinishLogin.
@ -44,16 +44,16 @@ func TestBridgeFinishLoginBadMailboxPassword(t *testing.T) {
m.pmapiClient.EXPECT().Logout(), m.pmapiClient.EXPECT().Logout(),
) )
checkBridgeFinishLogin(t, m, testAuth, testCredentials.MailboxPassword, "", err) checkUsersFinishLogin(t, m, testAuth, testCredentials.MailboxPassword, "", err)
} }
func TestBridgeFinishLoginUpgradeApplication(t *testing.T) { func TestUsersFinishLoginUpgradeApplication(t *testing.T) {
m := initMocks(t) m := initMocks(t)
defer m.ctrl.Finish() defer m.ctrl.Finish()
err := errors.New("Cannot logout when upgrade needed") err := errors.New("Cannot logout when upgrade needed")
gomock.InOrder( gomock.InOrder(
// Init bridge with no user from keychain. // Init users with no user from keychain.
m.credentialsStore.EXPECT().List().Return([]string{}, nil), m.credentialsStore.EXPECT().List().Return([]string{}, nil),
// Set up mocks for FinishLogin. // Set up mocks for FinishLogin.
@ -64,7 +64,7 @@ func TestBridgeFinishLoginUpgradeApplication(t *testing.T) {
m.pmapiClient.EXPECT().Logout(), m.pmapiClient.EXPECT().Logout(),
) )
checkBridgeFinishLogin(t, m, testAuth, testCredentials.MailboxPassword, "", pmapi.ErrUpgradeApplication) checkUsersFinishLogin(t, m, testAuth, testCredentials.MailboxPassword, "", pmapi.ErrUpgradeApplication)
} }
func refreshWithToken(token string) *pmapi.Auth { func refreshWithToken(token string) *pmapi.Auth {
@ -81,7 +81,7 @@ func credentialsWithToken(token string) *credentials.Credentials {
return tmp return tmp
} }
func TestBridgeFinishLoginNewUser(t *testing.T) { func TestUsersFinishLoginNewUser(t *testing.T) {
m := initMocks(t) m := initMocks(t)
defer m.ctrl.Finish() defer m.ctrl.Finish()
@ -89,7 +89,7 @@ func TestBridgeFinishLoginNewUser(t *testing.T) {
m.clientManager.EXPECT().GetClient("user").Return(m.pmapiClient).MinTimes(1) m.clientManager.EXPECT().GetClient("user").Return(m.pmapiClient).MinTimes(1)
gomock.InOrder( gomock.InOrder(
// bridge.New() finds no users in keychain. // users.New() finds no users in keychain.
m.credentialsStore.EXPECT().List().Return([]string{}, nil), m.credentialsStore.EXPECT().List().Return([]string{}, nil),
// getAPIUser() loads user info from API (e.g. userID). // getAPIUser() loads user info from API (e.g. userID).
@ -128,12 +128,12 @@ func TestBridgeFinishLoginNewUser(t *testing.T) {
mockEventLoopNoAction(m) mockEventLoopNoAction(m)
user := checkBridgeFinishLogin(t, m, testAuth, testCredentials.MailboxPassword, "user", nil) user := checkUsersFinishLogin(t, m, testAuth, testCredentials.MailboxPassword, "user", nil)
mockAuthUpdate(user, "afterCredentials", m) mockAuthUpdate(user, "afterCredentials", m)
} }
func TestBridgeFinishLoginExistingDisconnectedUser(t *testing.T) { func TestUsersFinishLoginExistingDisconnectedUser(t *testing.T) {
m := initMocks(t) m := initMocks(t)
defer m.ctrl.Finish() defer m.ctrl.Finish()
@ -144,7 +144,7 @@ func TestBridgeFinishLoginExistingDisconnectedUser(t *testing.T) {
m.clientManager.EXPECT().GetClient("user").Return(m.pmapiClient).MinTimes(1) m.clientManager.EXPECT().GetClient("user").Return(m.pmapiClient).MinTimes(1)
gomock.InOrder( gomock.InOrder(
// bridge.New() finds one existing user in keychain. // users.New() finds one existing user in keychain.
m.credentialsStore.EXPECT().List().Return([]string{"user"}, nil), m.credentialsStore.EXPECT().List().Return([]string{"user"}, nil),
// newUser() // newUser()
@ -186,12 +186,12 @@ func TestBridgeFinishLoginExistingDisconnectedUser(t *testing.T) {
mockEventLoopNoAction(m) mockEventLoopNoAction(m)
user := checkBridgeFinishLogin(t, m, testAuth, testCredentials.MailboxPassword, "user", nil) user := checkUsersFinishLogin(t, m, testAuth, testCredentials.MailboxPassword, "user", nil)
mockAuthUpdate(user, "afterCredentials", m) mockAuthUpdate(user, "afterCredentials", m)
} }
func TestBridgeFinishLoginConnectedUser(t *testing.T) { func TestUsersFinishLoginConnectedUser(t *testing.T) {
m := initMocks(t) m := initMocks(t)
defer m.ctrl.Finish() defer m.ctrl.Finish()
@ -201,8 +201,8 @@ func TestBridgeFinishLoginConnectedUser(t *testing.T) {
mockConnectedUser(m) mockConnectedUser(m)
mockEventLoopNoAction(m) mockEventLoopNoAction(m)
bridge := testNewBridge(t, m) users := testNewUsers(t, m)
defer cleanUpBridgeUserData(bridge) defer cleanUpUsersData(users)
// Then, try to log in again... // Then, try to log in again...
gomock.InOrder( gomock.InOrder(
@ -212,15 +212,15 @@ func TestBridgeFinishLoginConnectedUser(t *testing.T) {
m.pmapiClient.EXPECT().Logout(), m.pmapiClient.EXPECT().Logout(),
) )
_, err := bridge.FinishLogin(m.pmapiClient, testAuth, testCredentials.MailboxPassword) _, err := users.FinishLogin(m.pmapiClient, testAuth, testCredentials.MailboxPassword)
assert.Equal(t, "user is already connected", err.Error()) assert.Equal(t, "user is already connected", err.Error())
} }
func checkBridgeFinishLogin(t *testing.T, m mocks, auth *pmapi.Auth, mailboxPassword string, expectedUserID string, expectedErr error) *User { func checkUsersFinishLogin(t *testing.T, m mocks, auth *pmapi.Auth, mailboxPassword string, expectedUserID string, expectedErr error) *User {
bridge := testNewBridge(t, m) users := testNewUsers(t, m)
defer cleanUpBridgeUserData(bridge) defer cleanUpUsersData(users)
user, err := bridge.FinishLogin(m.pmapiClient, auth, mailboxPassword) user, err := users.FinishLogin(m.pmapiClient, auth, mailboxPassword)
waitForEvents() waitForEvents()
@ -228,11 +228,11 @@ func checkBridgeFinishLogin(t *testing.T, m mocks, auth *pmapi.Auth, mailboxPass
if expectedUserID != "" { if expectedUserID != "" {
assert.Equal(t, expectedUserID, user.ID()) assert.Equal(t, expectedUserID, user.ID())
assert.Equal(t, 1, len(bridge.users)) assert.Equal(t, 1, len(users.users))
assert.Equal(t, expectedUserID, bridge.users[0].ID()) assert.Equal(t, expectedUserID, users.users[0].ID())
} else { } else {
assert.Equal(t, (*User)(nil), user) assert.Equal(t, (*User)(nil), user)
assert.Equal(t, 0, len(bridge.users)) assert.Equal(t, 0, len(users.users))
} }
return user return user

View File

@ -15,40 +15,40 @@
// You should have received a copy of the GNU General Public License // You should have received a copy of the GNU General Public License
// along with ProtonMail Bridge. If not, see <https://www.gnu.org/licenses/>. // along with ProtonMail Bridge. If not, see <https://www.gnu.org/licenses/>.
package bridge package users
import ( import (
"errors" "errors"
"testing" "testing"
credentials "github.com/ProtonMail/proton-bridge/internal/bridge/credentials"
"github.com/ProtonMail/proton-bridge/internal/events" "github.com/ProtonMail/proton-bridge/internal/events"
"github.com/ProtonMail/proton-bridge/internal/metrics" "github.com/ProtonMail/proton-bridge/internal/metrics"
"github.com/ProtonMail/proton-bridge/internal/preferences" "github.com/ProtonMail/proton-bridge/internal/preferences"
"github.com/ProtonMail/proton-bridge/internal/users/credentials"
"github.com/ProtonMail/proton-bridge/pkg/pmapi" "github.com/ProtonMail/proton-bridge/pkg/pmapi"
gomock "github.com/golang/mock/gomock" gomock "github.com/golang/mock/gomock"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
) )
func TestNewBridgeNoKeychain(t *testing.T) { func TestNewUsersNoKeychain(t *testing.T) {
m := initMocks(t) m := initMocks(t)
defer m.ctrl.Finish() defer m.ctrl.Finish()
m.credentialsStore.EXPECT().List().Return([]string{}, errors.New("no keychain")) m.credentialsStore.EXPECT().List().Return([]string{}, errors.New("no keychain"))
checkBridgeNew(t, m, []*credentials.Credentials{}) checkUsersNew(t, m, []*credentials.Credentials{})
} }
func TestNewBridgeWithoutUsersInCredentialsStore(t *testing.T) { func TestNewUsersWithoutUsersInCredentialsStore(t *testing.T) {
m := initMocks(t) m := initMocks(t)
defer m.ctrl.Finish() defer m.ctrl.Finish()
m.credentialsStore.EXPECT().List().Return([]string{}, nil) m.credentialsStore.EXPECT().List().Return([]string{}, nil)
checkBridgeNew(t, m, []*credentials.Credentials{}) checkUsersNew(t, m, []*credentials.Credentials{})
} }
func TestNewBridgeWithDisconnectedUser(t *testing.T) { func TestNewUsersWithDisconnectedUser(t *testing.T) {
m := initMocks(t) m := initMocks(t)
defer m.ctrl.Finish() defer m.ctrl.Finish()
@ -63,10 +63,10 @@ func TestNewBridgeWithDisconnectedUser(t *testing.T) {
m.pmapiClient.EXPECT().Addresses().Return(nil), m.pmapiClient.EXPECT().Addresses().Return(nil),
) )
checkBridgeNew(t, m, []*credentials.Credentials{testCredentialsDisconnected}) checkUsersNew(t, m, []*credentials.Credentials{testCredentialsDisconnected})
} }
func TestNewBridgeWithConnectedUserWithBadToken(t *testing.T) { func TestNewUsersWithConnectedUserWithBadToken(t *testing.T) {
m := initMocks(t) m := initMocks(t)
defer m.ctrl.Finish() defer m.ctrl.Finish()
@ -85,7 +85,7 @@ func TestNewBridgeWithConnectedUserWithBadToken(t *testing.T) {
m.credentialsStore.EXPECT().Get("user").Return(testCredentialsDisconnected, nil) m.credentialsStore.EXPECT().Get("user").Return(testCredentialsDisconnected, nil)
m.eventListener.EXPECT().Emit(events.CloseConnectionEvent, "user@pm.me") m.eventListener.EXPECT().Emit(events.CloseConnectionEvent, "user@pm.me")
checkBridgeNew(t, m, []*credentials.Credentials{testCredentialsDisconnected}) checkUsersNew(t, m, []*credentials.Credentials{testCredentialsDisconnected})
} }
func mockConnectedUser(m mocks) { func mockConnectedUser(m mocks) {
@ -105,9 +105,9 @@ func mockConnectedUser(m mocks) {
) )
} }
// mockAuthUpdate simulates bridge calling UpdateAuthToken on the given user. // mockAuthUpdate simulates users calling UpdateAuthToken on the given user.
// This would normally be done by Bridge when it receives an auth from the ClientManager, // This would normally be done by users when it receives an auth from the ClientManager,
// but as we don't have a full bridge instance here, we do this manually. // but as we don't have a full users instance here, we do this manually.
func mockAuthUpdate(user *User, token string, m mocks) { func mockAuthUpdate(user *User, token string, m mocks) {
gomock.InOrder( gomock.InOrder(
m.credentialsStore.EXPECT().UpdateToken("user", ":"+token).Return(nil), m.credentialsStore.EXPECT().UpdateToken("user", ":"+token).Return(nil),
@ -119,7 +119,7 @@ func mockAuthUpdate(user *User, token string, m mocks) {
waitForEvents() waitForEvents()
} }
func TestNewBridgeWithConnectedUser(t *testing.T) { func TestNewUsersWithConnectedUser(t *testing.T) {
m := initMocks(t) m := initMocks(t)
defer m.ctrl.Finish() defer m.ctrl.Finish()
@ -129,12 +129,12 @@ func TestNewBridgeWithConnectedUser(t *testing.T) {
mockConnectedUser(m) mockConnectedUser(m)
mockEventLoopNoAction(m) mockEventLoopNoAction(m)
checkBridgeNew(t, m, []*credentials.Credentials{testCredentials}) checkUsersNew(t, m, []*credentials.Credentials{testCredentials})
} }
// Tests two users with different states and checks also the order from // Tests two users with different states and checks also the order from
// credentials store is kept also in array of Bridge users. // credentials store is kept also in array of users.
func TestNewBridgeWithUsers(t *testing.T) { func TestNewUsersWithUsers(t *testing.T) {
m := initMocks(t) m := initMocks(t)
defer m.ctrl.Finish() defer m.ctrl.Finish()
@ -155,10 +155,10 @@ func TestNewBridgeWithUsers(t *testing.T) {
mockEventLoopNoAction(m) mockEventLoopNoAction(m)
checkBridgeNew(t, m, []*credentials.Credentials{testCredentialsDisconnected, testCredentials}) checkUsersNew(t, m, []*credentials.Credentials{testCredentialsDisconnected, testCredentials})
} }
func TestNewBridgeFirstStart(t *testing.T) { func TestNewUsersFirstStart(t *testing.T) {
m := initMocks(t) m := initMocks(t)
defer m.ctrl.Finish() defer m.ctrl.Finish()
@ -170,17 +170,17 @@ func TestNewBridgeFirstStart(t *testing.T) {
m.pmapiClient.EXPECT().Logout(), m.pmapiClient.EXPECT().Logout(),
) )
testNewBridge(t, m) testNewUsers(t, m)
} }
func checkBridgeNew(t *testing.T, m mocks, expectedCredentials []*credentials.Credentials) { func checkUsersNew(t *testing.T, m mocks, expectedCredentials []*credentials.Credentials) {
bridge := testNewBridge(t, m) users := testNewUsers(t, m)
defer cleanUpBridgeUserData(bridge) defer cleanUpUsersData(users)
assert.Equal(m.t, len(expectedCredentials), len(bridge.GetUsers())) assert.Equal(m.t, len(expectedCredentials), len(users.GetUsers()))
credentials := []*credentials.Credentials{} credentials := []*credentials.Credentials{}
for _, user := range bridge.users { for _, user := range users.users {
credentials = append(credentials, user.creds) credentials = append(credentials, user.creds)
} }

View File

@ -15,7 +15,7 @@
// You should have received a copy of the GNU General Public License // You should have received a copy of the GNU General Public License
// along with ProtonMail Bridge. If not, see <https://www.gnu.org/licenses/>. // along with ProtonMail Bridge. If not, see <https://www.gnu.org/licenses/>.
package bridge package users
import ( import (
"fmt" "fmt"
@ -25,12 +25,12 @@ import (
"testing" "testing"
"time" "time"
"github.com/ProtonMail/proton-bridge/internal/bridge/credentials"
bridgemocks "github.com/ProtonMail/proton-bridge/internal/bridge/mocks"
"github.com/ProtonMail/proton-bridge/internal/events" "github.com/ProtonMail/proton-bridge/internal/events"
"github.com/ProtonMail/proton-bridge/internal/metrics" "github.com/ProtonMail/proton-bridge/internal/metrics"
"github.com/ProtonMail/proton-bridge/internal/preferences" "github.com/ProtonMail/proton-bridge/internal/preferences"
"github.com/ProtonMail/proton-bridge/internal/store" "github.com/ProtonMail/proton-bridge/internal/store"
"github.com/ProtonMail/proton-bridge/internal/users/credentials"
usersmocks "github.com/ProtonMail/proton-bridge/internal/users/mocks"
"github.com/ProtonMail/proton-bridge/pkg/pmapi" "github.com/ProtonMail/proton-bridge/pkg/pmapi"
pmapimocks "github.com/ProtonMail/proton-bridge/pkg/pmapi/mocks" pmapimocks "github.com/ProtonMail/proton-bridge/pkg/pmapi/mocks"
gomock "github.com/golang/mock/gomock" gomock "github.com/golang/mock/gomock"
@ -131,11 +131,11 @@ type mocks struct {
t *testing.T t *testing.T
ctrl *gomock.Controller ctrl *gomock.Controller
config *bridgemocks.MockConfiger config *usersmocks.MockConfiger
PanicHandler *bridgemocks.MockPanicHandler PanicHandler *usersmocks.MockPanicHandler
prefProvider *bridgemocks.MockPreferenceProvider prefProvider *usersmocks.MockPreferenceProvider
clientManager *bridgemocks.MockClientManager clientManager *usersmocks.MockClientManager
credentialsStore *bridgemocks.MockCredentialsStorer credentialsStore *usersmocks.MockCredentialsStorer
eventListener *MockListener eventListener *MockListener
pmapiClient *pmapimocks.MockClient pmapiClient *pmapimocks.MockClient
@ -172,11 +172,11 @@ func initMocks(t *testing.T) mocks {
t: t, t: t,
ctrl: mockCtrl, ctrl: mockCtrl,
config: bridgemocks.NewMockConfiger(mockCtrl), config: usersmocks.NewMockConfiger(mockCtrl),
PanicHandler: bridgemocks.NewMockPanicHandler(mockCtrl), PanicHandler: usersmocks.NewMockPanicHandler(mockCtrl),
prefProvider: bridgemocks.NewMockPreferenceProvider(mockCtrl), prefProvider: usersmocks.NewMockPreferenceProvider(mockCtrl),
clientManager: bridgemocks.NewMockClientManager(mockCtrl), clientManager: usersmocks.NewMockClientManager(mockCtrl),
credentialsStore: bridgemocks.NewMockCredentialsStorer(mockCtrl), credentialsStore: usersmocks.NewMockCredentialsStorer(mockCtrl),
eventListener: NewMockListener(mockCtrl), eventListener: NewMockListener(mockCtrl),
pmapiClient: pmapimocks.NewMockClient(mockCtrl), pmapiClient: pmapimocks.NewMockClient(mockCtrl),
@ -195,7 +195,7 @@ func initMocks(t *testing.T) mocks {
return m return m
} }
func testNewBridgeWithUsers(t *testing.T, m mocks) *Bridge { func testNewUsersWithUsers(t *testing.T, m mocks) *Users {
// Events are asynchronous // Events are asynchronous
m.pmapiClient.EXPECT().GetEvent("").Return(testPMAPIEvent, nil).Times(2) m.pmapiClient.EXPECT().GetEvent("").Return(testPMAPIEvent, nil).Times(2)
m.pmapiClient.EXPECT().GetEvent(testPMAPIEvent.EventID).Return(testPMAPIEvent, nil).Times(2) m.pmapiClient.EXPECT().GetEvent(testPMAPIEvent.EventID).Return(testPMAPIEvent, nil).Times(2)
@ -225,18 +225,18 @@ func testNewBridgeWithUsers(t *testing.T, m mocks) *Bridge {
m.pmapiClient.EXPECT().Addresses().Return(testPMAPIAddresses), m.pmapiClient.EXPECT().Addresses().Return(testPMAPIAddresses),
) )
bridge := testNewBridge(t, m) users := testNewUsers(t, m)
user, _ := bridge.GetUser("user") user, _ := users.GetUser("user")
mockAuthUpdate(user, "reftok", m) mockAuthUpdate(user, "reftok", m)
users, _ := bridge.GetUser("user") user, _ = users.GetUser("user")
mockAuthUpdate(users, "reftok", m) mockAuthUpdate(user, "reftok", m)
return bridge return users
} }
func testNewBridge(t *testing.T, m mocks) *Bridge { func testNewUsers(t *testing.T, m mocks) *Users {
cacheFile, err := ioutil.TempFile("", "bridge-store-cache-*.db") cacheFile, err := ioutil.TempFile("", "bridge-store-cache-*.db")
require.NoError(t, err, "could not get temporary file for store cache") require.NoError(t, err, "could not get temporary file for store cache")
@ -248,14 +248,14 @@ func testNewBridge(t *testing.T, m mocks) *Bridge {
m.eventListener.EXPECT().Add(events.UpgradeApplicationEvent, gomock.Any()) m.eventListener.EXPECT().Add(events.UpgradeApplicationEvent, gomock.Any())
m.clientManager.EXPECT().GetAuthUpdateChannel().Return(make(chan pmapi.ClientAuth)) m.clientManager.EXPECT().GetAuthUpdateChannel().Return(make(chan pmapi.ClientAuth))
bridge := New(m.config, m.prefProvider, m.PanicHandler, m.eventListener, m.clientManager, m.credentialsStore) users := New(m.config, m.prefProvider, m.PanicHandler, m.eventListener, m.clientManager, m.credentialsStore)
waitForEvents() waitForEvents()
return bridge return users
} }
func cleanUpBridgeUserData(b *Bridge) { func cleanUpUsersData(b *Users) {
for _, user := range b.users { for _, user := range b.users {
_ = user.clearStore() _ = user.clearStore()
} }
@ -268,8 +268,8 @@ func TestClearData(t *testing.T) {
m.clientManager.EXPECT().GetClient("user").Return(m.pmapiClient).MinTimes(1) m.clientManager.EXPECT().GetClient("user").Return(m.pmapiClient).MinTimes(1)
m.clientManager.EXPECT().GetClient("users").Return(m.pmapiClient).MinTimes(1) m.clientManager.EXPECT().GetClient("users").Return(m.pmapiClient).MinTimes(1)
bridge := testNewBridgeWithUsers(t, m) users := testNewUsersWithUsers(t, m)
defer cleanUpBridgeUserData(bridge) defer cleanUpUsersData(users)
m.eventListener.EXPECT().Emit(events.CloseConnectionEvent, "user@pm.me") m.eventListener.EXPECT().Emit(events.CloseConnectionEvent, "user@pm.me")
m.eventListener.EXPECT().Emit(events.CloseConnectionEvent, "users@pm.me") m.eventListener.EXPECT().Emit(events.CloseConnectionEvent, "users@pm.me")
@ -286,7 +286,7 @@ func TestClearData(t *testing.T) {
m.config.EXPECT().ClearData().Return(nil) m.config.EXPECT().ClearData().Return(nil)
require.NoError(t, bridge.ClearData()) require.NoError(t, users.ClearData())
waitForEvents() waitForEvents()
} }

View File

@ -15,7 +15,7 @@
// You should have received a copy of the GNU General Public License // You should have received a copy of the GNU General Public License
// along with ProtonMail Bridge. If not, see <https://www.gnu.org/licenses/>. // along with ProtonMail Bridge. If not, see <https://www.gnu.org/licenses/>.
package bridge package users
// IsAuthorized returns whether the user has received an Auth from the API yet. // IsAuthorized returns whether the user has received an Auth from the API yet.
func (u *User) IsAuthorized() bool { func (u *User) IsAuthorized() bool {

View File

@ -20,6 +20,7 @@ package context
import ( import (
"github.com/ProtonMail/proton-bridge/internal/bridge" "github.com/ProtonMail/proton-bridge/internal/bridge"
"github.com/ProtonMail/proton-bridge/internal/preferences" "github.com/ProtonMail/proton-bridge/internal/preferences"
"github.com/ProtonMail/proton-bridge/internal/users"
"github.com/ProtonMail/proton-bridge/pkg/listener" "github.com/ProtonMail/proton-bridge/pkg/listener"
) )
@ -57,9 +58,9 @@ func (ctx *TestContext) RestartBridge() error {
func newBridgeInstance( func newBridgeInstance(
t *bddT, t *bddT,
cfg *fakeConfig, cfg *fakeConfig,
credStore bridge.CredentialsStorer, credStore users.CredentialsStorer,
eventListener listener.Listener, eventListener listener.Listener,
clientManager bridge.ClientManager, clientManager users.ClientManager,
) *bridge.Bridge { ) *bridge.Bridge {
panicHandler := &panicHandler{t: t} panicHandler := &panicHandler{t: t}
pref := preferences.New(cfg) pref := preferences.New(cfg)

View File

@ -23,8 +23,8 @@ import (
"path/filepath" "path/filepath"
"time" "time"
"github.com/ProtonMail/proton-bridge/internal/bridge"
"github.com/ProtonMail/proton-bridge/internal/store" "github.com/ProtonMail/proton-bridge/internal/store"
"github.com/ProtonMail/proton-bridge/internal/users"
"github.com/ProtonMail/proton-bridge/pkg/srp" "github.com/ProtonMail/proton-bridge/pkg/srp"
"github.com/pkg/errors" "github.com/pkg/errors"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
@ -56,7 +56,7 @@ func (ctx *TestContext) LoginUser(username, password, mailboxPassword string) (e
} }
// GetUser retrieves the bridge user matching the given query string. // GetUser retrieves the bridge user matching the given query string.
func (ctx *TestContext) GetUser(username string) (*bridge.User, error) { func (ctx *TestContext) GetUser(username string) (*users.User, error) {
return ctx.bridge.GetUser(username) return ctx.bridge.GetUser(username)
} }

View File

@ -20,6 +20,7 @@ package context
import ( import (
"github.com/ProtonMail/proton-bridge/internal/bridge" "github.com/ProtonMail/proton-bridge/internal/bridge"
"github.com/ProtonMail/proton-bridge/internal/users"
"github.com/ProtonMail/proton-bridge/pkg/listener" "github.com/ProtonMail/proton-bridge/pkg/listener"
"github.com/ProtonMail/proton-bridge/pkg/pmapi" "github.com/ProtonMail/proton-bridge/pkg/pmapi"
"github.com/ProtonMail/proton-bridge/test/accounts" "github.com/ProtonMail/proton-bridge/test/accounts"
@ -48,7 +49,7 @@ type TestContext struct {
// Bridge core related variables. // Bridge core related variables.
bridge *bridge.Bridge bridge *bridge.Bridge
bridgeLastError error bridgeLastError error
credStore bridge.CredentialsStorer credStore users.CredentialsStorer
// IMAP related variables. // IMAP related variables.
imapAddr string imapAddr string

View File

@ -20,7 +20,7 @@ package context
import ( import (
"strings" "strings"
"github.com/ProtonMail/proton-bridge/internal/bridge/credentials" "github.com/ProtonMail/proton-bridge/internal/users/credentials"
) )
// bridgePassword is password to be used for IMAP or SMTP under tests. // bridgePassword is password to be used for IMAP or SMTP under tests.

View File

@ -12,7 +12,7 @@ Feature: IMAP auth
Scenario: Authenticates with disconnected user Scenario: Authenticates with disconnected user
Given there is disconnected user "user" Given there is disconnected user "user"
When IMAP client authenticates "user" When IMAP client authenticates "user"
Then IMAP response is "IMAP error: NO bridge account is logged out, use bridge to login again" Then IMAP response is "IMAP error: NO account is logged out, use the app to login again"
Scenario: Authenticates with connected user that was loaded without internet Scenario: Authenticates with connected user that was loaded without internet
Given there is connected user "user" Given there is connected user "user"
@ -31,13 +31,13 @@ Feature: IMAP auth
Given there is connected user "user" Given there is connected user "user"
When "user" logs out from bridge When "user" logs out from bridge
And IMAP client authenticates "user" And IMAP client authenticates "user"
Then IMAP response is "IMAP error: NO bridge account is logged out, use bridge to login again" Then IMAP response is "IMAP error: NO account is logged out, use the app to login again"
Scenario: Authenticates user which was re-logged in Scenario: Authenticates user which was re-logged in
Given there is connected user "user" Given there is connected user "user"
When "user" logs out from bridge When "user" logs out from bridge
And IMAP client authenticates "user" And IMAP client authenticates "user"
Then IMAP response is "IMAP error: NO bridge account is logged out, use bridge to login again" Then IMAP response is "IMAP error: NO account is logged out, use the app to login again"
When "user" logs in to bridge When "user" logs in to bridge
And IMAP client authenticates "user" And IMAP client authenticates "user"
Then IMAP response is "OK" Then IMAP response is "OK"

View File

@ -19,7 +19,7 @@ Feature: SMTP auth
Scenario: Authenticates with disconnected user Scenario: Authenticates with disconnected user
Given there is disconnected user "user" Given there is disconnected user "user"
When SMTP client authenticates "user" When SMTP client authenticates "user"
Then SMTP response is "SMTP error: 454 bridge account is logged out, use bridge to login again" Then SMTP response is "SMTP error: 454 account is logged out, use the app to login again"
Scenario: Authenticates with no user Scenario: Authenticates with no user
When SMTP client authenticates with username "user@pm.me" and password "bridgepassword" When SMTP client authenticates with username "user@pm.me" and password "bridgepassword"