Address Feedback

This commit is contained in:
Kenneth Tran
2026-07-02 15:34:49 -07:00
parent 78faf8f504
commit b3e97f4cf7
15 changed files with 122 additions and 118 deletions
@@ -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 [
+3
View File
@@ -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:""`
+3 -3
View File
@@ -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)
}
-1
View File
@@ -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,
-2
View File
@@ -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
-4
View File
@@ -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.
+11 -12
View File
@@ -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)
-4
View File
@@ -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.
+59 -42
View File
@@ -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)
}
+14 -10
View File
@@ -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))
+1 -1
View File
@@ -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())
})
@@ -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)
+7 -14
View File
@@ -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)
}
+18 -24
View File
@@ -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
@@ -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]: {