diff --git a/api/proto/teleport/legacy/types/events/events.proto b/api/proto/teleport/legacy/types/events/events.proto index 40cd0eabdea..aa046276685 100644 --- a/api/proto/teleport/legacy/types/events/events.proto +++ b/api/proto/teleport/legacy/types/events/events.proto @@ -3218,6 +3218,9 @@ message AppSessionEnd { } // AppSessionExpire is emitted when an application session has expired. +// +// This event is not emitted by default. To enable these events, set +// the TELEPORT_UNSTABLE_ENABLE_APP_SESSION_EXPIRY_EVENTS=yes variable. message AppSessionExpire { // Metadata is a common event metadata Metadata Metadata = 1 [ diff --git a/api/types/events/events.pb.go b/api/types/events/events.pb.go index d38ea3fcfdc..751ed5b3896 100644 --- a/api/types/events/events.pb.go +++ b/api/types/events/events.pb.go @@ -5624,6 +5624,9 @@ func (m *AppSessionEnd) XXX_DiscardUnknown() { var xxx_messageInfo_AppSessionEnd proto.InternalMessageInfo // AppSessionExpire is emitted when an application session has expired. +// +// This event is not emitted by default. To enable these events, set +// the TELEPORT_UNSTABLE_ENABLE_APP_SESSION_EXPIRY_EVENTS=yes variable. type AppSessionExpire struct { // Metadata is a common event metadata Metadata `protobuf:"bytes,1,opt,name=Metadata,proto3,embedded=Metadata" json:""` diff --git a/lib/auth/authtest/authtest.go b/lib/auth/authtest/authtest.go index 63304c9a458..83e67f9bd4f 100644 --- a/lib/auth/authtest/authtest.go +++ b/lib/auth/authtest/authtest.go @@ -126,9 +126,9 @@ type AuthServerConfig struct { Modules modules.Modules // ScopesFeatures dictates which scoped components are enabled for the test auth server. ScopesFeatures scopes.Features - // AppSessionExpiryService opts the test auth server into the app session + // EnableAppSessionExpiryService opts the test auth server into the app session // expiry service code path. - AppSessionExpiryService bool + EnableAppSessionExpiryService bool } // CheckAndSetDefaults checks and sets defaults @@ -338,7 +338,7 @@ func NewAuthServer(cfg AuthServerConfig) (*AuthServer, error) { } access := local.NewAccessService(srv.Backend) - identity, err := local.NewTestIdentityService(srv.Backend, local.WithAppSessionExpiryService(cfg.AppSessionExpiryService)) + identity, err := local.NewTestIdentityService(srv.Backend, local.WithAppSessionExpiryService(cfg.EnableAppSessionExpiryService)) if err != nil { return nil, trace.Wrap(err) } diff --git a/lib/auth/sessions.go b/lib/auth/sessions.go index 09ba5506d29..c91e34fcad3 100644 --- a/lib/auth/sessions.go +++ b/lib/auth/sessions.go @@ -540,7 +540,6 @@ func (a *Server) CreateAppSessionFromReq(ctx context.Context, req NewAppSessionR AppName: req.AppName, AppPublicAddr: req.PublicAddr, AppClusterName: req.ClusterName, - AppName: req.AppName, AppTargetPort: req.AppTargetPort, AWSRoleARN: req.AWSRoleARN, AzureIdentity: req.AzureIdentity, diff --git a/lib/config/configuration.go b/lib/config/configuration.go index 2d10bc9aebc..c9540d0d54f 100644 --- a/lib/config/configuration.go +++ b/lib/config/configuration.go @@ -1047,8 +1047,6 @@ func applyAuthConfig(fc *FileConfig, cfg *servicecfg.Config) error { cfg.Auth.LoadAllCAs = fc.Auth.LoadAllCAs - cfg.Auth.AppSessionExpiryService = fc.Auth.AppSessionExpiryService - // Setting this to true at all times to allow self hosting // of plugins that were previously cloud only. cfg.Auth.HostedPlugins.Enabled = true diff --git a/lib/config/fileconf.go b/lib/config/fileconf.go index e2fc617ce8f..b631e25c6e6 100644 --- a/lib/config/fileconf.go +++ b/lib/config/fileconf.go @@ -889,10 +889,6 @@ type Auth struct { // AccessMonitoring is a set of options related to the Access Monitoring feature. AccessMonitoring *servicecfg.AccessMonitoringOptions `yaml:"access_monitoring,omitempty"` - - // AppSessionExpiryService enables the expiry service to manage app session - // expiration and emit an app.session.expire audit event for each. - AppSessionExpiryService bool `yaml:"app_session_expiry_service,omitempty"` } // PluginService represents the configuration for the plugin service. diff --git a/lib/service/service.go b/lib/service/service.go index 97c0c7bfd29..1cd8fdde5e4 100644 --- a/lib/service/service.go +++ b/lib/service/service.go @@ -2454,13 +2454,12 @@ func (process *TeleportProcess) initAuthService() error { // latest known version in backend. skipVersionCheckFromEnv := os.Getenv("TELEPORT_UNSTABLE_SKIP_VERSION_UPGRADE_CHECK") != "" - if cfg.Identity == nil { - cfg.Identity, err = local.NewIdentityService(b, - local.WithAppSessionExpiryService(cfg.Auth.AppSessionExpiryService), - ) - if err != nil { - return trace.Wrap(err) - } + appSessionExpiryService := os.Getenv("TELEPORT_UNSTABLE_ENABLE_APP_SESSION_EXPIRY_EVENTS") == "yes" + identityService, err := local.NewIdentityService(b, + local.WithAppSessionExpiryService(appSessionExpiryService), + ) + if err != nil { + return trace.Wrap(err) } // first, create the AuthServer @@ -2491,7 +2490,7 @@ func (process *TeleportProcess) initAuthService() error { Presence: cfg.Presence, Events: cfg.Events, Provisioner: cfg.Provisioner, - Identity: cfg.Identity, + Identity: identityService, Access: cfg.Access, StaticTokens: cfg.Auth.StaticTokens, StaticScopedTokens: cfg.Auth.StaticScopedTokens, @@ -3029,10 +3028,10 @@ func (process *TeleportProcess) initAuthService() error { Log: logger.With( teleport.ComponentKey, teleport.Component(teleport.ComponentAuth, "expiry_service"), ), - Emitter: authServer, - AccessPoint: authServer.Services, - HostID: connector.HostUUID(), - AppSessionExpiryService: cfg.Auth.AppSessionExpiryService, + Emitter: authServer, + AccessPoint: authServer.Services, + HostID: connector.HostUUID(), + EnableAppSessionExpiryService: appSessionExpiryService, }) if err != nil { return trace.Wrap(err) diff --git a/lib/service/servicecfg/auth.go b/lib/service/servicecfg/auth.go index 8836dd18468..5b367434d88 100644 --- a/lib/service/servicecfg/auth.go +++ b/lib/service/servicecfg/auth.go @@ -128,10 +128,6 @@ type AuthConfig struct { // Empty value means the controller uses its default. // Used in tests. AgentRolloutControllerSyncPeriod time.Duration - - // AppSessionExpiryService, when set, has the expiry service delete expired - // app sessions and emit an app.session.expire audit event for each. - AppSessionExpiryService bool } // AccessMonitoringOptions configures access monitoring. diff --git a/lib/services/expiry/expiry.go b/lib/services/expiry/expiry.go index 0fe9110184a..0f0254e6720 100644 --- a/lib/services/expiry/expiry.go +++ b/lib/services/expiry/expiry.go @@ -67,29 +67,31 @@ const ( // expiryMetrics holds the Prometheus metrics emitted by the expiry service. // A new instance is constructed in New() per Service so tests are isolated. type expiryMetrics struct { - // expiredBeforeScan is the number of expired resources observed at the - // start of the most recent scan, partitioned by resource kind. - expiredBeforeScan *prometheus.GaugeVec + // resourcesAttempted is the number of expired resources the most recent scan + // attempted to expire this cycle, capped at maxExpiresPerCycle and + // partitioned by resource kind. + resourcesAttempted *prometheus.GaugeVec - // expiredAfterScan is the number of expired resources still awaiting - // deletion at the end of the most recent scan. - expiredAfterScan *prometheus.GaugeVec + // resourcesProcessed is the number of resources the most recent scan + // successfully expired this cycle. The gap between attempted and processed + // reflects failures encountered during expiration. + resourcesProcessed *prometheus.GaugeVec } // newExpiryMetrics constructs a fresh set of unregistered metrics. func newExpiryMetrics() *expiryMetrics { return &expiryMetrics{ - expiredBeforeScan: prometheus.NewGaugeVec(prometheus.GaugeOpts{ + resourcesAttempted: prometheus.NewGaugeVec(prometheus.GaugeOpts{ Namespace: teleport.MetricNamespace, Subsystem: metricsSubsystem, - Name: "expired_resources_before_scan", - Help: "Number of expired resources at the start of scan", + Name: "resources_attempted", + Help: "Number of expired resources the last scan attempted to expire, capped per cycle", }, []string{metricLabelResourceKind}), - expiredAfterScan: prometheus.NewGaugeVec(prometheus.GaugeOpts{ + resourcesProcessed: prometheus.NewGaugeVec(prometheus.GaugeOpts{ Namespace: teleport.MetricNamespace, Subsystem: metricsSubsystem, - Name: "expired_resources_after_scan", - Help: "Number of expired resources remaining at the end of scan", + Name: "resources_processed", + Help: "Number of resources the last scan successfully expired", }, []string{metricLabelResourceKind}), } } @@ -124,10 +126,9 @@ type Config struct { AccessPoint AccessPoint // HostID is a unique ID of this host. HostID string - // AppSessionExpiryService enables the app session expiry task. Must match - // the IdentityService option, or sessions written without a backend TTL - // will accumulate. - AppSessionExpiryService bool + // EnableAppSessionExpiryService enables the app session expiry task. Must match + // the IdentityService option. + EnableAppSessionExpiryService bool } // CheckAndSetDefaults checks required fields and sets default values. @@ -164,8 +165,8 @@ func New(cfg *Config) (*Service, error) { m := newExpiryMetrics() if err := metrics.RegisterPrometheusCollectors( - m.expiredBeforeScan, - m.expiredAfterScan, + m.resourcesAttempted, + m.resourcesProcessed, ); err != nil { return nil, trace.Wrap(err) } @@ -190,7 +191,7 @@ func New(cfg *Config) (*Service, error) { }, } - if cfg.AppSessionExpiryService { + if cfg.EnableAppSessionExpiryService { s.expiryTasks = append(s.expiryTasks, expiryTask{ semaphoreName: semaphoreNameAppSession, resourceKind: types.KindAppSession, @@ -215,7 +216,7 @@ func (s *Service) Run(ctx context.Context) error { return g.Wait() } -// run is there for testing, so a testing interval can be set. +// run drives a single expiry task. func (s *Service) run(ctx context.Context, task expiryTask) error { for { if err := s.runWithLock(ctx, task); err != nil && !errors.Is(err, context.Canceled) { @@ -232,6 +233,7 @@ func (s *Service) run(ctx context.Context, task expiryTask) error { } } +// runWithLock acquires a semaphore lock for the task and runs the loop. func (s *Service) runWithLock(ctx context.Context, task expiryTask) error { lease, err := services.AcquireSemaphoreLockWithRetry( ctx, @@ -268,7 +270,7 @@ func (s *Service) runWithLock(ctx context.Context, task expiryTask) error { return trace.Wrap(err) } -// run is for testing so a duration without jitter can be specified. +// loop processes the expired resources on the configured interval. func (s *Service) loop(ctx context.Context, task expiryTask) error { interval := interval.New(task.intervalCfg) defer interval.Stop() @@ -286,16 +288,17 @@ func (s *Service) loop(ctx context.Context, task expiryTask) error { func (s *Service) processRequests(ctx context.Context) { s.Log.DebugContext(ctx, "Cleaning up expired access requests.") - // expiredBefore counts requests waiting to be expired + // expiredBefore counts requests we attempted to expire this cycle // requestsExpired counts only successful expirations expiredBefore := 0 requestsExpired := 0 defer func() { - s.metrics.expiredBeforeScan.WithLabelValues(types.KindAccessRequest).Set(float64(expiredBefore)) - s.metrics.expiredAfterScan.WithLabelValues(types.KindAccessRequest).Set(float64(expiredBefore - requestsExpired)) + s.metrics.resourcesAttempted.WithLabelValues(types.KindAccessRequest).Set(float64(expiredBefore)) + s.metrics.resourcesProcessed.WithLabelValues(types.KindAccessRequest).Set(float64(requestsExpired)) }() readTime := time.Now() + capReached := false for expiredAccessRequest, err := range clientutils.Resources(ctx, s.AccessPoint.ListExpiredAccessRequests) { if err != nil { s.Log.ErrorContext(ctx, "Error listing expired access requests.", "error", err) @@ -309,12 +312,14 @@ func (s *Service) processRequests(ctx context.Context) { continue } } - expiredBefore++ - // Keep iterating to count remaining requests but stop expiring them. - if expiredBefore > maxExpiresPerCycle { - continue + // Stop scanning once we hit the per-cycle cap; remaining expired requests + // will be picked up in the next sweep. + if expiredBefore >= maxExpiresPerCycle { + capReached = true + break } + expiredBefore++ s.Log.DebugContext(ctx, "Expiring access request.", "request", expiredAccessRequest.GetName()) if err := s.expireRequest(ctx, expiredAccessRequest); err != nil { @@ -322,10 +327,14 @@ func (s *Service) processRequests(ctx context.Context) { continue } requestsExpired++ + } - if requestsExpired == maxExpiresPerCycle { - s.Log.DebugContext(ctx, "Cleaned up maximum amount of expired access requests. Will continue in the next run.", "max", maxExpiresPerCycle) - } + if capReached { + s.Log.WarnContext(ctx, + "Expired access request count reached per-scan cap. Additional expired requests will be processed in subsequent runs.", + "processed", requestsExpired, + "max_per_cycle", maxExpiresPerCycle, + ) } s.Log.DebugContext(ctx, "Successfully cleaned up expired access requests.", "count", requestsExpired) @@ -334,26 +343,29 @@ func (s *Service) processRequests(ctx context.Context) { func (s *Service) processAppSessions(ctx context.Context) { s.Log.DebugContext(ctx, "Cleaning up expired application sessions.") - // expiredBefore counts app sessions waiting to be expired + // expiredBefore counts app sessions we attempted to expire this cycle // sessionsExpired counts only successful expirations expiredBefore := 0 sessionsExpired := 0 defer func() { - s.metrics.expiredBeforeScan.WithLabelValues(types.KindAppSession).Set(float64(expiredBefore)) - s.metrics.expiredAfterScan.WithLabelValues(types.KindAppSession).Set(float64(expiredBefore - sessionsExpired)) + s.metrics.resourcesAttempted.WithLabelValues(types.KindAppSession).Set(float64(expiredBefore)) + s.metrics.resourcesProcessed.WithLabelValues(types.KindAppSession).Set(float64(sessionsExpired)) }() + capReached := false for expiredSession, err := range clientutils.Resources(ctx, s.AccessPoint.ListExpiredAppSessions) { if err != nil { s.Log.ErrorContext(ctx, "Error listing expired application sessions.", "error", err) return } - expiredBefore++ - // Keep iterating to count remaining sessions but stop expiring them. - if expiredBefore > maxExpiresPerCycle { - continue + // Stop scanning once we hit the per-cycle cap; remaining expired sessions + // will be picked up in the next sweep. + if expiredBefore >= maxExpiresPerCycle { + capReached = true + break } + expiredBefore++ s.Log.DebugContext(ctx, "Expiring application session.", "user", expiredSession.GetUser(), @@ -364,11 +376,16 @@ func (s *Service) processAppSessions(ctx context.Context) { continue } sessionsExpired++ - - if sessionsExpired == maxExpiresPerCycle { - s.Log.DebugContext(ctx, "Cleaned up maximum amount of expired application sessions. Will continue in the next run.", "max", maxExpiresPerCycle) - } } + + if capReached { + s.Log.WarnContext(ctx, + "Expired application session count reached per-scan cap. Additional expired sessions will be processed in subsequent runs.", + "processed", sessionsExpired, + "max_per_cycle", maxExpiresPerCycle, + ) + } + s.Log.DebugContext(ctx, "Successfully cleaned up expired application sessions.", "count", sessionsExpired) } diff --git a/lib/services/expiry/expiry_test.go b/lib/services/expiry/expiry_test.go index 84782ebb59b..92227a058c5 100644 --- a/lib/services/expiry/expiry_test.go +++ b/lib/services/expiry/expiry_test.go @@ -26,6 +26,7 @@ import ( "time" "github.com/google/uuid" + "github.com/jonboulle/clockwork" "github.com/prometheus/client_golang/prometheus/testutil" "github.com/stretchr/testify/require" @@ -381,8 +382,11 @@ var ( } ) -// TestExpiryMetrics verifies the before-scan and after-scan gauges for both -// resource kinds and the behavior when expired resources exceed maxExpiresPerCycle. +// TestExpiryMetrics verifies the resources_attempted and resources_processed +// gauges for both resource kinds and the behavior when expired resources exceed +// maxExpiresPerCycle. Attempted counts resources the scan tried to expire this +// cycle (capped at maxExpiresPerCycle); processed counts successful +// expirations. func TestExpiryMetrics(t *testing.T) { t.Parallel() @@ -405,7 +409,7 @@ func TestExpiryMetrics(t *testing.T) { expectProcessed: 3, }, { - name: "access request: cap hit, remainder expected", + name: "access request: cap hit, scan stops at cap", resource: accessRequestMetricsResource, numCreate: maxExpiresPerCycle + 5, expectProcessed: maxExpiresPerCycle, @@ -440,10 +444,10 @@ func TestExpiryMetrics(t *testing.T) { require.Equal(t, expectAfter, tc.resource.remaining(t, authServer)) require.Len(t, emitter.Events(), tc.expectProcessed) - beforeMetric := expiry.metrics.expiredBeforeScan.WithLabelValues(tc.resource.label) - afterMetric := expiry.metrics.expiredAfterScan.WithLabelValues(tc.resource.label) - require.Equal(t, float64(tc.numCreate), testutil.ToFloat64(beforeMetric)) - require.Equal(t, float64(expectAfter), testutil.ToFloat64(afterMetric)) + attemptedMetric := expiry.metrics.resourcesAttempted.WithLabelValues(tc.resource.label) + processedMetric := expiry.metrics.resourcesProcessed.WithLabelValues(tc.resource.label) + require.InDelta(t, float64(tc.expectProcessed), testutil.ToFloat64(attemptedMetric), 0) + require.InDelta(t, float64(tc.expectProcessed), testutil.ToFloat64(processedMetric), 0) }) }) } @@ -484,7 +488,8 @@ func setupExpiryService(t *testing.T, appSessionExpiryService bool) (*Service, * RPID: "localhost", }, }, - AppSessionExpiryService: appSessionExpiryService, + EnableAppSessionExpiryService: appSessionExpiryService, + Clock: clockwork.NewRealClock(), }) require.NoError(t, err) t.Cleanup(func() { authServer.Close() }) @@ -499,7 +504,7 @@ func setupExpiryService(t *testing.T, appSessionExpiryService bool) (*Service, * Server: authServer.AuthServer, appSessions: identity, }, - AppSessionExpiryService: appSessionExpiryService, + EnableAppSessionExpiryService: appSessionExpiryService, }) require.NoError(t, err) @@ -584,7 +589,6 @@ func mustListAppSessions(t *testing.T, auth *auth.Server) []types.WebSession { return resp } -// Helper to extract session names from slice of WebSessions func getAppSessionNames(t *testing.T, sessions []types.WebSession) []string { t.Helper() names := make([]string, 0, len(sessions)) diff --git a/lib/services/local/dynamic_access.go b/lib/services/local/dynamic_access.go index 04d0ce94bcf..144a3d9491c 100644 --- a/lib/services/local/dynamic_access.go +++ b/lib/services/local/dynamic_access.go @@ -342,7 +342,7 @@ func (s *DynamicAccessService) ListAccessRequests(ctx context.Context, req *prot // the expiry service. Access requests expiration handling is done outside the backend // because we need to emit audit events on the access requests expiry. func (s *DynamicAccessService) ListExpiredAccessRequests(ctx context.Context, limit int, pageToken string) ([]*types.AccessRequestV3, string, error) { - now := time.Now() + now := s.Clock().Now() return s.collectPage(ctx, limit, pageToken, func(r *types.AccessRequestV3) bool { return now.After(r.Expiry()) }) diff --git a/lib/services/local/dynamic_access_test.go b/lib/services/local/dynamic_access_test.go index 058b61a381f..fb188beda45 100644 --- a/lib/services/local/dynamic_access_test.go +++ b/lib/services/local/dynamic_access_test.go @@ -22,6 +22,7 @@ import ( "github.com/google/uuid" "github.com/gravitational/trace" + "github.com/jonboulle/clockwork" "github.com/stretchr/testify/require" "github.com/gravitational/teleport/api/client/proto" @@ -336,6 +337,7 @@ func setupDynamicAccessService(t *testing.T) (*DynamicAccessService, *memory.Mem mem, err := memory.New(memory.Config{ Context: ctx, + Clock: clockwork.NewRealClock(), }) require.NoError(t, err) diff --git a/lib/services/local/session.go b/lib/services/local/session.go index 24149fc224a..9b1de82e861 100644 --- a/lib/services/local/session.go +++ b/lib/services/local/session.go @@ -202,29 +202,22 @@ func (s *IdentityService) upsertSession(ctx context.Context, session types.WebSe return nil } -// appSessionBackendBufferTTL is how much past the session's logical expiry -// the backend will keep an app session before reaping it on its own. -const appSessionBackendBufferTTL = 30 * 24 * time.Hour - -// appSessionBackendExpiry returns the backend TTL for a session. App sessions -// extend their TTL by appSessionBackendBufferTTL when the expiry service is -// enabled so it can emit an app.session.expire audit event before deletion. +// appSessionBackendExpiry returns the backend TTL for a session. When the +// app session expiry service is enabled, app sessions are stored without a +// backend TTL so the expiry servicecan emit an app.session.expire audit event. func (s *IdentityService) appSessionBackendExpiry(session types.WebSession) time.Time { if s.appSessionExpiryService && session.GetSubKind() == types.KindAppSession { - return session.GetExpiryTime().Add(appSessionBackendBufferTTL) + return time.Time{} } return session.GetExpiryTime() } -// ListExpiredAppSessions lists all application sessions that are expired. The expiry service -// calls this when its app session opt-in is enabled, so it can emit an app.session.expire -// audit event before deletion. When the opt-in is disabled, the backend handles expiration -// via its TTL and no event is emitted. +// ListExpiredAppSessions lists all application sessions that are expired. func (s *IdentityService) ListExpiredAppSessions(ctx context.Context, limit int, pageToken string) ([]types.WebSession, string, error) { - now := time.Now() + now := s.Clock().Now() allSessions := s.rangeSessions(ctx, pageToken, "", "", appsPrefix, sessionsPrefix) expired := stream.FilterMap(allSessions, func(session types.WebSession) (types.WebSession, bool) { - return session, now.After(session.Expiry()) + return session, now.After(session.GetExpiryTime()) }) return generic.CollectPageAndCursor(expired, limit, types.WebSession.GetName) } diff --git a/lib/services/local/session_test.go b/lib/services/local/session_test.go index 7c9604ae32c..4514c061ba4 100644 --- a/lib/services/local/session_test.go +++ b/lib/services/local/session_test.go @@ -243,37 +243,35 @@ func TestListExpiredAppSessions(t *testing.T) { synctest.Test(t, func(t *testing.T) { ctx := t.Context() - backend, _ := memory.New(memory.Config{Context: ctx}) - identity, _ := NewTestIdentityService(backend, WithAppSessionExpiryService(true)) + backend, err := memory.New(memory.Config{Context: ctx, Clock: clockwork.NewRealClock()}) + require.NoError(t, err) + identity, err := NewTestIdentityService(backend, WithAppSessionExpiryService(true)) + require.NoError(t, err) const totalExpired = 200 const totalValid = 10 // Create 210 sessions: 5 valid, 100 expired, 5 valid, 100 expired for i := range totalValid / 2 { - sess := newTestAppSession(t, fmt.Sprintf("valid-%d", i)) - sess.SetExpiry(time.Now().Add(1 * time.Hour)) + sess := newTestAppSession(t, fmt.Sprintf("valid-%d", i), time.Now().Add(1*time.Hour)) err := identity.UpsertAppSession(ctx, sess) require.NoError(t, err) } for i := range totalExpired / 2 { - sess := newTestAppSession(t, fmt.Sprintf("expired-%d", i)) - sess.SetExpiry(time.Now().Add(-30 * time.Minute)) + sess := newTestAppSession(t, fmt.Sprintf("expired-%d", i), time.Now().Add(-30*time.Minute)) err := identity.UpsertAppSession(ctx, sess) require.NoError(t, err) } for i := range totalValid / 2 { - sess := newTestAppSession(t, fmt.Sprintf("valid-%d", i+5)) - sess.SetExpiry(time.Now().Add(1 * time.Hour)) + sess := newTestAppSession(t, fmt.Sprintf("valid-%d", i+5), time.Now().Add(1*time.Hour)) err := identity.UpsertAppSession(ctx, sess) require.NoError(t, err) } for i := range totalExpired / 2 { - sess := newTestAppSession(t, fmt.Sprintf("expired-%d", i+100)) - sess.SetExpiry(time.Now().Add(-30 * time.Minute)) + sess := newTestAppSession(t, fmt.Sprintf("expired-%d", i+100), time.Now().Add(-30*time.Minute)) err := identity.UpsertAppSession(ctx, sess) require.NoError(t, err) } @@ -302,35 +300,31 @@ func TestListExpiredAppSessions(t *testing.T) { }) } -func TestUpdateAppSession_ExtendsBackendExpiry(t *testing.T) { +func TestUpdateAppSession_ClearsBackendExpiry(t *testing.T) { t.Parallel() - ctx := context.Background() + ctx := t.Context() mem, err := memory.New(memory.Config{Context: ctx}) require.NoError(t, err) identity, err := NewTestIdentityService(mem, WithAppSessionExpiryService(true)) require.NoError(t, err) - session := newTestAppSession(t, "updated-session") + session := newTestAppSession(t, "updated-session", time.Now().Add(12*time.Hour)) require.NoError(t, identity.UpsertAppSession(ctx, session)) - expectedExpiry := session.GetExpiryTime().Add(appSessionBackendBufferTTL) - item, err := mem.Get(ctx, backend.NewKey(appsPrefix, sessionsPrefix, session.GetName())) require.NoError(t, err) - require.True(t, item.Expires.Equal(expectedExpiry), "new app sessions should have extended backend TTL") + require.True(t, item.Expires.IsZero(), "new app sessions should have no backend TTL when opt-in") session, err = identity.GetAppSession(ctx, types.GetAppSessionRequest{SessionID: session.GetName()}) require.NoError(t, err) - testDBSCPublicKey := []byte("test-dbsc-key") - session.SetDBSCPublicKey(testDBSCPublicKey) require.NoError(t, identity.UpdateAppSession(ctx, session)) item, err = mem.Get(ctx, backend.NewKey(appsPrefix, sessionsPrefix, session.GetName())) require.NoError(t, err) - require.True(t, item.Expires.Equal(expectedExpiry), "updated app sessions should keep extended backend TTL") + require.True(t, item.Expires.IsZero(), "updated app sessions should have no backend TTL when opt-in") } // TestUpdateAppSession_PreservesBackendExpiry verifies that when the expiry @@ -339,14 +333,14 @@ func TestUpdateAppSession_ExtendsBackendExpiry(t *testing.T) { func TestUpdateAppSession_PreservesBackendExpiry(t *testing.T) { t.Parallel() - ctx := context.Background() + ctx := t.Context() mem, err := memory.New(memory.Config{Context: ctx}) require.NoError(t, err) identity, err := NewTestIdentityService(mem) require.NoError(t, err) - session := newTestAppSession(t, "default-session") + session := newTestAppSession(t, "default-session", time.Now().Add(12*time.Hour)) require.NoError(t, identity.UpsertAppSession(ctx, session)) item, err := mem.Get(ctx, backend.NewKey(appsPrefix, sessionsPrefix, session.GetName())) @@ -362,12 +356,12 @@ func TestUpdateAppSession_PreservesBackendExpiry(t *testing.T) { require.True(t, item.Expires.Equal(session.GetExpiryTime()), "backend TTL should match session expiry after update") } -// Helper for quick session generation -func newTestAppSession(t *testing.T, name string) types.WebSession { +// newTestAppSession constructs an app session with the given Spec.Expires. +func newTestAppSession(t *testing.T, name string, expires time.Time) types.WebSession { t.Helper() s, err := types.NewWebSession(name, types.KindAppSession, types.WebSessionSpecV2{ User: "alice", - Expires: time.Now().Add(12 * time.Hour), + Expires: expires, }) require.NoError(t, err) return s diff --git a/web/packages/teleport/src/services/audit/makeEvent.ts b/web/packages/teleport/src/services/audit/makeEvent.ts index 2b9e36b3716..0e45b5aaf78 100644 --- a/web/packages/teleport/src/services/audit/makeEvent.ts +++ b/web/packages/teleport/src/services/audit/makeEvent.ts @@ -641,7 +641,7 @@ export const formatters: Formatters = { desc: 'App Session Expired', format: event => { const { app_name, sid } = event; - return `App session for [${app_name}] expired [${sid}]`; + return `App session for [${app_name}] (session [${sid}]) expired`; }, }, [eventCodes.APP_SESSION_CHUNK]: {