diff --git a/CHANGELOG.md b/CHANGELOG.md index e18ae6897..bd660f6bc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,8 +4,21 @@ Canonical reference for changes, improvements, and bugfixes for Boundary. ## Next +### New and Improved + +* controller: Improve response time for listing sessions. + This also creates a new periodic job that will delete terminated + sessions after 1 hour. + See Deprecations/Changes for some additional details. + [PR](https://github.com/hashicorp/boundary/pull/2160). + ### Deprecations/Changes +* controller: Change the default behavior of the session list endpoint + to no longer include sessions in a terminated state and introduces + a new query parameter/cli flag to include the terminated sessions. + This also removes the connection information from the list response. + [PR](https://github.com/hashicorp/boundary/pull/2160). * Anonymous user permissions: In order to reduce the risk of accidental and unintended granting of permissions to anonymous users, the permissions system now only allows certain actions on certain resources to be assigned to the diff --git a/api/sessions/option.gen.go b/api/sessions/option.gen.go index 6367b922b..d84fa64b6 100644 --- a/api/sessions/option.gen.go +++ b/api/sessions/option.gen.go @@ -1,6 +1,7 @@ package sessions import ( + "fmt" "strconv" "strings" @@ -86,3 +87,15 @@ func WithRecursive(recurse bool) Option { o.withRecursive = true } } + +func WithIncludeTerminated(inIncludeTerminated bool) Option { + return func(o *options) { + o.queryMap["include_terminated"] = fmt.Sprintf("%v", inIncludeTerminated) + } +} + +func DefaultIncludeTerminated() Option { + return func(o *options) { + o.postMap["include_terminated"] = nil + } +} diff --git a/internal/api/genapi/input.go b/internal/api/genapi/input.go index 9574ae296..7db6a6d5d 100644 --- a/internal/api/genapi/input.go +++ b/internal/api/genapi/input.go @@ -701,6 +701,14 @@ var inputStructs = []*structInfo{ readTemplate, listTemplate, }, + extraFields: []fieldInfo{ + { + Name: "IncludeTerminated", + ProtoName: "include_terminated", + FieldType: "bool", + Query: true, + }, + }, pluralResourceName: "sessions", createResponseTypes: true, fieldFilter: []string{"private_key"}, diff --git a/internal/cmd/commands/sessionscmd/funcs.go b/internal/cmd/commands/sessionscmd/funcs.go index cccfecb51..0910106c1 100644 --- a/internal/cmd/commands/sessionscmd/funcs.go +++ b/internal/cmd/commands/sessionscmd/funcs.go @@ -10,17 +10,48 @@ import ( "github.com/hashicorp/boundary/internal/cmd/base" ) +const ( + flagIncludeTerminated = "include-terminated" +) + func init() { extraActionsFlagsMapFunc = extraActionsFlagsMapFuncImpl + extraFlagsFunc = extraFlagsFuncImpl + extraFlagsHandlingFunc = extraFlagsHandlingFuncImpl executeExtraActions = executeExtraActionsImpl } func extraActionsFlagsMapFuncImpl() map[string][]string { return map[string][]string{ "cancel": {"id"}, + "list": {flagIncludeTerminated}, } } +type extraCmdVars struct { + flagIncludeTerminated bool +} + +func extraFlagsFuncImpl(c *Command, set *base.FlagSets, f *base.FlagSet) { + for _, name := range flagsMap[c.Func] { + switch name { + case flagIncludeTerminated: + f.BoolVar(&base.BoolVar{ + Name: flagIncludeTerminated, + Target: &c.flagIncludeTerminated, + Usage: "If set, terminated sessions will be included in the results.", + }) + } + } +} + +func extraFlagsHandlingFuncImpl(c *Command, _ *base.FlagSets, opts *[]sessions.Option) bool { + if c.flagIncludeTerminated { + *opts = append(*opts, sessions.WithIncludeTerminated(c.flagIncludeTerminated)) + } + return true +} + func (c *Command) extraHelpFunc(helpMap map[string]func() string) string { var helpStr string switch c.Func { diff --git a/internal/cmd/commands/sessionscmd/sessions.gen.go b/internal/cmd/commands/sessionscmd/sessions.gen.go index 372fec9d6..5732f46f6 100644 --- a/internal/cmd/commands/sessionscmd/sessions.gen.go +++ b/internal/cmd/commands/sessionscmd/sessions.gen.go @@ -35,6 +35,8 @@ type Command struct { Func string plural string + + extraCmdVars } func (c *Command) AutocompleteArgs() complete.Predictor { diff --git a/internal/cmd/gencli/input.go b/internal/cmd/gencli/input.go index 0c57d3827..6772eea7d 100644 --- a/internal/cmd/gencli/input.go +++ b/internal/cmd/gencli/input.go @@ -408,13 +408,14 @@ var inputStructs = map[string][]*cmdInfo{ }, "sessions": { { - ResourceType: resource.Session.String(), - Pkg: "sessions", - StdActions: []string{"read", "list"}, - Container: "Scope", - HasExtraHelpFunc: true, - HasId: true, - VersionedActions: []string{"cancel"}, + ResourceType: resource.Session.String(), + Pkg: "sessions", + StdActions: []string{"read", "list"}, + Container: "Scope", + HasExtraCommandVars: true, + HasExtraHelpFunc: true, + HasId: true, + VersionedActions: []string{"cancel"}, }, }, "targets": { diff --git a/internal/daemon/controller/controller.go b/internal/daemon/controller/controller.go index c89386c92..6c651a21f 100644 --- a/internal/daemon/controller/controller.go +++ b/internal/daemon/controller/controller.go @@ -353,7 +353,7 @@ func (c *Controller) registerJobs() error { if err := pluginhost.RegisterJobs(c.baseContext, c.scheduler, rw, rw, c.kms, c.conf.HostPlugins); err != nil { return err } - if err := session.RegisterJobs(c.baseContext, c.scheduler, rw, c.conf.StatusGracePeriodDuration); err != nil { + if err := session.RegisterJobs(c.baseContext, c.scheduler, rw, rw, c.kms, c.conf.StatusGracePeriodDuration); err != nil { return err } diff --git a/internal/daemon/controller/handlers/sessions/session_service.go b/internal/daemon/controller/handlers/sessions/session_service.go index 4fa4d01c6..3a0388544 100644 --- a/internal/daemon/controller/handlers/sessions/session_service.go +++ b/internal/daemon/controller/handlers/sessions/session_service.go @@ -152,7 +152,7 @@ func (s Service) ListSessions(ctx context.Context, req *pbs.ListSessionsRequest) RootScopeId: req.GetScopeId(), Type: resource.Session, Recursive: req.GetRecursive(), - AuthzProtectedEntityProvider: repo, + AuthzProtectedEntityProvider: session.ListForAuthzCheck(repo, session.WithTerminated(req.IncludeTerminated)), ActionSet: IdActions, }, ) diff --git a/internal/daemon/controller/handlers/sessions/session_service_test.go b/internal/daemon/controller/handlers/sessions/session_service_test.go index 2b3d7b7a6..cbd864502 100644 --- a/internal/daemon/controller/handlers/sessions/session_service_test.go +++ b/internal/daemon/controller/handlers/sessions/session_service_test.go @@ -232,6 +232,7 @@ func TestList(t *testing.T) { conn, _ := db.TestSetup(t, "postgres") wrap := db.TestWrapper(t) kms := kms.TestKms(t, conn, wrap) + ctx := context.Background() iamRepo := iam.TestRepo(t, conn, wrap) @@ -260,16 +261,17 @@ func TestList(t *testing.T) { hs := static.TestSets(t, conn, hc.GetPublicId(), 1)[0] h := static.TestHosts(t, conn, hc.GetPublicId(), 1)[0] static.TestSetMembers(t, conn, hs.GetPublicId(), []*static.Host{h}) - tar := tcp.TestTarget(context.Background(), t, conn, pWithSessions.GetPublicId(), "test", target.WithHostSources([]string{hs.GetPublicId()})) + tar := tcp.TestTarget(ctx, t, conn, pWithSessions.GetPublicId(), "test", target.WithHostSources([]string{hs.GetPublicId()})) hcOther := static.TestCatalogs(t, conn, pWithOtherSessions.GetPublicId(), 1)[0] hsOther := static.TestSets(t, conn, hcOther.GetPublicId(), 1)[0] hOther := static.TestHosts(t, conn, hcOther.GetPublicId(), 1)[0] static.TestSetMembers(t, conn, hsOther.GetPublicId(), []*static.Host{hOther}) - tarOther := tcp.TestTarget(context.Background(), t, conn, pWithOtherSessions.GetPublicId(), "test", target.WithHostSources([]string{hsOther.GetPublicId()})) + tarOther := tcp.TestTarget(ctx, t, conn, pWithOtherSessions.GetPublicId(), "test", target.WithHostSources([]string{hsOther.GetPublicId()})) var wantSession []*pb.Session var totalSession []*pb.Session + var wantIncludeTerminatedSessions []*pb.Session for i := 0; i < 10; i++ { sess := session.TestSession(t, conn, wrap, session.ComposedOf{ UserId: uId, @@ -281,7 +283,7 @@ func TestList(t *testing.T) { Endpoint: "tcp://127.0.0.1:22", }) - c := session.TestConnection(t, conn, sess.PublicId, "127.0.0.1", 22, "127.0.0.2", 23, "127.0.0.1") + session.TestConnection(t, conn, sess.PublicId, "127.0.0.1", 22, "127.0.0.2", 23, "127.0.0.1") status, states := convertStates(sess.States) @@ -304,17 +306,11 @@ func TestList(t *testing.T) { Certificate: sess.Certificate, Type: tcp.Subtype.String(), AuthorizedActions: testAuthorizedActions, - Connections: []*pb.Connection{ - { - ClientTcpAddress: c.ClientTcpAddress, - ClientTcpPort: c.ClientTcpPort, - EndpointTcpAddress: c.EndpointTcpAddress, - EndpointTcpPort: c.EndpointTcpPort, - }, - }, + Connections: []*pb.Connection{}, // connections should not be returned for list }) totalSession = append(totalSession, wantSession[i]) + wantIncludeTerminatedSessions = append(wantIncludeTerminatedSessions, wantSession[i]) sess = session.TestSession(t, conn, wrap, session.ComposedOf{ UserId: uIdOther, @@ -326,7 +322,7 @@ func TestList(t *testing.T) { Endpoint: "tcp://127.0.0.1:22", }) - c = session.TestConnection(t, conn, sess.PublicId, "127.0.0.1", 22, "127.0.0.2", 23, "127.0.0.1") + session.TestConnection(t, conn, sess.PublicId, "127.0.0.1", 22, "127.0.0.2", 23, "127.0.0.1") status, states = convertStates(sess.States) @@ -349,17 +345,57 @@ func TestList(t *testing.T) { Certificate: sess.Certificate, Type: tcp.Subtype.String(), AuthorizedActions: testAuthorizedActions, - Connections: []*pb.Connection{ - { - ClientTcpAddress: c.ClientTcpAddress, - ClientTcpPort: c.ClientTcpPort, - EndpointTcpAddress: c.EndpointTcpAddress, - EndpointTcpPort: c.EndpointTcpPort, - }, - }, + Connections: []*pb.Connection{}, // connections should not be returned for list }) } + { + sess := session.TestSession(t, conn, wrap, session.ComposedOf{ + UserId: uId, + HostId: h.GetPublicId(), + TargetId: tar.GetPublicId(), + HostSetId: hs.GetPublicId(), + AuthTokenId: at.GetPublicId(), + ScopeId: pWithSessions.GetPublicId(), + Endpoint: "tcp://127.0.0.1:22", + }) + + sess, err := sessRepo.CancelSession(ctx, sess.PublicId, sess.Version) + require.NoError(t, err) + terminated, err := sessRepo.TerminateCompletedSessions(ctx) + require.NoError(t, err) + require.Equal(t, 1, terminated) + + sess, _, err = sessRepo.LookupSession(ctx, sess.PublicId) + require.NoError(t, err) + status, states := convertStates(sess.States) + + expected := &pb.Session{ + Id: sess.GetPublicId(), + ScopeId: pWithSessions.GetPublicId(), + AuthTokenId: at.GetPublicId(), + UserId: at.GetIamUserId(), + TargetId: sess.TargetId, + Endpoint: sess.Endpoint, + HostSetId: sess.HostSetId, + HostId: sess.HostId, + Version: sess.Version, + UpdatedTime: sess.UpdateTime.GetTimestamp(), + CreatedTime: sess.CreateTime.GetTimestamp(), + ExpirationTime: sess.ExpirationTime.GetTimestamp(), + Scope: &scopes.ScopeInfo{Id: pWithSessions.GetPublicId(), Type: scope.Project.String(), ParentScopeId: o.GetPublicId()}, + Status: status, + States: states, + Certificate: sess.Certificate, + TerminationReason: sess.TerminationReason, + Type: tcp.Subtype.String(), + AuthorizedActions: testAuthorizedActions, + Connections: []*pb.Connection{}, // connections should not be returned for list + } + + wantIncludeTerminatedSessions = append(wantIncludeTerminatedSessions, expected) + } + cases := []struct { name string req *pbs.ListSessionsRequest @@ -371,6 +407,11 @@ func TestList(t *testing.T) { req: &pbs.ListSessionsRequest{ScopeId: pWithSessions.GetPublicId()}, res: &pbs.ListSessionsResponse{Items: wantSession}, }, + { + name: "List Many Include Terminated", + req: &pbs.ListSessionsRequest{ScopeId: pWithSessions.GetPublicId(), IncludeTerminated: true}, + res: &pbs.ListSessionsResponse{Items: wantIncludeTerminatedSessions}, + }, { name: "List No Sessions", req: &pbs.ListSessionsRequest{ScopeId: pNoSessions.GetPublicId()}, @@ -423,14 +464,8 @@ func TestList(t *testing.T) { require.Equal(len(tc.res.GetItems()), len(got.GetItems()), "Didn't get expected number of sessions: %v", got.GetItems()) for i, wantSess := range tc.res.GetItems() { assert.True(got.GetItems()[i].GetExpirationTime().AsTime().Sub(wantSess.GetExpirationTime().AsTime()) < 10*time.Millisecond) - assert.Equal(1, len(wantSess.GetConnections())) + assert.Equal(0, len(wantSess.GetConnections())) // no connections on list wantSess.ExpirationTime = got.GetItems()[i].GetExpirationTime() - for _, c := range got.GetItems()[i].GetConnections() { - assert.Equal("127.0.0.1", c.ClientTcpAddress) - assert.Equal(uint32(22), c.ClientTcpPort) - assert.Equal("127.0.0.2", c.EndpointTcpAddress) - assert.Equal(uint32(23), c.EndpointTcpPort) - } } } assert.Empty(cmp.Diff(got, tc.res, protocmp.Transform()), "ListSessions(%q) got response %q, wanted %q", tc.req, got, tc.res) diff --git a/internal/db/schema/migrations/oss/postgres/20/09_session_list_view.up.sql b/internal/db/schema/migrations/oss/postgres/20/09_session_list_view.up.sql index 2feb2a68c..8d9b9f30f 100644 --- a/internal/db/schema/migrations/oss/postgres/20/09_session_list_view.up.sql +++ b/internal/db/schema/migrations/oss/postgres/20/09_session_list_view.up.sql @@ -1,6 +1,7 @@ begin; -- Replaces the view created in 1/01 to include connections +-- Replaced in 31/0_session_list_no_connections drop view session_with_state; create view session_list as select diff --git a/internal/db/schema/migrations/oss/postgres/31/01_delete_termianted_sessions.up.sql b/internal/db/schema/migrations/oss/postgres/31/01_delete_termianted_sessions.up.sql new file mode 100644 index 000000000..9b0a49102 --- /dev/null +++ b/internal/db/schema/migrations/oss/postgres/31/01_delete_termianted_sessions.up.sql @@ -0,0 +1,12 @@ +begin; + delete from session + using session_state + where + session.public_id = session_state.session_id + and + session_state.state = 'terminated' + and + session_state.start_time < wt_sub_seconds_from_now(3600); + + analyze; +commit; diff --git a/internal/db/schema/migrations/oss/postgres/31/02_session_list_no_connections.up.sql b/internal/db/schema/migrations/oss/postgres/31/02_session_list_no_connections.up.sql new file mode 100644 index 000000000..e012f2d77 --- /dev/null +++ b/internal/db/schema/migrations/oss/postgres/31/02_session_list_no_connections.up.sql @@ -0,0 +1,38 @@ +begin; + +-- Replaces the view created in 2/09_session_list_view +drop view session_list; +create view session_list as + select + s.public_id, + s.user_id, + s.host_id, + s.server_id, + s.server_type, + s.target_id, + s.host_set_id, + s.auth_token_id, + s.scope_id, + s.certificate, + s.expiration_time, + s.connection_limit, + s.tofu_token, + s.key_id, + s.termination_reason, + s.version, + s.create_time, + s.update_time, + s.endpoint, + s.worker_filter, + ss.state, + ss.previous_end_time, + ss.start_time, + ss.end_time + from + session s + join + session_state ss + on + s.public_id = ss.session_id; + +commit; diff --git a/internal/gen/controller.swagger.json b/internal/gen/controller.swagger.json index 3c80e7784..5c1718c85 100644 --- a/internal/gen/controller.swagger.json +++ b/internal/gen/controller.swagger.json @@ -2523,6 +2523,13 @@ "in": "query", "required": false, "type": "string" + }, + { + "name": "include_terminated", + "description": "Experimental. By default only non-terminated (i.e. pending, active, canceling) are returned.\nSet this option to include terminated sessions as well.", + "in": "query", + "required": false, + "type": "boolean" } ], "tags": [ diff --git a/internal/gen/controller/api/services/session_service.pb.go b/internal/gen/controller/api/services/session_service.pb.go index fa7698e56..28a2aaf64 100644 --- a/internal/gen/controller/api/services/session_service.pb.go +++ b/internal/gen/controller/api/services/session_service.pb.go @@ -125,6 +125,9 @@ type ListSessionsRequest struct { ScopeId string `protobuf:"bytes,1,opt,name=scope_id,json=scopeId,proto3" json:"scope_id,omitempty" class:"public"` // @gotags: `class:"public"` Recursive bool `protobuf:"varint,20,opt,name=recursive,proto3" json:"recursive,omitempty" class:"public"` // @gotags: `class:"public"` Filter string `protobuf:"bytes,30,opt,name=filter,proto3" json:"filter,omitempty" class:"public"` // @gotags: `class:"public"` + // Experimental. By default only non-terminated (i.e. pending, active, canceling) are returned. + // Set this option to include terminated sessions as well. + IncludeTerminated bool `protobuf:"varint,40,opt,name=include_terminated,proto3" json:"include_terminated,omitempty" class:"public"` // @gotags: `class:"public"` } func (x *ListSessionsRequest) Reset() { @@ -180,6 +183,13 @@ func (x *ListSessionsRequest) GetFilter() string { return "" } +func (x *ListSessionsRequest) GetIncludeTerminated() bool { + if x != nil { + return x.IncludeTerminated + } + return false +} + type ListSessionsResponse struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache @@ -352,68 +362,72 @@ var file_controller_api_services_v1_session_service_proto_rawDesc = []byte{ 0x74, 0x65, 0x6d, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x2d, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x2e, 0x73, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x2e, 0x76, 0x31, - 0x2e, 0x53, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x52, 0x04, 0x69, 0x74, 0x65, 0x6d, 0x22, 0x66, - 0x0a, 0x13, 0x4c, 0x69, 0x73, 0x74, 0x53, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x52, 0x65, - 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x19, 0x0a, 0x08, 0x73, 0x63, 0x6f, 0x70, 0x65, 0x5f, 0x69, - 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x73, 0x63, 0x6f, 0x70, 0x65, 0x49, 0x64, - 0x12, 0x1c, 0x0a, 0x09, 0x72, 0x65, 0x63, 0x75, 0x72, 0x73, 0x69, 0x76, 0x65, 0x18, 0x14, 0x20, - 0x01, 0x28, 0x08, 0x52, 0x09, 0x72, 0x65, 0x63, 0x75, 0x72, 0x73, 0x69, 0x76, 0x65, 0x12, 0x16, - 0x0a, 0x06, 0x66, 0x69, 0x6c, 0x74, 0x65, 0x72, 0x18, 0x1e, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, - 0x66, 0x69, 0x6c, 0x74, 0x65, 0x72, 0x22, 0x5b, 0x0a, 0x14, 0x4c, 0x69, 0x73, 0x74, 0x53, 0x65, - 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x43, - 0x0a, 0x05, 0x69, 0x74, 0x65, 0x6d, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x2d, 0x2e, + 0x2e, 0x53, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x52, 0x04, 0x69, 0x74, 0x65, 0x6d, 0x22, 0x96, + 0x01, 0x0a, 0x13, 0x4c, 0x69, 0x73, 0x74, 0x53, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x52, + 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x19, 0x0a, 0x08, 0x73, 0x63, 0x6f, 0x70, 0x65, 0x5f, + 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x73, 0x63, 0x6f, 0x70, 0x65, 0x49, + 0x64, 0x12, 0x1c, 0x0a, 0x09, 0x72, 0x65, 0x63, 0x75, 0x72, 0x73, 0x69, 0x76, 0x65, 0x18, 0x14, + 0x20, 0x01, 0x28, 0x08, 0x52, 0x09, 0x72, 0x65, 0x63, 0x75, 0x72, 0x73, 0x69, 0x76, 0x65, 0x12, + 0x16, 0x0a, 0x06, 0x66, 0x69, 0x6c, 0x74, 0x65, 0x72, 0x18, 0x1e, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x06, 0x66, 0x69, 0x6c, 0x74, 0x65, 0x72, 0x12, 0x2e, 0x0a, 0x12, 0x69, 0x6e, 0x63, 0x6c, 0x75, + 0x64, 0x65, 0x5f, 0x74, 0x65, 0x72, 0x6d, 0x69, 0x6e, 0x61, 0x74, 0x65, 0x64, 0x18, 0x28, 0x20, + 0x01, 0x28, 0x08, 0x52, 0x12, 0x69, 0x6e, 0x63, 0x6c, 0x75, 0x64, 0x65, 0x5f, 0x74, 0x65, 0x72, + 0x6d, 0x69, 0x6e, 0x61, 0x74, 0x65, 0x64, 0x22, 0x5b, 0x0a, 0x14, 0x4c, 0x69, 0x73, 0x74, 0x53, + 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, + 0x43, 0x0a, 0x05, 0x69, 0x74, 0x65, 0x6d, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x2d, + 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2e, 0x61, 0x70, 0x69, 0x2e, + 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x2e, 0x73, 0x65, 0x73, 0x73, 0x69, 0x6f, + 0x6e, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x53, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x52, 0x05, 0x69, + 0x74, 0x65, 0x6d, 0x73, 0x22, 0x40, 0x0a, 0x14, 0x43, 0x61, 0x6e, 0x63, 0x65, 0x6c, 0x53, 0x65, + 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x0e, 0x0a, 0x02, + 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x12, 0x18, 0x0a, 0x07, + 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x07, 0x76, + 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x22, 0x5a, 0x0a, 0x15, 0x43, 0x61, 0x6e, 0x63, 0x65, 0x6c, + 0x53, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, + 0x41, 0x0a, 0x04, 0x69, 0x74, 0x65, 0x6d, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x2d, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x2e, 0x73, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, - 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x53, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x52, 0x05, 0x69, 0x74, - 0x65, 0x6d, 0x73, 0x22, 0x40, 0x0a, 0x14, 0x43, 0x61, 0x6e, 0x63, 0x65, 0x6c, 0x53, 0x65, 0x73, - 0x73, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x0e, 0x0a, 0x02, 0x69, - 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x12, 0x18, 0x0a, 0x07, 0x76, - 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x07, 0x76, 0x65, - 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x22, 0x5a, 0x0a, 0x15, 0x43, 0x61, 0x6e, 0x63, 0x65, 0x6c, 0x53, - 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x41, - 0x0a, 0x04, 0x69, 0x74, 0x65, 0x6d, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x2d, 0x2e, 0x63, - 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x72, 0x65, - 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x2e, 0x73, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x73, - 0x2e, 0x76, 0x31, 0x2e, 0x53, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x52, 0x04, 0x69, 0x74, 0x65, - 0x6d, 0x32, 0x95, 0x04, 0x0a, 0x0e, 0x53, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x53, 0x65, 0x72, - 0x76, 0x69, 0x63, 0x65, 0x12, 0xa7, 0x01, 0x0a, 0x0a, 0x47, 0x65, 0x74, 0x53, 0x65, 0x73, 0x73, - 0x69, 0x6f, 0x6e, 0x12, 0x2d, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, - 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x73, 0x2e, 0x76, 0x31, - 0x2e, 0x47, 0x65, 0x74, 0x53, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, - 0x73, 0x74, 0x1a, 0x2e, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2e, - 0x61, 0x70, 0x69, 0x2e, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x73, 0x2e, 0x76, 0x31, 0x2e, - 0x47, 0x65, 0x74, 0x53, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, - 0x73, 0x65, 0x22, 0x3a, 0x92, 0x41, 0x18, 0x12, 0x16, 0x47, 0x65, 0x74, 0x73, 0x20, 0x61, 0x20, - 0x73, 0x69, 0x6e, 0x67, 0x6c, 0x65, 0x20, 0x53, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x2e, 0x82, - 0xd3, 0xe4, 0x93, 0x02, 0x19, 0x12, 0x11, 0x2f, 0x76, 0x31, 0x2f, 0x73, 0x65, 0x73, 0x73, 0x69, - 0x6f, 0x6e, 0x73, 0x2f, 0x7b, 0x69, 0x64, 0x7d, 0x62, 0x04, 0x69, 0x74, 0x65, 0x6d, 0x12, 0x9f, - 0x01, 0x0a, 0x0c, 0x4c, 0x69, 0x73, 0x74, 0x53, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x12, - 0x2f, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2e, 0x61, 0x70, 0x69, - 0x2e, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x4c, 0x69, 0x73, - 0x74, 0x53, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, - 0x1a, 0x30, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2e, 0x61, 0x70, - 0x69, 0x2e, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x4c, 0x69, - 0x73, 0x74, 0x53, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, - 0x73, 0x65, 0x22, 0x2c, 0x92, 0x41, 0x15, 0x12, 0x13, 0x4c, 0x69, 0x73, 0x74, 0x73, 0x20, 0x61, - 0x6c, 0x6c, 0x20, 0x53, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x2e, 0x82, 0xd3, 0xe4, 0x93, - 0x02, 0x0e, 0x12, 0x0c, 0x2f, 0x76, 0x31, 0x2f, 0x73, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x73, - 0x12, 0xb6, 0x01, 0x0a, 0x0d, 0x43, 0x61, 0x6e, 0x63, 0x65, 0x6c, 0x53, 0x65, 0x73, 0x73, 0x69, - 0x6f, 0x6e, 0x12, 0x30, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2e, - 0x61, 0x70, 0x69, 0x2e, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x73, 0x2e, 0x76, 0x31, 0x2e, - 0x43, 0x61, 0x6e, 0x63, 0x65, 0x6c, 0x53, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x71, - 0x75, 0x65, 0x73, 0x74, 0x1a, 0x31, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, + 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x53, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x52, 0x04, 0x69, 0x74, + 0x65, 0x6d, 0x32, 0x95, 0x04, 0x0a, 0x0e, 0x53, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x53, 0x65, + 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0xa7, 0x01, 0x0a, 0x0a, 0x47, 0x65, 0x74, 0x53, 0x65, 0x73, + 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x2d, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x73, 0x2e, 0x76, - 0x31, 0x2e, 0x43, 0x61, 0x6e, 0x63, 0x65, 0x6c, 0x53, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x52, - 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x40, 0x92, 0x41, 0x14, 0x12, 0x12, 0x43, 0x61, - 0x6e, 0x63, 0x65, 0x6c, 0x73, 0x20, 0x61, 0x20, 0x53, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x2e, - 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x23, 0x22, 0x18, 0x2f, 0x76, 0x31, 0x2f, 0x73, 0x65, 0x73, 0x73, - 0x69, 0x6f, 0x6e, 0x73, 0x2f, 0x7b, 0x69, 0x64, 0x7d, 0x3a, 0x63, 0x61, 0x6e, 0x63, 0x65, 0x6c, - 0x3a, 0x01, 0x2a, 0x62, 0x04, 0x69, 0x74, 0x65, 0x6d, 0x42, 0x4d, 0x5a, 0x4b, 0x67, 0x69, 0x74, - 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x68, 0x61, 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72, - 0x70, 0x2f, 0x62, 0x6f, 0x75, 0x6e, 0x64, 0x61, 0x72, 0x79, 0x2f, 0x69, 0x6e, 0x74, 0x65, 0x72, - 0x6e, 0x61, 0x6c, 0x2f, 0x67, 0x65, 0x6e, 0x2f, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, - 0x65, 0x72, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x73, 0x3b, - 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x73, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, + 0x31, 0x2e, 0x47, 0x65, 0x74, 0x53, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x71, 0x75, + 0x65, 0x73, 0x74, 0x1a, 0x2e, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, + 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x73, 0x2e, 0x76, 0x31, + 0x2e, 0x47, 0x65, 0x74, 0x53, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, + 0x6e, 0x73, 0x65, 0x22, 0x3a, 0x92, 0x41, 0x18, 0x12, 0x16, 0x47, 0x65, 0x74, 0x73, 0x20, 0x61, + 0x20, 0x73, 0x69, 0x6e, 0x67, 0x6c, 0x65, 0x20, 0x53, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x2e, + 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x19, 0x12, 0x11, 0x2f, 0x76, 0x31, 0x2f, 0x73, 0x65, 0x73, 0x73, + 0x69, 0x6f, 0x6e, 0x73, 0x2f, 0x7b, 0x69, 0x64, 0x7d, 0x62, 0x04, 0x69, 0x74, 0x65, 0x6d, 0x12, + 0x9f, 0x01, 0x0a, 0x0c, 0x4c, 0x69, 0x73, 0x74, 0x53, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x73, + 0x12, 0x2f, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2e, 0x61, 0x70, + 0x69, 0x2e, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x4c, 0x69, + 0x73, 0x74, 0x53, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, + 0x74, 0x1a, 0x30, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2e, 0x61, + 0x70, 0x69, 0x2e, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x4c, + 0x69, 0x73, 0x74, 0x53, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, + 0x6e, 0x73, 0x65, 0x22, 0x2c, 0x92, 0x41, 0x15, 0x12, 0x13, 0x4c, 0x69, 0x73, 0x74, 0x73, 0x20, + 0x61, 0x6c, 0x6c, 0x20, 0x53, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x2e, 0x82, 0xd3, 0xe4, + 0x93, 0x02, 0x0e, 0x12, 0x0c, 0x2f, 0x76, 0x31, 0x2f, 0x73, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, + 0x73, 0x12, 0xb6, 0x01, 0x0a, 0x0d, 0x43, 0x61, 0x6e, 0x63, 0x65, 0x6c, 0x53, 0x65, 0x73, 0x73, + 0x69, 0x6f, 0x6e, 0x12, 0x30, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, + 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x73, 0x2e, 0x76, 0x31, + 0x2e, 0x43, 0x61, 0x6e, 0x63, 0x65, 0x6c, 0x53, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x52, 0x65, + 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x31, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, + 0x65, 0x72, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x73, 0x2e, + 0x76, 0x31, 0x2e, 0x43, 0x61, 0x6e, 0x63, 0x65, 0x6c, 0x53, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, + 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x40, 0x92, 0x41, 0x14, 0x12, 0x12, 0x43, + 0x61, 0x6e, 0x63, 0x65, 0x6c, 0x73, 0x20, 0x61, 0x20, 0x53, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, + 0x2e, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x23, 0x22, 0x18, 0x2f, 0x76, 0x31, 0x2f, 0x73, 0x65, 0x73, + 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x2f, 0x7b, 0x69, 0x64, 0x7d, 0x3a, 0x63, 0x61, 0x6e, 0x63, 0x65, + 0x6c, 0x3a, 0x01, 0x2a, 0x62, 0x04, 0x69, 0x74, 0x65, 0x6d, 0x42, 0x4d, 0x5a, 0x4b, 0x67, 0x69, + 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x68, 0x61, 0x73, 0x68, 0x69, 0x63, 0x6f, + 0x72, 0x70, 0x2f, 0x62, 0x6f, 0x75, 0x6e, 0x64, 0x61, 0x72, 0x79, 0x2f, 0x69, 0x6e, 0x74, 0x65, + 0x72, 0x6e, 0x61, 0x6c, 0x2f, 0x67, 0x65, 0x6e, 0x2f, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, + 0x6c, 0x65, 0x72, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x73, + 0x3b, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x73, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, + 0x33, } var ( diff --git a/internal/proto/controller/api/services/v1/session_service.proto b/internal/proto/controller/api/services/v1/session_service.proto index 6d379450d..e0eeed08b 100644 --- a/internal/proto/controller/api/services/v1/session_service.proto +++ b/internal/proto/controller/api/services/v1/session_service.proto @@ -63,6 +63,9 @@ message ListSessionsRequest { string scope_id = 1; // @gotags: `class:"public"` bool recursive = 20 [json_name = "recursive"]; // @gotags: `class:"public"` string filter = 30 [json_name = "filter"]; // @gotags: `class:"public"` + // Experimental. By default only non-terminated (i.e. pending, active, canceling) are returned. + // Set this option to include terminated sessions as well. + bool include_terminated = 40 [json_name = "include_terminated"]; // @gotags: `class:"public"` } message ListSessionsResponse { diff --git a/internal/session/job_delete_terminated_sessions.go b/internal/session/job_delete_terminated_sessions.go new file mode 100644 index 000000000..4318a9e12 --- /dev/null +++ b/internal/session/job_delete_terminated_sessions.go @@ -0,0 +1,75 @@ +package session + +import ( + "context" + "time" + + "github.com/hashicorp/boundary/internal/errors" + "github.com/hashicorp/boundary/internal/scheduler" +) + +type deleteTerminatedJob struct { + repo *Repository + + // the amount of time that a session must be in the terminated + // state for it to be deleted. + threshold time.Duration + + // the number of sessions deleted in the most recent run + deletedInRun int +} + +func newDeleteTerminatedJob(ctx context.Context, repo *Repository, threshold time.Duration) (*deleteTerminatedJob, error) { + const op = "session.newDeleteTerminatedJob" + switch { + case repo == nil: + return nil, errors.New(ctx, errors.InvalidParameter, op, "missing repository") + } + + return &deleteTerminatedJob{ + repo: repo, + threshold: threshold, + }, nil +} + +// Status reports the job’s current status. The status is periodically persisted by +// the scheduler when a job is running, and will be used to verify a job is making progress. +func (d *deleteTerminatedJob) Status() scheduler.JobStatus { + return scheduler.JobStatus{ + Completed: d.deletedInRun, + Total: d.deletedInRun, + } +} + +// Run performs the required work depending on the implementation. +// The context is used to notify the job that it should exit early. +func (d *deleteTerminatedJob) Run(ctx context.Context) error { + const op = "session.(deleteTerminatedJob).Run" + d.deletedInRun = 0 + var err error + + d.deletedInRun, err = d.repo.deleteSessionsTerminatedBefore(ctx, d.threshold) + if err != nil { + return errors.Wrap(ctx, err, op) + } + return nil +} + +// NextRunIn returns the duration until the next job run should be scheduled. This +// method is invoked after a run has successfully completed and the next run time +// is being persisted by the scheduler. If an error is returned, the error will be logged +// but the duration returned will still be used in scheduling. If a zero duration is returned +// the job will be scheduled to run again immediately. +func (d *deleteTerminatedJob) NextRunIn(_ context.Context) (time.Duration, error) { + return time.Minute * 30, nil +} + +// Name is the unique name of the job. +func (d *deleteTerminatedJob) Name() string { + return "delete_terminated_sessions" +} + +// Description is the human readable description of the job. +func (d *deleteTerminatedJob) Description() string { + return "Delete terminated sessions that were terminated " +} diff --git a/internal/session/job_delete_terminated_sessions_test.go b/internal/session/job_delete_terminated_sessions_test.go new file mode 100644 index 000000000..40fc58d92 --- /dev/null +++ b/internal/session/job_delete_terminated_sessions_test.go @@ -0,0 +1,106 @@ +package session + +import ( + "context" + "fmt" + "testing" + "time" + + "github.com/hashicorp/boundary/internal/db" + "github.com/hashicorp/boundary/internal/iam" + "github.com/hashicorp/boundary/internal/kms" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestDeleteTermiantedSessionsJob(t *testing.T) { + ctx := context.Background() + conn, _ := db.TestSetup(t, "postgres") + rw := db.New(conn) + wrapper := db.TestWrapper(t) + iamRepo := iam.TestRepo(t, conn, wrapper) + kms := kms.TestKms(t, conn, wrapper) + repo, err := NewRepository(rw, rw, kms) + require.NoError(t, err) + composedOf := TestSessionParams(t, conn, wrapper, iamRepo) + require.NoError(t, err) + + cases := []struct { + sessionCount int + terminateCount int + threshold time.Duration + expected int + }{ + { + 0, + 0, + time.Nanosecond, + 0, + }, + { + 1, + 1, + time.Nanosecond, + 1, + }, + { + 1, + 1, + time.Hour, + 0, + }, + { + 10, + 10, + time.Nanosecond, + 10, + }, + { + 10, + 4, + time.Nanosecond, + 4, + }, + { + 10, + 0, + time.Nanosecond, + 0, + }, + { + 10, + 10, + time.Hour, + 0, + }, + } + + for _, tc := range cases { + t.Run(fmt.Sprintf("%d_%d_%s", tc.sessionCount, tc.terminateCount, tc.threshold), func(t *testing.T) { + t.Cleanup(func() { + sdb, err := conn.SqlDB(ctx) + require.NoError(t, err) + _, err = sdb.Exec(`delete from session;`) + require.NoError(t, err) + }) + + for i := 0; i < tc.sessionCount; i++ { + s := TestSession(t, conn, wrapper, composedOf) + if i < tc.terminateCount { + _, err = repo.CancelSession(ctx, s.PublicId, s.Version) + require.NoError(t, err) + } + + } + c, err := repo.TerminateCompletedSessions(ctx) + require.NoError(t, err) + assert.Equal(t, tc.terminateCount, c) + + job, err := newDeleteTerminatedJob(ctx, repo, tc.threshold) + require.NoError(t, err) + err = job.Run(ctx) + require.NoError(t, err) + assert.Equal(t, tc.expected, job.deletedInRun) + }) + } +} diff --git a/internal/session/session_cleanup_job.go b/internal/session/job_session_cleanup.go similarity index 100% rename from internal/session/session_cleanup_job.go rename to internal/session/job_session_cleanup.go diff --git a/internal/session/session_cleanup_job_test.go b/internal/session/job_session_cleanup_test.go similarity index 100% rename from internal/session/session_cleanup_job_test.go rename to internal/session/job_session_cleanup_test.go diff --git a/internal/session/jobs.go b/internal/session/jobs.go index f99d7ed06..70f1e113b 100644 --- a/internal/session/jobs.go +++ b/internal/session/jobs.go @@ -6,12 +6,16 @@ import ( "time" "github.com/hashicorp/boundary/internal/db" + "github.com/hashicorp/boundary/internal/kms" "github.com/hashicorp/boundary/internal/scheduler" ) -// RegisterJobs registers plugin host related jobs with the provided scheduler. -func RegisterJobs(ctx context.Context, scheduler *scheduler.Scheduler, w db.Writer, gracePeriod time.Duration) error { +const deleteTerminatedThreshold = time.Hour + +// RegisterJobs registers session related jobs with the provided scheduler. +func RegisterJobs(ctx context.Context, scheduler *scheduler.Scheduler, w db.Writer, r db.Reader, k *kms.Kms, gracePeriod time.Duration) error { const op = "session.RegisterJobs" + sessionConnectionCleanupJob, err := newSessionConnectionCleanupJob(w, gracePeriod) if err != nil { return fmt.Errorf("error creating session cleanup job: %w", err) @@ -20,5 +24,17 @@ func RegisterJobs(ctx context.Context, scheduler *scheduler.Scheduler, w db.Writ return fmt.Errorf("error registering session cleanup job: %w", err) } + repo, err := NewRepository(r, w, k) + if err != nil { + return fmt.Errorf("error creating repository: %w", err) + } + deleteTerminatedJob, err := newDeleteTerminatedJob(ctx, repo, deleteTerminatedThreshold) + if err != nil { + return fmt.Errorf("error creating delete terminated session job: %w", err) + } + if err = scheduler.RegisterJob(ctx, deleteTerminatedJob); err != nil { + return fmt.Errorf("error registering delete terminated session job: %w", err) + } + return nil } diff --git a/internal/session/options.go b/internal/session/options.go index a4e5a335b..e69ec2c7d 100644 --- a/internal/session/options.go +++ b/internal/session/options.go @@ -32,6 +32,7 @@ type options struct { withServerId string withDbOpts []db.Option withWorkerStateDelay time.Duration + withTerminated bool } func getDefaultOptions() options { @@ -121,3 +122,10 @@ func WithWorkerStateDelay(d time.Duration) Option { o.withWorkerStateDelay = d } } + +// WithTerminated is used to include terminated sessions in a list request. +func WithTerminated(withTerminated bool) Option { + return func(o *options) { + o.withTerminated = withTerminated + } +} diff --git a/internal/session/query.go b/internal/session/query.go index 7a10af70f..5242ff9a1 100644 --- a/internal/session/query.go +++ b/internal/session/query.go @@ -117,11 +117,20 @@ session_connection_limit(expiration_time, connection_limit) as ( select expiration_time, connection_limit, current_connection_count from session_connection_limit, session_connection_count; +` + nonTerminatedSessionPublicIdList = ` +select public_id, scope_id, user_id from session +where + session.termination_reason is null +and + scope_id = any(@scope_ids) +; ` sessionPublicIdList = ` select public_id, scope_id, user_id from session -%s +where + scope_id = any(@scope_ids) ; ` @@ -388,6 +397,17 @@ where and ss.end_time is null %s ; +` + deleteTerminated = ` +delete from session +using session_state +where + session.public_id = session_state.session_id +and + session_state.state = 'terminated' +and + session_state.start_time < wt_sub_seconds_from_now(@threshold_seconds) +; ` ) diff --git a/internal/session/repository.go b/internal/session/repository.go index 8735c8d7f..d9f4c860a 100644 --- a/internal/session/repository.go +++ b/internal/session/repository.go @@ -83,8 +83,6 @@ func (r *Repository) convertToSessions(ctx context.Context, sessionList []*sessi return nil, nil } sessions := []*Session{} - // deduplication map of connections by connection_id - connections := map[string]*Connection{} // deduplication map of states by end_time states := map[*timestamp.Timestamp]*State{} var prevSessionId string @@ -92,10 +90,6 @@ func (r *Repository) convertToSessions(ctx context.Context, sessionList []*sessi for _, sv := range sessionList { if sv.PublicId != prevSessionId { if prevSessionId != "" { - for _, c := range connections { - workingSession.Connections = append(workingSession.Connections, c) - } - connections = map[string]*Connection{} for _, s := range states { workingSession.States = append(workingSession.States, s) } @@ -157,22 +151,6 @@ func (r *Repository) convertToSessions(ctx context.Context, sessionList []*sessi } } - if sv.ConnectionId != "" { - if _, ok := connections[sv.ConnectionId]; !ok { - connections[sv.ConnectionId] = &Connection{ - PublicId: sv.ConnectionId, - SessionId: sv.PublicId, - ClientTcpAddress: sv.ClientTcpAddress, - ClientTcpPort: sv.ClientTcpPort, - EndpointTcpAddress: sv.EndpointTcpAddress, - EndpointTcpPort: sv.EndpointTcpPort, - BytesUp: sv.BytesUp, - BytesDown: sv.BytesDown, - ClosedReason: sv.ClosedReason, - } - } - } - } for _, s := range states { workingSession.States = append(workingSession.States, s) @@ -180,9 +158,6 @@ func (r *Repository) convertToSessions(ctx context.Context, sessionList []*sessi sort.Slice(workingSession.States, func(i, j int) bool { return workingSession.States[i].StartTime.GetTimestamp().AsTime().After(workingSession.States[j].StartTime.GetTimestamp().AsTime()) }) - for _, c := range connections { - workingSession.Connections = append(workingSession.Connections, c) - } sessions = append(sessions, workingSession) return sessions, nil } diff --git a/internal/session/repository_session.go b/internal/session/repository_session.go index d16edfa21..07cfd76fb 100644 --- a/internal/session/repository_session.go +++ b/internal/session/repository_session.go @@ -7,6 +7,7 @@ import ( "database/sql" "fmt" "strings" + "time" "github.com/hashicorp/boundary/internal/boundary" "github.com/hashicorp/boundary/internal/db" @@ -206,32 +207,31 @@ func (r *Repository) LookupSession(ctx context.Context, sessionId string, _ ...O return &session, authzSummary, nil } -// FetchAuthzProtectedEntitiesByScope implements boundary.AuthzProtectedEntityProvider -func (r *Repository) FetchAuthzProtectedEntitiesByScope(ctx context.Context, scopeIds []string) (map[string][]boundary.AuthzProtectedEntity, error) { - const op = "session.(Repository).FetchAuthzProtectedEntityInfo" +// fetchAuthzProtectedSessionsByScope fetches sessions for the given scopes. +// Note that the sessions are not fully populated, and only contain the +// necessary information to implement the boundary.AuthzProtectedEntity +// interface. Supports the WithTerminated option. +func (r *Repository) fetchAuthzProtectedSessionsByScope( + ctx context.Context, scopeIds []string, opt ...Option, +) (map[string][]boundary.AuthzProtectedEntity, error) { + const op = "session.(Repository).fetchAuthzProtectedSessionsByScope" - var where string - var args []interface{} + opts := getOpts(opt...) - inClauseCnt := 0 - - switch len(scopeIds) { - case 0: + if len(scopeIds) == 0 { return nil, errors.New(ctx, errors.InvalidParameter, op, "no scopes given") - case 1: - inClauseCnt += 1 - where, args = fmt.Sprintf("where scope_id = @%d", inClauseCnt), append(args, sql.Named(fmt.Sprintf("%d", inClauseCnt), scopeIds[0])) - default: - idsInClause := make([]string, 0, len(scopeIds)) - for _, id := range scopeIds { - inClauseCnt += 1 - idsInClause, args = append(idsInClause, fmt.Sprintf("@%d", inClauseCnt)), append(args, sql.Named(fmt.Sprintf("%d", inClauseCnt), id)) - } - where = fmt.Sprintf("where scope_id in (%s)", strings.Join(idsInClause, ",")) } - q := sessionPublicIdList - query := fmt.Sprintf(q, where) + args := []interface{}{ + sql.Named("scope_ids", "{"+strings.Join(scopeIds, ",")+"}"), + } + + var query string + if opts.withTerminated { + query = sessionPublicIdList + } else { + query = nonTerminatedSessionPublicIdList + } rows, err := r.reader.Query(ctx, query, args) if err != nil { @@ -700,6 +700,20 @@ func (r *Repository) checkIfNoLongerActive(ctx context.Context, reportedSessions return notActive, nil } +func (r *Repository) deleteSessionsTerminatedBefore(ctx context.Context, threshold time.Duration) (int, error) { + const op = "session.(Repository).deleteTerminated" + + args := []any{ + sql.Named("threshold_seconds", threshold.Seconds()), + } + + c, err := r.writer.Exec(ctx, deleteTerminated, args) + if err != nil { + return 0, errors.Wrap(ctx, err, op, errors.WithMsg("error deleting terminated sessions")) + } + return c, nil +} + func fetchStates(ctx context.Context, r db.Reader, sessionId string, opt ...db.Option) ([]*State, error) { const op = "session.fetchStates" var states []*State diff --git a/internal/session/repository_session_test.go b/internal/session/repository_session_test.go index 408633743..45309cd20 100644 --- a/internal/session/repository_session_test.go +++ b/internal/session/repository_session_test.go @@ -126,7 +126,8 @@ func TestRepository_ListSession(t *testing.T) { require.NoError(err) assert.Equal(tt.wantCnt, len(got)) for i := 0; i < len(got); i++ { - assert.Equal(tt.withConnections, len(got[i].Connections)) + // connections should not be returned for list requests + assert.Equal(0, len(got[i].Connections)) for _, c := range got[i].Connections { assert.Equal("127.0.0.1", c.ClientTcpAddress) assert.Equal(uint32(22), c.ClientTcpPort) @@ -1503,3 +1504,320 @@ func TestRepository_deleteTargetFKey(t *testing.T) { }) } } + +func TestRepository_deleteTerminated(t *testing.T) { + ctx := context.Background() + conn, _ := db.TestSetup(t, "postgres") + rw := db.New(conn) + wrapper := db.TestWrapper(t) + iamRepo := iam.TestRepo(t, conn, wrapper) + kms := kms.TestKms(t, conn, wrapper) + repo, err := NewRepository(rw, rw, kms) + composedOf := TestSessionParams(t, conn, wrapper, iamRepo) + + cases := []struct { + sessionCount int + terminateCount int + threshold time.Duration + expected int + }{ + { + 0, + 0, + time.Nanosecond, + 0, + }, + { + 1, + 1, + time.Nanosecond, + 1, + }, + { + 1, + 1, + time.Hour, + 0, + }, + { + 10, + 10, + time.Nanosecond, + 10, + }, + { + 10, + 4, + time.Nanosecond, + 4, + }, + { + 10, + 0, + time.Nanosecond, + 0, + }, + { + 10, + 10, + time.Hour, + 0, + }, + } + + for _, tc := range cases { + t.Run(fmt.Sprintf("%d_%d_%s", tc.sessionCount, tc.terminateCount, tc.threshold), func(t *testing.T) { + t.Cleanup(func() { + sdb, err := conn.SqlDB(ctx) + require.NoError(t, err) + _, err = sdb.Exec(`delete from session;`) + require.NoError(t, err) + }) + + for i := 0; i < tc.sessionCount; i++ { + s := TestSession(t, conn, wrapper, composedOf) + if i < tc.terminateCount { + _, err = repo.CancelSession(ctx, s.PublicId, s.Version) + require.NoError(t, err) + } + + } + c, err := repo.TerminateCompletedSessions(ctx) + require.NoError(t, err) + assert.Equal(t, tc.terminateCount, c) + + c, err = repo.deleteSessionsTerminatedBefore(ctx, tc.threshold) + require.NoError(t, err) + assert.Equal(t, tc.expected, c) + }) + } +} + +func TestFetchAuthzProtectedSessionsByScopes(t *testing.T) { + conn, _ := db.TestSetup(t, "postgres") + ctx := context.Background() + wrapper := db.TestWrapper(t) + iamRepo := iam.TestRepo(t, conn, wrapper) + rw := db.New(conn) + kms := kms.TestKms(t, conn, wrapper) + repo, err := NewRepository(rw, rw, kms) + require.NoError(t, err) + composedOf := TestSessionParams(t, conn, wrapper, iamRepo) + + _, pWithOtherSessions := iam.TestScopes(t, iamRepo) + + hcOther := static.TestCatalogs(t, conn, pWithOtherSessions.GetPublicId(), 1)[0] + hsOther := static.TestSets(t, conn, hcOther.GetPublicId(), 1)[0] + hOther := static.TestHosts(t, conn, hcOther.GetPublicId(), 1)[0] + static.TestSetMembers(t, conn, hsOther.GetPublicId(), []*static.Host{hOther}) + tarOther := tcp.TestTarget(ctx, t, conn, pWithOtherSessions.GetPublicId(), "test", target.WithHostSources([]string{hsOther.GetPublicId()})) + + composedOfOther := ComposedOf{ + UserId: composedOf.UserId, + HostId: hOther.GetPublicId(), + TargetId: tarOther.GetPublicId(), + HostSetId: hsOther.GetPublicId(), + AuthTokenId: composedOf.AuthTokenId, + ScopeId: pWithOtherSessions.GetPublicId(), + Endpoint: "tcp://127.0.0.1:22", + } + + type testCase struct { + name string + createCnt int + terminateCnt int + otherCnt int + otherTerminateCnt int + opts []Option + reqScopes []string + wantCnt int + wantOtherCnt int + wantErr bool + } + + cases := []testCase{ + { + name: "NonTerminated/none", + createCnt: 0, + reqScopes: []string{composedOf.ScopeId}, + wantCnt: 0, + wantErr: false, + }, + { + name: "NonTerminated/one", + createCnt: 1, + reqScopes: []string{composedOf.ScopeId}, + wantCnt: 1, + wantErr: false, + }, + { + name: "NonTerminated/many", + createCnt: 5, + reqScopes: []string{composedOf.ScopeId}, + wantCnt: 5, + wantErr: false, + }, + { + name: "NonTerminated/many one terminated", + createCnt: 5, + terminateCnt: 1, + reqScopes: []string{composedOf.ScopeId}, + wantCnt: 4, + wantErr: false, + }, + { + name: "NonTerminated/many terminated", + createCnt: 5, + terminateCnt: 3, + reqScopes: []string{composedOf.ScopeId}, + wantCnt: 2, + wantErr: false, + }, + { + name: "NonTerminated/many multiple scopes", + createCnt: 5, + terminateCnt: 3, + reqScopes: []string{composedOf.ScopeId, composedOfOther.ScopeId}, + wantCnt: 2, + wantErr: false, + }, + { + name: "NonTerminated/many multiple scopes", + createCnt: 5, + terminateCnt: 3, + otherCnt: 3, + otherTerminateCnt: 1, + reqScopes: []string{composedOf.ScopeId, composedOfOther.ScopeId}, + wantCnt: 2, + wantOtherCnt: 2, + wantErr: false, + }, + { + name: "NonTerminated/no scopes", + createCnt: 2, + terminateCnt: 1, + otherCnt: 2, + otherTerminateCnt: 1, + reqScopes: []string{}, + wantErr: true, + }, + { + name: "none", + opts: []Option{WithTerminated(true)}, + createCnt: 0, + reqScopes: []string{composedOf.ScopeId}, + wantCnt: 0, + wantErr: false, + }, + { + name: "one", + opts: []Option{WithTerminated(true)}, + createCnt: 1, + reqScopes: []string{composedOf.ScopeId}, + wantCnt: 1, + wantErr: false, + }, + { + name: "many", + opts: []Option{WithTerminated(true)}, + createCnt: 5, + reqScopes: []string{composedOf.ScopeId}, + wantCnt: 5, + wantErr: false, + }, + { + name: "many one terminated", + opts: []Option{WithTerminated(true)}, + createCnt: 5, + terminateCnt: 1, + reqScopes: []string{composedOf.ScopeId}, + wantCnt: 5, + wantErr: false, + }, + { + name: "many terminated", + opts: []Option{WithTerminated(true)}, + createCnt: 5, + terminateCnt: 3, + reqScopes: []string{composedOf.ScopeId}, + wantCnt: 5, + wantErr: false, + }, + { + name: "many multiple scopes", + opts: []Option{WithTerminated(true)}, + createCnt: 5, + terminateCnt: 3, + reqScopes: []string{composedOf.ScopeId, composedOfOther.ScopeId}, + wantCnt: 5, + wantErr: false, + }, + { + name: "many multiple scopes", + opts: []Option{WithTerminated(true)}, + createCnt: 5, + terminateCnt: 3, + otherCnt: 3, + otherTerminateCnt: 1, + reqScopes: []string{composedOf.ScopeId, composedOfOther.ScopeId}, + wantCnt: 5, + wantOtherCnt: 3, + wantErr: false, + }, + { + name: "no scopes", + opts: []Option{WithTerminated(true)}, + createCnt: 2, + terminateCnt: 1, + otherCnt: 2, + otherTerminateCnt: 1, + reqScopes: []string{}, + wantErr: true, + }, + } + for _, tt := range cases { + t.Run(tt.name, func(t *testing.T) { + assert, require := assert.New(t), require.New(t) + db.TestDeleteWhere(t, conn, func() interface{} { i := AllocSession(); return &i }(), "1=1") + + testSessions := []*Session{} + for i := 0; i < tt.createCnt; i++ { + s := TestSession(t, conn, wrapper, composedOf) + _ = TestState(t, conn, s.PublicId, StatusActive) + testSessions = append(testSessions, s) + } + for i := 0; i < tt.terminateCnt; i++ { + _, err := repo.CancelSession(ctx, testSessions[i].PublicId, testSessions[i].Version) + require.NoError(err) + } + terminated, err := repo.TerminateCompletedSessions(ctx) + require.NoError(err) + require.Equal(tt.terminateCnt, terminated) + + otherTestSessions := []*Session{} + for i := 0; i < tt.otherCnt; i++ { + s := TestSession(t, conn, wrapper, composedOfOther) + _ = TestState(t, conn, s.PublicId, StatusActive) + otherTestSessions = append(otherTestSessions, s) + } + for i := 0; i < tt.otherTerminateCnt; i++ { + _, err := repo.CancelSession(ctx, otherTestSessions[i].PublicId, otherTestSessions[i].Version) + require.NoError(err) + } + terminated, err = repo.TerminateCompletedSessions(ctx) + require.NoError(err) + require.Equal(tt.otherTerminateCnt, terminated) + + assert.Equal(tt.otherCnt, len(otherTestSessions)) + + got, err := repo.fetchAuthzProtectedSessionsByScope(ctx, tt.reqScopes, tt.opts...) + if tt.wantErr { + require.Error(err) + return + } + require.NoError(err) + assert.Equal(tt.wantCnt, len(got[composedOf.ScopeId])) + assert.Equal(tt.wantOtherCnt, len(got[composedOfOther.ScopeId])) + }) + } +} diff --git a/internal/session/service_list_for_authz_check.go b/internal/session/service_list_for_authz_check.go new file mode 100644 index 000000000..c1c2f0508 --- /dev/null +++ b/internal/session/service_list_for_authz_check.go @@ -0,0 +1,23 @@ +package session + +import ( + "context" + + "github.com/hashicorp/boundary/internal/boundary" +) + +type listForAuthzCheckFunc func(ctx context.Context, scopeIds []string) (map[string][]boundary.AuthzProtectedEntity, error) + +func (a listForAuthzCheckFunc) FetchAuthzProtectedEntitiesByScope(ctx context.Context, scopeIds []string) (map[string][]boundary.AuthzProtectedEntity, error) { + return a(ctx, scopeIds) +} + +// ListForAuthzCheck returns a functions that fetches sessions for the given +// scopes. Note that the sessions are not fully populated, and only contain the +// necessary information to implement the boundary.AuthzProtectedEntity +// interface. Supports the WithTerminated option. +func ListForAuthzCheck(repo *Repository, opt ...Option) listForAuthzCheckFunc { + return func(ctx context.Context, scopeIds []string) (map[string][]boundary.AuthzProtectedEntity, error) { + return repo.fetchAuthzProtectedSessionsByScope(ctx, scopeIds, opt...) + } +} diff --git a/internal/session/session.go b/internal/session/session.go index 7026656e7..5d6faf74a 100644 --- a/internal/session/session.go +++ b/internal/session/session.go @@ -472,16 +472,6 @@ type sessionListView struct { PreviousEndTime *timestamp.Timestamp `json:"previous_end_time,omitempty" gorm:"default:current_timestamp"` StartTime *timestamp.Timestamp `json:"start_time,omitempty" gorm:"default:current_timestamp;primary_key"` EndTime *timestamp.Timestamp `json:"end_time,omitempty" gorm:"default:current_timestamp"` - - // Connection fields - ConnectionId string `json:"connection_id,omitempty" gorm:"default:null"` - ClientTcpAddress string `json:"client_tcp_address,omitempty" gorm:"default:null"` - ClientTcpPort uint32 `json:"client_tcp_port,omitempty" gorm:"default:null"` - EndpointTcpAddress string `json:"endpoint_tcp_address,omitempty" gorm:"default:null"` - EndpointTcpPort uint32 `json:"endpoint_tcp_port,omitempty" gorm:"default:null"` - BytesUp uint64 `json:"bytes_up,omitempty" gorm:"default:null"` - BytesDown uint64 `json:"bytes_down,omitempty" gorm:"default:null"` - ClosedReason string `json:"closed_reason,omitempty" gorm:"default:null"` } // TableName returns the tablename to override the default gorm table name diff --git a/internal/ui/VERSION b/internal/ui/VERSION index c5193b209..cf707da16 100644 --- a/internal/ui/VERSION +++ b/internal/ui/VERSION @@ -1,4 +1,4 @@ -698160d423e8fee640d3286c3a3f17434f56a12b +dc78fe90286d8b24e70dca36b19ce77a5a032dcb # This file determines the version of the UI to embed in the boundary binary. # Update this file by running 'make update-ui-version' from the root of this repo. # Set UI_COMMITISH when running the above target to update to a specific version.