mirror of
https://github.com/gravitational/teleport.git
synced 2026-09-17 17:40:30 +08:00
Address Feedback
This commit is contained in:
@@ -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 [
|
||||
|
||||
@@ -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:""`
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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)
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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]: {
|
||||
|
||||
Reference in New Issue
Block a user