zitadel/internal/api/grpc/session/v2/session_test.go
Livio Spring 7015d347e1
fix(query): user performance (#7081)
* fix(query): user performance (#6537)

* start user by id

* ignore debug bin

* use new user by id

* new sql

* fix(sql): replace STRING with text for psql compatabilit

* some changes

* fix: correct user queries

* fix tests

* unify sql statements

* use specific get user methods

* search login name case insensitive

* refactor: optimise user statements

* add index

* fix queries

* fix: correct domain segregation

* return all login names

* fix queries

* improve readability

* query should be correct now

* cleanup statements

* fix username / loginname handling

* fix: psql doesn't support create view if not exists

* fix: create pre-release

* ignore release comments

* add lower fields

* fix: always to lower

* update to latest projection

---------

Co-authored-by: Livio Spring <livio.a@gmail.com>

(cherry picked from commit ddbea119f1c49845a3e364fa6dc565ab3180989a)

* update projection versions

* trigger workflow

---------

Co-authored-by: Silvan <silvan.reusser@gmail.com>
2023-12-14 11:17:35 +00:00

745 lines
19 KiB
Go

package session
import (
"context"
"net"
"net/http"
"testing"
"time"
"github.com/muhlemmer/gu"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"google.golang.org/protobuf/proto"
"google.golang.org/protobuf/types/known/timestamppb"
"github.com/zitadel/zitadel/internal/api/authz"
objpb "github.com/zitadel/zitadel/pkg/grpc/object"
"github.com/zitadel/zitadel/internal/domain"
caos_errs "github.com/zitadel/zitadel/internal/errors"
"github.com/zitadel/zitadel/internal/query"
object "github.com/zitadel/zitadel/pkg/grpc/object/v2beta"
session "github.com/zitadel/zitadel/pkg/grpc/session/v2beta"
)
var (
creationDate = time.Date(2023, 10, 10, 14, 15, 0, 0, time.UTC)
)
func Test_sessionsToPb(t *testing.T) {
now := time.Now()
past := now.Add(-time.Hour)
sessions := []*query.Session{
{ // no factor, with user agent and expiration
ID: "999",
CreationDate: now,
ChangeDate: now,
Sequence: 123,
State: domain.SessionStateActive,
ResourceOwner: "me",
Creator: "he",
Metadata: map[string][]byte{"hello": []byte("world")},
UserAgent: domain.UserAgent{
FingerprintID: gu.Ptr("fingerprintID"),
Description: gu.Ptr("description"),
IP: net.IPv4(1, 2, 3, 4),
Header: http.Header{"foo": []string{"foo", "bar"}},
},
Expiration: now,
},
{ // user factor
ID: "999",
CreationDate: now,
ChangeDate: now,
Sequence: 123,
State: domain.SessionStateActive,
ResourceOwner: "me",
Creator: "he",
UserFactor: query.SessionUserFactor{
UserID: "345",
UserCheckedAt: past,
LoginName: "donald",
DisplayName: "donald duck",
ResourceOwner: "org1",
},
Metadata: map[string][]byte{"hello": []byte("world")},
},
{ // password factor
ID: "999",
CreationDate: now,
ChangeDate: now,
Sequence: 123,
State: domain.SessionStateActive,
ResourceOwner: "me",
Creator: "he",
UserFactor: query.SessionUserFactor{
UserID: "345",
UserCheckedAt: past,
LoginName: "donald",
DisplayName: "donald duck",
ResourceOwner: "org1",
},
PasswordFactor: query.SessionPasswordFactor{
PasswordCheckedAt: past,
},
Metadata: map[string][]byte{"hello": []byte("world")},
},
{ // webAuthN factor
ID: "999",
CreationDate: now,
ChangeDate: now,
Sequence: 123,
State: domain.SessionStateActive,
ResourceOwner: "me",
Creator: "he",
UserFactor: query.SessionUserFactor{
UserID: "345",
UserCheckedAt: past,
LoginName: "donald",
DisplayName: "donald duck",
ResourceOwner: "org1",
},
WebAuthNFactor: query.SessionWebAuthNFactor{
WebAuthNCheckedAt: past,
UserVerified: true,
},
Metadata: map[string][]byte{"hello": []byte("world")},
},
{ // totp factor
ID: "999",
CreationDate: now,
ChangeDate: now,
Sequence: 123,
State: domain.SessionStateActive,
ResourceOwner: "me",
Creator: "he",
UserFactor: query.SessionUserFactor{
UserID: "345",
UserCheckedAt: past,
LoginName: "donald",
DisplayName: "donald duck",
ResourceOwner: "org1",
},
TOTPFactor: query.SessionTOTPFactor{
TOTPCheckedAt: past,
},
Metadata: map[string][]byte{"hello": []byte("world")},
},
}
want := []*session.Session{
{ // no factor, with user agent and expiration
Id: "999",
CreationDate: timestamppb.New(now),
ChangeDate: timestamppb.New(now),
Sequence: 123,
Factors: nil,
Metadata: map[string][]byte{"hello": []byte("world")},
UserAgent: &session.UserAgent{
FingerprintId: gu.Ptr("fingerprintID"),
Description: gu.Ptr("description"),
Ip: gu.Ptr("1.2.3.4"),
Header: map[string]*session.UserAgent_HeaderValues{
"foo": {Values: []string{"foo", "bar"}},
},
},
ExpirationDate: timestamppb.New(now),
},
{ // user factor
Id: "999",
CreationDate: timestamppb.New(now),
ChangeDate: timestamppb.New(now),
Sequence: 123,
Factors: &session.Factors{
User: &session.UserFactor{
VerifiedAt: timestamppb.New(past),
Id: "345",
LoginName: "donald",
DisplayName: "donald duck",
OrganisationId: "org1",
OrganizationId: "org1",
},
},
Metadata: map[string][]byte{"hello": []byte("world")},
},
{ // password factor
Id: "999",
CreationDate: timestamppb.New(now),
ChangeDate: timestamppb.New(now),
Sequence: 123,
Factors: &session.Factors{
User: &session.UserFactor{
VerifiedAt: timestamppb.New(past),
Id: "345",
LoginName: "donald",
DisplayName: "donald duck",
OrganisationId: "org1",
OrganizationId: "org1",
},
Password: &session.PasswordFactor{
VerifiedAt: timestamppb.New(past),
},
},
Metadata: map[string][]byte{"hello": []byte("world")},
},
{ // webAuthN factor
Id: "999",
CreationDate: timestamppb.New(now),
ChangeDate: timestamppb.New(now),
Sequence: 123,
Factors: &session.Factors{
User: &session.UserFactor{
VerifiedAt: timestamppb.New(past),
Id: "345",
LoginName: "donald",
DisplayName: "donald duck",
OrganisationId: "org1",
OrganizationId: "org1",
},
WebAuthN: &session.WebAuthNFactor{
VerifiedAt: timestamppb.New(past),
UserVerified: true,
},
},
Metadata: map[string][]byte{"hello": []byte("world")},
},
{ // totp factor
Id: "999",
CreationDate: timestamppb.New(now),
ChangeDate: timestamppb.New(now),
Sequence: 123,
Factors: &session.Factors{
User: &session.UserFactor{
VerifiedAt: timestamppb.New(past),
Id: "345",
LoginName: "donald",
DisplayName: "donald duck",
OrganisationId: "org1",
OrganizationId: "org1",
},
Totp: &session.TOTPFactor{
VerifiedAt: timestamppb.New(past),
},
},
Metadata: map[string][]byte{"hello": []byte("world")},
},
}
out := sessionsToPb(sessions)
require.Len(t, out, len(want))
for i, got := range out {
if !proto.Equal(got, want[i]) {
t.Errorf("session %d got:\n%v\nwant:\n%v", i, got, want[i])
}
}
}
func Test_userAgentToPb(t *testing.T) {
type args struct {
ua domain.UserAgent
}
tests := []struct {
name string
args args
want *session.UserAgent
}{
{
name: "empty",
args: args{domain.UserAgent{}},
},
{
name: "fingerprint id and description",
args: args{domain.UserAgent{
FingerprintID: gu.Ptr("fingerPrintID"),
Description: gu.Ptr("description"),
}},
want: &session.UserAgent{
FingerprintId: gu.Ptr("fingerPrintID"),
Description: gu.Ptr("description"),
},
},
{
name: "with ip",
args: args{domain.UserAgent{
FingerprintID: gu.Ptr("fingerPrintID"),
Description: gu.Ptr("description"),
IP: net.IPv4(1, 2, 3, 4),
}},
want: &session.UserAgent{
FingerprintId: gu.Ptr("fingerPrintID"),
Description: gu.Ptr("description"),
Ip: gu.Ptr("1.2.3.4"),
},
},
{
name: "with header",
args: args{domain.UserAgent{
FingerprintID: gu.Ptr("fingerPrintID"),
Description: gu.Ptr("description"),
Header: http.Header{
"foo": []string{"foo", "bar"},
"hello": []string{"world"},
},
}},
want: &session.UserAgent{
FingerprintId: gu.Ptr("fingerPrintID"),
Description: gu.Ptr("description"),
Header: map[string]*session.UserAgent_HeaderValues{
"foo": {Values: []string{"foo", "bar"}},
"hello": {Values: []string{"world"}},
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := userAgentToPb(tt.args.ua)
assert.Equal(t, tt.want, got)
})
}
}
func mustNewTextQuery(t testing.TB, column query.Column, value string, compare query.TextComparison) query.SearchQuery {
q, err := query.NewTextQuery(column, value, compare)
require.NoError(t, err)
return q
}
func mustNewListQuery(t testing.TB, column query.Column, list []any, compare query.ListComparison) query.SearchQuery {
q, err := query.NewListQuery(query.SessionColumnID, list, compare)
require.NoError(t, err)
return q
}
func mustNewTimestampQuery(t testing.TB, column query.Column, ts time.Time, compare query.TimestampComparison) query.SearchQuery {
q, err := query.NewTimestampQuery(column, ts, compare)
require.NoError(t, err)
return q
}
func Test_listSessionsRequestToQuery(t *testing.T) {
type args struct {
ctx context.Context
req *session.ListSessionsRequest
}
tests := []struct {
name string
args args
want *query.SessionsSearchQueries
wantErr error
}{
{
name: "default request",
args: args{
ctx: authz.NewMockContext("123", "456", "789"),
req: &session.ListSessionsRequest{},
},
want: &query.SessionsSearchQueries{
SearchRequest: query.SearchRequest{
Offset: 0,
Limit: 0,
Asc: false,
},
Queries: []query.SearchQuery{
mustNewTextQuery(t, query.SessionColumnCreator, "789", query.TextEquals),
},
},
},
{
name: "default request with sorting column",
args: args{
ctx: authz.NewMockContext("123", "456", "789"),
req: &session.ListSessionsRequest{
SortingColumn: session.SessionFieldName_SESSION_FIELD_NAME_CREATION_DATE,
},
},
want: &query.SessionsSearchQueries{
SearchRequest: query.SearchRequest{
Offset: 0,
Limit: 0,
SortingColumn: query.SessionColumnCreationDate,
Asc: false,
},
Queries: []query.SearchQuery{
mustNewTextQuery(t, query.SessionColumnCreator, "789", query.TextEquals),
},
},
},
{
name: "with list query and sessions",
args: args{
ctx: authz.NewMockContext("123", "456", "789"),
req: &session.ListSessionsRequest{
Query: &object.ListQuery{
Offset: 10,
Limit: 20,
Asc: true,
},
Queries: []*session.SearchQuery{
{Query: &session.SearchQuery_IdsQuery{
IdsQuery: &session.IDsQuery{
Ids: []string{"1", "2", "3"},
},
}},
{Query: &session.SearchQuery_IdsQuery{
IdsQuery: &session.IDsQuery{
Ids: []string{"4", "5", "6"},
},
}},
{Query: &session.SearchQuery_UserIdQuery{
UserIdQuery: &session.UserIDQuery{
Id: "10",
},
}},
{Query: &session.SearchQuery_CreationDateQuery{
CreationDateQuery: &session.CreationDateQuery{
CreationDate: timestamppb.New(creationDate),
Method: objpb.TimestampQueryMethod_TIMESTAMP_QUERY_METHOD_GREATER,
},
}},
},
},
},
want: &query.SessionsSearchQueries{
SearchRequest: query.SearchRequest{
Offset: 10,
Limit: 20,
Asc: true,
},
Queries: []query.SearchQuery{
mustNewListQuery(t, query.SessionColumnID, []interface{}{"1", "2", "3"}, query.ListIn),
mustNewListQuery(t, query.SessionColumnID, []interface{}{"4", "5", "6"}, query.ListIn),
mustNewTextQuery(t, query.SessionColumnUserID, "10", query.TextEquals),
mustNewTimestampQuery(t, query.SessionColumnCreationDate, creationDate, query.TimestampGreater),
mustNewTextQuery(t, query.SessionColumnCreator, "789", query.TextEquals),
},
},
},
{
name: "invalid argument error",
args: args{
ctx: authz.NewMockContext("123", "456", "789"),
req: &session.ListSessionsRequest{
Query: &object.ListQuery{
Offset: 10,
Limit: 20,
Asc: true,
},
Queries: []*session.SearchQuery{
{Query: &session.SearchQuery_IdsQuery{
IdsQuery: &session.IDsQuery{
Ids: []string{"1", "2", "3"},
},
}},
{Query: nil},
},
},
},
wantErr: caos_errs.ThrowInvalidArgument(nil, "GRPC-Sfefs", "List.Query.Invalid"),
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := listSessionsRequestToQuery(tt.args.ctx, tt.args.req)
require.ErrorIs(t, err, tt.wantErr)
assert.Equal(t, tt.want, got)
})
}
}
func Test_sessionQueriesToQuery(t *testing.T) {
type args struct {
ctx context.Context
queries []*session.SearchQuery
}
tests := []struct {
name string
args args
want []query.SearchQuery
wantErr error
}{
{
name: "creator only",
args: args{
ctx: authz.NewMockContext("123", "456", "789"),
},
want: []query.SearchQuery{
mustNewTextQuery(t, query.SessionColumnCreator, "789", query.TextEquals),
},
},
{
name: "invalid argument",
args: args{
ctx: authz.NewMockContext("123", "456", "789"),
queries: []*session.SearchQuery{
{Query: nil},
},
},
wantErr: caos_errs.ThrowInvalidArgument(nil, "GRPC-Sfefs", "List.Query.Invalid"),
},
{
name: "creator and sessions",
args: args{
ctx: authz.NewMockContext("123", "456", "789"),
queries: []*session.SearchQuery{
{Query: &session.SearchQuery_IdsQuery{
IdsQuery: &session.IDsQuery{
Ids: []string{"1", "2", "3"},
},
}},
{Query: &session.SearchQuery_IdsQuery{
IdsQuery: &session.IDsQuery{
Ids: []string{"4", "5", "6"},
},
}},
},
},
want: []query.SearchQuery{
mustNewListQuery(t, query.SessionColumnID, []interface{}{"1", "2", "3"}, query.ListIn),
mustNewListQuery(t, query.SessionColumnID, []interface{}{"4", "5", "6"}, query.ListIn),
mustNewTextQuery(t, query.SessionColumnCreator, "789", query.TextEquals),
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := sessionQueriesToQuery(tt.args.ctx, tt.args.queries)
require.ErrorIs(t, err, tt.wantErr)
assert.Equal(t, tt.want, got)
})
}
}
func Test_sessionQueryToQuery(t *testing.T) {
type args struct {
query *session.SearchQuery
}
tests := []struct {
name string
args args
want query.SearchQuery
wantErr error
}{
{
name: "invalid argument",
args: args{&session.SearchQuery{
Query: nil,
}},
wantErr: caos_errs.ThrowInvalidArgument(nil, "GRPC-Sfefs", "List.Query.Invalid"),
},
{
name: "ids query",
args: args{&session.SearchQuery{
Query: &session.SearchQuery_IdsQuery{
IdsQuery: &session.IDsQuery{
Ids: []string{"1", "2", "3"},
},
},
}},
want: mustNewListQuery(t, query.SessionColumnID, []interface{}{"1", "2", "3"}, query.ListIn),
},
{
name: "user id query",
args: args{&session.SearchQuery{
Query: &session.SearchQuery_UserIdQuery{
UserIdQuery: &session.UserIDQuery{
Id: "10",
},
},
}},
want: mustNewTextQuery(t, query.SessionColumnUserID, "10", query.TextEquals),
},
{
name: "creation date query",
args: args{&session.SearchQuery{
Query: &session.SearchQuery_CreationDateQuery{
CreationDateQuery: &session.CreationDateQuery{
CreationDate: timestamppb.New(creationDate),
Method: objpb.TimestampQueryMethod_TIMESTAMP_QUERY_METHOD_LESS,
},
},
}},
want: mustNewTimestampQuery(t, query.SessionColumnCreationDate, creationDate, query.TimestampLess),
},
{
name: "creation date query with default method",
args: args{&session.SearchQuery{
Query: &session.SearchQuery_CreationDateQuery{
CreationDateQuery: &session.CreationDateQuery{
CreationDate: timestamppb.New(creationDate),
},
},
}},
want: mustNewTimestampQuery(t, query.SessionColumnCreationDate, creationDate, query.TimestampEquals),
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := sessionQueryToQuery(tt.args.query)
require.ErrorIs(t, err, tt.wantErr)
assert.Equal(t, tt.want, got)
})
}
}
func Test_userCheck(t *testing.T) {
type args struct {
user *session.CheckUser
}
tests := []struct {
name string
args args
want userSearch
wantErr error
}{
{
name: "nil user",
args: args{nil},
want: nil,
},
{
name: "by user id",
args: args{&session.CheckUser{
Search: &session.CheckUser_UserId{
UserId: "foo",
},
}},
want: userSearchByID{"foo"},
},
{
name: "by user id",
args: args{&session.CheckUser{
Search: &session.CheckUser_LoginName{
LoginName: "bar",
},
}},
want: userSearchByLoginName{"bar"},
},
{
name: "unimplemented error",
args: args{&session.CheckUser{
Search: nil,
}},
wantErr: caos_errs.ThrowUnimplementedf(nil, "SESSION-d3b4g0", "user search %T not implemented", nil),
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := userCheck(tt.args.user)
require.ErrorIs(t, err, tt.wantErr)
assert.Equal(t, tt.want, got)
})
}
}
func Test_userVerificationRequirementToDomain(t *testing.T) {
type args struct {
req session.UserVerificationRequirement
}
tests := []struct {
args args
want domain.UserVerificationRequirement
}{
{
args: args{session.UserVerificationRequirement_USER_VERIFICATION_REQUIREMENT_UNSPECIFIED},
want: domain.UserVerificationRequirementUnspecified,
},
{
args: args{session.UserVerificationRequirement_USER_VERIFICATION_REQUIREMENT_REQUIRED},
want: domain.UserVerificationRequirementRequired,
},
{
args: args{session.UserVerificationRequirement_USER_VERIFICATION_REQUIREMENT_PREFERRED},
want: domain.UserVerificationRequirementPreferred,
},
{
args: args{session.UserVerificationRequirement_USER_VERIFICATION_REQUIREMENT_DISCOURAGED},
want: domain.UserVerificationRequirementDiscouraged,
},
{
args: args{999},
want: domain.UserVerificationRequirementUnspecified,
},
}
for _, tt := range tests {
t.Run(tt.args.req.String(), func(t *testing.T) {
got := userVerificationRequirementToDomain(tt.args.req)
assert.Equal(t, tt.want, got)
})
}
}
func Test_userAgentToCommand(t *testing.T) {
type args struct {
userAgent *session.UserAgent
}
tests := []struct {
name string
args args
want *domain.UserAgent
}{
{
name: "nil",
args: args{nil},
want: nil,
},
{
name: "all fields",
args: args{&session.UserAgent{
FingerprintId: gu.Ptr("fp1"),
Ip: gu.Ptr("1.2.3.4"),
Description: gu.Ptr("firefox"),
Header: map[string]*session.UserAgent_HeaderValues{
"hello": {
Values: []string{"foo", "bar"},
},
},
}},
want: &domain.UserAgent{
FingerprintID: gu.Ptr("fp1"),
IP: net.ParseIP("1.2.3.4"),
Description: gu.Ptr("firefox"),
Header: http.Header{
"hello": []string{"foo", "bar"},
},
},
},
{
name: "invalid ip",
args: args{&session.UserAgent{
FingerprintId: gu.Ptr("fp1"),
Ip: gu.Ptr("oops"),
Description: gu.Ptr("firefox"),
Header: map[string]*session.UserAgent_HeaderValues{
"hello": {
Values: []string{"foo", "bar"},
},
},
}},
want: &domain.UserAgent{
FingerprintID: gu.Ptr("fp1"),
IP: nil,
Description: gu.Ptr("firefox"),
Header: http.Header{
"hello": []string{"foo", "bar"},
},
},
},
{
name: "nil fields",
args: args{&session.UserAgent{}},
want: &domain.UserAgent{},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := userAgentToCommand(tt.args.userAgent)
assert.Equal(t, tt.want, got)
})
}
}