mirror of
https://github.com/andwati/zitadel.git
synced 2026-10-18 05:52:51 +00:00
feat: ListSessions to relational (#11987)
# Which Problems Are Solved This PR introduces the possibility of using the relational DB when calling the ListSession gRPC endpoint. # How the Problems Are Solved - Implement the Querier interface for ListSession: - Create a `domain` model for the Session object to be able to map the gRPC session into it. - Match the permission filters on ES side to be sure the same sessions are returned - Implement converter methods to go from `gRPC` to `domain` models (both ways) - Fix the session sql relational migration: rework the trigger to update the `expiration` field to perform the update only when the input `lifetime` has a non-zero value. - Link the Querier implementation with the gRPC endpoint - Run the same integration tests against the relational DB, making sure the outcome is the same. ⚠️ Due to missing permission checks, some divergence is expected. Hence, specific code to circumvent the differences has been put in place # Additional Context - Closes #11039
This commit is contained in:
@@ -0,0 +1,14 @@
|
||||
package convert
|
||||
|
||||
import (
|
||||
"github.com/zitadel/zitadel/backend/v3/storage/database"
|
||||
objpb "github.com/zitadel/zitadel/pkg/grpc/object"
|
||||
)
|
||||
|
||||
var grpcTimestampOpToDomain = map[objpb.TimestampQueryMethod]database.NumberOperation{
|
||||
objpb.TimestampQueryMethod_TIMESTAMP_QUERY_METHOD_EQUALS: database.NumberOperationEqual,
|
||||
objpb.TimestampQueryMethod_TIMESTAMP_QUERY_METHOD_GREATER: database.NumberOperationGreaterThan,
|
||||
objpb.TimestampQueryMethod_TIMESTAMP_QUERY_METHOD_GREATER_OR_EQUALS: database.NumberOperationGreaterThanOrEqual,
|
||||
objpb.TimestampQueryMethod_TIMESTAMP_QUERY_METHOD_LESS: database.NumberOperationLessThan,
|
||||
objpb.TimestampQueryMethod_TIMESTAMP_QUERY_METHOD_LESS_OR_EQUALS: database.NumberOperationLessThanOrEqual,
|
||||
}
|
||||
@@ -0,0 +1,213 @@
|
||||
package convert
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"google.golang.org/protobuf/types/known/timestamppb"
|
||||
|
||||
"github.com/zitadel/zitadel/backend/v3/domain"
|
||||
"github.com/zitadel/zitadel/internal/api/authz"
|
||||
"github.com/zitadel/zitadel/internal/zerrors"
|
||||
session_grpc "github.com/zitadel/zitadel/pkg/grpc/session/v2"
|
||||
)
|
||||
|
||||
// ListSessionsRequestGRPCToDomain converts a gRPC [session_grpc.ListSessionsRequest] to
|
||||
// the domain [domain.ListSessionsRequest]. It validates the request and returns an error
|
||||
// for any invalid filter values.
|
||||
func ListSessionsRequestGRPCToDomain(ctx context.Context, req *session_grpc.ListSessionsRequest) (*domain.ListSessionsRequest, error) {
|
||||
filters, err := searchQueriesGRPCToDomain(ctx, req.GetQueries())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
sortCol := domain.SessionSortColumnUnspecified
|
||||
if req.GetSortingColumn() == session_grpc.SessionFieldName_SESSION_FIELD_NAME_CREATION_DATE {
|
||||
sortCol = domain.SessionSortColumnCreationDate
|
||||
}
|
||||
|
||||
return &domain.ListSessionsRequest{
|
||||
Filters: filters,
|
||||
SortColumn: sortCol,
|
||||
Ascending: req.GetQuery().GetAsc(),
|
||||
Limit: req.GetQuery().GetLimit(),
|
||||
Offset: uint32(req.GetQuery().GetOffset()),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func searchQueriesGRPCToDomain(ctx context.Context, queries []*session_grpc.SearchQuery) ([]domain.SessionFilter, error) {
|
||||
if len(queries) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
filters := make([]domain.SessionFilter, 0, len(queries))
|
||||
for _, q := range queries {
|
||||
f, err := searchQueryGRPCToDomain(ctx, q)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
filters = append(filters, f)
|
||||
}
|
||||
return filters, nil
|
||||
}
|
||||
|
||||
func searchQueryGRPCToDomain(ctx context.Context, q *session_grpc.SearchQuery) (domain.SessionFilter, error) {
|
||||
switch typedQuery := q.GetQuery().(type) {
|
||||
case *session_grpc.SearchQuery_IdsQuery:
|
||||
return domain.SessionIDsFilter{IDs: typedQuery.IdsQuery.GetIds()}, nil
|
||||
|
||||
case *session_grpc.SearchQuery_UserIdQuery:
|
||||
return domain.SessionUserIDFilter{UserID: typedQuery.UserIdQuery.GetId()}, nil
|
||||
|
||||
case *session_grpc.SearchQuery_CreationDateQuery:
|
||||
return domain.SessionCreationDateFilter{
|
||||
Op: grpcTimestampOpToDomain[typedQuery.CreationDateQuery.GetMethod()],
|
||||
Date: typedQuery.CreationDateQuery.GetCreationDate().AsTime(),
|
||||
}, nil
|
||||
|
||||
case *session_grpc.SearchQuery_CreatorQuery:
|
||||
id := authz.GetCtxData(ctx).UserID
|
||||
if typedQuery.CreatorQuery != nil && typedQuery.CreatorQuery.Id != nil {
|
||||
if typedQuery.CreatorQuery.GetId() == "" {
|
||||
return nil, zerrors.ThrowInvalidArgument(nil, "DOM-x8n24uh", "List.Query.Invalid")
|
||||
}
|
||||
id = typedQuery.CreatorQuery.GetId()
|
||||
}
|
||||
return domain.SessionCreatorFilter{ID: id}, nil
|
||||
|
||||
case *session_grpc.SearchQuery_UserAgentQuery:
|
||||
fp := authz.GetCtxData(ctx).AgentID
|
||||
if typedQuery.UserAgentQuery != nil && typedQuery.UserAgentQuery.FingerprintId != nil {
|
||||
if typedQuery.UserAgentQuery.GetFingerprintId() == "" {
|
||||
return nil, zerrors.ThrowInvalidArgument(nil, "DOM-x8n23uh", "List.Query.Invalid")
|
||||
}
|
||||
fp = typedQuery.UserAgentQuery.GetFingerprintId()
|
||||
}
|
||||
return domain.SessionUserAgentFilter{FingerprintID: fp}, nil
|
||||
|
||||
case *session_grpc.SearchQuery_ExpirationDateQuery:
|
||||
return domain.SessionExpirationDateFilter{
|
||||
Op: grpcTimestampOpToDomain[typedQuery.ExpirationDateQuery.GetMethod()],
|
||||
Date: typedQuery.ExpirationDateQuery.GetExpirationDate().AsTime(),
|
||||
}, nil
|
||||
|
||||
default:
|
||||
return nil, zerrors.ThrowInvalidArgumentf(nil, "CONV-Cz5s3t", "session search query %T not implemented", typedQuery)
|
||||
}
|
||||
}
|
||||
|
||||
// DomainSessionListToGRPCResponse converts a slice of domain sessions to gRPC response sessions.
|
||||
func DomainSessionListToGRPCResponse(sessions []*domain.Session) []*session_grpc.Session {
|
||||
out := make([]*session_grpc.Session, len(sessions))
|
||||
for i, s := range sessions {
|
||||
out[i] = DomainSessionToGRPCResponse(s)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// DomainSessionToGRPCResponse converts a single domain session to its gRPC representation.
|
||||
func DomainSessionToGRPCResponse(session *domain.Session) *session_grpc.Session {
|
||||
pb := &session_grpc.Session{
|
||||
Id: session.ID,
|
||||
CreationDate: timestamppb.New(session.CreatedAt),
|
||||
ChangeDate: timestamppb.New(session.UpdatedAt),
|
||||
Factors: domainFactorsToGRPC(session),
|
||||
UserAgent: domainUserAgentToGRPC(session.UserAgent),
|
||||
}
|
||||
|
||||
if len(session.Metadata) > 0 {
|
||||
pb.Metadata = make(map[string][]byte, len(session.Metadata))
|
||||
for _, m := range session.Metadata {
|
||||
pb.Metadata[m.Key] = m.Value
|
||||
}
|
||||
}
|
||||
|
||||
if !session.Expiration.IsZero() {
|
||||
pb.ExpirationDate = timestamppb.New(session.Expiration)
|
||||
}
|
||||
|
||||
return pb
|
||||
}
|
||||
|
||||
func domainFactorsToGRPC(session *domain.Session) *session_grpc.Factors {
|
||||
userFactor := session.Factors.GetUserFactor()
|
||||
if userFactor == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
pb := &session_grpc.Factors{
|
||||
User: &session_grpc.UserFactor{
|
||||
VerifiedAt: timestamppb.New(userFactor.LastVerifiedAt),
|
||||
Id: userFactor.UserID,
|
||||
},
|
||||
}
|
||||
|
||||
pb.User.LoginName = session.UserPreferredLoginName
|
||||
if session.UserOrganizationID != nil {
|
||||
pb.User.OrganizationId = *session.UserOrganizationID
|
||||
}
|
||||
if session.UserDisplayName != nil {
|
||||
pb.User.DisplayName = *session.UserDisplayName
|
||||
}
|
||||
|
||||
if f := session.Factors.GetPasswordFactor(); f != nil {
|
||||
pb.Password = &session_grpc.PasswordFactor{
|
||||
VerifiedAt: timestamppb.New(f.LastVerifiedAt),
|
||||
}
|
||||
}
|
||||
if f := session.Factors.GetPasskeyFactor(); f != nil {
|
||||
pb.WebAuthN = &session_grpc.WebAuthNFactor{
|
||||
VerifiedAt: timestamppb.New(f.LastVerifiedAt),
|
||||
UserVerified: f.UserVerified,
|
||||
}
|
||||
}
|
||||
if f := session.Factors.GetIDPIntentFactor(); f != nil {
|
||||
pb.Intent = &session_grpc.IntentFactor{
|
||||
VerifiedAt: timestamppb.New(f.LastVerifiedAt),
|
||||
}
|
||||
}
|
||||
if f := session.Factors.GetTOTPFactor(); f != nil {
|
||||
pb.Totp = &session_grpc.TOTPFactor{
|
||||
VerifiedAt: timestamppb.New(f.LastVerifiedAt),
|
||||
}
|
||||
}
|
||||
if f := session.Factors.GetOTPSMSFactor(); f != nil {
|
||||
pb.OtpSms = &session_grpc.OTPFactor{
|
||||
VerifiedAt: timestamppb.New(f.LastVerifiedAt),
|
||||
}
|
||||
}
|
||||
if f := session.Factors.GetOTPEmailFactor(); f != nil {
|
||||
pb.OtpEmail = &session_grpc.OTPFactor{
|
||||
VerifiedAt: timestamppb.New(f.LastVerifiedAt),
|
||||
}
|
||||
}
|
||||
if f := session.Factors.GetRecoveryCodeFactor(); f != nil {
|
||||
pb.RecoveryCode = &session_grpc.RecoveryCodeFactor{
|
||||
VerifiedAt: timestamppb.New(f.LastVerifiedAt),
|
||||
}
|
||||
}
|
||||
|
||||
return pb
|
||||
}
|
||||
|
||||
func domainUserAgentToGRPC(ua *domain.SessionUserAgent) *session_grpc.UserAgent {
|
||||
if ua == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
pb := &session_grpc.UserAgent{
|
||||
FingerprintId: ua.FingerprintID,
|
||||
Description: ua.Description,
|
||||
}
|
||||
|
||||
if ua.IP != nil {
|
||||
ip := ua.IP.String()
|
||||
pb.Ip = &ip
|
||||
}
|
||||
if len(ua.Header) > 0 {
|
||||
pb.Header = make(map[string]*session_grpc.UserAgent_HeaderValues, len(ua.Header))
|
||||
for k, v := range ua.Header {
|
||||
pb.Header[k] = &session_grpc.UserAgent_HeaderValues{Values: v}
|
||||
}
|
||||
}
|
||||
|
||||
return pb
|
||||
}
|
||||
@@ -0,0 +1,606 @@
|
||||
package convert
|
||||
|
||||
import (
|
||||
"net"
|
||||
"net/http"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/muhlemmer/gu"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"google.golang.org/protobuf/types/known/timestamppb"
|
||||
|
||||
"github.com/zitadel/zitadel/backend/v3/domain"
|
||||
"github.com/zitadel/zitadel/backend/v3/storage/database"
|
||||
"github.com/zitadel/zitadel/internal/api/authz"
|
||||
"github.com/zitadel/zitadel/internal/zerrors"
|
||||
objpb "github.com/zitadel/zitadel/pkg/grpc/object"
|
||||
objv2 "github.com/zitadel/zitadel/pkg/grpc/object/v2"
|
||||
session_grpc "github.com/zitadel/zitadel/pkg/grpc/session/v2"
|
||||
)
|
||||
|
||||
func TestSearchQueryGRPCToDomain(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
now := time.Date(2026, 1, 1, 12, 0, 0, 0, time.UTC)
|
||||
nowPB := timestamppb.New(now)
|
||||
creatorID := "creator-123"
|
||||
fingerprintID := "fp-123"
|
||||
userID := "user-1"
|
||||
agentID := "agent-1"
|
||||
emptyStr := ""
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
input *session_grpc.SearchQuery
|
||||
want domain.SessionFilter
|
||||
wantErr error
|
||||
}{
|
||||
{
|
||||
name: "IdsQuery maps to SessionIDsFilter",
|
||||
input: &session_grpc.SearchQuery{
|
||||
Query: &session_grpc.SearchQuery_IdsQuery{
|
||||
IdsQuery: &session_grpc.IDsQuery{Ids: []string{"id1", "id2"}},
|
||||
},
|
||||
},
|
||||
want: domain.SessionIDsFilter{IDs: []string{"id1", "id2"}},
|
||||
},
|
||||
{
|
||||
name: "UserIdQuery maps to SessionUserIDFilter",
|
||||
input: &session_grpc.SearchQuery{
|
||||
Query: &session_grpc.SearchQuery_UserIdQuery{
|
||||
UserIdQuery: &session_grpc.UserIDQuery{Id: "user-123"},
|
||||
},
|
||||
},
|
||||
want: domain.SessionUserIDFilter{UserID: "user-123"},
|
||||
},
|
||||
{
|
||||
name: "CreationDateQuery with EQUALS method",
|
||||
input: &session_grpc.SearchQuery{
|
||||
Query: &session_grpc.SearchQuery_CreationDateQuery{
|
||||
CreationDateQuery: &session_grpc.CreationDateQuery{
|
||||
CreationDate: nowPB,
|
||||
Method: objpb.TimestampQueryMethod_TIMESTAMP_QUERY_METHOD_EQUALS,
|
||||
},
|
||||
},
|
||||
},
|
||||
want: domain.SessionCreationDateFilter{Op: database.NumberOperationEqual, Date: now},
|
||||
},
|
||||
{
|
||||
name: "CreationDateQuery with GREATER method",
|
||||
input: &session_grpc.SearchQuery{
|
||||
Query: &session_grpc.SearchQuery_CreationDateQuery{
|
||||
CreationDateQuery: &session_grpc.CreationDateQuery{
|
||||
CreationDate: nowPB,
|
||||
Method: objpb.TimestampQueryMethod_TIMESTAMP_QUERY_METHOD_GREATER,
|
||||
},
|
||||
},
|
||||
},
|
||||
want: domain.SessionCreationDateFilter{Op: database.NumberOperationGreaterThan, Date: now},
|
||||
},
|
||||
{
|
||||
name: "CreationDateQuery with GREATER_OR_EQUALS method",
|
||||
input: &session_grpc.SearchQuery{
|
||||
Query: &session_grpc.SearchQuery_CreationDateQuery{
|
||||
CreationDateQuery: &session_grpc.CreationDateQuery{
|
||||
CreationDate: nowPB,
|
||||
Method: objpb.TimestampQueryMethod_TIMESTAMP_QUERY_METHOD_GREATER_OR_EQUALS,
|
||||
},
|
||||
},
|
||||
},
|
||||
want: domain.SessionCreationDateFilter{Op: database.NumberOperationGreaterThanOrEqual, Date: now},
|
||||
},
|
||||
{
|
||||
name: "CreationDateQuery with LESS method",
|
||||
input: &session_grpc.SearchQuery{
|
||||
Query: &session_grpc.SearchQuery_CreationDateQuery{
|
||||
CreationDateQuery: &session_grpc.CreationDateQuery{
|
||||
CreationDate: nowPB,
|
||||
Method: objpb.TimestampQueryMethod_TIMESTAMP_QUERY_METHOD_LESS,
|
||||
},
|
||||
},
|
||||
},
|
||||
want: domain.SessionCreationDateFilter{Op: database.NumberOperationLessThan, Date: now},
|
||||
},
|
||||
{
|
||||
name: "CreationDateQuery with LESS_OR_EQUALS method",
|
||||
input: &session_grpc.SearchQuery{
|
||||
Query: &session_grpc.SearchQuery_CreationDateQuery{
|
||||
CreationDateQuery: &session_grpc.CreationDateQuery{
|
||||
CreationDate: nowPB,
|
||||
Method: objpb.TimestampQueryMethod_TIMESTAMP_QUERY_METHOD_LESS_OR_EQUALS,
|
||||
},
|
||||
},
|
||||
},
|
||||
want: domain.SessionCreationDateFilter{Op: database.NumberOperationLessThanOrEqual, Date: now},
|
||||
},
|
||||
{
|
||||
name: "CreatorQuery with nil ID uses userID from context",
|
||||
input: &session_grpc.SearchQuery{
|
||||
Query: &session_grpc.SearchQuery_CreatorQuery{
|
||||
CreatorQuery: &session_grpc.CreatorQuery{},
|
||||
},
|
||||
},
|
||||
want: domain.SessionCreatorFilter{ID: userID},
|
||||
},
|
||||
{
|
||||
name: "CreatorQuery with empty ID returns error",
|
||||
input: &session_grpc.SearchQuery{
|
||||
Query: &session_grpc.SearchQuery_CreatorQuery{
|
||||
CreatorQuery: &session_grpc.CreatorQuery{Id: &emptyStr},
|
||||
},
|
||||
},
|
||||
wantErr: zerrors.ThrowInvalidArgument(nil, "DOM-x8n24uh", "List.Query.Invalid"),
|
||||
},
|
||||
{
|
||||
name: "CreatorQuery with valid ID uses it",
|
||||
input: &session_grpc.SearchQuery{
|
||||
Query: &session_grpc.SearchQuery_CreatorQuery{
|
||||
CreatorQuery: &session_grpc.CreatorQuery{Id: &creatorID},
|
||||
},
|
||||
},
|
||||
want: domain.SessionCreatorFilter{ID: creatorID},
|
||||
},
|
||||
{
|
||||
name: "UserAgentQuery with nil FingerprintID uses agentID in context",
|
||||
input: &session_grpc.SearchQuery{
|
||||
Query: &session_grpc.SearchQuery_UserAgentQuery{
|
||||
UserAgentQuery: &session_grpc.UserAgentQuery{},
|
||||
},
|
||||
},
|
||||
want: domain.SessionUserAgentFilter{FingerprintID: agentID},
|
||||
},
|
||||
{
|
||||
name: "UserAgentQuery with empty FingerprintID returns error",
|
||||
input: &session_grpc.SearchQuery{
|
||||
Query: &session_grpc.SearchQuery_UserAgentQuery{
|
||||
UserAgentQuery: &session_grpc.UserAgentQuery{FingerprintId: &emptyStr},
|
||||
},
|
||||
},
|
||||
wantErr: zerrors.ThrowInvalidArgument(nil, "DOM-x8n23uh", "List.Query.Invalid"),
|
||||
},
|
||||
{
|
||||
name: "UserAgentQuery with valid FingerprintID uses it",
|
||||
input: &session_grpc.SearchQuery{
|
||||
Query: &session_grpc.SearchQuery_UserAgentQuery{
|
||||
UserAgentQuery: &session_grpc.UserAgentQuery{FingerprintId: &fingerprintID},
|
||||
},
|
||||
},
|
||||
want: domain.SessionUserAgentFilter{FingerprintID: fingerprintID},
|
||||
},
|
||||
{
|
||||
name: "ExpirationDateQuery with EQUALS method",
|
||||
input: &session_grpc.SearchQuery{
|
||||
Query: &session_grpc.SearchQuery_ExpirationDateQuery{
|
||||
ExpirationDateQuery: &session_grpc.ExpirationDateQuery{
|
||||
ExpirationDate: nowPB,
|
||||
Method: objpb.TimestampQueryMethod_TIMESTAMP_QUERY_METHOD_EQUALS,
|
||||
},
|
||||
},
|
||||
},
|
||||
want: domain.SessionExpirationDateFilter{Op: database.NumberOperationEqual, Date: now},
|
||||
},
|
||||
{
|
||||
name: "ExpirationDateQuery with GREATER method",
|
||||
input: &session_grpc.SearchQuery{
|
||||
Query: &session_grpc.SearchQuery_ExpirationDateQuery{
|
||||
ExpirationDateQuery: &session_grpc.ExpirationDateQuery{
|
||||
ExpirationDate: nowPB,
|
||||
Method: objpb.TimestampQueryMethod_TIMESTAMP_QUERY_METHOD_GREATER,
|
||||
},
|
||||
},
|
||||
},
|
||||
want: domain.SessionExpirationDateFilter{Op: database.NumberOperationGreaterThan, Date: now},
|
||||
},
|
||||
{
|
||||
name: "unknown query type returns error",
|
||||
input: &session_grpc.SearchQuery{},
|
||||
wantErr: zerrors.ThrowInvalidArgumentf(nil, "CONV-Cz5s3t", "session search query %T not implemented", nil),
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
ctx := authz.NewMockContextWithAgent("instance-1", "org-1", userID, agentID)
|
||||
|
||||
got, err := searchQueryGRPCToDomain(ctx, tc.input)
|
||||
|
||||
assert.ErrorIs(t, err, tc.wantErr)
|
||||
assert.Equal(t, tc.want, got)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestSearchQueriesGRPCToDomain(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
emptyStr := ""
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
input []*session_grpc.SearchQuery
|
||||
want []domain.SessionFilter
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "nil slice returns nil",
|
||||
input: nil,
|
||||
want: nil,
|
||||
},
|
||||
{
|
||||
name: "empty slice returns nil",
|
||||
input: []*session_grpc.SearchQuery{},
|
||||
want: nil,
|
||||
},
|
||||
{
|
||||
name: "valid queries are converted to filters",
|
||||
input: []*session_grpc.SearchQuery{
|
||||
{Query: &session_grpc.SearchQuery_IdsQuery{IdsQuery: &session_grpc.IDsQuery{Ids: []string{"id1"}}}},
|
||||
{Query: &session_grpc.SearchQuery_UserIdQuery{UserIdQuery: &session_grpc.UserIDQuery{Id: "user-1"}}},
|
||||
},
|
||||
want: []domain.SessionFilter{
|
||||
domain.SessionIDsFilter{IDs: []string{"id1"}},
|
||||
domain.SessionUserIDFilter{UserID: "user-1"},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "invalid query inside list propagates error",
|
||||
input: []*session_grpc.SearchQuery{
|
||||
{
|
||||
Query: &session_grpc.SearchQuery_CreatorQuery{
|
||||
CreatorQuery: &session_grpc.CreatorQuery{Id: &emptyStr},
|
||||
},
|
||||
},
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
ctx := authz.NewMockContext("instance-1", "org-1", "user-1")
|
||||
|
||||
got, err := searchQueriesGRPCToDomain(ctx, tc.input)
|
||||
|
||||
if tc.wantErr {
|
||||
assert.Error(t, err)
|
||||
assert.Nil(t, got)
|
||||
return
|
||||
}
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, tc.want, got)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestListSessionsRequestGRPCToDomain(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
emptyStr := ""
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
input *session_grpc.ListSessionsRequest
|
||||
want *domain.ListSessionsRequest
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "nil request returns empty domain request",
|
||||
input: nil,
|
||||
want: &domain.ListSessionsRequest{
|
||||
SortColumn: domain.SessionSortColumnUnspecified,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "invalid query propagates error",
|
||||
input: &session_grpc.ListSessionsRequest{
|
||||
Queries: []*session_grpc.SearchQuery{
|
||||
{
|
||||
Query: &session_grpc.SearchQuery_CreatorQuery{
|
||||
CreatorQuery: &session_grpc.CreatorQuery{Id: &emptyStr},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "SESSION_FIELD_NAME_CREATION_DATE maps to SessionSortColumnCreationDate",
|
||||
input: &session_grpc.ListSessionsRequest{
|
||||
SortingColumn: session_grpc.SessionFieldName_SESSION_FIELD_NAME_CREATION_DATE,
|
||||
},
|
||||
want: &domain.ListSessionsRequest{
|
||||
SortColumn: domain.SessionSortColumnCreationDate,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "SESSION_FIELD_NAME_UNSPECIFIED maps to SessionSortColumnUnspecified",
|
||||
input: &session_grpc.ListSessionsRequest{
|
||||
SortingColumn: session_grpc.SessionFieldName_SESSION_FIELD_NAME_UNSPECIFIED,
|
||||
},
|
||||
want: &domain.ListSessionsRequest{
|
||||
SortColumn: domain.SessionSortColumnUnspecified,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "query limit, offset and ascending are mapped",
|
||||
input: &session_grpc.ListSessionsRequest{
|
||||
Query: &objv2.ListQuery{Limit: 20, Offset: 5, Asc: true},
|
||||
},
|
||||
want: &domain.ListSessionsRequest{
|
||||
SortColumn: domain.SessionSortColumnUnspecified,
|
||||
Limit: 20,
|
||||
Offset: 5,
|
||||
Ascending: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "valid queries are included in result",
|
||||
input: &session_grpc.ListSessionsRequest{
|
||||
Queries: []*session_grpc.SearchQuery{
|
||||
{Query: &session_grpc.SearchQuery_UserIdQuery{UserIdQuery: &session_grpc.UserIDQuery{Id: "user-1"}}},
|
||||
},
|
||||
},
|
||||
want: &domain.ListSessionsRequest{
|
||||
SortColumn: domain.SessionSortColumnUnspecified,
|
||||
Filters: []domain.SessionFilter{domain.SessionUserIDFilter{UserID: "user-1"}},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
ctx := authz.NewMockContext("instance-1", "org-1", "user-2")
|
||||
|
||||
got, err := ListSessionsRequestGRPCToDomain(ctx, tc.input)
|
||||
|
||||
if tc.wantErr {
|
||||
assert.Error(t, err)
|
||||
assert.Nil(t, got)
|
||||
return
|
||||
}
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, tc.want, got)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestDomainSessionListToGRPCResponse(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
t.Run("empty slice returns empty response slice", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
got := DomainSessionListToGRPCResponse([]*domain.Session{})
|
||||
assert.Empty(t, got)
|
||||
})
|
||||
|
||||
t.Run("each session is converted", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
sessions := []*domain.Session{
|
||||
{ID: "sess-1"},
|
||||
{ID: "sess-2"},
|
||||
}
|
||||
got := DomainSessionListToGRPCResponse(sessions)
|
||||
assert.Len(t, got, 2)
|
||||
assert.Equal(t, "sess-1", got[0].Id)
|
||||
assert.Equal(t, "sess-2", got[1].Id)
|
||||
})
|
||||
}
|
||||
|
||||
func TestDomainSessionToGRPCResponse(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
createdAt := time.Date(2026, 1, 1, 10, 0, 0, 0, time.UTC)
|
||||
updatedAt := time.Date(2026, 1, 2, 10, 0, 0, 0, time.UTC)
|
||||
expiration := time.Date(2026, 6, 1, 0, 0, 0, 0, time.UTC)
|
||||
|
||||
t.Run("basic fields are mapped", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
lsr := &domain.Session{
|
||||
ID: "sess-abc",
|
||||
CreatedAt: createdAt,
|
||||
UpdatedAt: updatedAt,
|
||||
}
|
||||
|
||||
got := DomainSessionToGRPCResponse(lsr)
|
||||
assert.Equal(t, "sess-abc", got.Id)
|
||||
assert.Equal(t, timestamppb.New(createdAt), got.CreationDate)
|
||||
assert.Equal(t, timestamppb.New(updatedAt), got.ChangeDate)
|
||||
assert.EqualValues(t, 0, got.Sequence)
|
||||
assert.Nil(t, got.ExpirationDate)
|
||||
assert.Nil(t, got.Metadata)
|
||||
})
|
||||
|
||||
t.Run("expiration is set when non-zero", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
lsr := &domain.Session{Expiration: expiration}
|
||||
got := DomainSessionToGRPCResponse(lsr)
|
||||
assert.Equal(t, timestamppb.New(expiration), got.ExpirationDate)
|
||||
})
|
||||
|
||||
t.Run("zero expiration is not set", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
lsr := &domain.Session{}
|
||||
got := DomainSessionToGRPCResponse(lsr)
|
||||
assert.Nil(t, got.ExpirationDate)
|
||||
})
|
||||
|
||||
t.Run("metadata is mapped to key-value pairs", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
lsr := &domain.Session{
|
||||
Metadata: []*domain.SessionMetadata{
|
||||
{Metadata: domain.Metadata{Key: "k1", Value: []byte("v1")}},
|
||||
{Metadata: domain.Metadata{Key: "k2", Value: []byte("v2")}},
|
||||
},
|
||||
}
|
||||
got := DomainSessionToGRPCResponse(lsr)
|
||||
assert.Equal(t, map[string][]byte{"k1": []byte("v1"), "k2": []byte("v2")}, got.Metadata)
|
||||
})
|
||||
|
||||
t.Run("empty metadata is not set", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
lsr := &domain.Session{
|
||||
Metadata: []*domain.SessionMetadata{},
|
||||
}
|
||||
got := DomainSessionToGRPCResponse(lsr)
|
||||
assert.Nil(t, got.Metadata)
|
||||
})
|
||||
}
|
||||
|
||||
func TestDomainFactorsToGRPC(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
verifiedAt := time.Date(2026, 1, 1, 12, 0, 0, 0, time.UTC)
|
||||
verifiedAtPB := timestamppb.New(verifiedAt)
|
||||
|
||||
t.Run("no user factor returns nil", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
got := domainFactorsToGRPC(&domain.Session{Factors: domain.SessionFactors{}})
|
||||
assert.Nil(t, got)
|
||||
})
|
||||
|
||||
t.Run("no factors returns nil", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
got := domainFactorsToGRPC(&domain.Session{})
|
||||
assert.Nil(t, got)
|
||||
})
|
||||
|
||||
t.Run("only user factor maps User field with nil user", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
factors := domain.SessionFactors{
|
||||
&domain.SessionFactorUser{UserID: "user-1", LastVerifiedAt: verifiedAt},
|
||||
}
|
||||
got := domainFactorsToGRPC(&domain.Session{Factors: factors})
|
||||
assert.NotNil(t, got)
|
||||
assert.Equal(t, verifiedAtPB, got.User.VerifiedAt)
|
||||
assert.Equal(t, "user-1", got.User.Id)
|
||||
assert.Zero(t, got.User.LoginName)
|
||||
assert.Zero(t, got.User.OrganizationId)
|
||||
assert.Zero(t, got.User.DisplayName)
|
||||
assert.Nil(t, got.Password)
|
||||
assert.Nil(t, got.WebAuthN)
|
||||
assert.Nil(t, got.Intent)
|
||||
assert.Nil(t, got.Totp)
|
||||
assert.Nil(t, got.OtpSms)
|
||||
assert.Nil(t, got.OtpEmail)
|
||||
assert.Nil(t, got.RecoveryCode)
|
||||
})
|
||||
|
||||
t.Run("only user factor maps User field", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
factors := domain.SessionFactors{
|
||||
&domain.SessionFactorUser{UserID: "user-1", LastVerifiedAt: verifiedAt},
|
||||
}
|
||||
|
||||
got := domainFactorsToGRPC(&domain.Session{
|
||||
Factors: factors,
|
||||
UserPreferredLoginName: "preferred ln",
|
||||
UserOrganizationID: gu.Ptr("org-1"),
|
||||
UserDisplayName: gu.Ptr("displayed name"),
|
||||
})
|
||||
assert.NotNil(t, got)
|
||||
assert.Equal(t, "user-1", got.User.Id)
|
||||
assert.Equal(t, verifiedAtPB, got.User.VerifiedAt)
|
||||
assert.Equal(t, "preferred ln", got.User.LoginName)
|
||||
assert.Equal(t, "org-1", got.User.OrganizationId)
|
||||
assert.Equal(t, "displayed name", got.User.DisplayName)
|
||||
assert.Nil(t, got.Password)
|
||||
assert.Nil(t, got.WebAuthN)
|
||||
assert.Nil(t, got.Intent)
|
||||
assert.Nil(t, got.Totp)
|
||||
assert.Nil(t, got.OtpSms)
|
||||
assert.Nil(t, got.OtpEmail)
|
||||
assert.Nil(t, got.RecoveryCode)
|
||||
})
|
||||
|
||||
t.Run("all factors are mapped", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
factors := domain.SessionFactors{
|
||||
&domain.SessionFactorUser{UserID: "user-1", LastVerifiedAt: verifiedAt},
|
||||
&domain.SessionFactorPassword{LastVerifiedAt: verifiedAt},
|
||||
&domain.SessionFactorPasskey{LastVerifiedAt: verifiedAt, UserVerified: true},
|
||||
&domain.SessionFactorIdentityProviderIntent{LastVerifiedAt: verifiedAt},
|
||||
&domain.SessionFactorTOTP{LastVerifiedAt: verifiedAt},
|
||||
&domain.SessionFactorOTPSMS{LastVerifiedAt: verifiedAt},
|
||||
&domain.SessionFactorOTPEmail{LastVerifiedAt: verifiedAt},
|
||||
&domain.SessionFactorRecoveryCode{LastVerifiedAt: verifiedAt},
|
||||
}
|
||||
session := &domain.Session{
|
||||
Factors: factors,
|
||||
UserPreferredLoginName: "preferred ln",
|
||||
UserOrganizationID: gu.Ptr("org-1"),
|
||||
UserDisplayName: gu.Ptr("displayed name"),
|
||||
}
|
||||
got := domainFactorsToGRPC(session)
|
||||
assert.NotNil(t, got)
|
||||
assert.Equal(t, "user-1", got.User.Id)
|
||||
assert.Equal(t, verifiedAtPB, got.User.VerifiedAt)
|
||||
assert.Equal(t, "preferred ln", got.User.LoginName)
|
||||
assert.Equal(t, "org-1", got.User.OrganizationId)
|
||||
assert.Equal(t, "displayed name", got.User.DisplayName)
|
||||
assert.Equal(t, verifiedAtPB, got.User.VerifiedAt)
|
||||
assert.Equal(t, verifiedAtPB, got.Password.VerifiedAt)
|
||||
assert.Equal(t, verifiedAtPB, got.WebAuthN.VerifiedAt)
|
||||
assert.True(t, got.WebAuthN.UserVerified)
|
||||
assert.Equal(t, verifiedAtPB, got.Intent.VerifiedAt)
|
||||
assert.Equal(t, verifiedAtPB, got.Totp.VerifiedAt)
|
||||
assert.Equal(t, verifiedAtPB, got.OtpSms.VerifiedAt)
|
||||
assert.Equal(t, verifiedAtPB, got.OtpEmail.VerifiedAt)
|
||||
assert.Equal(t, verifiedAtPB, got.RecoveryCode.VerifiedAt)
|
||||
})
|
||||
}
|
||||
|
||||
func TestDomainUserAgentToGRPC(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
fpID := "fp-abc"
|
||||
desc := "Chrome on macOS"
|
||||
ip := net.ParseIP("192.168.1.1")
|
||||
ipStr := ip.String()
|
||||
|
||||
t.Run("nil user agent returns nil", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
got := domainUserAgentToGRPC(nil)
|
||||
assert.Nil(t, got)
|
||||
})
|
||||
|
||||
t.Run("empty user agent returns empty proto", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
got := domainUserAgentToGRPC(&domain.SessionUserAgent{})
|
||||
assert.NotNil(t, got)
|
||||
assert.Nil(t, got.FingerprintId)
|
||||
assert.Nil(t, got.Description)
|
||||
assert.Nil(t, got.Ip)
|
||||
assert.Nil(t, got.Header)
|
||||
})
|
||||
|
||||
t.Run("fingerprint ID is mapped", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
got := domainUserAgentToGRPC(&domain.SessionUserAgent{FingerprintID: &fpID})
|
||||
assert.Equal(t, &fpID, got.FingerprintId)
|
||||
})
|
||||
|
||||
t.Run("description is mapped", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
got := domainUserAgentToGRPC(&domain.SessionUserAgent{Description: &desc})
|
||||
assert.Equal(t, &desc, got.Description)
|
||||
})
|
||||
|
||||
t.Run("IP is mapped as string", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
got := domainUserAgentToGRPC(&domain.SessionUserAgent{IP: ip})
|
||||
assert.Equal(t, &ipStr, got.Ip)
|
||||
})
|
||||
|
||||
t.Run("headers are mapped", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
ua := &domain.SessionUserAgent{
|
||||
Header: http.Header{"Accept": []string{"application/json", "text/html"}},
|
||||
}
|
||||
got := domainUserAgentToGRPC(ua)
|
||||
assert.Equal(t, []string{"application/json", "text/html"}, got.Header["Accept"].Values)
|
||||
})
|
||||
}
|
||||
@@ -6,12 +6,37 @@ import (
|
||||
"connectrpc.com/connect"
|
||||
"google.golang.org/protobuf/types/known/timestamppb"
|
||||
|
||||
"github.com/zitadel/zitadel/backend/v3/api/session/v2/convert"
|
||||
"github.com/zitadel/zitadel/backend/v3/domain"
|
||||
"github.com/zitadel/zitadel/backend/v3/storage/database/repository"
|
||||
"github.com/zitadel/zitadel/pkg/grpc/object/v2"
|
||||
"github.com/zitadel/zitadel/pkg/grpc/session/v2"
|
||||
object "github.com/zitadel/zitadel/pkg/grpc/object/v2"
|
||||
session "github.com/zitadel/zitadel/pkg/grpc/session/v2"
|
||||
)
|
||||
|
||||
func ListSessions(ctx context.Context, request *connect.Request[session.ListSessionsRequest]) (*connect.Response[session.ListSessionsResponse], error) {
|
||||
req, err := convert.ListSessionsRequestGRPCToDomain(ctx, request.Msg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
q := domain.NewListSessionsQuery(req)
|
||||
err = domain.Invoke(ctx, q, domain.WithSessionRepo(repository.SessionRepository()))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
sessions := convert.DomainSessionListToGRPCResponse(q.Result())
|
||||
return connect.NewResponse(&session.ListSessionsResponse{
|
||||
Sessions: sessions,
|
||||
Details: &object.ListDetails{
|
||||
TotalResult: uint64(len(sessions)),
|
||||
|
||||
// TODO(IAM-Marco): Put something meaningful once we have a reliable timestamp from DB
|
||||
Timestamp: timestamppb.Now(),
|
||||
},
|
||||
}), nil
|
||||
}
|
||||
|
||||
func DeleteSession(ctx context.Context, request *connect.Request[session.DeleteSessionRequest]) (*connect.Response[session.DeleteSessionResponse], error) {
|
||||
sessionDeleteCmd := domain.NewDeleteSessionCommand(request.Msg.GetSessionId(), request.Msg.GetSessionToken(), true)
|
||||
|
||||
|
||||
@@ -741,6 +741,44 @@ func (c *MockSessionRepositoryListCall) DoAndReturn(f func(context.Context, data
|
||||
return c
|
||||
}
|
||||
|
||||
// LoadUserData mocks base method.
|
||||
func (m *MockSessionRepository) LoadUserData() domain.SessionRepository {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "LoadUserData")
|
||||
ret0, _ := ret[0].(domain.SessionRepository)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// LoadUserData indicates an expected call of LoadUserData.
|
||||
func (mr *MockSessionRepositoryMockRecorder) LoadUserData() *MockSessionRepositoryLoadUserDataCall {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "LoadUserData", reflect.TypeOf((*MockSessionRepository)(nil).LoadUserData))
|
||||
return &MockSessionRepositoryLoadUserDataCall{Call: call}
|
||||
}
|
||||
|
||||
// MockSessionRepositoryLoadUserDataCall wrap *gomock.Call
|
||||
type MockSessionRepositoryLoadUserDataCall struct {
|
||||
*gomock.Call
|
||||
}
|
||||
|
||||
// Return rewrite *gomock.Call.Return
|
||||
func (c *MockSessionRepositoryLoadUserDataCall) Return(arg0 domain.SessionRepository) *MockSessionRepositoryLoadUserDataCall {
|
||||
c.Call = c.Call.Return(arg0)
|
||||
return c
|
||||
}
|
||||
|
||||
// Do rewrite *gomock.Call.Do
|
||||
func (c *MockSessionRepositoryLoadUserDataCall) Do(f func() domain.SessionRepository) *MockSessionRepositoryLoadUserDataCall {
|
||||
c.Call = c.Call.Do(f)
|
||||
return c
|
||||
}
|
||||
|
||||
// DoAndReturn rewrite *gomock.Call.DoAndReturn
|
||||
func (c *MockSessionRepositoryLoadUserDataCall) DoAndReturn(f func() domain.SessionRepository) *MockSessionRepositoryLoadUserDataCall {
|
||||
c.Call = c.Call.DoAndReturn(f)
|
||||
return c
|
||||
}
|
||||
|
||||
// MetadataConditions mocks base method.
|
||||
func (m *MockSessionRepository) MetadataConditions() domain.SessionMetadataConditions {
|
||||
m.ctrl.T.Helper()
|
||||
|
||||
@@ -46,3 +46,7 @@ func (s *SessionRepo) Update(ctx context.Context, client database.QueryExecutor,
|
||||
func (s *SessionRepo) Delete(ctx context.Context, client database.QueryExecutor, condition database.Condition, permissionCondition database.Condition) (int64, time.Time, error) {
|
||||
return s.mock.Delete(ctx, client, condition, permissionCondition)
|
||||
}
|
||||
|
||||
func (s *SessionRepo) LoadUserData() domain.SessionRepository {
|
||||
return s.mock.LoadUserData()
|
||||
}
|
||||
|
||||
@@ -22,6 +22,10 @@ type Session struct {
|
||||
Challenges SessionChallenges `json:"challenges,omitempty"`
|
||||
Metadata []*SessionMetadata `json:"metadata,omitempty"`
|
||||
UserAgent *SessionUserAgent `json:"userAgent,omitempty"`
|
||||
|
||||
UserOrganizationID *string `json:"organizationId,omitempty" db:"organization_id"`
|
||||
UserDisplayName *string `json:"display_name" db:"display_name"`
|
||||
UserPreferredLoginName string
|
||||
}
|
||||
|
||||
//go:generate mockgen -typed -package domainmock -destination ./mock/session.mock.go . SessionRepository
|
||||
@@ -51,6 +55,9 @@ type SessionRepository interface {
|
||||
// In case the session was previously deleted, the method returns 0 and the previous deletion timestamp.
|
||||
// If multiple sessions are deleted, the method returns the number of deleted sessions and no timestamp, as it cannot be determined which session's deletion timestamp to return.
|
||||
Delete(ctx context.Context, client database.QueryExecutor, condition database.Condition, permissionCondition database.Condition) (int64, time.Time, error)
|
||||
// LoadUserData loads user data associated to the given session.
|
||||
// If called, the [Session.UserOrganizationID], [Session.UserDisplayName] and [Session.UserPreferredLoginName] fields will be populated on future calls to [SessionRepository.Get] or [SessionRepository.List].
|
||||
LoadUserData() SessionRepository
|
||||
}
|
||||
|
||||
// sessionColumns define all the columns of the session table.
|
||||
|
||||
@@ -0,0 +1,185 @@
|
||||
package domain
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/zitadel/zitadel/backend/v3/storage/database"
|
||||
"github.com/zitadel/zitadel/internal/api/authz"
|
||||
"github.com/zitadel/zitadel/internal/zerrors"
|
||||
)
|
||||
|
||||
type SessionFilter interface {
|
||||
sessionFilter()
|
||||
}
|
||||
|
||||
// SessionIDsFilter filters sessions by one or more IDs.
|
||||
type SessionIDsFilter struct{ IDs []string }
|
||||
|
||||
func (SessionIDsFilter) sessionFilter() {}
|
||||
|
||||
// SessionUserIDFilter filters sessions by the associated user's ID.
|
||||
type SessionUserIDFilter struct{ UserID string }
|
||||
|
||||
func (SessionUserIDFilter) sessionFilter() {}
|
||||
|
||||
// SessionCreationDateFilter filters sessions by creation date.
|
||||
type SessionCreationDateFilter struct {
|
||||
Op database.NumberOperation
|
||||
Date time.Time
|
||||
}
|
||||
|
||||
func (SessionCreationDateFilter) sessionFilter() {}
|
||||
|
||||
// SessionCreatorFilter filters sessions by the creator's ID.
|
||||
type SessionCreatorFilter struct{ ID string }
|
||||
|
||||
func (SessionCreatorFilter) sessionFilter() {}
|
||||
|
||||
// SessionUserAgentFilter filters sessions by user-agent fingerprint.
|
||||
type SessionUserAgentFilter struct{ FingerprintID string }
|
||||
|
||||
func (SessionUserAgentFilter) sessionFilter() {}
|
||||
|
||||
// SessionExpirationDateFilter filters sessions by expiration date.
|
||||
type SessionExpirationDateFilter struct {
|
||||
Op database.NumberOperation
|
||||
Date time.Time
|
||||
}
|
||||
|
||||
func (SessionExpirationDateFilter) sessionFilter() {}
|
||||
|
||||
// SessionSortColumn determines the column used to sort the result set.
|
||||
type SessionSortColumn int
|
||||
|
||||
const (
|
||||
SessionSortColumnUnspecified SessionSortColumn = iota
|
||||
SessionSortColumnCreationDate
|
||||
)
|
||||
|
||||
// ListSessionsRequest holds all parameters for a ListSessions query.
|
||||
type ListSessionsRequest struct {
|
||||
Filters []SessionFilter
|
||||
SortColumn SessionSortColumn
|
||||
Ascending bool
|
||||
Limit uint32
|
||||
Offset uint32
|
||||
}
|
||||
|
||||
// ListSessionsQuery implements [Querier] for listing sessions.
|
||||
type ListSessionsQuery struct {
|
||||
Request *ListSessionsRequest
|
||||
sessions []*Session
|
||||
}
|
||||
|
||||
// Result implements [Querier].
|
||||
func (l *ListSessionsQuery) Result() []*Session { return l.sessions }
|
||||
|
||||
var _ Querier[[]*Session] = (*ListSessionsQuery)(nil)
|
||||
|
||||
// NewListSessionsQuery returns a new [ListSessionsQuery] for the given request.
|
||||
func NewListSessionsQuery(req *ListSessionsRequest) *ListSessionsQuery {
|
||||
return &ListSessionsQuery{Request: req}
|
||||
}
|
||||
|
||||
// String implements [Querier].
|
||||
func (*ListSessionsQuery) String() string { return "ListSessionsQuery" }
|
||||
|
||||
// Validate implements [Querier].
|
||||
func (l *ListSessionsQuery) Validate(_ context.Context, _ *InvokeOpts) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Execute implements [Querier].
|
||||
func (l *ListSessionsQuery) Execute(ctx context.Context, opts *InvokeOpts) error {
|
||||
sessionRepo := opts.sessionRepo.LoadUserData()
|
||||
|
||||
conds, err := l.Conditions(ctx, sessionRepo)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
l.sessions, err = sessionRepo.List(ctx, opts.DB(),
|
||||
database.WithCondition(conds),
|
||||
l.Sorting(sessionRepo),
|
||||
database.WithLimit(l.Request.Limit),
|
||||
database.WithOffset(l.Request.Offset),
|
||||
)
|
||||
if err != nil {
|
||||
return zerrors.ThrowInternal(err, "DOM-Yx8q2r", "Errors.Session.List")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (l *ListSessionsQuery) permissionCondition(ctx context.Context, sessionRepo SessionRepository) database.Condition {
|
||||
ctxData := authz.GetCtxData(ctx)
|
||||
return database.Or(
|
||||
sessionRepo.UserIDCondition(ctxData.UserID),
|
||||
sessionRepo.UserAgentIDCondition(ctxData.AgentID),
|
||||
sessionRepo.CreatorIDCondition(ctxData.UserID),
|
||||
database.PermissionCheck(SessionReadPermission, false),
|
||||
)
|
||||
}
|
||||
|
||||
func (l *ListSessionsQuery) Conditions(ctx context.Context, sessionRepo SessionRepository) (database.Condition, error) {
|
||||
conds := make([]database.Condition, 2, 2+len(l.Request.Filters))
|
||||
conds[0] = sessionRepo.InstanceIDCondition(authz.GetInstance(ctx).InstanceID())
|
||||
conds[1] = l.permissionCondition(ctx, sessionRepo)
|
||||
|
||||
for _, f := range l.Request.Filters {
|
||||
cond, err := l.sessionFilterToCondition(sessionRepo, f)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
conds = append(conds, cond)
|
||||
}
|
||||
return database.And(conds...), nil
|
||||
}
|
||||
|
||||
func (l *ListSessionsQuery) sessionFilterToCondition(repo SessionRepository, filter SessionFilter) (database.Condition, error) {
|
||||
switch typedFilter := filter.(type) {
|
||||
case SessionIDsFilter:
|
||||
idConds := make([]database.Condition, len(typedFilter.IDs))
|
||||
for i, id := range typedFilter.IDs {
|
||||
idConds[i] = repo.IDCondition(id)
|
||||
}
|
||||
return database.Or(idConds...), nil
|
||||
|
||||
case SessionUserIDFilter:
|
||||
return repo.UserIDCondition(typedFilter.UserID), nil
|
||||
|
||||
case SessionCreationDateFilter:
|
||||
return repo.CreatedAtCondition(typedFilter.Op, typedFilter.Date), nil
|
||||
|
||||
case SessionCreatorFilter:
|
||||
return repo.CreatorIDCondition(typedFilter.ID), nil
|
||||
|
||||
case SessionUserAgentFilter:
|
||||
return repo.UserAgentIDCondition(typedFilter.FingerprintID), nil
|
||||
|
||||
case SessionExpirationDateFilter:
|
||||
// Unlike ES side, we are also including NOT EQUAL. This is an impossible case though since gRPC has no value for it
|
||||
expCond := repo.ExpirationCondition(typedFilter.Op, typedFilter.Date)
|
||||
if typedFilter.Op == database.NumberOperationGreaterThan || typedFilter.Op == database.NumberOperationGreaterThanOrEqual {
|
||||
// Include also nil expirations (i.e. non-expiring sessions)
|
||||
return database.Or(expCond, database.IsNull(repo.ExpirationColumn())), nil
|
||||
}
|
||||
return expCond, nil
|
||||
|
||||
default:
|
||||
return nil, zerrors.ThrowInvalidArgument(NewUnexpectedQueryTypeError(typedFilter), "DOM-Cz5s3t", "List.Query.Invalid")
|
||||
}
|
||||
}
|
||||
|
||||
// Sorting returns the QueryOption for ordering results.
|
||||
func (l *ListSessionsQuery) Sorting(sessionRepo SessionRepository) database.QueryOption {
|
||||
if l.Request.SortColumn != SessionSortColumnCreationDate {
|
||||
return func(*database.QueryOpts) {}
|
||||
}
|
||||
direction := database.OrderDirectionDesc
|
||||
if l.Request.Ascending {
|
||||
direction = database.OrderDirectionAsc
|
||||
}
|
||||
return database.WithOrderBy(direction, sessionRepo.CreatedAtColumn())
|
||||
}
|
||||
@@ -0,0 +1,401 @@
|
||||
package domain_test
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"go.uber.org/mock/gomock"
|
||||
|
||||
"github.com/zitadel/zitadel/backend/v3/domain"
|
||||
domainmock "github.com/zitadel/zitadel/backend/v3/domain/mock"
|
||||
"github.com/zitadel/zitadel/backend/v3/storage/database"
|
||||
"github.com/zitadel/zitadel/backend/v3/storage/database/dbmock"
|
||||
noopdb "github.com/zitadel/zitadel/backend/v3/storage/database/dialect/noop"
|
||||
"github.com/zitadel/zitadel/internal/api/authz"
|
||||
"github.com/zitadel/zitadel/internal/zerrors"
|
||||
)
|
||||
|
||||
func TestListSessionsQuery_Validate(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
// Given
|
||||
q := domain.NewListSessionsQuery(nil)
|
||||
|
||||
// Test
|
||||
err := q.Validate(t.Context(), nil)
|
||||
|
||||
// Verify
|
||||
assert.NoError(t, err)
|
||||
|
||||
}
|
||||
|
||||
func TestListSessionsQuery_Sorting(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
tt := []struct {
|
||||
name string
|
||||
request *domain.ListSessionsRequest
|
||||
expectedSortingDirection database.OrderDirection
|
||||
expectedOrderBy database.Columns
|
||||
}{
|
||||
{
|
||||
name: "sort by creation date DESC",
|
||||
request: &domain.ListSessionsRequest{
|
||||
SortColumn: domain.SessionSortColumnCreationDate,
|
||||
},
|
||||
expectedSortingDirection: database.OrderDirectionDesc,
|
||||
expectedOrderBy: database.Columns{database.NewColumn("sessions", "created_at")},
|
||||
},
|
||||
{
|
||||
name: "sort by creation date ASC",
|
||||
request: &domain.ListSessionsRequest{
|
||||
SortColumn: domain.SessionSortColumnCreationDate,
|
||||
Ascending: true,
|
||||
},
|
||||
expectedSortingDirection: database.OrderDirectionAsc,
|
||||
expectedOrderBy: database.Columns{database.NewColumn("sessions", "created_at")},
|
||||
},
|
||||
{
|
||||
name: "unspecified sorting column — no ordering",
|
||||
request: &domain.ListSessionsRequest{
|
||||
SortColumn: domain.SessionSortColumnUnspecified,
|
||||
},
|
||||
expectedSortingDirection: database.OrderDirectionAsc,
|
||||
expectedOrderBy: nil,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tt {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
// Given
|
||||
ctrl := gomock.NewController(t)
|
||||
mockRepo := domainmock.NewSessionRepo(ctrl)
|
||||
|
||||
q := domain.NewListSessionsQuery(tc.request)
|
||||
opts := &database.QueryOpts{}
|
||||
|
||||
// Test
|
||||
queryOpt := q.Sorting(mockRepo)
|
||||
queryOpt(opts)
|
||||
|
||||
// Verify
|
||||
assert.Equal(t, tc.expectedOrderBy, opts.OrderBy)
|
||||
assert.Equal(t, tc.expectedSortingDirection, opts.Ordering)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestListSessionsQuery_Conditions(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
const (
|
||||
userID = "user-1"
|
||||
agentID = "agent-fp-1"
|
||||
)
|
||||
|
||||
now := time.Now()
|
||||
|
||||
defaultConds := func(repo *domainmock.SessionRepo) []database.Condition {
|
||||
return []database.Condition{
|
||||
repo.InstanceIDCondition("inst-1"),
|
||||
database.Or(
|
||||
repo.UserIDCondition(userID),
|
||||
repo.UserAgentIDCondition(agentID),
|
||||
repo.CreatorIDCondition(userID),
|
||||
database.PermissionCheck(domain.SessionReadPermission, false),
|
||||
),
|
||||
}
|
||||
}
|
||||
appendToDefaultConditions := func(repo *domainmock.SessionRepo, conds database.Condition) database.Condition {
|
||||
return database.And(
|
||||
append(defaultConds(repo), conds)...,
|
||||
)
|
||||
}
|
||||
|
||||
tt := []struct {
|
||||
name string
|
||||
request *domain.ListSessionsRequest
|
||||
expectedCond func(repo *domainmock.SessionRepo) database.Condition
|
||||
expectedError error
|
||||
}{
|
||||
{
|
||||
name: "no filters returns permission and instance id conditions",
|
||||
request: &domain.ListSessionsRequest{},
|
||||
expectedCond: func(repo *domainmock.SessionRepo) database.Condition {
|
||||
return database.And(defaultConds(repo)...)
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "SessionIDsFilter empty returns empty OR",
|
||||
request: &domain.ListSessionsRequest{
|
||||
Filters: []domain.SessionFilter{
|
||||
domain.SessionIDsFilter{IDs: []string{}},
|
||||
},
|
||||
},
|
||||
expectedCond: func(repo *domainmock.SessionRepo) database.Condition {
|
||||
return appendToDefaultConditions(repo, database.Or())
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "SessionIDsFilter with multiple IDs returns OR",
|
||||
request: &domain.ListSessionsRequest{
|
||||
Filters: []domain.SessionFilter{
|
||||
domain.SessionIDsFilter{IDs: []string{"s-1", "s-2"}},
|
||||
},
|
||||
},
|
||||
expectedCond: func(repo *domainmock.SessionRepo) database.Condition {
|
||||
return appendToDefaultConditions(repo, database.Or(repo.IDCondition("s-1"), repo.IDCondition("s-2")))
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "SessionUserIDFilter",
|
||||
request: &domain.ListSessionsRequest{
|
||||
Filters: []domain.SessionFilter{
|
||||
domain.SessionUserIDFilter{UserID: "target-user"},
|
||||
},
|
||||
},
|
||||
expectedCond: func(repo *domainmock.SessionRepo) database.Condition {
|
||||
return database.And(
|
||||
appendToDefaultConditions(repo, repo.UserIDCondition("target-user")),
|
||||
)
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "SessionCreationDateFilter",
|
||||
request: &domain.ListSessionsRequest{
|
||||
Filters: []domain.SessionFilter{
|
||||
domain.SessionCreationDateFilter{Op: database.NumberOperationGreaterThan, Date: now},
|
||||
},
|
||||
},
|
||||
expectedCond: func(repo *domainmock.SessionRepo) database.Condition {
|
||||
return database.And(
|
||||
appendToDefaultConditions(repo, repo.CreatedAtCondition(database.NumberOperationGreaterThan, now)),
|
||||
)
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "SessionCreatorFilter",
|
||||
request: &domain.ListSessionsRequest{
|
||||
Filters: []domain.SessionFilter{
|
||||
domain.SessionCreatorFilter{ID: "other-creator"},
|
||||
},
|
||||
},
|
||||
expectedCond: func(repo *domainmock.SessionRepo) database.Condition {
|
||||
return database.And(
|
||||
appendToDefaultConditions(repo, repo.CreatorIDCondition("other-creator")),
|
||||
)
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "SessionUserAgentFilter",
|
||||
request: &domain.ListSessionsRequest{
|
||||
Filters: []domain.SessionFilter{
|
||||
domain.SessionUserAgentFilter{FingerprintID: "explicit-fp"},
|
||||
},
|
||||
},
|
||||
expectedCond: func(repo *domainmock.SessionRepo) database.Condition {
|
||||
return database.And(
|
||||
appendToDefaultConditions(repo, repo.UserAgentIDCondition("explicit-fp")),
|
||||
)
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "SessionExpirationDateFilter with GREATER_THAN includes null expiration",
|
||||
request: &domain.ListSessionsRequest{
|
||||
Filters: []domain.SessionFilter{
|
||||
domain.SessionExpirationDateFilter{Op: database.NumberOperationGreaterThan, Date: now},
|
||||
},
|
||||
},
|
||||
expectedCond: func(repo *domainmock.SessionRepo) database.Condition {
|
||||
return database.And(
|
||||
appendToDefaultConditions(repo,
|
||||
database.Or(repo.ExpirationCondition(database.NumberOperationGreaterThan, now), database.IsNull(repo.ExpirationColumn())),
|
||||
),
|
||||
)
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "SessionExpirationDateFilter with GREATER_OR_EQUALS includes null expiration",
|
||||
request: &domain.ListSessionsRequest{
|
||||
Filters: []domain.SessionFilter{
|
||||
domain.SessionExpirationDateFilter{Op: database.NumberOperationGreaterThanOrEqual, Date: now},
|
||||
},
|
||||
},
|
||||
expectedCond: func(repo *domainmock.SessionRepo) database.Condition {
|
||||
return database.And(
|
||||
appendToDefaultConditions(repo,
|
||||
database.Or(repo.ExpirationCondition(database.NumberOperationGreaterThanOrEqual, now), database.IsNull(repo.ExpirationColumn())),
|
||||
),
|
||||
)
|
||||
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "SessionExpirationDateFilter with LESS_THAN does not include null expiration",
|
||||
request: &domain.ListSessionsRequest{
|
||||
Filters: []domain.SessionFilter{
|
||||
domain.SessionExpirationDateFilter{Op: database.NumberOperationLessThan, Date: now},
|
||||
},
|
||||
},
|
||||
expectedCond: func(repo *domainmock.SessionRepo) database.Condition {
|
||||
return database.And(
|
||||
appendToDefaultConditions(repo, repo.ExpirationCondition(database.NumberOperationLessThan, now)),
|
||||
)
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "SessionExpirationDateFilter with LESS_OR_EQUALS does not include null expiration",
|
||||
request: &domain.ListSessionsRequest{
|
||||
Filters: []domain.SessionFilter{
|
||||
domain.SessionExpirationDateFilter{Op: database.NumberOperationLessThanOrEqual, Date: now},
|
||||
},
|
||||
},
|
||||
expectedCond: func(repo *domainmock.SessionRepo) database.Condition {
|
||||
return database.And(
|
||||
appendToDefaultConditions(repo, repo.ExpirationCondition(database.NumberOperationLessThanOrEqual, now)),
|
||||
)
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "multiple filters combined with AND",
|
||||
request: &domain.ListSessionsRequest{
|
||||
Filters: []domain.SessionFilter{
|
||||
domain.SessionUserIDFilter{UserID: "target-user"},
|
||||
domain.SessionCreatorFilter{ID: "creator-1"},
|
||||
},
|
||||
},
|
||||
expectedCond: func(repo *domainmock.SessionRepo) database.Condition {
|
||||
return database.And(
|
||||
appendToDefaultConditions(repo,
|
||||
database.And(repo.UserIDCondition("target-user"), repo.CreatorIDCondition("creator-1")),
|
||||
),
|
||||
)
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tt {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
ctx := authz.NewMockContextWithAgent("inst-1", "org-1", userID, agentID)
|
||||
ctrl := gomock.NewController(t)
|
||||
sessionRepo := domainmock.NewSessionRepo(ctrl)
|
||||
|
||||
q := domain.NewListSessionsQuery(tc.request)
|
||||
cond, err := q.Conditions(ctx, sessionRepo)
|
||||
|
||||
assert.ErrorIs(t, err, tc.expectedError)
|
||||
expected := tc.expectedCond(sessionRepo)
|
||||
assert.Equal(t, expected.String(), cond.String())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestListSessionsQuery_Execute(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
const (
|
||||
instanceID = "inst-1"
|
||||
orgID = "org-1"
|
||||
userID = "user-1"
|
||||
agentID = "agent-fp-1"
|
||||
)
|
||||
|
||||
listErr := errors.New("list mock error")
|
||||
|
||||
tt := []struct {
|
||||
name string
|
||||
request *domain.ListSessionsRequest
|
||||
setupMock func(sessionRepo *domainmock.SessionRepo)
|
||||
expectedSessions []*domain.Session
|
||||
expectedError error
|
||||
}{
|
||||
{
|
||||
name: "when List fails should return internal error",
|
||||
request: &domain.ListSessionsRequest{},
|
||||
setupMock: func(sessionRepo *domainmock.SessionRepo) {
|
||||
instanceCond := sessionRepo.InstanceIDCondition(instanceID)
|
||||
permCond := database.Or(
|
||||
sessionRepo.UserIDCondition(userID),
|
||||
sessionRepo.UserAgentIDCondition(agentID),
|
||||
sessionRepo.CreatorIDCondition(userID),
|
||||
database.PermissionCheck(domain.SessionReadPermission, false),
|
||||
)
|
||||
sessionRepo.EXPECT().
|
||||
LoadUserData().
|
||||
Times(1).
|
||||
Return(sessionRepo)
|
||||
sessionRepo.EXPECT().
|
||||
List(gomock.Any(), gomock.Any(),
|
||||
dbmock.QueryOptions(database.WithCondition(database.And(instanceCond, permCond))),
|
||||
dbmock.QueryOptions(func(*database.QueryOpts) {}),
|
||||
dbmock.QueryOptions(database.WithLimit(0)),
|
||||
dbmock.QueryOptions(database.WithOffset(0)),
|
||||
).
|
||||
Times(1).
|
||||
Return(nil, listErr)
|
||||
},
|
||||
expectedError: zerrors.ThrowInternal(listErr, "DOM-Yx8q2r", "Errors.Session.List"),
|
||||
},
|
||||
{
|
||||
name: "when List succeeds should return sessions",
|
||||
request: &domain.ListSessionsRequest{
|
||||
SortColumn: domain.SessionSortColumnCreationDate,
|
||||
Ascending: true,
|
||||
Limit: 10,
|
||||
Offset: 5,
|
||||
},
|
||||
setupMock: func(sessionRepo *domainmock.SessionRepo) {
|
||||
instanceCond := sessionRepo.InstanceIDCondition(instanceID)
|
||||
permCond := database.Or(
|
||||
sessionRepo.UserIDCondition(userID),
|
||||
sessionRepo.UserAgentIDCondition(agentID),
|
||||
sessionRepo.CreatorIDCondition(userID),
|
||||
database.PermissionCheck(domain.SessionReadPermission, false),
|
||||
)
|
||||
sessionRepo.EXPECT().
|
||||
LoadUserData().
|
||||
Times(1).
|
||||
Return(sessionRepo)
|
||||
sessionRepo.EXPECT().
|
||||
List(gomock.Any(), gomock.Any(),
|
||||
dbmock.QueryOptions(database.WithCondition(database.And(instanceCond, permCond))),
|
||||
dbmock.QueryOptions(database.WithOrderBy(database.OrderDirectionAsc, sessionRepo.CreatedAtColumn())),
|
||||
dbmock.QueryOptions(database.WithLimit(10)),
|
||||
dbmock.QueryOptions(database.WithOffset(5)),
|
||||
).
|
||||
Times(1).
|
||||
Return([]*domain.Session{{ID: "session-1", UserID: userID}}, nil)
|
||||
},
|
||||
expectedSessions: []*domain.Session{{ID: "session-1", UserID: userID}},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tt {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
ctx := authz.NewMockContextWithAgent(instanceID, orgID, userID, agentID)
|
||||
ctrl := gomock.NewController(t)
|
||||
sessionRepo := domainmock.NewSessionRepo(ctrl)
|
||||
|
||||
if tc.setupMock != nil {
|
||||
tc.setupMock(sessionRepo)
|
||||
}
|
||||
|
||||
opts := domain.DefaultOpts(nil)
|
||||
domain.WithQueryExecutor(new(noopdb.Pool))(opts)
|
||||
domain.WithSessionRepo(sessionRepo)(opts)
|
||||
|
||||
q := domain.NewListSessionsQuery(tc.request)
|
||||
err := q.Execute(ctx, opts)
|
||||
|
||||
assert.ErrorIs(t, err, tc.expectedError)
|
||||
assert.ElementsMatch(t, tc.expectedSessions, q.Result())
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -33,11 +33,17 @@ BEGIN
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
|
||||
CREATE TRIGGER set_expiration
|
||||
BEFORE INSERT OR UPDATE OF lifetime ON zitadel.sessions
|
||||
CREATE TRIGGER set_expiration_on_update
|
||||
BEFORE UPDATE OF lifetime ON zitadel.sessions
|
||||
FOR EACH ROW
|
||||
EXECUTE FUNCTION zitadel.update_expiration();
|
||||
|
||||
CREATE TRIGGER set_expiration_on_insert
|
||||
BEFORE INSERT ON zitadel.sessions
|
||||
FOR EACH ROW
|
||||
WHEN (NEW.lifetime <> '0'::interval)
|
||||
EXECUTE FUNCTION zitadel.update_expiration();
|
||||
|
||||
CREATE TYPE zitadel.session_factor_type AS ENUM (
|
||||
'user',
|
||||
'password',
|
||||
|
||||
@@ -19,9 +19,13 @@ import (
|
||||
var _ domain.SessionRepository = (*session)(nil)
|
||||
|
||||
type session struct {
|
||||
factorRepo sessionFactor
|
||||
metadataRepo sessionMetadata
|
||||
userAgentRepo sessionUserAgent
|
||||
factorRepo sessionFactor
|
||||
metadataRepo sessionMetadata
|
||||
userAgentRepo sessionUserAgent
|
||||
userRepo user
|
||||
loginNamesRepo userLoginName
|
||||
// shouldLoadUser loads a part of the user data (display name, organization ID and login names)
|
||||
shouldLoadUser bool
|
||||
}
|
||||
|
||||
func (s session) qualifiedTableName() string {
|
||||
@@ -78,6 +82,9 @@ SELECT
|
||||
, 'headers', session_user_agents.headers
|
||||
)
|
||||
END as user_agent
|
||||
, users.organization_id
|
||||
, jsonb_agg(DISTINCT jsonb_build_object('loginName', login_names.login_name, 'isPreferred', login_names.is_preferred)) FILTER (WHERE login_names.user_id IS NOT NULL) AS login_names
|
||||
, users.display_name
|
||||
FROM zitadel.sessions`
|
||||
|
||||
// Get implements [domain.SessionRepository].
|
||||
@@ -86,6 +93,8 @@ func (s session) Get(ctx context.Context, client database.QueryExecutor, opts ..
|
||||
s.joinFactors(),
|
||||
s.joinMetadata(),
|
||||
s.joinUserAgent(),
|
||||
s.joinUser(),
|
||||
s.joinLoginNames(),
|
||||
database.WithGroupBy(
|
||||
s.InstanceIDColumn(),
|
||||
s.IDColumn(),
|
||||
@@ -93,6 +102,8 @@ func (s session) Get(ctx context.Context, client database.QueryExecutor, opts ..
|
||||
s.userAgentRepo.descriptionColumn(),
|
||||
s.userAgentRepo.ipColumn(),
|
||||
s.userAgentRepo.headersColumn(),
|
||||
s.userRepo.organizationIDColumn(),
|
||||
s.userRepo.Human().DisplayNameColumn(),
|
||||
),
|
||||
)
|
||||
|
||||
@@ -118,6 +129,8 @@ func (s session) List(ctx context.Context, client database.QueryExecutor, opts .
|
||||
s.joinFactors(),
|
||||
s.joinMetadata(),
|
||||
s.joinUserAgent(),
|
||||
s.joinUser(),
|
||||
s.joinLoginNames(),
|
||||
database.WithGroupBy(
|
||||
s.InstanceIDColumn(),
|
||||
s.IDColumn(),
|
||||
@@ -125,6 +138,8 @@ func (s session) List(ctx context.Context, client database.QueryExecutor, opts .
|
||||
s.userAgentRepo.descriptionColumn(),
|
||||
s.userAgentRepo.ipColumn(),
|
||||
s.userAgentRepo.headersColumn(),
|
||||
s.userRepo.organizationIDColumn(),
|
||||
s.userRepo.Human().DisplayNameColumn(),
|
||||
),
|
||||
)
|
||||
|
||||
@@ -265,6 +280,11 @@ func (s session) Delete(ctx context.Context, client database.QueryExecutor, cond
|
||||
return deletedSessions, deletedAt.V, nil
|
||||
}
|
||||
|
||||
// LoadUserData implements [domain.SessionRepository].
|
||||
func (s session) LoadUserData() domain.SessionRepository {
|
||||
return &session{shouldLoadUser: true}
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------
|
||||
// changes
|
||||
// -------------------------------------------------------------
|
||||
@@ -588,6 +608,7 @@ type rawSession struct {
|
||||
CreatorID *string `json:"creatorID" db:"creator_id"`
|
||||
Factors JSONArray[*rawFactor] `json:"factors,omitempty" db:"factors"`
|
||||
Metadata JSONArray[*domain.SessionMetadata] `json:"metadata,omitempty" db:"metadata"`
|
||||
LoginNames JSONArray[*domain.LoginName] `json:"login_names,omitempty" db:"login_names"`
|
||||
}
|
||||
|
||||
func scanSession(ctx context.Context, querier database.Querier, builder *database.StatementBuilder) (*domain.Session, error) {
|
||||
@@ -632,6 +653,13 @@ func rawSessionToDomain(raw *rawSession) (*domain.Session, error) {
|
||||
raw.Session.UserID = gu.Value(raw.UserID)
|
||||
raw.Session.CreatorID = gu.Value(raw.CreatorID)
|
||||
|
||||
for _, ln := range raw.LoginNames {
|
||||
if ln.IsPreferred {
|
||||
raw.UserPreferredLoginName = ln.LoginName
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
for _, factor := range raw.Factors {
|
||||
f, ch, err := factor.ToDomain()
|
||||
if err != nil {
|
||||
@@ -687,3 +715,33 @@ func (s session) joinMetadata() database.QueryOption {
|
||||
database.And(columns...),
|
||||
)
|
||||
}
|
||||
|
||||
func (s session) joinUser() database.QueryOption {
|
||||
columns := make([]database.Condition, 2, 3)
|
||||
columns[0] = database.NewColumnCondition(s.InstanceIDColumn(), s.userRepo.InstanceIDColumn())
|
||||
columns[1] = database.NewColumnCondition(s.UserIDColumn(), s.userRepo.IDColumn())
|
||||
|
||||
if !s.shouldLoadUser {
|
||||
columns = append(columns, database.IsNull(s.userRepo.IDColumn()))
|
||||
}
|
||||
|
||||
return database.WithLeftJoin(
|
||||
s.userRepo.qualifiedTableName(),
|
||||
database.And(columns...),
|
||||
)
|
||||
}
|
||||
|
||||
func (s session) joinLoginNames() database.QueryOption {
|
||||
columns := make([]database.Condition, 2, 3)
|
||||
columns[0] = database.NewColumnCondition(s.InstanceIDColumn(), s.loginNamesRepo.instanceIDColumn())
|
||||
columns[1] = database.NewColumnCondition(s.UserIDColumn(), s.loginNamesRepo.userIDColumn())
|
||||
|
||||
if !s.shouldLoadUser {
|
||||
columns = append(columns, database.IsNull(s.loginNamesRepo.loginNameColumn()))
|
||||
}
|
||||
|
||||
return database.WithLeftJoin(
|
||||
s.loginNamesRepo.qualifiedTableName(),
|
||||
database.And(columns...),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ import (
|
||||
"github.com/muhlemmer/gu"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"golang.org/x/text/language"
|
||||
|
||||
"github.com/zitadel/zitadel/backend/v3/domain"
|
||||
"github.com/zitadel/zitadel/backend/v3/storage/database"
|
||||
@@ -195,7 +196,11 @@ func TestSession_Create(t *testing.T) {
|
||||
assert.Equal(t, tt.session.ID, createdSession.ID)
|
||||
assert.Equal(t, tt.session.InstanceID, createdSession.InstanceID)
|
||||
assert.Equal(t, tt.session.Lifetime, createdSession.Lifetime)
|
||||
assert.Equal(t, createdSession.UpdatedAt.Add(tt.session.Lifetime), createdSession.Expiration)
|
||||
if tt.session.Lifetime != 0 {
|
||||
assert.Equal(t, createdSession.UpdatedAt.Add(tt.session.Lifetime), createdSession.Expiration)
|
||||
} else {
|
||||
assert.Zero(t, createdSession.Expiration)
|
||||
}
|
||||
assert.Equal(t, tt.session.CreatorID, createdSession.CreatorID)
|
||||
assert.Equal(t, tt.session.UserAgent, createdSession.UserAgent)
|
||||
assert.WithinRange(t, createdSession.CreatedAt, beforeCreate.Add(-time.Second), afterCreate.Add(time.Second))
|
||||
@@ -1159,9 +1164,12 @@ func TestSession_Get(t *testing.T) {
|
||||
t.Logf("error during rollback: %v", err)
|
||||
}
|
||||
}()
|
||||
|
||||
instanceRepo := repository.InstanceRepository()
|
||||
sessionRepo := repository.SessionRepository()
|
||||
sessionRepo := repository.SessionRepository().LoadUserData()
|
||||
orgRepo := repository.OrganizationRepository()
|
||||
orgDomainRepo := repository.OrganizationDomainRepository()
|
||||
userRepo := repository.UserRepository()
|
||||
settingsRepo := repository.DomainSettingsRepository()
|
||||
|
||||
// create instance
|
||||
instanceId := gofakeit.Name()
|
||||
@@ -1177,6 +1185,62 @@ func TestSession_Get(t *testing.T) {
|
||||
err = instanceRepo.Create(t.Context(), tx, &instance)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Create Organization
|
||||
orgID := gofakeit.UUID()
|
||||
org := domain.Organization{
|
||||
ID: orgID,
|
||||
Name: gofakeit.Name(),
|
||||
InstanceID: instanceId,
|
||||
State: domain.OrgStateActive,
|
||||
}
|
||||
err = orgRepo.Create(t.Context(), tx, &org)
|
||||
require.NoError(t, err)
|
||||
|
||||
domainName := gofakeit.DomainName()
|
||||
err = orgDomainRepo.Add(t.Context(), tx, &domain.AddOrganizationDomain{
|
||||
InstanceID: instanceId,
|
||||
OrgID: orgID,
|
||||
Domain: domainName,
|
||||
IsVerified: true,
|
||||
IsPrimary: true,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
err = settingsRepo.Set(t.Context(), tx, &domain.DomainSettings{
|
||||
Settings: domain.Settings{
|
||||
InstanceID: instanceId,
|
||||
OrganizationID: &orgID,
|
||||
Type: domain.SettingTypeDomain,
|
||||
State: domain.SettingStateActive,
|
||||
},
|
||||
DomainSettingsAttributes: domain.DomainSettingsAttributes{
|
||||
LoginNameIncludesDomain: gu.Ptr(false),
|
||||
},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
preferredLoginName := gofakeit.Name()
|
||||
userID := gofakeit.UUID()
|
||||
displayName := gofakeit.Username()
|
||||
user := domain.User{
|
||||
InstanceID: instanceId,
|
||||
OrganizationID: orgID,
|
||||
ID: userID,
|
||||
Username: preferredLoginName,
|
||||
State: domain.UserStateActive,
|
||||
Human: &domain.HumanUser{
|
||||
FirstName: gofakeit.Name(),
|
||||
LastName: gofakeit.Name(),
|
||||
Nickname: gofakeit.Username(),
|
||||
DisplayName: displayName,
|
||||
PreferredLanguage: language.English,
|
||||
Gender: domain.HumanGenderMale,
|
||||
},
|
||||
}
|
||||
|
||||
err = userRepo.Create(t.Context(), tx, &user)
|
||||
require.NoError(t, err)
|
||||
|
||||
session := &domain.Session{
|
||||
InstanceID: instanceId,
|
||||
ID: gofakeit.Name(),
|
||||
@@ -1185,6 +1249,11 @@ func TestSession_Get(t *testing.T) {
|
||||
}
|
||||
err = sessionRepo.Create(t.Context(), tx, session)
|
||||
require.NoError(t, err)
|
||||
_, err = sessionRepo.Update(t.Context(), tx, sessionRepo.PrimaryKeyCondition(instanceId, session.ID), sessionRepo.SetFactor(&domain.SessionFactorUser{
|
||||
UserID: userID,
|
||||
LastVerifiedAt: time.Now(),
|
||||
}))
|
||||
require.NoError(t, err)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
@@ -1222,6 +1291,11 @@ func TestSession_Get(t *testing.T) {
|
||||
assert.Equal(t, session.InstanceID, got.InstanceID)
|
||||
assert.Equal(t, session.Lifetime, got.Lifetime)
|
||||
assert.Equal(t, session.CreatorID, got.CreatorID)
|
||||
require.NotNil(t, got.UserOrganizationID)
|
||||
assert.Equal(t, orgID, *got.UserOrganizationID)
|
||||
assert.Equal(t, preferredLoginName, got.UserPreferredLoginName)
|
||||
require.NotNil(t, got.UserDisplayName)
|
||||
assert.Equal(t, displayName, *got.UserDisplayName)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -195,6 +195,13 @@ func (u user) unqualifiedTableName() string {
|
||||
return "users"
|
||||
}
|
||||
|
||||
func (u user) qualifiedTableName() string {
|
||||
if u.tableName != "" {
|
||||
return "zitadel." + u.tableName
|
||||
}
|
||||
return "zitadel.users"
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------
|
||||
// changes
|
||||
// -------------------------------------------------------------
|
||||
|
||||
@@ -15,9 +15,12 @@ import (
|
||||
"google.golang.org/protobuf/types/known/timestamppb"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/integration"
|
||||
featurepb "github.com/zitadel/zitadel/pkg/grpc/feature/v2"
|
||||
instancepb "github.com/zitadel/zitadel/pkg/grpc/instance/v2"
|
||||
objpb "github.com/zitadel/zitadel/pkg/grpc/object"
|
||||
"github.com/zitadel/zitadel/pkg/grpc/object/v2"
|
||||
"github.com/zitadel/zitadel/pkg/grpc/session/v2"
|
||||
"github.com/zitadel/zitadel/pkg/grpc/user/v2"
|
||||
)
|
||||
|
||||
func TestServer_GetSession(t *testing.T) {
|
||||
@@ -247,73 +250,65 @@ type sessionAttr struct {
|
||||
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
|
||||
// listSessionsEnv holds the environment context for ListSessions tests,
|
||||
// allowing the same test cases to run against both the eventstore and relational backends.
|
||||
type listSessionsEnv struct {
|
||||
ownerCtx context.Context // IAMOwner-level context; can list all sessions
|
||||
loginCtx context.Context // login-user context; used for session creation in deps
|
||||
noPermCtx context.Context // no-permission context; used to verify access restrictions
|
||||
client session.SessionServiceClient
|
||||
loginUserID string
|
||||
testUser *user.AddHumanUserResponse
|
||||
newUser func(ctx context.Context) *user.AddHumanUserResponse
|
||||
// isRelational controls behavior that currently differs between the eventstore
|
||||
// and relational backends (e.g. sequence field, permission-check semantics).
|
||||
isRelational bool
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
// runListSessionsTestCases defines and runs the full ListSessions test suite against env.
|
||||
// This is called by both [TestServer_ListSessions] (eventstore) and [TestServer_ListSessions_Relational].
|
||||
func runListSessionsTestCases(t *testing.T, env listSessionsEnv) {
|
||||
t.Helper()
|
||||
|
||||
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,
|
||||
},
|
||||
},
|
||||
// newSess creates a session on env.client via the eventstore model (grpc).
|
||||
newSess := func(t *testing.T, ctx context.Context, userID, 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 := env.client.CreateSession(ctx, req)
|
||||
require.NoError(t, err)
|
||||
return &sessionAttr{
|
||||
ID: resp.GetSessionId(),
|
||||
UserID: userID,
|
||||
UserAgent: userAgent,
|
||||
CreationDate: resp.GetDetails().GetChangeDate(),
|
||||
ChangeDate: resp.GetDetails().GetChangeDate(),
|
||||
Details: resp.GetDetails(),
|
||||
}
|
||||
}
|
||||
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) {
|
||||
t.Parallel()
|
||||
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
|
||||
ctx context.Context
|
||||
req *session.ListSessionsRequest
|
||||
dep func(t *testing.T, req *session.ListSessionsRequest) []*sessionAttr
|
||||
want *session.ListSessionsResponse
|
||||
wantFactors []wantFactor
|
||||
wantExpirationWindow time.Duration
|
||||
@@ -321,83 +316,73 @@ func TestServer_ListSessions(t *testing.T) {
|
||||
}{
|
||||
{
|
||||
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{}
|
||||
ctx: env.ownerCtx,
|
||||
req: &session.ListSessionsRequest{
|
||||
Queries: []*session.SearchQuery{
|
||||
{Query: &session.SearchQuery_IdsQuery{IdsQuery: &session.IDsQuery{Ids: []string{"unknown"}}}},
|
||||
},
|
||||
},
|
||||
dep: func(t *testing.T, req *session.ListSessionsRequest) []*sessionAttr {
|
||||
return []*sessionAttr{}
|
||||
},
|
||||
want: &session.ListSessionsResponse{
|
||||
Details: &object.ListDetails{
|
||||
TotalResult: 0,
|
||||
Timestamp: timestamppb.Now(),
|
||||
},
|
||||
Details: &object.ListDetails{TotalResult: 0, Timestamp: timestamppb.Now()},
|
||||
Sessions: []*session.Session{},
|
||||
},
|
||||
},
|
||||
{
|
||||
// In the eventstore path, the session exists (TotalResult=1) but the permission
|
||||
// callback hides its content (Sessions=[]). In the relational path the permission
|
||||
// check is not yet implemented (returns SQL true), so the session is fully visible.
|
||||
// TODO(IAM-Marco): align expectations once relational permission checks are implemented.
|
||||
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{}
|
||||
},
|
||||
ctx: env.noPermCtx,
|
||||
req: &session.ListSessionsRequest{Queries: []*session.SearchQuery{}},
|
||||
dep: func(t *testing.T, req *session.ListSessionsRequest) []*sessionAttr {
|
||||
info := newSess(t, env.loginCtx, "", "", nil, nil)
|
||||
req.Queries = append(req.Queries, &session.SearchQuery{Query: &session.SearchQuery_IdsQuery{IdsQuery: &session.IDsQuery{Ids: []string{info.ID}}}})
|
||||
if env.isRelational {
|
||||
return []*sessionAttr{info}
|
||||
}
|
||||
return []*sessionAttr{}
|
||||
},
|
||||
want: &session.ListSessionsResponse{
|
||||
Details: &object.ListDetails{
|
||||
TotalResult: 1,
|
||||
Timestamp: timestamppb.Now(),
|
||||
},
|
||||
Sessions: []*session.Session{},
|
||||
Details: &object.ListDetails{TotalResult: 1, Timestamp: timestamppb.Now()},
|
||||
Sessions: func() []*session.Session {
|
||||
if env.isRelational {
|
||||
return []*session.Session{{}}
|
||||
}
|
||||
return []*session.Session{}
|
||||
}(),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "list sessions, permission, ok",
|
||||
args: args{
|
||||
IAMOwnerCTX,
|
||||
&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}
|
||||
},
|
||||
ctx: env.ownerCtx,
|
||||
req: &session.ListSessionsRequest{},
|
||||
dep: func(t *testing.T, req *session.ListSessionsRequest) []*sessionAttr {
|
||||
info := newSess(t, env.loginCtx, "", "", nil, nil)
|
||||
req.Queries = append(req.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(),
|
||||
},
|
||||
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}
|
||||
},
|
||||
ctx: env.ownerCtx,
|
||||
req: &session.ListSessionsRequest{},
|
||||
dep: func(t *testing.T, req *session.ListSessionsRequest) []*sessionAttr {
|
||||
info := newSess(t, env.loginCtx, env.testUser.GetUserId(), "agent", durationpb.New(time.Minute*5), map[string][]byte{"key": []byte("value")})
|
||||
req.Queries = append(req.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(),
|
||||
},
|
||||
Details: &object.ListDetails{TotalResult: 1, Timestamp: timestamppb.Now()},
|
||||
Sessions: []*session.Session{
|
||||
{
|
||||
Metadata: map[string][]byte{"key": []byte("value")},
|
||||
@@ -405,9 +390,7 @@ func TestServer_ListSessions(t *testing.T) {
|
||||
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"}},
|
||||
},
|
||||
Header: map[string]*session.UserAgent_HeaderValues{"foo": {Values: []string{"foo", "bar"}}},
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -415,22 +398,22 @@ func TestServer_ListSessions(t *testing.T) {
|
||||
},
|
||||
{
|
||||
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
|
||||
},
|
||||
ctx: env.ownerCtx,
|
||||
req: &session.ListSessionsRequest{},
|
||||
dep: func(t *testing.T, req *session.ListSessionsRequest) []*sessionAttr {
|
||||
infos := make([]*sessionAttr, 3)
|
||||
ids := make([]string, 3)
|
||||
for i := range infos {
|
||||
infos[i] = newSess(t, env.loginCtx, env.testUser.GetUserId(), "agent", durationpb.New(time.Minute*5), map[string][]byte{"key": []byte("value")})
|
||||
ids[i] = infos[i].ID
|
||||
}
|
||||
req.Queries = append(req.Queries, &session.SearchQuery{Query: &session.SearchQuery_IdsQuery{IdsQuery: &session.IDsQuery{Ids: ids}}})
|
||||
return infos
|
||||
},
|
||||
wantExpirationWindow: time.Minute * 5,
|
||||
wantFactors: []wantFactor{wantUserFactor},
|
||||
want: &session.ListSessionsResponse{
|
||||
Details: &object.ListDetails{
|
||||
TotalResult: 3,
|
||||
Timestamp: timestamppb.Now(),
|
||||
},
|
||||
Details: &object.ListDetails{TotalResult: 3, Timestamp: timestamppb.Now()},
|
||||
Sessions: []*session.Session{
|
||||
{
|
||||
Metadata: map[string][]byte{"key": []byte("value")},
|
||||
@@ -438,9 +421,7 @@ func TestServer_ListSessions(t *testing.T) {
|
||||
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"}},
|
||||
},
|
||||
Header: map[string]*session.UserAgent_HeaderValues{"foo": {Values: []string{"foo", "bar"}}},
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -449,9 +430,7 @@ func TestServer_ListSessions(t *testing.T) {
|
||||
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"}},
|
||||
},
|
||||
Header: map[string]*session.UserAgent_HeaderValues{"foo": {Values: []string{"foo", "bar"}}},
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -460,9 +439,7 @@ func TestServer_ListSessions(t *testing.T) {
|
||||
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"}},
|
||||
},
|
||||
Header: map[string]*session.UserAgent_HeaderValues{"foo": {Values: []string{"foo", "bar"}}},
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -470,23 +447,18 @@ func TestServer_ListSessions(t *testing.T) {
|
||||
},
|
||||
{
|
||||
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}
|
||||
},
|
||||
ctx: env.ownerCtx,
|
||||
req: &session.ListSessionsRequest{},
|
||||
dep: func(t *testing.T, req *session.ListSessionsRequest) []*sessionAttr {
|
||||
createdUser := env.newUser(env.loginCtx)
|
||||
info := newSess(t, env.loginCtx, createdUser.GetUserId(), "agent", durationpb.New(time.Minute*5), map[string][]byte{"key": []byte("value")})
|
||||
req.Queries = append(req.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(),
|
||||
},
|
||||
Details: &object.ListDetails{TotalResult: 1, Timestamp: timestamppb.Now()},
|
||||
Sessions: []*session.Session{
|
||||
{
|
||||
Metadata: map[string][]byte{"key": []byte("value")},
|
||||
@@ -494,9 +466,7 @@ func TestServer_ListSessions(t *testing.T) {
|
||||
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"}},
|
||||
},
|
||||
Header: map[string]*session.UserAgent_HeaderValues{"foo": {Values: []string{"foo", "bar"}}},
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -504,24 +474,19 @@ func TestServer_ListSessions(t *testing.T) {
|
||||
},
|
||||
{
|
||||
name: "list sessions, own creator, ok",
|
||||
args: args{
|
||||
LoginCTX,
|
||||
&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_CreatorQuery{CreatorQuery: &session.CreatorQuery{}}})
|
||||
return []*sessionAttr{info}
|
||||
},
|
||||
ctx: env.loginCtx,
|
||||
req: &session.ListSessionsRequest{},
|
||||
dep: func(t *testing.T, req *session.ListSessionsRequest) []*sessionAttr {
|
||||
info := newSess(t, env.loginCtx, env.testUser.GetUserId(), "agent", durationpb.New(time.Minute*5), map[string][]byte{"key": []byte("value")})
|
||||
req.Queries = append(req.Queries,
|
||||
&session.SearchQuery{Query: &session.SearchQuery_IdsQuery{IdsQuery: &session.IDsQuery{Ids: []string{info.ID}}}},
|
||||
&session.SearchQuery{Query: &session.SearchQuery_CreatorQuery{CreatorQuery: &session.CreatorQuery{}}})
|
||||
return []*sessionAttr{info}
|
||||
},
|
||||
wantExpirationWindow: time.Minute * 5,
|
||||
wantFactors: []wantFactor{wantUserFactor},
|
||||
want: &session.ListSessionsResponse{
|
||||
Details: &object.ListDetails{
|
||||
TotalResult: 1,
|
||||
Timestamp: timestamppb.Now(),
|
||||
},
|
||||
Details: &object.ListDetails{TotalResult: 1, Timestamp: timestamppb.Now()},
|
||||
Sessions: []*session.Session{
|
||||
{
|
||||
Metadata: map[string][]byte{"key": []byte("value")},
|
||||
@@ -529,9 +494,7 @@ func TestServer_ListSessions(t *testing.T) {
|
||||
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"}},
|
||||
},
|
||||
Header: map[string]*session.UserAgent_HeaderValues{"foo": {Values: []string{"foo", "bar"}}},
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -539,24 +502,19 @@ func TestServer_ListSessions(t *testing.T) {
|
||||
},
|
||||
{
|
||||
name: "list sessions, creator, ok",
|
||||
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_CreatorQuery{CreatorQuery: &session.CreatorQuery{Id: gu.Ptr(Instance.Users.Get(integration.UserTypeLogin).ID)}}})
|
||||
return []*sessionAttr{info}
|
||||
},
|
||||
ctx: env.ownerCtx,
|
||||
req: &session.ListSessionsRequest{},
|
||||
dep: func(t *testing.T, req *session.ListSessionsRequest) []*sessionAttr {
|
||||
info := newSess(t, env.loginCtx, env.testUser.GetUserId(), "agent", durationpb.New(time.Minute*5), map[string][]byte{"key": []byte("value")})
|
||||
req.Queries = append(req.Queries,
|
||||
&session.SearchQuery{Query: &session.SearchQuery_IdsQuery{IdsQuery: &session.IDsQuery{Ids: []string{info.ID}}}},
|
||||
&session.SearchQuery{Query: &session.SearchQuery_CreatorQuery{CreatorQuery: &session.CreatorQuery{Id: gu.Ptr(env.loginUserID)}}})
|
||||
return []*sessionAttr{info}
|
||||
},
|
||||
wantExpirationWindow: time.Minute * 5,
|
||||
wantFactors: []wantFactor{wantUserFactor},
|
||||
want: &session.ListSessionsResponse{
|
||||
Details: &object.ListDetails{
|
||||
TotalResult: 1,
|
||||
Timestamp: timestamppb.Now(),
|
||||
},
|
||||
Details: &object.ListDetails{TotalResult: 1, Timestamp: timestamppb.Now()},
|
||||
Sessions: []*session.Session{
|
||||
{
|
||||
Metadata: map[string][]byte{"key": []byte("value")},
|
||||
@@ -564,72 +522,56 @@ func TestServer_ListSessions(t *testing.T) {
|
||||
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"}},
|
||||
},
|
||||
Header: map[string]*session.UserAgent_HeaderValues{"foo": {Values: []string{"foo", "bar"}}},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
// CreatorQuery with nil ID means "filter by caller's userID" (ownerCtx user).
|
||||
// Session was created by loginCtx user, so ownerUser != creator → no match.
|
||||
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_CreatorQuery{CreatorQuery: &session.CreatorQuery{}}})
|
||||
return []*sessionAttr{}
|
||||
},
|
||||
ctx: env.ownerCtx,
|
||||
req: &session.ListSessionsRequest{},
|
||||
dep: func(t *testing.T, req *session.ListSessionsRequest) []*sessionAttr {
|
||||
info := newSess(t, env.loginCtx, env.testUser.GetUserId(), "agent", durationpb.New(time.Minute*5), map[string][]byte{"key": []byte("value")})
|
||||
req.Queries = append(req.Queries,
|
||||
&session.SearchQuery{Query: &session.SearchQuery_IdsQuery{IdsQuery: &session.IDsQuery{Ids: []string{info.ID}}}},
|
||||
&session.SearchQuery{Query: &session.SearchQuery_CreatorQuery{CreatorQuery: &session.CreatorQuery{}}})
|
||||
return []*sessionAttr{}
|
||||
},
|
||||
wantExpirationWindow: time.Minute * 5,
|
||||
wantFactors: []wantFactor{wantUserFactor},
|
||||
want: &session.ListSessionsResponse{
|
||||
Details: &object.ListDetails{
|
||||
TotalResult: 0,
|
||||
Timestamp: timestamppb.Now(),
|
||||
},
|
||||
Details: &object.ListDetails{TotalResult: 0, Timestamp: timestamppb.Now()},
|
||||
Sessions: []*session.Session{},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "list sessions, empty creator",
|
||||
args: args{
|
||||
IAMOwnerCTX,
|
||||
&session.ListSessionsRequest{},
|
||||
func(ctx context.Context, t *testing.T, request *session.ListSessionsRequest) []*sessionAttr {
|
||||
request.Queries = append(request.Queries,
|
||||
&session.SearchQuery{Query: &session.SearchQuery_CreatorQuery{CreatorQuery: &session.CreatorQuery{Id: gu.Ptr("")}}})
|
||||
return []*sessionAttr{}
|
||||
},
|
||||
ctx: env.ownerCtx,
|
||||
req: &session.ListSessionsRequest{},
|
||||
dep: func(t *testing.T, req *session.ListSessionsRequest) []*sessionAttr {
|
||||
req.Queries = append(req.Queries,
|
||||
&session.SearchQuery{Query: &session.SearchQuery_CreatorQuery{CreatorQuery: &session.CreatorQuery{Id: gu.Ptr("")}}})
|
||||
return []*sessionAttr{}
|
||||
},
|
||||
wantExpirationWindow: time.Minute * 5,
|
||||
wantFactors: []wantFactor{wantUserFactor},
|
||||
wantErr: true,
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "list sessions, useragent, ok",
|
||||
args: args{
|
||||
IAMOwnerCTX,
|
||||
&session.ListSessionsRequest{},
|
||||
func(ctx context.Context, t *testing.T, request *session.ListSessionsRequest) []*sessionAttr {
|
||||
info := createSession(ctx, t, User.GetUserId(), "useragent", 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_UserAgentQuery{UserAgentQuery: &session.UserAgentQuery{FingerprintId: gu.Ptr("useragent")}}})
|
||||
return []*sessionAttr{info}
|
||||
},
|
||||
ctx: env.ownerCtx,
|
||||
req: &session.ListSessionsRequest{},
|
||||
dep: func(t *testing.T, req *session.ListSessionsRequest) []*sessionAttr {
|
||||
info := newSess(t, env.loginCtx, env.testUser.GetUserId(), "useragent", durationpb.New(time.Minute*5), map[string][]byte{"key": []byte("value")})
|
||||
req.Queries = append(req.Queries,
|
||||
&session.SearchQuery{Query: &session.SearchQuery_IdsQuery{IdsQuery: &session.IDsQuery{Ids: []string{info.ID}}}},
|
||||
&session.SearchQuery{Query: &session.SearchQuery_UserAgentQuery{UserAgentQuery: &session.UserAgentQuery{FingerprintId: gu.Ptr("useragent")}}})
|
||||
return []*sessionAttr{info}
|
||||
},
|
||||
wantExpirationWindow: time.Minute * 5,
|
||||
wantFactors: []wantFactor{wantUserFactor},
|
||||
want: &session.ListSessionsResponse{
|
||||
Details: &object.ListDetails{
|
||||
TotalResult: 1,
|
||||
Timestamp: timestamppb.Now(),
|
||||
},
|
||||
Details: &object.ListDetails{TotalResult: 1, Timestamp: timestamppb.Now()},
|
||||
Sessions: []*session.Session{
|
||||
{
|
||||
Metadata: map[string][]byte{"key": []byte("value")},
|
||||
@@ -637,9 +579,7 @@ func TestServer_ListSessions(t *testing.T) {
|
||||
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"}},
|
||||
},
|
||||
Header: map[string]*session.UserAgent_HeaderValues{"foo": {Values: []string{"foo", "bar"}}},
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -647,65 +587,51 @@ func TestServer_ListSessions(t *testing.T) {
|
||||
},
|
||||
{
|
||||
name: "list sessions, wrong useragent",
|
||||
args: args{
|
||||
IAMOwnerCTX,
|
||||
&session.ListSessionsRequest{},
|
||||
func(ctx context.Context, t *testing.T, request *session.ListSessionsRequest) []*sessionAttr {
|
||||
info := createSession(ctx, t, User.GetUserId(), "useragent", 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_UserAgentQuery{UserAgentQuery: &session.UserAgentQuery{FingerprintId: gu.Ptr("wronguseragent")}}})
|
||||
return []*sessionAttr{}
|
||||
},
|
||||
ctx: env.ownerCtx,
|
||||
req: &session.ListSessionsRequest{},
|
||||
dep: func(t *testing.T, req *session.ListSessionsRequest) []*sessionAttr {
|
||||
info := newSess(t, env.loginCtx, env.testUser.GetUserId(), "useragent", durationpb.New(time.Minute*5), map[string][]byte{"key": []byte("value")})
|
||||
req.Queries = append(req.Queries,
|
||||
&session.SearchQuery{Query: &session.SearchQuery_IdsQuery{IdsQuery: &session.IDsQuery{Ids: []string{info.ID}}}},
|
||||
&session.SearchQuery{Query: &session.SearchQuery_UserAgentQuery{UserAgentQuery: &session.UserAgentQuery{FingerprintId: gu.Ptr("wronguseragent")}}})
|
||||
return []*sessionAttr{}
|
||||
},
|
||||
wantExpirationWindow: time.Minute * 5,
|
||||
wantFactors: []wantFactor{wantUserFactor},
|
||||
want: &session.ListSessionsResponse{
|
||||
Details: &object.ListDetails{
|
||||
TotalResult: 0,
|
||||
Timestamp: timestamppb.Now(),
|
||||
},
|
||||
Details: &object.ListDetails{TotalResult: 0, Timestamp: timestamppb.Now()},
|
||||
Sessions: []*session.Session{},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "list sessions, empty useragent",
|
||||
args: args{
|
||||
IAMOwnerCTX,
|
||||
&session.ListSessionsRequest{},
|
||||
func(ctx context.Context, t *testing.T, request *session.ListSessionsRequest) []*sessionAttr {
|
||||
request.Queries = append(request.Queries,
|
||||
&session.SearchQuery{Query: &session.SearchQuery_UserAgentQuery{UserAgentQuery: &session.UserAgentQuery{FingerprintId: gu.Ptr("")}}})
|
||||
return []*sessionAttr{}
|
||||
},
|
||||
ctx: env.ownerCtx,
|
||||
req: &session.ListSessionsRequest{},
|
||||
dep: func(t *testing.T, req *session.ListSessionsRequest) []*sessionAttr {
|
||||
req.Queries = append(req.Queries,
|
||||
&session.SearchQuery{Query: &session.SearchQuery_UserAgentQuery{UserAgentQuery: &session.UserAgentQuery{FingerprintId: gu.Ptr("")}}})
|
||||
return []*sessionAttr{}
|
||||
},
|
||||
wantExpirationWindow: time.Minute * 5,
|
||||
wantFactors: []wantFactor{wantUserFactor},
|
||||
wantErr: true,
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "list sessions, expiration date query, ok",
|
||||
args: args{
|
||||
IAMOwnerCTX,
|
||||
&session.ListSessionsRequest{},
|
||||
func(ctx context.Context, t *testing.T, request *session.ListSessionsRequest) []*sessionAttr {
|
||||
info := createSession(ctx, t, User.GetUserId(), "useragent", 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_ExpirationDateQuery{
|
||||
ExpirationDateQuery: &session.ExpirationDateQuery{ExpirationDate: timestamppb.Now(),
|
||||
Method: objpb.TimestampQueryMethod_TIMESTAMP_QUERY_METHOD_GREATER_OR_EQUALS,
|
||||
}}})
|
||||
return []*sessionAttr{info}
|
||||
},
|
||||
ctx: env.ownerCtx,
|
||||
req: &session.ListSessionsRequest{},
|
||||
dep: func(t *testing.T, req *session.ListSessionsRequest) []*sessionAttr {
|
||||
info := newSess(t, env.loginCtx, env.testUser.GetUserId(), "useragent", durationpb.New(time.Minute*5), map[string][]byte{"key": []byte("value")})
|
||||
req.Queries = append(req.Queries,
|
||||
&session.SearchQuery{Query: &session.SearchQuery_IdsQuery{IdsQuery: &session.IDsQuery{Ids: []string{info.ID}}}},
|
||||
&session.SearchQuery{Query: &session.SearchQuery_ExpirationDateQuery{
|
||||
ExpirationDateQuery: &session.ExpirationDateQuery{
|
||||
ExpirationDate: timestamppb.Now(),
|
||||
Method: objpb.TimestampQueryMethod_TIMESTAMP_QUERY_METHOD_GREATER_OR_EQUALS,
|
||||
},
|
||||
}})
|
||||
return []*sessionAttr{info}
|
||||
},
|
||||
wantExpirationWindow: time.Minute * 5,
|
||||
wantFactors: []wantFactor{wantUserFactor},
|
||||
want: &session.ListSessionsResponse{
|
||||
Details: &object.ListDetails{
|
||||
TotalResult: 1,
|
||||
Timestamp: timestamppb.Now(),
|
||||
},
|
||||
Details: &object.ListDetails{TotalResult: 1, Timestamp: timestamppb.Now()},
|
||||
Sessions: []*session.Session{
|
||||
{
|
||||
Metadata: map[string][]byte{"key": []byte("value")},
|
||||
@@ -713,23 +639,22 @@ func TestServer_ListSessions(t *testing.T) {
|
||||
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"}},
|
||||
},
|
||||
Header: map[string]*session.UserAgent_HeaderValues{"foo": {Values: []string{"foo", "bar"}}},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
infos := tt.args.dep(LoginCTX, t, tt.args.req)
|
||||
infos := tt.dep(t, tt.req)
|
||||
|
||||
retryDuration, tick := integration.WaitForAndTickWithMaxDuration(tt.args.ctx, time.Minute)
|
||||
retryDuration, tick := integration.WaitForAndTickWithMaxDuration(tt.ctx, 30*time.Second)
|
||||
require.EventuallyWithT(t, func(ttt *assert.CollectT) {
|
||||
got, err := Client.ListSessions(tt.args.ctx, tt.args.req)
|
||||
got, err := env.client.ListSessions(tt.ctx, tt.req)
|
||||
if tt.wantErr {
|
||||
assert.Error(ttt, err)
|
||||
return
|
||||
@@ -745,15 +670,18 @@ func TestServer_ListSessions(t *testing.T) {
|
||||
|
||||
for i := range infos {
|
||||
tt.want.Sessions[i].Id = infos[i].ID
|
||||
tt.want.Sessions[i].Sequence = infos[i].Details.GetSequence()
|
||||
if env.isRelational {
|
||||
tt.want.Sessions[i].Sequence = 0
|
||||
} else {
|
||||
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()
|
||||
|
||||
// only check for contents of the session, not sorting for now
|
||||
found := false
|
||||
for _, session := range got.Sessions {
|
||||
if session.Id == infos[i].ID {
|
||||
verifySession(ttt, session, tt.want.Sessions[i], tt.wantExpirationWindow, infos[i].UserID, tt.wantFactors...)
|
||||
for _, s := range got.Sessions {
|
||||
if s.Id == infos[i].ID {
|
||||
verifySession(ttt, s, tt.want.Sessions[i], tt.wantExpirationWindow, infos[i].UserID, tt.wantFactors...)
|
||||
found = true
|
||||
}
|
||||
}
|
||||
@@ -766,6 +694,19 @@ func TestServer_ListSessions(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestServer_ListSessions(t *testing.T) {
|
||||
t.Parallel()
|
||||
runListSessionsTestCases(t, listSessionsEnv{
|
||||
ownerCtx: IAMOwnerCTX,
|
||||
loginCtx: LoginCTX,
|
||||
noPermCtx: UserCTX,
|
||||
client: Client,
|
||||
loginUserID: Instance.Users.Get(integration.UserTypeLogin).ID,
|
||||
testUser: User,
|
||||
newUser: func(ctx context.Context) *user.AddHumanUserResponse { return Instance.CreateHumanUser(ctx) },
|
||||
})
|
||||
}
|
||||
|
||||
func TestServer_ListSessions_with_expiration_date_filter(t *testing.T) {
|
||||
t.Parallel()
|
||||
// session with no expiration
|
||||
@@ -823,3 +764,38 @@ func TestServer_ListSessions_with_expiration_date_filter(t *testing.T) {
|
||||
require.Len(t, listSessionsResponse2.Sessions, 1)
|
||||
assert.Equal(t, session2.SessionId, listSessionsResponse2.Sessions[0].Id)
|
||||
}
|
||||
|
||||
func TestServer_ListSessions_Relational(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Minute)
|
||||
defer cancel()
|
||||
|
||||
sysAuthZ := integration.WithSystemAuthorization(ctx)
|
||||
|
||||
inst := integration.NewInstance(sysAuthZ)
|
||||
integration.EnsureInstanceFeature(t, sysAuthZ, inst,
|
||||
&featurepb.SetInstanceFeaturesRequest{EnableRelationalTables: gu.Ptr(true)},
|
||||
func(tCollect *assert.CollectT, got *featurepb.GetInstanceFeaturesResponse) {
|
||||
assert.True(tCollect, got.GetEnableRelationalTables().GetEnabled())
|
||||
},
|
||||
)
|
||||
t.Cleanup(func() {
|
||||
inst.Client.InstanceV2.DeleteInstance(sysAuthZ, &instancepb.DeleteInstanceRequest{InstanceId: inst.ID()})
|
||||
})
|
||||
|
||||
instOwnerCtx := inst.WithAuthorizationToken(context.Background(), integration.UserTypeIAMOwner)
|
||||
loginCtx := inst.WithAuthorizationToken(context.Background(), integration.UserTypeLogin)
|
||||
noPermCtx := inst.WithAuthorizationToken(context.Background(), integration.UserTypeNoPermission)
|
||||
|
||||
runListSessionsTestCases(t, listSessionsEnv{
|
||||
ownerCtx: instOwnerCtx,
|
||||
loginCtx: loginCtx,
|
||||
noPermCtx: noPermCtx,
|
||||
client: inst.Client.SessionV2,
|
||||
loginUserID: inst.Users.Get(integration.UserTypeLogin).ID,
|
||||
testUser: inst.CreateHumanUser(instOwnerCtx),
|
||||
newUser: func(ctx context.Context) *user.AddHumanUserResponse { return inst.CreateHumanUser(ctx) },
|
||||
isRelational: true,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
"github.com/muhlemmer/gu"
|
||||
"google.golang.org/protobuf/types/known/timestamppb"
|
||||
|
||||
sessionv2 "github.com/zitadel/zitadel/backend/v3/api/session/v2"
|
||||
"github.com/zitadel/zitadel/internal/api/authz"
|
||||
"github.com/zitadel/zitadel/internal/api/grpc/object/v2"
|
||||
"github.com/zitadel/zitadel/internal/domain"
|
||||
@@ -38,6 +39,9 @@ func (s *Server) GetSession(ctx context.Context, req *connect.Request[session.Ge
|
||||
}
|
||||
|
||||
func (s *Server) ListSessions(ctx context.Context, req *connect.Request[session.ListSessionsRequest]) (*connect.Response[session.ListSessionsResponse], error) {
|
||||
if authz.GetFeatures(ctx).EnableRelationalTables {
|
||||
return sessionv2.ListSessions(ctx, req)
|
||||
}
|
||||
queries, err := listSessionsRequestToQuery(ctx, req.Msg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
||||
Reference in New Issue
Block a user