diff --git a/internal/api/grpc/session/v2/integration_test/query_test.go b/internal/api/grpc/session/v2/integration_test/query_test.go index 1889be1588..0ee7b91947 100644 --- a/internal/api/grpc/session/v2/integration_test/query_test.go +++ b/internal/api/grpc/session/v2/integration_test/query_test.go @@ -7,12 +7,15 @@ import ( "testing" "time" + "github.com/golang/protobuf/ptypes/timestamp" "github.com/muhlemmer/gu" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "google.golang.org/protobuf/types/known/durationpb" + "google.golang.org/protobuf/types/known/timestamppb" "github.com/zitadel/zitadel/internal/integration" + "github.com/zitadel/zitadel/pkg/grpc/object/v2" "github.com/zitadel/zitadel/pkg/grpc/session/v2" ) @@ -231,3 +234,358 @@ func TestServer_GetSession(t *testing.T) { }) } } + +type sessionAttr struct { + ID string + UserID string + UserAgent string + CreationDate *timestamp.Timestamp + ChangeDate *timestamppb.Timestamp + Details *object.Details +} + +type sessionAttrs []*sessionAttr + +func (u sessionAttrs) ids() []string { + ids := make([]string, len(u)) + for i := range u { + ids[i] = u[i].ID + } + return ids +} + +func createSessions(ctx context.Context, t *testing.T, count int, userID string, userAgent string, lifetime *durationpb.Duration, metadata map[string][]byte) sessionAttrs { + infos := make([]*sessionAttr, count) + for i := 0; i < count; i++ { + infos[i] = createSession(ctx, t, userID, userAgent, lifetime, metadata) + } + return infos +} + +func createSession(ctx context.Context, t *testing.T, userID string, userAgent string, lifetime *durationpb.Duration, metadata map[string][]byte) *sessionAttr { + req := &session.CreateSessionRequest{} + if userID != "" { + req.Checks = &session.Checks{ + User: &session.CheckUser{ + Search: &session.CheckUser_UserId{ + UserId: userID, + }, + }, + } + } + if userAgent != "" { + req.UserAgent = &session.UserAgent{ + FingerprintId: gu.Ptr(userAgent), + Ip: gu.Ptr("1.2.3.4"), + Description: gu.Ptr("Description"), + Header: map[string]*session.UserAgent_HeaderValues{ + "foo": {Values: []string{"foo", "bar"}}, + }, + } + } + if lifetime != nil { + req.Lifetime = lifetime + } + if metadata != nil { + req.Metadata = metadata + } + resp, err := Client.CreateSession(ctx, req) + require.NoError(t, err) + return &sessionAttr{ + resp.GetSessionId(), + userID, + userAgent, + resp.GetDetails().GetChangeDate(), + resp.GetDetails().GetChangeDate(), + resp.GetDetails(), + } +} + +func TestServer_ListSessions(t *testing.T) { + type args struct { + ctx context.Context + req *session.ListSessionsRequest + dep func(ctx context.Context, t *testing.T, request *session.ListSessionsRequest) []*sessionAttr + } + tests := []struct { + name string + args args + want *session.ListSessionsResponse + wantFactors []wantFactor + wantExpirationWindow time.Duration + wantErr bool + }{ + { + name: "list sessions, not found", + args: args{ + CTX, + &session.ListSessionsRequest{ + Queries: []*session.SearchQuery{ + {Query: &session.SearchQuery_IdsQuery{IdsQuery: &session.IDsQuery{Ids: []string{"unknown"}}}}, + }, + }, + func(ctx context.Context, t *testing.T, request *session.ListSessionsRequest) []*sessionAttr { + return []*sessionAttr{} + }, + }, + want: &session.ListSessionsResponse{ + Details: &object.ListDetails{ + TotalResult: 0, + Timestamp: timestamppb.Now(), + }, + Sessions: []*session.Session{}, + }, + }, + { + name: "list sessions, no permission", + args: args{ + UserCTX, + &session.ListSessionsRequest{ + Queries: []*session.SearchQuery{}, + }, + func(ctx context.Context, t *testing.T, request *session.ListSessionsRequest) []*sessionAttr { + info := createSession(ctx, t, "", "", nil, nil) + request.Queries = append(request.Queries, &session.SearchQuery{Query: &session.SearchQuery_IdsQuery{IdsQuery: &session.IDsQuery{Ids: []string{info.ID}}}}) + return []*sessionAttr{} + }, + }, + want: &session.ListSessionsResponse{ + Details: &object.ListDetails{ + TotalResult: 1, + Timestamp: timestamppb.Now(), + }, + Sessions: []*session.Session{}, + }, + }, + { + name: "list sessions, permission, ok", + args: args{ + CTX, + &session.ListSessionsRequest{}, + func(ctx context.Context, t *testing.T, request *session.ListSessionsRequest) []*sessionAttr { + info := createSession(ctx, t, "", "", nil, nil) + request.Queries = append(request.Queries, &session.SearchQuery{Query: &session.SearchQuery_IdsQuery{IdsQuery: &session.IDsQuery{Ids: []string{info.ID}}}}) + return []*sessionAttr{info} + }, + }, + want: &session.ListSessionsResponse{ + Details: &object.ListDetails{ + TotalResult: 1, + Timestamp: timestamppb.Now(), + }, + Sessions: []*session.Session{{}}, + }, + }, + { + name: "list sessions, full, ok", + args: args{ + CTX, + &session.ListSessionsRequest{}, + func(ctx context.Context, t *testing.T, request *session.ListSessionsRequest) []*sessionAttr { + info := createSession(ctx, t, User.GetUserId(), "agent", durationpb.New(time.Minute*5), map[string][]byte{"key": []byte("value")}) + request.Queries = append(request.Queries, &session.SearchQuery{Query: &session.SearchQuery_IdsQuery{IdsQuery: &session.IDsQuery{Ids: []string{info.ID}}}}) + return []*sessionAttr{info} + }, + }, + wantExpirationWindow: time.Minute * 5, + wantFactors: []wantFactor{wantUserFactor}, + want: &session.ListSessionsResponse{ + Details: &object.ListDetails{ + TotalResult: 1, + Timestamp: timestamppb.Now(), + }, + Sessions: []*session.Session{ + { + Metadata: map[string][]byte{"key": []byte("value")}, + UserAgent: &session.UserAgent{ + FingerprintId: gu.Ptr("agent"), + Ip: gu.Ptr("1.2.3.4"), + Description: gu.Ptr("Description"), + Header: map[string]*session.UserAgent_HeaderValues{ + "foo": {Values: []string{"foo", "bar"}}, + }, + }, + }, + }, + }, + }, + { + name: "list sessions, multiple, ok", + args: args{ + CTX, + &session.ListSessionsRequest{}, + func(ctx context.Context, t *testing.T, request *session.ListSessionsRequest) []*sessionAttr { + infos := createSessions(ctx, t, 3, User.GetUserId(), "agent", durationpb.New(time.Minute*5), map[string][]byte{"key": []byte("value")}) + request.Queries = append(request.Queries, &session.SearchQuery{Query: &session.SearchQuery_IdsQuery{IdsQuery: &session.IDsQuery{Ids: infos.ids()}}}) + return infos + }, + }, + wantExpirationWindow: time.Minute * 5, + wantFactors: []wantFactor{wantUserFactor}, + want: &session.ListSessionsResponse{ + Details: &object.ListDetails{ + TotalResult: 3, + Timestamp: timestamppb.Now(), + }, + Sessions: []*session.Session{ + { + Metadata: map[string][]byte{"key": []byte("value")}, + UserAgent: &session.UserAgent{ + FingerprintId: gu.Ptr("agent"), + Ip: gu.Ptr("1.2.3.4"), + Description: gu.Ptr("Description"), + Header: map[string]*session.UserAgent_HeaderValues{ + "foo": {Values: []string{"foo", "bar"}}, + }, + }, + }, + { + Metadata: map[string][]byte{"key": []byte("value")}, + UserAgent: &session.UserAgent{ + FingerprintId: gu.Ptr("agent"), + Ip: gu.Ptr("1.2.3.4"), + Description: gu.Ptr("Description"), + Header: map[string]*session.UserAgent_HeaderValues{ + "foo": {Values: []string{"foo", "bar"}}, + }, + }, + }, + { + Metadata: map[string][]byte{"key": []byte("value")}, + UserAgent: &session.UserAgent{ + FingerprintId: gu.Ptr("agent"), + Ip: gu.Ptr("1.2.3.4"), + Description: gu.Ptr("Description"), + Header: map[string]*session.UserAgent_HeaderValues{ + "foo": {Values: []string{"foo", "bar"}}, + }, + }, + }, + }, + }, + }, + { + name: "list sessions, userid, ok", + args: args{ + CTX, + &session.ListSessionsRequest{}, + func(ctx context.Context, t *testing.T, request *session.ListSessionsRequest) []*sessionAttr { + createdUser := createFullUser(ctx) + info := createSession(ctx, t, createdUser.GetUserId(), "agent", durationpb.New(time.Minute*5), map[string][]byte{"key": []byte("value")}) + request.Queries = append(request.Queries, &session.SearchQuery{Query: &session.SearchQuery_UserIdQuery{UserIdQuery: &session.UserIDQuery{Id: createdUser.GetUserId()}}}) + return []*sessionAttr{info} + }, + }, + wantExpirationWindow: time.Minute * 5, + wantFactors: []wantFactor{wantUserFactor}, + want: &session.ListSessionsResponse{ + Details: &object.ListDetails{ + TotalResult: 1, + Timestamp: timestamppb.Now(), + }, + Sessions: []*session.Session{ + { + Metadata: map[string][]byte{"key": []byte("value")}, + UserAgent: &session.UserAgent{ + FingerprintId: gu.Ptr("agent"), + Ip: gu.Ptr("1.2.3.4"), + Description: gu.Ptr("Description"), + Header: map[string]*session.UserAgent_HeaderValues{ + "foo": {Values: []string{"foo", "bar"}}, + }, + }, + }, + }, + }, + }, + { + name: "list sessions, creator, ok", + args: args{ + CTX, + &session.ListSessionsRequest{}, + func(ctx context.Context, t *testing.T, request *session.ListSessionsRequest) []*sessionAttr { + info := createSession(ctx, t, User.GetUserId(), "agent", durationpb.New(time.Minute*5), map[string][]byte{"key": []byte("value")}) + request.Queries = append(request.Queries, + &session.SearchQuery{Query: &session.SearchQuery_IdsQuery{IdsQuery: &session.IDsQuery{Ids: []string{info.ID}}}}, + &session.SearchQuery{Query: &session.SearchQuery_OwnCreatorQuery{OwnCreatorQuery: &session.OwnCreatorQuery{}}}) + return []*sessionAttr{info} + }, + }, + wantExpirationWindow: time.Minute * 5, + wantFactors: []wantFactor{wantUserFactor}, + want: &session.ListSessionsResponse{ + Details: &object.ListDetails{ + TotalResult: 1, + Timestamp: timestamppb.Now(), + }, + Sessions: []*session.Session{ + { + Metadata: map[string][]byte{"key": []byte("value")}, + UserAgent: &session.UserAgent{ + FingerprintId: gu.Ptr("agent"), + Ip: gu.Ptr("1.2.3.4"), + Description: gu.Ptr("Description"), + Header: map[string]*session.UserAgent_HeaderValues{ + "foo": {Values: []string{"foo", "bar"}}, + }, + }, + }, + }, + }, + }, + { + name: "list sessions, wrong creator", + args: args{ + IAMOwnerCTX, + &session.ListSessionsRequest{}, + func(ctx context.Context, t *testing.T, request *session.ListSessionsRequest) []*sessionAttr { + info := createSession(ctx, t, User.GetUserId(), "agent", durationpb.New(time.Minute*5), map[string][]byte{"key": []byte("value")}) + request.Queries = append(request.Queries, + &session.SearchQuery{Query: &session.SearchQuery_IdsQuery{IdsQuery: &session.IDsQuery{Ids: []string{info.ID}}}}, + &session.SearchQuery{Query: &session.SearchQuery_OwnCreatorQuery{OwnCreatorQuery: &session.OwnCreatorQuery{}}}) + return []*sessionAttr{} + }, + }, + wantExpirationWindow: time.Minute * 5, + wantFactors: []wantFactor{wantUserFactor}, + want: &session.ListSessionsResponse{ + Details: &object.ListDetails{ + TotalResult: 0, + Timestamp: timestamppb.Now(), + }, + Sessions: []*session.Session{}, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + infos := tt.args.dep(CTX, t, tt.args.req) + + retryDuration, tick := integration.WaitForAndTickWithMaxDuration(tt.args.ctx, time.Minute) + require.EventuallyWithT(t, func(ttt *assert.CollectT) { + got, err := Client.ListSessions(tt.args.ctx, tt.args.req) + if tt.wantErr { + assert.Error(ttt, err) + return + } + if !assert.NoError(ttt, err) { + return + } + + if !assert.Equal(ttt, got.Details.TotalResult, tt.want.Details.TotalResult) || !assert.Len(ttt, got.Sessions, len(tt.want.Sessions)) { + return + } + + for i := range infos { + tt.want.Sessions[i].Id = infos[i].ID + tt.want.Sessions[i].Sequence = infos[i].Details.GetSequence() + tt.want.Sessions[i].CreationDate = infos[i].Details.GetChangeDate() + tt.want.Sessions[i].ChangeDate = infos[i].Details.GetChangeDate() + + verifySession(ttt, got.Sessions[i], tt.want.Sessions[i], time.Minute, tt.wantExpirationWindow, infos[i].UserID, tt.wantFactors...) + } + integration.AssertListDetails(ttt, tt.want, got) + }, retryDuration, tick) + }) + } +} diff --git a/internal/api/grpc/session/v2beta/integration_test/query_test.go b/internal/api/grpc/session/v2beta/integration_test/query_test.go new file mode 100644 index 0000000000..b347ba8224 --- /dev/null +++ b/internal/api/grpc/session/v2beta/integration_test/query_test.go @@ -0,0 +1,512 @@ +//go:build integration + +package session_test + +import ( + "context" + "testing" + "time" + + "github.com/golang/protobuf/ptypes/timestamp" + "github.com/muhlemmer/gu" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "google.golang.org/protobuf/types/known/durationpb" + "google.golang.org/protobuf/types/known/timestamppb" + + "github.com/zitadel/zitadel/internal/integration" + object "github.com/zitadel/zitadel/pkg/grpc/object/v2beta" + session "github.com/zitadel/zitadel/pkg/grpc/session/v2beta" +) + +func TestServer_GetSession(t *testing.T) { + type args struct { + ctx context.Context + req *session.GetSessionRequest + dep func(ctx context.Context, t *testing.T, request *session.GetSessionRequest) uint64 + } + tests := []struct { + name string + args args + want *session.GetSessionResponse + wantFactors []wantFactor + wantExpirationWindow time.Duration + wantErr bool + }{ + { + name: "get session, no id provided", + args: args{ + CTX, + &session.GetSessionRequest{ + SessionId: "", + }, + nil, + }, + wantErr: true, + }, + { + name: "get session, not found", + args: args{ + CTX, + &session.GetSessionRequest{ + SessionId: "unknown", + }, + nil, + }, + wantErr: true, + }, + { + name: "get session, no permission", + args: args{ + UserCTX, + &session.GetSessionRequest{}, + func(ctx context.Context, t *testing.T, request *session.GetSessionRequest) uint64 { + resp, err := Client.CreateSession(CTX, &session.CreateSessionRequest{}) + require.NoError(t, err) + request.SessionId = resp.SessionId + return resp.GetDetails().GetSequence() + }, + }, + wantErr: true, + }, + { + name: "get session, permission, ok", + args: args{ + CTX, + &session.GetSessionRequest{}, + func(ctx context.Context, t *testing.T, request *session.GetSessionRequest) uint64 { + resp, err := Client.CreateSession(CTX, &session.CreateSessionRequest{}) + require.NoError(t, err) + request.SessionId = resp.SessionId + return resp.GetDetails().GetSequence() + }, + }, + want: &session.GetSessionResponse{ + Session: &session.Session{}, + }, + }, + { + name: "get session, token, ok", + args: args{ + UserCTX, + &session.GetSessionRequest{}, + func(ctx context.Context, t *testing.T, request *session.GetSessionRequest) uint64 { + resp, err := Client.CreateSession(CTX, &session.CreateSessionRequest{}) + require.NoError(t, err) + request.SessionId = resp.SessionId + request.SessionToken = gu.Ptr(resp.SessionToken) + return resp.GetDetails().GetSequence() + }, + }, + want: &session.GetSessionResponse{ + Session: &session.Session{}, + }, + }, + { + name: "get session, user agent, ok", + args: args{ + UserCTX, + &session.GetSessionRequest{}, + func(ctx context.Context, t *testing.T, request *session.GetSessionRequest) uint64 { + resp, err := Client.CreateSession(CTX, &session.CreateSessionRequest{ + UserAgent: &session.UserAgent{ + FingerprintId: gu.Ptr("fingerPrintID"), + Ip: gu.Ptr("1.2.3.4"), + Description: gu.Ptr("Description"), + Header: map[string]*session.UserAgent_HeaderValues{ + "foo": {Values: []string{"foo", "bar"}}, + }, + }, + }, + ) + require.NoError(t, err) + request.SessionId = resp.SessionId + request.SessionToken = gu.Ptr(resp.SessionToken) + return resp.GetDetails().GetSequence() + }, + }, + want: &session.GetSessionResponse{ + Session: &session.Session{ + UserAgent: &session.UserAgent{ + FingerprintId: gu.Ptr("fingerPrintID"), + Ip: gu.Ptr("1.2.3.4"), + Description: gu.Ptr("Description"), + Header: map[string]*session.UserAgent_HeaderValues{ + "foo": {Values: []string{"foo", "bar"}}, + }, + }, + }, + }, + }, + { + name: "get session, lifetime, ok", + args: args{ + UserCTX, + &session.GetSessionRequest{}, + func(ctx context.Context, t *testing.T, request *session.GetSessionRequest) uint64 { + resp, err := Client.CreateSession(CTX, &session.CreateSessionRequest{ + Lifetime: durationpb.New(5 * time.Minute), + }, + ) + require.NoError(t, err) + request.SessionId = resp.SessionId + request.SessionToken = gu.Ptr(resp.SessionToken) + return resp.GetDetails().GetSequence() + }, + }, + wantExpirationWindow: 5 * time.Minute, + want: &session.GetSessionResponse{ + Session: &session.Session{}, + }, + }, + { + name: "get session, metadata, ok", + args: args{ + UserCTX, + &session.GetSessionRequest{}, + func(ctx context.Context, t *testing.T, request *session.GetSessionRequest) uint64 { + resp, err := Client.CreateSession(CTX, &session.CreateSessionRequest{ + Metadata: map[string][]byte{"foo": []byte("bar")}, + }, + ) + require.NoError(t, err) + request.SessionId = resp.SessionId + request.SessionToken = gu.Ptr(resp.SessionToken) + return resp.GetDetails().GetSequence() + }, + }, + want: &session.GetSessionResponse{ + Session: &session.Session{ + Metadata: map[string][]byte{"foo": []byte("bar")}, + }, + }, + }, + { + name: "get session, user, ok", + args: args{ + UserCTX, + &session.GetSessionRequest{}, + func(ctx context.Context, t *testing.T, request *session.GetSessionRequest) uint64 { + resp, err := Client.CreateSession(CTX, &session.CreateSessionRequest{ + Checks: &session.Checks{ + User: &session.CheckUser{ + Search: &session.CheckUser_UserId{ + UserId: User.GetUserId(), + }, + }, + }, + }, + ) + require.NoError(t, err) + request.SessionId = resp.SessionId + request.SessionToken = gu.Ptr(resp.SessionToken) + return resp.GetDetails().GetSequence() + }, + }, + wantFactors: []wantFactor{wantUserFactor}, + want: &session.GetSessionResponse{ + Session: &session.Session{}, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var sequence uint64 + if tt.args.dep != nil { + sequence = tt.args.dep(tt.args.ctx, t, tt.args.req) + } + + retryDuration, tick := integration.WaitForAndTickWithMaxDuration(tt.args.ctx, time.Minute) + require.EventuallyWithT(t, func(ttt *assert.CollectT) { + got, err := Client.GetSession(tt.args.ctx, tt.args.req) + if tt.wantErr { + assert.Error(ttt, err) + return + } + if !assert.NoError(ttt, err) { + return + } + + tt.want.Session.Id = tt.args.req.SessionId + tt.want.Session.Sequence = sequence + verifySession(ttt, got.GetSession(), tt.want.GetSession(), time.Minute, tt.wantExpirationWindow, User.GetUserId(), tt.wantFactors...) + }, retryDuration, tick) + }) + } +} + +type sessionAttr struct { + ID string + UserID string + UserAgent string + CreationDate *timestamp.Timestamp + ChangeDate *timestamppb.Timestamp + Details *object.Details +} + +type sessionAttrs []*sessionAttr + +func (u sessionAttrs) ids() []string { + ids := make([]string, len(u)) + for i := range u { + ids[i] = u[i].ID + } + return ids +} + +func createSessions(ctx context.Context, t *testing.T, count int, userID string, userAgent string, lifetime *durationpb.Duration, metadata map[string][]byte) sessionAttrs { + infos := make([]*sessionAttr, count) + for i := 0; i < count; i++ { + infos[i] = createSession(ctx, t, userID, userAgent, lifetime, metadata) + } + return infos +} + +func createSession(ctx context.Context, t *testing.T, userID string, userAgent string, lifetime *durationpb.Duration, metadata map[string][]byte) *sessionAttr { + req := &session.CreateSessionRequest{} + if userID != "" { + req.Checks = &session.Checks{ + User: &session.CheckUser{ + Search: &session.CheckUser_UserId{ + UserId: userID, + }, + }, + } + } + if userAgent != "" { + req.UserAgent = &session.UserAgent{ + FingerprintId: gu.Ptr(userAgent), + Ip: gu.Ptr("1.2.3.4"), + Description: gu.Ptr("Description"), + Header: map[string]*session.UserAgent_HeaderValues{ + "foo": {Values: []string{"foo", "bar"}}, + }, + } + } + if lifetime != nil { + req.Lifetime = lifetime + } + if metadata != nil { + req.Metadata = metadata + } + resp, err := Client.CreateSession(ctx, req) + require.NoError(t, err) + return &sessionAttr{ + resp.GetSessionId(), + userID, + userAgent, + resp.GetDetails().GetChangeDate(), + resp.GetDetails().GetChangeDate(), + resp.GetDetails(), + } +} + +func TestServer_ListSessions(t *testing.T) { + type args struct { + ctx context.Context + req *session.ListSessionsRequest + dep func(ctx context.Context, t *testing.T, request *session.ListSessionsRequest) []*sessionAttr + } + tests := []struct { + name string + args args + want *session.ListSessionsResponse + wantFactors []wantFactor + wantExpirationWindow time.Duration + wantErr bool + }{ + { + name: "list sessions, not found", + args: args{ + CTX, + &session.ListSessionsRequest{ + Queries: []*session.SearchQuery{ + {Query: &session.SearchQuery_IdsQuery{IdsQuery: &session.IDsQuery{Ids: []string{"unknown"}}}}, + }, + }, + func(ctx context.Context, t *testing.T, request *session.ListSessionsRequest) []*sessionAttr { + return []*sessionAttr{} + }, + }, + want: &session.ListSessionsResponse{ + Details: &object.ListDetails{ + TotalResult: 0, + Timestamp: timestamppb.Now(), + }, + Sessions: []*session.Session{}, + }, + }, + { + name: "list sessions, wrong creator", + args: args{ + UserCTX, + &session.ListSessionsRequest{}, + func(ctx context.Context, t *testing.T, request *session.ListSessionsRequest) []*sessionAttr { + info := createSession(ctx, t, "", "", nil, nil) + request.Queries = append(request.Queries, &session.SearchQuery{Query: &session.SearchQuery_IdsQuery{IdsQuery: &session.IDsQuery{Ids: []string{info.ID}}}}) + return []*sessionAttr{} + }, + }, + want: &session.ListSessionsResponse{ + Details: &object.ListDetails{ + TotalResult: 0, + Timestamp: timestamppb.Now(), + }, + Sessions: []*session.Session{}, + }, + }, + { + name: "list sessions, full, ok", + args: args{ + CTX, + &session.ListSessionsRequest{}, + func(ctx context.Context, t *testing.T, request *session.ListSessionsRequest) []*sessionAttr { + info := createSession(ctx, t, User.GetUserId(), "agent", durationpb.New(time.Minute*5), map[string][]byte{"key": []byte("value")}) + request.Queries = append(request.Queries, &session.SearchQuery{Query: &session.SearchQuery_IdsQuery{IdsQuery: &session.IDsQuery{Ids: []string{info.ID}}}}) + return []*sessionAttr{info} + }, + }, + wantExpirationWindow: time.Minute * 5, + wantFactors: []wantFactor{wantUserFactor}, + want: &session.ListSessionsResponse{ + Details: &object.ListDetails{ + TotalResult: 1, + Timestamp: timestamppb.Now(), + }, + Sessions: []*session.Session{ + { + Metadata: map[string][]byte{"key": []byte("value")}, + UserAgent: &session.UserAgent{ + FingerprintId: gu.Ptr("agent"), + Ip: gu.Ptr("1.2.3.4"), + Description: gu.Ptr("Description"), + Header: map[string]*session.UserAgent_HeaderValues{ + "foo": {Values: []string{"foo", "bar"}}, + }, + }, + }, + }, + }, + }, + { + name: "list sessions, multiple, ok", + args: args{ + CTX, + &session.ListSessionsRequest{}, + func(ctx context.Context, t *testing.T, request *session.ListSessionsRequest) []*sessionAttr { + infos := createSessions(ctx, t, 3, User.GetUserId(), "agent", durationpb.New(time.Minute*5), map[string][]byte{"key": []byte("value")}) + request.Queries = append(request.Queries, &session.SearchQuery{Query: &session.SearchQuery_IdsQuery{IdsQuery: &session.IDsQuery{Ids: infos.ids()}}}) + return infos + }, + }, + wantExpirationWindow: time.Minute * 5, + wantFactors: []wantFactor{wantUserFactor}, + want: &session.ListSessionsResponse{ + Details: &object.ListDetails{ + TotalResult: 3, + Timestamp: timestamppb.Now(), + }, + Sessions: []*session.Session{ + { + Metadata: map[string][]byte{"key": []byte("value")}, + UserAgent: &session.UserAgent{ + FingerprintId: gu.Ptr("agent"), + Ip: gu.Ptr("1.2.3.4"), + Description: gu.Ptr("Description"), + Header: map[string]*session.UserAgent_HeaderValues{ + "foo": {Values: []string{"foo", "bar"}}, + }, + }, + }, + { + Metadata: map[string][]byte{"key": []byte("value")}, + UserAgent: &session.UserAgent{ + FingerprintId: gu.Ptr("agent"), + Ip: gu.Ptr("1.2.3.4"), + Description: gu.Ptr("Description"), + Header: map[string]*session.UserAgent_HeaderValues{ + "foo": {Values: []string{"foo", "bar"}}, + }, + }, + }, + { + Metadata: map[string][]byte{"key": []byte("value")}, + UserAgent: &session.UserAgent{ + FingerprintId: gu.Ptr("agent"), + Ip: gu.Ptr("1.2.3.4"), + Description: gu.Ptr("Description"), + Header: map[string]*session.UserAgent_HeaderValues{ + "foo": {Values: []string{"foo", "bar"}}, + }, + }, + }, + }, + }, + }, + { + name: "list sessions, userid, ok", + args: args{ + CTX, + &session.ListSessionsRequest{}, + func(ctx context.Context, t *testing.T, request *session.ListSessionsRequest) []*sessionAttr { + createdUser := createFullUser(ctx) + info := createSession(ctx, t, createdUser.GetUserId(), "agent", durationpb.New(time.Minute*5), map[string][]byte{"key": []byte("value")}) + request.Queries = append(request.Queries, &session.SearchQuery{Query: &session.SearchQuery_UserIdQuery{UserIdQuery: &session.UserIDQuery{Id: createdUser.GetUserId()}}}) + return []*sessionAttr{info} + }, + }, + wantExpirationWindow: time.Minute * 5, + wantFactors: []wantFactor{wantUserFactor}, + want: &session.ListSessionsResponse{ + Details: &object.ListDetails{ + TotalResult: 1, + Timestamp: timestamppb.Now(), + }, + Sessions: []*session.Session{ + { + Metadata: map[string][]byte{"key": []byte("value")}, + UserAgent: &session.UserAgent{ + FingerprintId: gu.Ptr("agent"), + Ip: gu.Ptr("1.2.3.4"), + Description: gu.Ptr("Description"), + Header: map[string]*session.UserAgent_HeaderValues{ + "foo": {Values: []string{"foo", "bar"}}, + }, + }, + }, + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + infos := tt.args.dep(CTX, t, tt.args.req) + + retryDuration, tick := integration.WaitForAndTickWithMaxDuration(tt.args.ctx, time.Minute) + require.EventuallyWithT(t, func(ttt *assert.CollectT) { + got, err := Client.ListSessions(tt.args.ctx, tt.args.req) + if tt.wantErr { + assert.Error(ttt, err) + return + } + if !assert.NoError(ttt, err) { + return + } + + if !assert.Equal(ttt, got.Details.TotalResult, tt.want.Details.TotalResult) || !assert.Len(ttt, got.Sessions, len(tt.want.Sessions)) { + return + } + + for i := range infos { + tt.want.Sessions[i].Id = infos[i].ID + tt.want.Sessions[i].Sequence = infos[i].Details.GetSequence() + tt.want.Sessions[i].CreationDate = infos[i].Details.GetChangeDate() + tt.want.Sessions[i].ChangeDate = infos[i].Details.GetChangeDate() + + verifySession(ttt, got.Sessions[i], tt.want.Sessions[i], time.Minute, tt.wantExpirationWindow, infos[i].UserID, tt.wantFactors...) + } + integration.AssertListDetails(ttt, tt.want, got) + }, retryDuration, tick) + }) + } +} diff --git a/internal/api/grpc/session/v2beta/integration_test/server_test.go b/internal/api/grpc/session/v2beta/integration_test/server_test.go new file mode 100644 index 0000000000..4920e6ec35 --- /dev/null +++ b/internal/api/grpc/session/v2beta/integration_test/server_test.go @@ -0,0 +1,74 @@ +//go:build integration + +package session_test + +import ( + "context" + "os" + "testing" + "time" + + "github.com/zitadel/logging" + + "github.com/zitadel/zitadel/internal/integration" + session "github.com/zitadel/zitadel/pkg/grpc/session/v2beta" + "github.com/zitadel/zitadel/pkg/grpc/user/v2" +) + +var ( + CTX context.Context + IAMOwnerCTX context.Context + UserCTX context.Context + Instance *integration.Instance + Client session.SessionServiceClient + User *user.AddHumanUserResponse + DeactivatedUser *user.AddHumanUserResponse + LockedUser *user.AddHumanUserResponse +) + +func TestMain(m *testing.M) { + os.Exit(func() int { + ctx, cancel := context.WithTimeout(context.Background(), 15*time.Minute) + defer cancel() + + Instance = integration.NewInstance(ctx) + Client = Instance.Client.SessionV2beta + + CTX = Instance.WithAuthorization(ctx, integration.UserTypeOrgOwner) + IAMOwnerCTX = Instance.WithAuthorization(ctx, integration.UserTypeIAMOwner) + UserCTX = Instance.WithAuthorization(ctx, integration.UserTypeNoPermission) + User = createFullUser(CTX) + DeactivatedUser = createDeactivatedUser(CTX) + LockedUser = createLockedUser(CTX) + return m.Run() + }()) +} + +func createFullUser(ctx context.Context) *user.AddHumanUserResponse { + userResp := Instance.CreateHumanUser(ctx) + Instance.Client.UserV2.VerifyEmail(ctx, &user.VerifyEmailRequest{ + UserId: userResp.GetUserId(), + VerificationCode: userResp.GetEmailCode(), + }) + Instance.Client.UserV2.VerifyPhone(ctx, &user.VerifyPhoneRequest{ + UserId: userResp.GetUserId(), + VerificationCode: userResp.GetPhoneCode(), + }) + Instance.SetUserPassword(ctx, userResp.GetUserId(), integration.UserPassword, false) + Instance.RegisterUserPasskey(ctx, userResp.GetUserId()) + return userResp +} + +func createDeactivatedUser(ctx context.Context) *user.AddHumanUserResponse { + userResp := Instance.CreateHumanUser(ctx) + _, err := Instance.Client.UserV2.DeactivateUser(ctx, &user.DeactivateUserRequest{UserId: userResp.GetUserId()}) + logging.OnError(err).Fatal("deactivate human user") + return userResp +} + +func createLockedUser(ctx context.Context) *user.AddHumanUserResponse { + userResp := Instance.CreateHumanUser(ctx) + _, err := Instance.Client.UserV2.LockUser(ctx, &user.LockUserRequest{UserId: userResp.GetUserId()}) + logging.OnError(err).Fatal("lock human user") + return userResp +} diff --git a/internal/api/grpc/session/v2beta/integration_test/session_test.go b/internal/api/grpc/session/v2beta/integration_test/session_test.go index 52e355204d..be9fb67734 100644 --- a/internal/api/grpc/session/v2beta/integration_test/session_test.go +++ b/internal/api/grpc/session/v2beta/integration_test/session_test.go @@ -5,7 +5,6 @@ package session_test import ( "context" "fmt" - "os" "testing" "time" @@ -14,7 +13,6 @@ import ( "github.com/pquerna/otp/totp" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "github.com/zitadel/logging" "google.golang.org/grpc/codes" "google.golang.org/grpc/metadata" "google.golang.org/grpc/status" @@ -29,62 +27,6 @@ import ( "github.com/zitadel/zitadel/pkg/grpc/user/v2" ) -var ( - CTX context.Context - IAMOwnerCTX context.Context - Instance *integration.Instance - Client session.SessionServiceClient - User *user.AddHumanUserResponse - DeactivatedUser *user.AddHumanUserResponse - LockedUser *user.AddHumanUserResponse -) - -func TestMain(m *testing.M) { - os.Exit(func() int { - ctx, cancel := context.WithTimeout(context.Background(), 15*time.Minute) - defer cancel() - - Instance = integration.NewInstance(ctx) - Client = Instance.Client.SessionV2beta - - CTX = Instance.WithAuthorization(ctx, integration.UserTypeOrgOwner) - IAMOwnerCTX = Instance.WithAuthorization(ctx, integration.UserTypeIAMOwner) - User = createFullUser(CTX) - DeactivatedUser = createDeactivatedUser(CTX) - LockedUser = createLockedUser(CTX) - return m.Run() - }()) -} - -func createFullUser(ctx context.Context) *user.AddHumanUserResponse { - userResp := Instance.CreateHumanUser(ctx) - Instance.Client.UserV2.VerifyEmail(ctx, &user.VerifyEmailRequest{ - UserId: userResp.GetUserId(), - VerificationCode: userResp.GetEmailCode(), - }) - Instance.Client.UserV2.VerifyPhone(ctx, &user.VerifyPhoneRequest{ - UserId: userResp.GetUserId(), - VerificationCode: userResp.GetPhoneCode(), - }) - Instance.SetUserPassword(ctx, userResp.GetUserId(), integration.UserPassword, false) - Instance.RegisterUserPasskey(ctx, userResp.GetUserId()) - return userResp -} - -func createDeactivatedUser(ctx context.Context) *user.AddHumanUserResponse { - userResp := Instance.CreateHumanUser(ctx) - _, err := Instance.Client.UserV2.DeactivateUser(ctx, &user.DeactivateUserRequest{UserId: userResp.GetUserId()}) - logging.OnError(err).Fatal("deactivate human user") - return userResp -} - -func createLockedUser(ctx context.Context) *user.AddHumanUserResponse { - userResp := Instance.CreateHumanUser(ctx) - _, err := Instance.Client.UserV2.LockUser(ctx, &user.LockUserRequest{UserId: userResp.GetUserId()}) - logging.OnError(err).Fatal("lock human user") - return userResp -} - func verifyCurrentSession(t testing.TB, id, token string, sequence uint64, window time.Duration, metadata map[string][]byte, userAgent *session.UserAgent, expirationWindow time.Duration, userID string, factors ...wantFactor) *session.Session { t.Helper() require.NotEmpty(t, id) @@ -96,15 +38,25 @@ func verifyCurrentSession(t testing.TB, id, token string, sequence uint64, windo }) require.NoError(t, err) s := resp.GetSession() + want := &session.Session{ + Id: id, + Sequence: sequence, + Metadata: metadata, + UserAgent: userAgent, + } + verifySession(t, s, want, window, expirationWindow, userID, factors...) + return s +} - assert.Equal(t, id, s.GetId()) +func verifySession(t assert.TestingT, s *session.Session, want *session.Session, window time.Duration, expirationWindow time.Duration, userID string, factors ...wantFactor) { + assert.Equal(t, want.Id, s.GetId()) assert.WithinRange(t, s.GetCreationDate().AsTime(), time.Now().Add(-window), time.Now().Add(window)) assert.WithinRange(t, s.GetChangeDate().AsTime(), time.Now().Add(-window), time.Now().Add(window)) - assert.Equal(t, sequence, s.GetSequence()) - assert.Equal(t, metadata, s.GetMetadata()) + assert.Equal(t, want.Sequence, s.GetSequence()) + assert.Equal(t, want.Metadata, s.GetMetadata()) - if !proto.Equal(userAgent, s.GetUserAgent()) { - t.Errorf("user agent =\n%v\nwant\n%v", s.GetUserAgent(), userAgent) + if !proto.Equal(want.UserAgent, s.GetUserAgent()) { + t.Errorf("user agent =\n%v\nwant\n%v", s.GetUserAgent(), want.UserAgent) } if expirationWindow == 0 { assert.Nil(t, s.GetExpirationDate()) @@ -113,7 +65,6 @@ func verifyCurrentSession(t testing.TB, id, token string, sequence uint64, windo } verifyFactors(t, s.GetFactors(), window, userID, factors) - return s } type wantFactor int @@ -129,7 +80,7 @@ const ( wantOTPEmailFactor ) -func verifyFactors(t testing.TB, factors *session.Factors, window time.Duration, userID string, want []wantFactor) { +func verifyFactors(t assert.TestingT, factors *session.Factors, window time.Duration, userID string, want []wantFactor) { for _, w := range want { switch w { case wantUserFactor: @@ -194,8 +145,15 @@ func TestServer_CreateSession(t *testing.T) { }, }, { - name: "user agent", + name: "full session", req: &session.CreateSessionRequest{ + Checks: &session.Checks{ + User: &session.CheckUser{ + Search: &session.CheckUser_UserId{ + UserId: User.GetUserId(), + }, + }, + }, Metadata: map[string][]byte{"foo": []byte("bar")}, UserAgent: &session.UserAgent{ FingerprintId: gu.Ptr("fingerPrintID"), @@ -205,6 +163,7 @@ func TestServer_CreateSession(t *testing.T) { "foo": {Values: []string{"foo", "bar"}}, }, }, + Lifetime: durationpb.New(5 * time.Minute), }, want: &session.CreateSessionResponse{ Details: &object.Details{ @@ -212,14 +171,6 @@ func TestServer_CreateSession(t *testing.T) { ResourceOwner: Instance.ID(), }, }, - wantUserAgent: &session.UserAgent{ - FingerprintId: gu.Ptr("fingerPrintID"), - Ip: gu.Ptr("1.2.3.4"), - Description: gu.Ptr("Description"), - Header: map[string]*session.UserAgent_HeaderValues{ - "foo": {Values: []string{"foo", "bar"}}, - }, - }, }, { name: "negative lifetime", @@ -229,40 +180,6 @@ func TestServer_CreateSession(t *testing.T) { }, wantErr: true, }, - { - name: "lifetime", - req: &session.CreateSessionRequest{ - Metadata: map[string][]byte{"foo": []byte("bar")}, - Lifetime: durationpb.New(5 * time.Minute), - }, - want: &session.CreateSessionResponse{ - Details: &object.Details{ - ChangeDate: timestamppb.Now(), - ResourceOwner: Instance.ID(), - }, - }, - wantExpirationWindow: 5 * time.Minute, - }, - { - name: "with user", - req: &session.CreateSessionRequest{ - Checks: &session.Checks{ - User: &session.CheckUser{ - Search: &session.CheckUser_UserId{ - UserId: User.GetUserId(), - }, - }, - }, - Metadata: map[string][]byte{"foo": []byte("bar")}, - }, - want: &session.CreateSessionResponse{ - Details: &object.Details{ - ChangeDate: timestamppb.Now(), - ResourceOwner: Instance.ID(), - }, - }, - wantFactors: []wantFactor{wantUserFactor}, - }, { name: "deactivated user", req: &session.CreateSessionRequest{ @@ -340,8 +257,6 @@ func TestServer_CreateSession(t *testing.T) { } require.NoError(t, err) integration.AssertDetails(t, tt.want, got) - - verifyCurrentSession(t, got.GetSessionId(), got.GetSessionToken(), got.GetDetails().GetSequence(), time.Minute, tt.req.GetMetadata(), tt.wantUserAgent, tt.wantExpirationWindow, User.GetUserId(), tt.wantFactors...) }) } } diff --git a/internal/query/session.go b/internal/query/session.go index 53607510f7..683585b3e7 100644 --- a/internal/query/session.go +++ b/internal/query/session.go @@ -459,6 +459,10 @@ func prepareSessionsQuery(ctx context.Context, db prepareDatabase) (sq.SelectBui SessionColumnOTPSMSCheckedAt.identifier(), SessionColumnOTPEmailCheckedAt.identifier(), SessionColumnMetadata.identifier(), + SessionColumnUserAgentFingerprintID.identifier(), + SessionColumnUserAgentIP.identifier(), + SessionColumnUserAgentDescription.identifier(), + SessionColumnUserAgentHeader.identifier(), SessionColumnExpiration.identifier(), countColumn.identifier(), ).From(sessionsTable.identifier()). @@ -485,6 +489,8 @@ func prepareSessionsQuery(ctx context.Context, db prepareDatabase) (sq.SelectBui otpSMSCheckedAt sql.NullTime otpEmailCheckedAt sql.NullTime metadata database.Map[[]byte] + userAgentIP sql.NullString + userAgentHeader database.Map[[]string] expiration sql.NullTime ) @@ -509,6 +515,10 @@ func prepareSessionsQuery(ctx context.Context, db prepareDatabase) (sq.SelectBui &otpSMSCheckedAt, &otpEmailCheckedAt, &metadata, + &session.UserAgent.FingerprintID, + &userAgentIP, + &session.UserAgent.Description, + &userAgentHeader, &expiration, &sessions.Count, ) @@ -529,6 +539,10 @@ func prepareSessionsQuery(ctx context.Context, db prepareDatabase) (sq.SelectBui session.OTPSMSFactor.OTPCheckedAt = otpSMSCheckedAt.Time session.OTPEmailFactor.OTPCheckedAt = otpEmailCheckedAt.Time session.Metadata = metadata + session.UserAgent.Header = http.Header(userAgentHeader) + if userAgentIP.Valid { + session.UserAgent.IP = net.ParseIP(userAgentIP.String) + } session.Expiration = expiration.Time sessions.Sessions = append(sessions.Sessions, session) diff --git a/internal/query/sessions_test.go b/internal/query/sessions_test.go index c7929a98a8..af60ae9eda 100644 --- a/internal/query/sessions_test.go +++ b/internal/query/sessions_test.go @@ -71,6 +71,10 @@ var ( ` projections.sessions8.otp_sms_checked_at,` + ` projections.sessions8.otp_email_checked_at,` + ` projections.sessions8.metadata,` + + ` projections.sessions8.user_agent_fingerprint_id,` + + ` projections.sessions8.user_agent_ip,` + + ` projections.sessions8.user_agent_description,` + + ` projections.sessions8.user_agent_header,` + ` projections.sessions8.expiration,` + ` COUNT(*) OVER ()` + ` FROM projections.sessions8` + @@ -129,6 +133,10 @@ var ( "otp_sms_checked_at", "otp_email_checked_at", "metadata", + "user_agent_fingerprint_id", + "user_agent_ip", + "user_agent_description", + "user_agent_header", "expiration", "count", } @@ -186,6 +194,10 @@ func Test_SessionsPrepare(t *testing.T) { testNow, testNow, []byte(`{"key": "dmFsdWU="}`), + "fingerPrintID", + "1.2.3.4", + "agentDescription", + []byte(`{"foo":["foo","bar"]}`), testNow, }, }, @@ -233,6 +245,12 @@ func Test_SessionsPrepare(t *testing.T) { Metadata: map[string][]byte{ "key": []byte("value"), }, + UserAgent: domain.UserAgent{ + FingerprintID: gu.Ptr("fingerPrintID"), + IP: net.IPv4(1, 2, 3, 4), + Description: gu.Ptr("agentDescription"), + Header: http.Header{"foo": []string{"foo", "bar"}}, + }, Expiration: testNow, }, }, @@ -267,6 +285,10 @@ func Test_SessionsPrepare(t *testing.T) { testNow, testNow, []byte(`{"key": "dmFsdWU="}`), + "fingerPrintID", + "1.2.3.4", + "agentDescription", + []byte(`{"foo":["foo","bar"]}`), testNow, }, { @@ -290,6 +312,10 @@ func Test_SessionsPrepare(t *testing.T) { testNow, testNow, []byte(`{"key": "dmFsdWU="}`), + "fingerPrintID", + "1.2.3.4", + "agentDescription", + []byte(`{"foo":["foo","bar"]}`), testNow, }, }, @@ -337,6 +363,12 @@ func Test_SessionsPrepare(t *testing.T) { Metadata: map[string][]byte{ "key": []byte("value"), }, + UserAgent: domain.UserAgent{ + FingerprintID: gu.Ptr("fingerPrintID"), + IP: net.IPv4(1, 2, 3, 4), + Description: gu.Ptr("agentDescription"), + Header: http.Header{"foo": []string{"foo", "bar"}}, + }, Expiration: testNow, }, { @@ -376,6 +408,12 @@ func Test_SessionsPrepare(t *testing.T) { Metadata: map[string][]byte{ "key": []byte("value"), }, + UserAgent: domain.UserAgent{ + FingerprintID: gu.Ptr("fingerPrintID"), + IP: net.IPv4(1, 2, 3, 4), + Description: gu.Ptr("agentDescription"), + Header: http.Header{"foo": []string{"foo", "bar"}}, + }, Expiration: testNow, }, },