feat(GODT-2801): Identity State Cloning & Auth Check

This patch moves the `user.User.CheckAuth` function into the `State`
type as it contains all the necessary information to perform this check.

A couple of more interfaces are added to abstract the retrieval of the
Bridge and the User's key passwords, as well as telemetry reports.

Finally, adds `State.OnAddressEvents` which other services should use to
update the state.
This commit is contained in:
Leander Beernaert
2023-07-24 17:00:57 +02:00
parent bdbf1bdd76
commit 7be1a8ae8a
5 changed files with 119 additions and 25 deletions

View File

@ -0,0 +1,26 @@
// Copyright (c) 2023 Proton AG
//
// This file is part of Proton Mail Bridge.
//
// Proton Mail 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.
//
// Proton Mail 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 Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
package useridentity
type KeyPassProvider interface {
KeyPass() []byte
}
type BridgePassProvider interface {
BridgePass() []byte
}

View File

@ -51,33 +51,25 @@ type Service struct {
} }
func NewService( func NewService(
ctx context.Context,
service userevents.Subscribable, service userevents.Subscribable,
user proton.User,
eventPublisher events.EventPublisher, eventPublisher events.EventPublisher,
provider IdentityProvider, state *State,
) (*Service, error) { ) *Service {
addresses, err := provider.GetAddresses(ctx) subscriberName := fmt.Sprintf("identity-%v", state.User.ID)
if err != nil {
return nil, fmt.Errorf("failed to get addresses: %w", err)
}
subscriberName := fmt.Sprintf("identity-%v", user.ID)
return &Service{ return &Service{
eventService: service, eventService: service,
identity: NewState(user, addresses, provider), identity: *state,
eventPublisher: eventPublisher, eventPublisher: eventPublisher,
log: logrus.WithFields(logrus.Fields{ log: logrus.WithFields(logrus.Fields{
"service": "user-identity", "service": "user-identity",
"user": user.ID, "user": state.User.ID,
}), }),
userSubscriber: userevents.NewUserSubscriber(subscriberName), userSubscriber: userevents.NewUserSubscriber(subscriberName),
refreshSubscriber: userevents.NewRefreshSubscriber(subscriberName), refreshSubscriber: userevents.NewRefreshSubscriber(subscriberName),
addressSubscriber: userevents.NewAddressSubscriber(subscriberName), addressSubscriber: userevents.NewAddressSubscriber(subscriberName),
usedSpaceSubscriber: userevents.NewUserUsedSpaceSubscriber(subscriberName), usedSpaceSubscriber: userevents.NewUserUsedSpaceSubscriber(subscriberName),
}, nil }
} }
func (s *Service) Start(group *async.Group) { func (s *Service) Start(group *async.Group) {

View File

@ -356,17 +356,13 @@ func TestService_OnAddressDeletedUnknownDoesNotProduceEvent(t *testing.T) {
require.NoError(t, err) require.NoError(t, err)
} }
func newTestService(t *testing.T, mockCtrl *gomock.Controller) (*Service, *mocks2.MockEventPublisher, *mocks.MockIdentityProvider) { func newTestService(_ *testing.T, mockCtrl *gomock.Controller) (*Service, *mocks2.MockEventPublisher, *mocks.MockIdentityProvider) {
subscribable := &userevents.NoOpSubscribable{} subscribable := &userevents.NoOpSubscribable{}
eventPublisher := mocks2.NewMockEventPublisher(mockCtrl) eventPublisher := mocks2.NewMockEventPublisher(mockCtrl)
provider := mocks.NewMockIdentityProvider(mockCtrl) provider := mocks.NewMockIdentityProvider(mockCtrl)
user := newTestUser() user := newTestUser()
provider.EXPECT().GetAddresses(gomock.Any()).Times(1).Return(newTestAddresses(), nil) service := NewService(subscribable, eventPublisher, NewState(user, newTestAddresses(), provider))
service, err := NewService(context.Background(), subscribable, user, eventPublisher, provider)
require.NoError(t, err)
return service, eventPublisher, provider return service, eventPublisher, provider
} }

View File

@ -19,12 +19,15 @@ package useridentity
import ( import (
"context" "context"
"crypto/subtle"
"fmt" "fmt"
"strings" "strings"
"github.com/ProtonMail/go-proton-api" "github.com/ProtonMail/go-proton-api"
"github.com/ProtonMail/proton-bridge/v3/internal/usertypes" "github.com/ProtonMail/proton-bridge/v3/internal/usertypes"
"github.com/ProtonMail/proton-bridge/v3/pkg/algo"
"golang.org/x/exp/maps" "golang.org/x/exp/maps"
"golang.org/x/exp/slices"
) )
// State holds all the required user identity state. The idea of this type is that // State holds all the required user identity state. The idea of this type is that
@ -42,9 +45,9 @@ func NewState(
user proton.User, user proton.User,
addresses []proton.Address, addresses []proton.Address,
provider IdentityProvider, provider IdentityProvider,
) State { ) *State {
addressMap := buildAddressMapFromSlice(addresses) addressMap := buildAddressMapFromSlice(addresses)
return State{ return &State{
AddressesSorted: sortAddresses(maps.Values(addressMap)), AddressesSorted: sortAddresses(maps.Values(addressMap)),
Addresses: addressMap, Addresses: addressMap,
User: user, User: user,
@ -53,15 +56,15 @@ func NewState(
} }
} }
func NewStateFromProvider(ctx context.Context, provider IdentityProvider) (State, error) { func NewStateFromProvider(ctx context.Context, provider IdentityProvider) (*State, error) {
user, err := provider.GetUser(ctx) user, err := provider.GetUser(ctx)
if err != nil { if err != nil {
return State{}, fmt.Errorf("failed to get user: %w", err) return nil, fmt.Errorf("failed to get user: %w", err)
} }
addresses, err := provider.GetAddresses(ctx) addresses, err := provider.GetAddresses(ctx)
if err != nil { if err != nil {
return State{}, fmt.Errorf("failed to get user addresses: %w", err) return nil, fmt.Errorf("failed to get user addresses: %w", err)
} }
return NewState(user, addresses, provider), nil return NewState(user, addresses, provider), nil
@ -185,3 +188,58 @@ func (s *State) OnAddressDeleted(event proton.AddressEvent) (proton.Address, Add
return addr, AddressUpdateDeleted return addr, AddressUpdateDeleted
} }
func (s *State) OnAddressEvents(events []proton.AddressEvent) {
for _, evt := range events {
switch evt.Action {
case proton.EventCreate:
s.OnAddressCreated(evt)
case proton.EventUpdate, proton.EventUpdateFlags:
s.OnAddressUpdated(evt)
case proton.EventDelete:
s.OnAddressDeleted(evt)
}
}
}
func (s *State) Clone() *State {
return &State{
AddressesSorted: slices.Clone(s.AddressesSorted),
Addresses: maps.Clone(s.Addresses),
User: s.User,
provider: s.provider,
}
}
// CheckAuth returns whether the given email and password can be used to authenticate over IMAP or SMTP with this user.
// It returns the address ID of the authenticated address.
func (s *State) CheckAuth(email string, password []byte, bridgePassProvider BridgePassProvider, telemetry Telemetry) (string, error) {
if email == "crash@bandicoot" {
panic("your wish is my command.. I crash")
}
dec, err := algo.B64RawDecode(password)
if err != nil {
return "", fmt.Errorf("failed to decode password: %w", err)
}
if subtle.ConstantTimeCompare(bridgePassProvider.BridgePass(), dec) != 1 {
err := fmt.Errorf("invalid password")
if telemetry != nil {
telemetry.ReportConfigStatusFailure(err.Error())
}
return "", err
}
for _, addr := range s.AddressesSorted {
if addr.Status != proton.AddressStatusEnabled {
continue
}
if strings.EqualFold(addr.Email, email) {
return addr.ID, nil
}
}
return "", fmt.Errorf("invalid email")
}

View File

@ -0,0 +1,22 @@
// Copyright (c) 2023 Proton AG
//
// This file is part of Proton Mail Bridge.
//
// Proton Mail 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.
//
// Proton Mail 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 Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
package useridentity
type Telemetry interface {
ReportConfigStatusFailure(errDetails string)
}