package service import ( "context" "fmt" "time" "github.com/go-errors/errors" "github.com/gin-gonic/gin" "github.com/google/uuid" "github.com/phishingclub/phishingclub/data" "github.com/phishingclub/phishingclub/errs" "github.com/phishingclub/phishingclub/model" "github.com/phishingclub/phishingclub/repository" "gorm.io/gorm" ) // Session is a service for Session type Session struct { Common SessionRepository *repository.Session } // GetSession returns a session if one exists associated with the request // if the session exists it will extend the session expiry date // else it will invalidate the session cookie if provided // modifies the response headers func (s *Session) GetAndExtendSession(g *gin.Context) (*model.Session, error) { session, err := s.validateAndExtendSession(g) hasErr := errors.Is(err, gorm.ErrRecordNotFound) || errors.Is(err, errs.ErrSessionCookieNotFound) if hasErr { return nil, errs.Wrap(err) } if err != nil { // TODO audit log? if the error is because the session IP changed s.Logger.Debugw("failed to validate and extend session", "error", err) return nil, errs.Wrap(err) } return session, nil } // GetByID returns a session by ID func (s *Session) GetByID( ctx context.Context, sessionID *uuid.UUID, options *repository.SessionOption, ) (*model.Session, error) { session, err := s.SessionRepository.GetByID( ctx, sessionID, options, ) if errors.Is(err, gorm.ErrRecordNotFound) { return nil, gorm.ErrRecordNotFound } return session, nil } // GetSessionsByUserID returns all sessions by user ID func (s *Session) GetSessionsByUserID( ctx context.Context, session *model.Session, userID *uuid.UUID, options *repository.SessionOption, ) (*model.Result[model.Session], error) { result := model.NewEmptyResult[model.Session]() ae := NewAuditEvent("Session.GetSessionsByUserID", session) ae.Details["userID"] = userID.String() // check permissions isAuthorized, err := IsAuthorized(session, data.PERMISSION_ALLOW_GLOBAL) if err != nil && !errors.Is(err, errs.ErrAuthorizationFailed) { s.LogAuthError(err) return result, errs.Wrap(err) } if !isAuthorized { s.AuditLogNotAuthorized(ae) return result, errs.ErrAuthorizationFailed } // get all sessions by user ID result, err = s.SessionRepository.GetAllActiveSessionByUserID( ctx, userID, options, ) if errors.Is(err, gorm.ErrRecordNotFound) { return result, gorm.ErrRecordNotFound } if err != nil { s.Logger.Errorw("failed to get sessions by user ID", "error", err) return result, errs.Wrap(err) } // no audit on read return result, nil } // validateAndExtendSession returns a session if one exists associated with the request func (s *Session) validateAndExtendSession(g *gin.Context) (*model.Session, error) { cookie, err := g.Cookie(data.SessionCookieKey) if err != nil { return nil, errs.ErrSessionCookieNotFound } id, err := uuid.Parse(cookie) if err != nil { return nil, errs.Wrap(err) } // checks that the session is not expired ctx := g.Request.Context() session, err := s.SessionRepository.GetByID(ctx, &id, &repository.SessionOption{ WithUser: true, WithUserRole: true, WithUserCompany: true, }) // there is a valid session cookie but no valid session, so we expire the session cookie if errors.Is(err, gorm.ErrRecordNotFound) { g.SetCookie( data.SessionCookieKey, "", -1, "/", "", true, true, ) return nil, errs.Wrap(err) } if err != nil { return nil, errs.Wrap(err) } // handle session and that IP has not changed // if it has changed - we expire the session sessionIP := session.IP clientIP := g.ClientIP() if session.IP != clientIP { err := s.Expire(ctx, session.ID) if err != nil { return nil, fmt.Errorf( "failed to expire session upon changed IP (%s != %s): %s", sessionIP, clientIP, err, ) } // audit log - session invliad due to IP change ae := NewAuditEvent("Session.Renew", session) ae.Details["reason"] = "IP changed" ae.Details["previousIP"] = sessionIP ae.Details["newIP"] = clientIP s.AuditLogNotAuthorized(ae) return nil, fmt.Errorf( "session IP changed (%s != %s)", sessionIP, clientIP, ) } // session is valid - update the session expiry date session.Renew(model.SessionIdleTimeout) err = s.SessionRepository.UpdateExpiry(ctx, session) if err != nil { return nil, fmt.Errorf("failed to update session expiry: %s", err) } return session, nil } // Create creates a new session // no auth - anyone can create a session func (s *Session) Create( ctx context.Context, user *model.User, ip string, ) (*model.Session, error) { now := time.Now() expiredAt := now.Add(model.SessionIdleTimeout).UTC() maxAgeAt := now.Add(model.SessionMaxAgeAt).UTC() id := uuid.New() newSession := &model.Session{ ID: &id, User: user, IP: ip, ExpiresAt: &expiredAt, MaxAgeAt: &maxAgeAt, } sessionID, err := s.SessionRepository.Insert( ctx, newSession, ) if err != nil { s.Logger.Errorw("failed to insert session when creating a new session", "error", err) return nil, errs.Wrap(err) } createdSession, err := s.SessionRepository.GetByID( ctx, sessionID, &repository.SessionOption{ WithUser: true, WithUserRole: true, WithUserCompany: true, }, ) if err != nil { s.Logger.Errorw("failed to get session after creating it", "error", err) return nil, errs.Wrap(err) } return createdSession, nil } // Expire expires a session func (s *Session) Expire( ctx context.Context, sessionID *uuid.UUID, ) error { err := s.SessionRepository.Expire(ctx, sessionID) if err != nil { s.Logger.Errorw("failed to expire session", "error", err) return err } return nil } // ExpireAllByUserID expires all sessions by user ID func (s *Session) ExpireAllByUserID( ctx context.Context, session *model.Session, userID *uuid.UUID, ) error { ae := NewAuditEvent("Session.ExpireAllByUserID", session) ae.Details["userID"] = userID.String() // check permissions isAuthorized, err := IsAuthorized(session, data.PERMISSION_ALLOW_GLOBAL) if err != nil && !errors.Is(err, errs.ErrAuthorizationFailed) { s.LogAuthError(err) return err } if !isAuthorized { s.AuditLogNotAuthorized(ae) return errs.ErrAuthorizationFailed } if session.User == nil { s.Logger.Errorw("failed to get user from session when expiring session", "error", err) return err } sessions, err := s.SessionRepository.GetAllActiveSessionByUserID( ctx, userID, &repository.SessionOption{}, ) if err != nil { s.Logger.Errorw("failed to get user sessions when expiring session", "error", err) return err } if len(sessions.Rows) == 0 { s.Logger.Debugw("no sessions to remove", "userID", userID.String()) } for _, session := range sessions.Rows { err = s.SessionRepository.Expire(ctx, session.ID) if err != nil { s.Logger.Errorw("failed a users expiring session", "error", err) return err } } s.AuditLogAuthorized(ae) return nil }