Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit fb2d403

Browse files
VitaliiShpitalStorj Robot
authored andcommitted
satellite/{console,mailservice}: added ghost session warning email
Added new email type to be sent via HubSpot API if ghost session is detected. Issue: storj/storj-private#1406 Change-Id: I1c2b89942c7e097793f97bee93a0e08b916008a6
1 parent 03b8b2e commit fb2d403

File tree

5 files changed

+104
-14
lines changed

5 files changed

+104
-14
lines changed

satellite/console/consoleweb/server.go

Lines changed: 84 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import (
2121
"path/filepath"
2222
"strconv"
2323
"strings"
24+
"sync"
2425
"time"
2526

2627
"github.com/gorilla/mux"
@@ -34,10 +35,12 @@ import (
3435
"storj.io/common/http/requestid"
3536
"storj.io/common/memory"
3637
"storj.io/common/storj"
38+
"storj.io/common/uuid"
3739
"storj.io/storj/private/web"
3840
"storj.io/storj/satellite/abtesting"
3941
"storj.io/storj/satellite/analytics"
4042
"storj.io/storj/satellite/console"
43+
"storj.io/storj/satellite/console/consoleauth"
4144
"storj.io/storj/satellite/console/consoleauth/csrf"
4245
"storj.io/storj/satellite/console/consoleauth/sso"
4346
"storj.io/storj/satellite/console/consoleservice"
@@ -131,6 +134,7 @@ type Config struct {
131134
ZipDownloadLimit int `help:"maximum number of objects allowed for a zip format download" default:"1000"`
132135
LiveCheckBadPasswords bool `help:"whether to check if provided password is in bad passwords list" default:"false"`
133136
UseGeneratedPrivateAPI bool `help:"whether to use generated private API" default:"false"`
137+
GhostSessionCheckEnabled bool `help:"whether to enable ghost session detection and notification" default:"false"`
134138

135139
OauthCodeExpiry time.Duration `help:"how long oauth authorization codes are issued for" default:"10m"`
136140
OauthAccessTokenExpiry time.Duration `help:"how long oauth access tokens are issued for" default:"24h"`
@@ -193,6 +197,11 @@ type Server struct {
193197
usagePrices payments.ProjectUsagePriceModel
194198

195199
errorTemplate *template.Template
200+
201+
// ghostSessionEmailSent tracks when ghost session emails were last sent to users.
202+
// Key: userID, Value: timestamp of last email sent
203+
ghostSessionEmailSent map[uuid.UUID]time.Time
204+
ghostSessionEmailMutex sync.Mutex
196205
}
197206

198207
// NewServer creates new instance of console server.
@@ -226,6 +235,7 @@ func NewServer(logger *zap.Logger, config Config, service *console.Service, cons
226235
minimumChargeConfig: minimumChargeConfig,
227236
usagePrices: usagePrices,
228237
objectLockAndVersioningConfig: objectLockAndVersioningConfig,
238+
ghostSessionEmailSent: make(map[uuid.UUID]time.Time),
229239
}
230240

231241
logger.Debug("Starting Satellite Console server.", zap.Stringer("Address", server.listener.Addr()))
@@ -561,6 +571,10 @@ func (server *Server) Run(ctx context.Context) (err error) {
561571
server.addCardRateLimiter.Run(ctx)
562572
return nil
563573
})
574+
group.Go(func() error {
575+
server.runGhostSessionCacheCleanup(ctx)
576+
return nil
577+
})
564578
group.Go(func() error {
565579
defer cancel()
566580
err := server.server.Serve(server.listener)
@@ -879,16 +893,83 @@ func (server *Server) withAuth(handler http.Handler) http.Handler {
879893
return
880894
}
881895

882-
newCtx, err := server.service.TokenAuth(ctx, tokenInfo.Token, time.Now())
896+
newCtx, session, err := server.service.TokenAuth(ctx, tokenInfo.Token, time.Now())
883897
if err != nil {
884898
return
885899
}
886900
ctx = newCtx
887901

902+
if server.config.GhostSessionCheckEnabled {
903+
if gsErr := server.checkGhostSession(ctx, r, session); gsErr != nil {
904+
server.log.Error("failed to check ghost session", zap.Error(gsErr))
905+
}
906+
}
907+
888908
handler.ServeHTTP(w, r.Clone(ctx))
889909
})
890910
}
891911

912+
func (server *Server) checkGhostSession(ctx context.Context, r *http.Request, session *consoleauth.WebappSession) error {
913+
if session == nil {
914+
return nil
915+
}
916+
917+
ip, err := web.GetRequestIP(r)
918+
if err != nil {
919+
return err
920+
}
921+
userAgent := r.UserAgent()
922+
923+
if session.UserAgent != userAgent || session.Address != ip {
924+
user, err := console.GetUser(ctx)
925+
if err != nil {
926+
return err
927+
}
928+
929+
// Atomic check-and-send operation to prevent race conditions.
930+
server.ghostSessionEmailMutex.Lock()
931+
defer server.ghostSessionEmailMutex.Unlock()
932+
933+
lastSent, exists := server.ghostSessionEmailSent[user.ID]
934+
if exists && time.Since(lastSent) < 2*time.Hour {
935+
return nil // Email sent recently, skip.
936+
}
937+
938+
server.ghostSessionEmailSent[user.ID] = time.Now()
939+
server.hubspotMailService.SendAsync(ctx, &hubspotmails.SendEmailRequest{
940+
Kind: hubspotmails.GhostSessionWarning,
941+
To: user.Email,
942+
})
943+
}
944+
945+
return nil
946+
}
947+
948+
// runGhostSessionCacheCleanup periodically cleans up old ghost session email timestamps to prevent memory leaks.
949+
func (server *Server) runGhostSessionCacheCleanup(ctx context.Context) {
950+
ticker := time.NewTicker(24 * time.Hour) // Clean up daily.
951+
defer ticker.Stop()
952+
953+
for {
954+
select {
955+
case <-ctx.Done():
956+
return
957+
case <-ticker.C:
958+
// Remove timestamps older than 24 hours.
959+
cutoff := time.Now().Add(-24 * time.Hour)
960+
server.ghostSessionEmailMutex.Lock()
961+
for userID, timestamp := range server.ghostSessionEmailSent {
962+
if timestamp.Before(cutoff) {
963+
delete(server.ghostSessionEmailSent, userID)
964+
}
965+
}
966+
server.ghostSessionEmailMutex.Unlock()
967+
968+
server.log.Info("Cleaned up old ghost session email timestamps")
969+
}
970+
}
971+
}
972+
892973
// withRequest ensures the http request itself is reachable from the context.
893974
func (server *Server) withRequest(handler http.Handler) http.Handler {
894975
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
@@ -1493,7 +1574,8 @@ func (a *apiAuth) cookieAuth(ctx context.Context, r *http.Request) (context.Cont
14931574
return nil, err
14941575
}
14951576

1496-
return a.server.service.TokenAuth(ctx, tokenInfo.Token, time.Now())
1577+
newCtx, _, err := a.server.service.TokenAuth(ctx, tokenInfo.Token, time.Now())
1578+
return newCtx, err
14971579
}
14981580

14991581
// cookieAuth returns an authenticated context by api key.

satellite/console/service.go

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5724,37 +5724,37 @@ func (s *Service) getProjectUsageLimits(ctx context.Context, projectID uuid.UUID
57245724
}
57255725

57265726
// TokenAuth returns an authenticated context by session token.
5727-
func (s *Service) TokenAuth(ctx context.Context, token consoleauth.Token, authTime time.Time) (_ context.Context, err error) {
5727+
func (s *Service) TokenAuth(ctx context.Context, token consoleauth.Token, authTime time.Time) (_ context.Context, _ *consoleauth.WebappSession, err error) {
57285728
defer mon.Task()(&ctx)(&err)
57295729

57305730
valid, err := s.tokens.ValidateToken(token)
57315731
if err != nil {
5732-
return nil, Error.Wrap(err)
5732+
return nil, nil, Error.Wrap(err)
57335733
}
57345734
if !valid {
5735-
return nil, Error.New("incorrect signature")
5735+
return nil, nil, Error.New("incorrect signature")
57365736
}
57375737

57385738
sessionID, err := uuid.FromBytes(token.Payload)
57395739
if err != nil {
5740-
return nil, Error.Wrap(err)
5740+
return nil, nil, Error.Wrap(err)
57415741
}
57425742

57435743
session, err := s.store.WebappSessions().GetBySessionID(ctx, sessionID)
57445744
if err != nil {
5745-
return nil, Error.Wrap(err)
5745+
return nil, nil, Error.Wrap(err)
57465746
}
57475747

57485748
ctx, err = s.authorize(ctx, session.UserID, session.ExpiresAt, authTime)
57495749
if err != nil {
57505750
err := errs.Combine(err, s.store.WebappSessions().DeleteBySessionID(ctx, sessionID))
57515751
if err != nil {
5752-
return nil, Error.Wrap(err)
5752+
return nil, nil, Error.Wrap(err)
57535753
}
5754-
return nil, err
5754+
return nil, nil, err
57555755
}
57565756

5757-
return ctx, nil
5757+
return ctx, &session, nil
57585758
}
57595759

57605760
// KeyAuth returns an authenticated context by api key.

satellite/console/service_test.go

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4755,7 +4755,7 @@ func TestSessionExpiration(t *testing.T) {
47554755
tokenInfo, err := service.Token(ctx, console.AuthUser{Email: user.Email, Password: user.FullName})
47564756
require.NoError(t, err)
47574757

4758-
_, err = service.TokenAuth(ctx, tokenInfo.Token, time.Now())
4758+
_, _, err = service.TokenAuth(ctx, tokenInfo.Token, time.Now())
47594759
require.NoError(t, err)
47604760

47614761
sessionID, err := uuid.FromBytes(tokenInfo.Token.Payload)
@@ -4765,7 +4765,7 @@ func TestSessionExpiration(t *testing.T) {
47654765
require.NoError(t, err)
47664766

47674767
// Session should be removed from DB after it has expired
4768-
_, err = service.TokenAuth(ctx, tokenInfo.Token, time.Now().Add(2*time.Hour))
4768+
_, _, err = service.TokenAuth(ctx, tokenInfo.Token, time.Now().Add(2*time.Hour))
47694769
require.True(t, console.ErrTokenExpiration.Has(err))
47704770

47714771
_, err = sat.DB.Console().WebappSessions().GetBySessionID(ctx, sessionID)
@@ -4835,7 +4835,7 @@ func TestDeleteAllSessionsByUserIDExcept(t *testing.T) {
48354835
tokenInfo, err := service.Token(ctx, console.AuthUser{Email: user.Email, Password: user.FullName})
48364836
require.NoError(t, err)
48374837

4838-
_, err = service.TokenAuth(ctx, tokenInfo.Token, time.Now())
4838+
_, _, err = service.TokenAuth(ctx, tokenInfo.Token, time.Now())
48394839
require.NoError(t, err)
48404840

48414841
sessionID, err := uuid.FromBytes(tokenInfo.Token.Payload)
@@ -4848,7 +4848,7 @@ func TestDeleteAllSessionsByUserIDExcept(t *testing.T) {
48484848
tokenInfo2, err := service.Token(ctx, console.AuthUser{Email: user.Email, Password: user.FullName})
48494849
require.NoError(t, err)
48504850

4851-
_, err = service.TokenAuth(ctx, tokenInfo2.Token, time.Now())
4851+
_, _, err = service.TokenAuth(ctx, tokenInfo2.Token, time.Now())
48524852
require.NoError(t, err)
48534853

48544854
sessionID2, err := uuid.FromBytes(tokenInfo2.Token.Payload)

satellite/mailservice/hubspotmails/service.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,11 @@ var (
2929
// MailKind represents the type of the email.
3030
type MailKind string
3131

32+
var (
33+
// GhostSessionWarning is sent when a ghost session is detected.
34+
GhostSessionWarning MailKind = "ghostSessionWarning"
35+
)
36+
3237
// SendEmailRequest is a request to send an email via HubSpot.
3338
type SendEmailRequest struct {
3439
Kind MailKind

satellite/satellite-config.yaml.lock

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -457,6 +457,9 @@ compensation.withheld-percents: 75,75,75,50,50,50,25,25,25,0,0,0,0,0,0
457457
# indicates if generated console api should be used
458458
# console.generated-api-enabled: true
459459

460+
# whether to enable ghost session detection and notification
461+
# console.ghost-session-check-enabled: false
462+
460463
# url link to storj.io homepage
461464
# console.homepage-url: https://www.storj.io
462465

0 commit comments

Comments
 (0)