fix(session v2): allow searching for own sessions or user agent (fingerprintID) (#9110)

# Which Problems Are Solved

ListSessions only works to list the sessions that you are the creator
of.

# How the Problems Are Solved

Add options to search for sessions created by other users, sessions
belonging to the same useragent and sessions belonging to your user.
Possible through additional search parameters which as default use the
information contained in your session token but can also be filled with
specific IDs.

# Additional Changes

Remodel integration tests, to separate the Create and Get of sessions
correctly.

# Additional Context

Closes #8301

---------

Co-authored-by: Livio Spring <livio.a@gmail.com>
This commit is contained in:
Stefan Benz
2025-01-14 14:15:59 +01:00
committed by GitHub
parent e2a2e13d44
commit 84997ffe1a
25 changed files with 2398 additions and 702 deletions

View File

@@ -7,6 +7,11 @@ func NewMockContext(instanceID, orgID, userID string) context.Context {
return context.WithValue(ctx, instanceKey, &instance{id: instanceID})
}
func NewMockContextWithAgent(instanceID, orgID, userID, agentID string) context.Context {
ctx := context.WithValue(context.Background(), dataKey, CtxData{UserID: userID, OrgID: orgID, AgentID: agentID})
return context.WithValue(ctx, instanceKey, &instance{id: instanceID})
}
func NewMockContextWithPermissions(instanceID, orgID, userID string, permissions []string) context.Context {
ctx := context.WithValue(context.Background(), dataKey, CtxData{UserID: userID, OrgID: orgID})
ctx = context.WithValue(ctx, instanceKey, &instance{id: instanceID})

View File

@@ -0,0 +1,714 @@
//go:build integration
package session_test
import (
"context"
"testing"
"time"
"github.com/golang/protobuf/ptypes/timestamp"
"github.com/muhlemmer/gu"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"google.golang.org/protobuf/types/known/durationpb"
"google.golang.org/protobuf/types/known/timestamppb"
"github.com/zitadel/zitadel/internal/integration"
"github.com/zitadel/zitadel/pkg/grpc/object/v2"
"github.com/zitadel/zitadel/pkg/grpc/session/v2"
)
func TestServer_GetSession(t *testing.T) {
type args struct {
ctx context.Context
req *session.GetSessionRequest
dep func(ctx context.Context, t *testing.T, request *session.GetSessionRequest) uint64
}
tests := []struct {
name string
args args
want *session.GetSessionResponse
wantFactors []wantFactor
wantExpirationWindow time.Duration
wantErr bool
}{
{
name: "get session, no id provided",
args: args{
CTX,
&session.GetSessionRequest{
SessionId: "",
},
nil,
},
wantErr: true,
},
{
name: "get session, not found",
args: args{
CTX,
&session.GetSessionRequest{
SessionId: "unknown",
},
nil,
},
wantErr: true,
},
{
name: "get session, no permission",
args: args{
UserCTX,
&session.GetSessionRequest{},
func(ctx context.Context, t *testing.T, request *session.GetSessionRequest) uint64 {
resp, err := Client.CreateSession(ctx, &session.CreateSessionRequest{})
require.NoError(t, err)
request.SessionId = resp.SessionId
return resp.GetDetails().GetSequence()
},
},
wantErr: true,
},
{
name: "get session, permission, ok",
args: args{
CTX,
&session.GetSessionRequest{},
func(ctx context.Context, t *testing.T, request *session.GetSessionRequest) uint64 {
resp, err := Client.CreateSession(ctx, &session.CreateSessionRequest{})
require.NoError(t, err)
request.SessionId = resp.SessionId
return resp.GetDetails().GetSequence()
},
},
want: &session.GetSessionResponse{
Session: &session.Session{},
},
},
{
name: "get session, token, ok",
args: args{
UserCTX,
&session.GetSessionRequest{},
func(ctx context.Context, t *testing.T, request *session.GetSessionRequest) uint64 {
resp, err := Client.CreateSession(ctx, &session.CreateSessionRequest{})
require.NoError(t, err)
request.SessionId = resp.SessionId
request.SessionToken = gu.Ptr(resp.SessionToken)
return resp.GetDetails().GetSequence()
},
},
want: &session.GetSessionResponse{
Session: &session.Session{},
},
},
{
name: "get session, user agent, ok",
args: args{
UserCTX,
&session.GetSessionRequest{},
func(ctx context.Context, t *testing.T, request *session.GetSessionRequest) uint64 {
resp, err := Client.CreateSession(ctx, &session.CreateSessionRequest{
UserAgent: &session.UserAgent{
FingerprintId: gu.Ptr("fingerPrintID"),
Ip: gu.Ptr("1.2.3.4"),
Description: gu.Ptr("Description"),
Header: map[string]*session.UserAgent_HeaderValues{
"foo": {Values: []string{"foo", "bar"}},
},
},
},
)
require.NoError(t, err)
request.SessionId = resp.SessionId
request.SessionToken = gu.Ptr(resp.SessionToken)
return resp.GetDetails().GetSequence()
},
},
want: &session.GetSessionResponse{
Session: &session.Session{
UserAgent: &session.UserAgent{
FingerprintId: gu.Ptr("fingerPrintID"),
Ip: gu.Ptr("1.2.3.4"),
Description: gu.Ptr("Description"),
Header: map[string]*session.UserAgent_HeaderValues{
"foo": {Values: []string{"foo", "bar"}},
},
},
},
},
},
{
name: "get session, lifetime, ok",
args: args{
UserCTX,
&session.GetSessionRequest{},
func(ctx context.Context, t *testing.T, request *session.GetSessionRequest) uint64 {
resp, err := Client.CreateSession(ctx, &session.CreateSessionRequest{
Lifetime: durationpb.New(5 * time.Minute),
},
)
require.NoError(t, err)
request.SessionId = resp.SessionId
request.SessionToken = gu.Ptr(resp.SessionToken)
return resp.GetDetails().GetSequence()
},
},
wantExpirationWindow: 5 * time.Minute,
want: &session.GetSessionResponse{
Session: &session.Session{},
},
},
{
name: "get session, metadata, ok",
args: args{
UserCTX,
&session.GetSessionRequest{},
func(ctx context.Context, t *testing.T, request *session.GetSessionRequest) uint64 {
resp, err := Client.CreateSession(ctx, &session.CreateSessionRequest{
Metadata: map[string][]byte{"foo": []byte("bar")},
},
)
require.NoError(t, err)
request.SessionId = resp.SessionId
request.SessionToken = gu.Ptr(resp.SessionToken)
return resp.GetDetails().GetSequence()
},
},
want: &session.GetSessionResponse{
Session: &session.Session{
Metadata: map[string][]byte{"foo": []byte("bar")},
},
},
},
{
name: "get session, user, ok",
args: args{
UserCTX,
&session.GetSessionRequest{},
func(ctx context.Context, t *testing.T, request *session.GetSessionRequest) uint64 {
resp, err := Client.CreateSession(ctx, &session.CreateSessionRequest{
Checks: &session.Checks{
User: &session.CheckUser{
Search: &session.CheckUser_UserId{
UserId: User.GetUserId(),
},
},
},
},
)
require.NoError(t, err)
request.SessionId = resp.SessionId
request.SessionToken = gu.Ptr(resp.SessionToken)
return resp.GetDetails().GetSequence()
},
},
wantFactors: []wantFactor{wantUserFactor},
want: &session.GetSessionResponse{
Session: &session.Session{},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
var sequence uint64
if tt.args.dep != nil {
sequence = tt.args.dep(CTX, t, tt.args.req)
}
retryDuration, tick := integration.WaitForAndTickWithMaxDuration(tt.args.ctx, time.Minute)
require.EventuallyWithT(t, func(ttt *assert.CollectT) {
got, err := Client.GetSession(tt.args.ctx, tt.args.req)
if tt.wantErr {
assert.Error(ttt, err)
return
}
if !assert.NoError(ttt, err) {
return
}
tt.want.Session.Id = tt.args.req.SessionId
tt.want.Session.Sequence = sequence
verifySession(ttt, got.GetSession(), tt.want.GetSession(), time.Minute, tt.wantExpirationWindow, User.GetUserId(), tt.wantFactors...)
}, retryDuration, tick)
})
}
}
type sessionAttr struct {
ID string
UserID string
UserAgent string
CreationDate *timestamp.Timestamp
ChangeDate *timestamppb.Timestamp
Details *object.Details
}
type sessionAttrs []*sessionAttr
func (u sessionAttrs) ids() []string {
ids := make([]string, len(u))
for i := range u {
ids[i] = u[i].ID
}
return ids
}
func createSessions(ctx context.Context, t *testing.T, count int, userID string, userAgent string, lifetime *durationpb.Duration, metadata map[string][]byte) sessionAttrs {
infos := make([]*sessionAttr, count)
for i := 0; i < count; i++ {
infos[i] = createSession(ctx, t, userID, userAgent, lifetime, metadata)
}
return infos
}
func createSession(ctx context.Context, t *testing.T, userID string, userAgent string, lifetime *durationpb.Duration, metadata map[string][]byte) *sessionAttr {
req := &session.CreateSessionRequest{}
if userID != "" {
req.Checks = &session.Checks{
User: &session.CheckUser{
Search: &session.CheckUser_UserId{
UserId: userID,
},
},
}
}
if userAgent != "" {
req.UserAgent = &session.UserAgent{
FingerprintId: gu.Ptr(userAgent),
Ip: gu.Ptr("1.2.3.4"),
Description: gu.Ptr("Description"),
Header: map[string]*session.UserAgent_HeaderValues{
"foo": {Values: []string{"foo", "bar"}},
},
}
}
if lifetime != nil {
req.Lifetime = lifetime
}
if metadata != nil {
req.Metadata = metadata
}
resp, err := Client.CreateSession(ctx, req)
require.NoError(t, err)
return &sessionAttr{
resp.GetSessionId(),
userID,
userAgent,
resp.GetDetails().GetChangeDate(),
resp.GetDetails().GetChangeDate(),
resp.GetDetails(),
}
}
func TestServer_ListSessions(t *testing.T) {
type args struct {
ctx context.Context
req *session.ListSessionsRequest
dep func(ctx context.Context, t *testing.T, request *session.ListSessionsRequest) []*sessionAttr
}
tests := []struct {
name string
args args
want *session.ListSessionsResponse
wantFactors []wantFactor
wantExpirationWindow time.Duration
wantErr bool
}{
{
name: "list sessions, not found",
args: args{
CTX,
&session.ListSessionsRequest{
Queries: []*session.SearchQuery{
{Query: &session.SearchQuery_IdsQuery{IdsQuery: &session.IDsQuery{Ids: []string{"unknown"}}}},
},
},
func(ctx context.Context, t *testing.T, request *session.ListSessionsRequest) []*sessionAttr {
return []*sessionAttr{}
},
},
want: &session.ListSessionsResponse{
Details: &object.ListDetails{
TotalResult: 0,
Timestamp: timestamppb.Now(),
},
Sessions: []*session.Session{},
},
},
{
name: "list sessions, no permission",
args: args{
UserCTX,
&session.ListSessionsRequest{
Queries: []*session.SearchQuery{},
},
func(ctx context.Context, t *testing.T, request *session.ListSessionsRequest) []*sessionAttr {
info := createSession(ctx, t, "", "", nil, nil)
request.Queries = append(request.Queries, &session.SearchQuery{Query: &session.SearchQuery_IdsQuery{IdsQuery: &session.IDsQuery{Ids: []string{info.ID}}}})
return []*sessionAttr{}
},
},
want: &session.ListSessionsResponse{
Details: &object.ListDetails{
TotalResult: 1,
Timestamp: timestamppb.Now(),
},
Sessions: []*session.Session{},
},
},
{
name: "list sessions, permission, ok",
args: args{
CTX,
&session.ListSessionsRequest{},
func(ctx context.Context, t *testing.T, request *session.ListSessionsRequest) []*sessionAttr {
info := createSession(ctx, t, "", "", nil, nil)
request.Queries = append(request.Queries, &session.SearchQuery{Query: &session.SearchQuery_IdsQuery{IdsQuery: &session.IDsQuery{Ids: []string{info.ID}}}})
return []*sessionAttr{info}
},
},
want: &session.ListSessionsResponse{
Details: &object.ListDetails{
TotalResult: 1,
Timestamp: timestamppb.Now(),
},
Sessions: []*session.Session{{}},
},
},
{
name: "list sessions, full, ok",
args: args{
CTX,
&session.ListSessionsRequest{},
func(ctx context.Context, t *testing.T, request *session.ListSessionsRequest) []*sessionAttr {
info := createSession(ctx, t, User.GetUserId(), "agent", durationpb.New(time.Minute*5), map[string][]byte{"key": []byte("value")})
request.Queries = append(request.Queries, &session.SearchQuery{Query: &session.SearchQuery_IdsQuery{IdsQuery: &session.IDsQuery{Ids: []string{info.ID}}}})
return []*sessionAttr{info}
},
},
wantExpirationWindow: time.Minute * 5,
wantFactors: []wantFactor{wantUserFactor},
want: &session.ListSessionsResponse{
Details: &object.ListDetails{
TotalResult: 1,
Timestamp: timestamppb.Now(),
},
Sessions: []*session.Session{
{
Metadata: map[string][]byte{"key": []byte("value")},
UserAgent: &session.UserAgent{
FingerprintId: gu.Ptr("agent"),
Ip: gu.Ptr("1.2.3.4"),
Description: gu.Ptr("Description"),
Header: map[string]*session.UserAgent_HeaderValues{
"foo": {Values: []string{"foo", "bar"}},
},
},
},
},
},
},
{
name: "list sessions, multiple, ok",
args: args{
CTX,
&session.ListSessionsRequest{},
func(ctx context.Context, t *testing.T, request *session.ListSessionsRequest) []*sessionAttr {
infos := createSessions(ctx, t, 3, User.GetUserId(), "agent", durationpb.New(time.Minute*5), map[string][]byte{"key": []byte("value")})
request.Queries = append(request.Queries, &session.SearchQuery{Query: &session.SearchQuery_IdsQuery{IdsQuery: &session.IDsQuery{Ids: infos.ids()}}})
return infos
},
},
wantExpirationWindow: time.Minute * 5,
wantFactors: []wantFactor{wantUserFactor},
want: &session.ListSessionsResponse{
Details: &object.ListDetails{
TotalResult: 3,
Timestamp: timestamppb.Now(),
},
Sessions: []*session.Session{
{
Metadata: map[string][]byte{"key": []byte("value")},
UserAgent: &session.UserAgent{
FingerprintId: gu.Ptr("agent"),
Ip: gu.Ptr("1.2.3.4"),
Description: gu.Ptr("Description"),
Header: map[string]*session.UserAgent_HeaderValues{
"foo": {Values: []string{"foo", "bar"}},
},
},
},
{
Metadata: map[string][]byte{"key": []byte("value")},
UserAgent: &session.UserAgent{
FingerprintId: gu.Ptr("agent"),
Ip: gu.Ptr("1.2.3.4"),
Description: gu.Ptr("Description"),
Header: map[string]*session.UserAgent_HeaderValues{
"foo": {Values: []string{"foo", "bar"}},
},
},
},
{
Metadata: map[string][]byte{"key": []byte("value")},
UserAgent: &session.UserAgent{
FingerprintId: gu.Ptr("agent"),
Ip: gu.Ptr("1.2.3.4"),
Description: gu.Ptr("Description"),
Header: map[string]*session.UserAgent_HeaderValues{
"foo": {Values: []string{"foo", "bar"}},
},
},
},
},
},
},
{
name: "list sessions, userid, ok",
args: args{
CTX,
&session.ListSessionsRequest{},
func(ctx context.Context, t *testing.T, request *session.ListSessionsRequest) []*sessionAttr {
createdUser := createFullUser(ctx)
info := createSession(ctx, t, createdUser.GetUserId(), "agent", durationpb.New(time.Minute*5), map[string][]byte{"key": []byte("value")})
request.Queries = append(request.Queries, &session.SearchQuery{Query: &session.SearchQuery_UserIdQuery{UserIdQuery: &session.UserIDQuery{Id: createdUser.GetUserId()}}})
return []*sessionAttr{info}
},
},
wantExpirationWindow: time.Minute * 5,
wantFactors: []wantFactor{wantUserFactor},
want: &session.ListSessionsResponse{
Details: &object.ListDetails{
TotalResult: 1,
Timestamp: timestamppb.Now(),
},
Sessions: []*session.Session{
{
Metadata: map[string][]byte{"key": []byte("value")},
UserAgent: &session.UserAgent{
FingerprintId: gu.Ptr("agent"),
Ip: gu.Ptr("1.2.3.4"),
Description: gu.Ptr("Description"),
Header: map[string]*session.UserAgent_HeaderValues{
"foo": {Values: []string{"foo", "bar"}},
},
},
},
},
},
},
{
name: "list sessions, own creator, ok",
args: args{
CTX,
&session.ListSessionsRequest{},
func(ctx context.Context, t *testing.T, request *session.ListSessionsRequest) []*sessionAttr {
info := createSession(ctx, t, User.GetUserId(), "agent", durationpb.New(time.Minute*5), map[string][]byte{"key": []byte("value")})
request.Queries = append(request.Queries,
&session.SearchQuery{Query: &session.SearchQuery_IdsQuery{IdsQuery: &session.IDsQuery{Ids: []string{info.ID}}}},
&session.SearchQuery{Query: &session.SearchQuery_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(),
},
Sessions: []*session.Session{
{
Metadata: map[string][]byte{"key": []byte("value")},
UserAgent: &session.UserAgent{
FingerprintId: gu.Ptr("agent"),
Ip: gu.Ptr("1.2.3.4"),
Description: gu.Ptr("Description"),
Header: map[string]*session.UserAgent_HeaderValues{
"foo": {Values: []string{"foo", "bar"}},
},
},
},
},
},
},
{
name: "list sessions, creator, ok",
args: args{
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.UserTypeOrgOwner).ID)}}})
return []*sessionAttr{info}
},
},
wantExpirationWindow: time.Minute * 5,
wantFactors: []wantFactor{wantUserFactor},
want: &session.ListSessionsResponse{
Details: &object.ListDetails{
TotalResult: 1,
Timestamp: timestamppb.Now(),
},
Sessions: []*session.Session{
{
Metadata: map[string][]byte{"key": []byte("value")},
UserAgent: &session.UserAgent{
FingerprintId: gu.Ptr("agent"),
Ip: gu.Ptr("1.2.3.4"),
Description: gu.Ptr("Description"),
Header: map[string]*session.UserAgent_HeaderValues{
"foo": {Values: []string{"foo", "bar"}},
},
},
},
},
},
},
{
name: "list sessions, 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{}
},
},
wantExpirationWindow: time.Minute * 5,
wantFactors: []wantFactor{wantUserFactor},
want: &session.ListSessionsResponse{
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{}
},
},
wantExpirationWindow: time.Minute * 5,
wantFactors: []wantFactor{wantUserFactor},
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}
},
},
wantExpirationWindow: time.Minute * 5,
wantFactors: []wantFactor{wantUserFactor},
want: &session.ListSessionsResponse{
Details: &object.ListDetails{
TotalResult: 1,
Timestamp: timestamppb.Now(),
},
Sessions: []*session.Session{
{
Metadata: map[string][]byte{"key": []byte("value")},
UserAgent: &session.UserAgent{
FingerprintId: gu.Ptr("useragent"),
Ip: gu.Ptr("1.2.3.4"),
Description: gu.Ptr("Description"),
Header: map[string]*session.UserAgent_HeaderValues{
"foo": {Values: []string{"foo", "bar"}},
},
},
},
},
},
},
{
name: "list sessions, wrong 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{}
},
},
wantExpirationWindow: time.Minute * 5,
wantFactors: []wantFactor{wantUserFactor},
want: &session.ListSessionsResponse{
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{}
},
},
wantExpirationWindow: time.Minute * 5,
wantFactors: []wantFactor{wantUserFactor},
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
infos := tt.args.dep(CTX, t, tt.args.req)
retryDuration, tick := integration.WaitForAndTickWithMaxDuration(tt.args.ctx, time.Minute)
require.EventuallyWithT(t, func(ttt *assert.CollectT) {
got, err := Client.ListSessions(tt.args.ctx, tt.args.req)
if tt.wantErr {
assert.Error(ttt, err)
return
}
if !assert.NoError(ttt, err) {
return
}
if !assert.Equal(ttt, got.Details.TotalResult, tt.want.Details.TotalResult) || !assert.Len(ttt, got.Sessions, len(tt.want.Sessions)) {
return
}
for i := range infos {
tt.want.Sessions[i].Id = infos[i].ID
tt.want.Sessions[i].Sequence = infos[i].Details.GetSequence()
tt.want.Sessions[i].CreationDate = infos[i].Details.GetChangeDate()
tt.want.Sessions[i].ChangeDate = infos[i].Details.GetChangeDate()
verifySession(ttt, got.Sessions[i], tt.want.Sessions[i], time.Minute, tt.wantExpirationWindow, infos[i].UserID, tt.wantFactors...)
}
integration.AssertListDetails(ttt, tt.want, got)
}, retryDuration, tick)
})
}
}

View File

@@ -0,0 +1,74 @@
//go:build integration
package session_test
import (
"context"
"os"
"testing"
"time"
"github.com/zitadel/logging"
"github.com/zitadel/zitadel/internal/integration"
"github.com/zitadel/zitadel/pkg/grpc/session/v2"
"github.com/zitadel/zitadel/pkg/grpc/user/v2"
)
var (
CTX context.Context
IAMOwnerCTX context.Context
UserCTX context.Context
Instance *integration.Instance
Client session.SessionServiceClient
User *user.AddHumanUserResponse
DeactivatedUser *user.AddHumanUserResponse
LockedUser *user.AddHumanUserResponse
)
func TestMain(m *testing.M) {
os.Exit(func() int {
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Minute)
defer cancel()
Instance = integration.NewInstance(ctx)
Client = Instance.Client.SessionV2
CTX = Instance.WithAuthorization(ctx, integration.UserTypeOrgOwner)
IAMOwnerCTX = Instance.WithAuthorization(ctx, integration.UserTypeIAMOwner)
UserCTX = Instance.WithAuthorization(ctx, integration.UserTypeNoPermission)
User = createFullUser(CTX)
DeactivatedUser = createDeactivatedUser(CTX)
LockedUser = createLockedUser(CTX)
return m.Run()
}())
}
func createFullUser(ctx context.Context) *user.AddHumanUserResponse {
userResp := Instance.CreateHumanUser(ctx)
Instance.Client.UserV2.VerifyEmail(ctx, &user.VerifyEmailRequest{
UserId: userResp.GetUserId(),
VerificationCode: userResp.GetEmailCode(),
})
Instance.Client.UserV2.VerifyPhone(ctx, &user.VerifyPhoneRequest{
UserId: userResp.GetUserId(),
VerificationCode: userResp.GetPhoneCode(),
})
Instance.SetUserPassword(ctx, userResp.GetUserId(), integration.UserPassword, false)
Instance.RegisterUserPasskey(ctx, userResp.GetUserId())
return userResp
}
func createDeactivatedUser(ctx context.Context) *user.AddHumanUserResponse {
userResp := Instance.CreateHumanUser(ctx)
_, err := Instance.Client.UserV2.DeactivateUser(ctx, &user.DeactivateUserRequest{UserId: userResp.GetUserId()})
logging.OnError(err).Fatal("deactivate human user")
return userResp
}
func createLockedUser(ctx context.Context) *user.AddHumanUserResponse {
userResp := Instance.CreateHumanUser(ctx)
_, err := Instance.Client.UserV2.LockUser(ctx, &user.LockUserRequest{UserId: userResp.GetUserId()})
logging.OnError(err).Fatal("lock human user")
return userResp
}

View File

@@ -5,7 +5,6 @@ package session_test
import (
"context"
"fmt"
"os"
"testing"
"time"
@@ -14,7 +13,6 @@ import (
"github.com/pquerna/otp/totp"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/zitadel/logging"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/metadata"
"google.golang.org/grpc/status"
@@ -29,63 +27,7 @@ import (
"github.com/zitadel/zitadel/pkg/grpc/user/v2"
)
var (
CTX context.Context
IAMOwnerCTX context.Context
Instance *integration.Instance
Client session.SessionServiceClient
User *user.AddHumanUserResponse
DeactivatedUser *user.AddHumanUserResponse
LockedUser *user.AddHumanUserResponse
)
func TestMain(m *testing.M) {
os.Exit(func() int {
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Minute)
defer cancel()
Instance = integration.NewInstance(ctx)
Client = Instance.Client.SessionV2
CTX = Instance.WithAuthorization(ctx, integration.UserTypeOrgOwner)
IAMOwnerCTX = Instance.WithAuthorization(ctx, integration.UserTypeIAMOwner)
User = createFullUser(CTX)
DeactivatedUser = createDeactivatedUser(CTX)
LockedUser = createLockedUser(CTX)
return m.Run()
}())
}
func createFullUser(ctx context.Context) *user.AddHumanUserResponse {
userResp := Instance.CreateHumanUser(ctx)
Instance.Client.UserV2.VerifyEmail(ctx, &user.VerifyEmailRequest{
UserId: userResp.GetUserId(),
VerificationCode: userResp.GetEmailCode(),
})
Instance.Client.UserV2.VerifyPhone(ctx, &user.VerifyPhoneRequest{
UserId: userResp.GetUserId(),
VerificationCode: userResp.GetPhoneCode(),
})
Instance.SetUserPassword(ctx, userResp.GetUserId(), integration.UserPassword, false)
Instance.RegisterUserPasskey(ctx, userResp.GetUserId())
return userResp
}
func createDeactivatedUser(ctx context.Context) *user.AddHumanUserResponse {
userResp := Instance.CreateHumanUser(ctx)
_, err := Instance.Client.UserV2.DeactivateUser(ctx, &user.DeactivateUserRequest{UserId: userResp.GetUserId()})
logging.OnError(err).Fatal("deactivate human user")
return userResp
}
func createLockedUser(ctx context.Context) *user.AddHumanUserResponse {
userResp := Instance.CreateHumanUser(ctx)
_, err := Instance.Client.UserV2.LockUser(ctx, &user.LockUserRequest{UserId: userResp.GetUserId()})
logging.OnError(err).Fatal("lock human user")
return userResp
}
func verifyCurrentSession(t testing.TB, id, token string, sequence uint64, window time.Duration, metadata map[string][]byte, userAgent *session.UserAgent, expirationWindow time.Duration, userID string, factors ...wantFactor) *session.Session {
func verifyCurrentSession(t *testing.T, id, token string, sequence uint64, window time.Duration, metadata map[string][]byte, userAgent *session.UserAgent, expirationWindow time.Duration, userID string, factors ...wantFactor) *session.Session {
t.Helper()
require.NotEmpty(t, id)
require.NotEmpty(t, token)
@@ -96,15 +38,25 @@ func verifyCurrentSession(t testing.TB, id, token string, sequence uint64, windo
})
require.NoError(t, err)
s := resp.GetSession()
want := &session.Session{
Id: id,
Sequence: sequence,
Metadata: metadata,
UserAgent: userAgent,
}
verifySession(t, s, want, window, expirationWindow, userID, factors...)
return s
}
assert.Equal(t, id, s.GetId())
func verifySession(t assert.TestingT, s *session.Session, want *session.Session, window time.Duration, expirationWindow time.Duration, userID string, factors ...wantFactor) {
assert.Equal(t, want.Id, s.GetId())
assert.WithinRange(t, s.GetCreationDate().AsTime(), time.Now().Add(-window), time.Now().Add(window))
assert.WithinRange(t, s.GetChangeDate().AsTime(), time.Now().Add(-window), time.Now().Add(window))
assert.Equal(t, sequence, s.GetSequence())
assert.Equal(t, metadata, s.GetMetadata())
assert.Equal(t, want.Sequence, s.GetSequence())
assert.Equal(t, want.Metadata, s.GetMetadata())
if !proto.Equal(userAgent, s.GetUserAgent()) {
t.Errorf("user agent =\n%v\nwant\n%v", s.GetUserAgent(), userAgent)
if !proto.Equal(want.UserAgent, s.GetUserAgent()) {
t.Errorf("user agent =\n%v\nwant\n%v", s.GetUserAgent(), want.UserAgent)
}
if expirationWindow == 0 {
assert.Nil(t, s.GetExpirationDate())
@@ -113,7 +65,6 @@ func verifyCurrentSession(t testing.TB, id, token string, sequence uint64, windo
}
verifyFactors(t, s.GetFactors(), window, userID, factors)
return s
}
type wantFactor int
@@ -129,7 +80,7 @@ const (
wantOTPEmailFactor
)
func verifyFactors(t testing.TB, factors *session.Factors, window time.Duration, userID string, want []wantFactor) {
func verifyFactors(t assert.TestingT, factors *session.Factors, window time.Duration, userID string, want []wantFactor) {
for _, w := range want {
switch w {
case wantUserFactor:
@@ -194,8 +145,15 @@ func TestServer_CreateSession(t *testing.T) {
},
},
{
name: "user agent",
name: "full session",
req: &session.CreateSessionRequest{
Checks: &session.Checks{
User: &session.CheckUser{
Search: &session.CheckUser_UserId{
UserId: User.GetUserId(),
},
},
},
Metadata: map[string][]byte{"foo": []byte("bar")},
UserAgent: &session.UserAgent{
FingerprintId: gu.Ptr("fingerPrintID"),
@@ -205,6 +163,7 @@ func TestServer_CreateSession(t *testing.T) {
"foo": {Values: []string{"foo", "bar"}},
},
},
Lifetime: durationpb.New(5 * time.Minute),
},
want: &session.CreateSessionResponse{
Details: &object.Details{
@@ -212,14 +171,6 @@ func TestServer_CreateSession(t *testing.T) {
ResourceOwner: Instance.ID(),
},
},
wantUserAgent: &session.UserAgent{
FingerprintId: gu.Ptr("fingerPrintID"),
Ip: gu.Ptr("1.2.3.4"),
Description: gu.Ptr("Description"),
Header: map[string]*session.UserAgent_HeaderValues{
"foo": {Values: []string{"foo", "bar"}},
},
},
},
{
name: "negative lifetime",
@@ -229,40 +180,6 @@ func TestServer_CreateSession(t *testing.T) {
},
wantErr: true,
},
{
name: "lifetime",
req: &session.CreateSessionRequest{
Metadata: map[string][]byte{"foo": []byte("bar")},
Lifetime: durationpb.New(5 * time.Minute),
},
want: &session.CreateSessionResponse{
Details: &object.Details{
ChangeDate: timestamppb.Now(),
ResourceOwner: Instance.ID(),
},
},
wantExpirationWindow: 5 * time.Minute,
},
{
name: "with user",
req: &session.CreateSessionRequest{
Checks: &session.Checks{
User: &session.CheckUser{
Search: &session.CheckUser_UserId{
UserId: User.GetUserId(),
},
},
},
Metadata: map[string][]byte{"foo": []byte("bar")},
},
want: &session.CreateSessionResponse{
Details: &object.Details{
ChangeDate: timestamppb.Now(),
ResourceOwner: Instance.ID(),
},
},
wantFactors: []wantFactor{wantUserFactor},
},
{
name: "deactivated user",
req: &session.CreateSessionRequest{
@@ -340,8 +257,6 @@ func TestServer_CreateSession(t *testing.T) {
}
require.NoError(t, err)
integration.AssertDetails(t, tt.want, got)
verifyCurrentSession(t, got.GetSessionId(), got.GetSessionToken(), got.GetDetails().GetSequence(), time.Minute, tt.req.GetMetadata(), tt.wantUserAgent, tt.wantExpirationWindow, User.GetUserId(), tt.wantFactors...)
})
}
}
@@ -946,21 +861,30 @@ func Test_ZITADEL_API_missing_authentication(t *testing.T) {
require.NoError(t, err)
ctx := metadata.AppendToOutgoingContext(context.Background(), "Authorization", fmt.Sprintf("Bearer %s", createResp.GetSessionToken()))
sessionResp, err := Client.GetSession(ctx, &session.GetSessionRequest{SessionId: createResp.GetSessionId()})
require.Error(t, err)
require.Nil(t, sessionResp)
retryDuration, tick := integration.WaitForAndTickWithMaxDuration(ctx, time.Minute)
require.EventuallyWithT(t, func(tt *assert.CollectT) {
sessionResp, err := Client.GetSession(ctx, &session.GetSessionRequest{SessionId: createResp.GetSessionId()})
if !assert.Error(tt, err) {
return
}
assert.Nil(tt, sessionResp)
}, retryDuration, tick)
}
func Test_ZITADEL_API_success(t *testing.T) {
id, token, _, _ := Instance.CreateVerifiedWebAuthNSession(t, CTX, User.GetUserId())
ctx := integration.WithAuthorizationToken(context.Background(), token)
sessionResp, err := Client.GetSession(ctx, &session.GetSessionRequest{SessionId: id})
require.NoError(t, err)
webAuthN := sessionResp.GetSession().GetFactors().GetWebAuthN()
require.NotNil(t, id, webAuthN.GetVerifiedAt().AsTime())
require.True(t, webAuthN.GetUserVerified())
retryDuration, tick := integration.WaitForAndTickWithMaxDuration(ctx, time.Minute)
require.EventuallyWithT(t, func(tt *assert.CollectT) {
sessionResp, err := Client.GetSession(ctx, &session.GetSessionRequest{SessionId: id})
if !assert.NoError(tt, err) {
return
}
webAuthN := sessionResp.GetSession().GetFactors().GetWebAuthN()
assert.NotNil(tt, id, webAuthN.GetVerifiedAt().AsTime())
assert.True(tt, webAuthN.GetUserVerified())
}, retryDuration, tick)
}
func Test_ZITADEL_API_session_not_found(t *testing.T) {
@@ -968,18 +892,30 @@ func Test_ZITADEL_API_session_not_found(t *testing.T) {
// test session token works
ctx := integration.WithAuthorizationToken(context.Background(), token)
_, err := Client.GetSession(ctx, &session.GetSessionRequest{SessionId: id})
require.NoError(t, err)
retryDuration, tick := integration.WaitForAndTickWithMaxDuration(ctx, time.Minute)
require.EventuallyWithT(t, func(tt *assert.CollectT) {
_, err := Client.GetSession(ctx, &session.GetSessionRequest{SessionId: id})
if !assert.NoError(tt, err) {
return
}
}, retryDuration, tick)
//terminate the session and test it does not work anymore
_, err = Client.DeleteSession(CTX, &session.DeleteSessionRequest{
_, err := Client.DeleteSession(CTX, &session.DeleteSessionRequest{
SessionId: id,
SessionToken: gu.Ptr(token),
})
require.NoError(t, err)
ctx = integration.WithAuthorizationToken(context.Background(), token)
_, err = Client.GetSession(ctx, &session.GetSessionRequest{SessionId: id})
require.Error(t, err)
retryDuration, tick = integration.WaitForAndTickWithMaxDuration(ctx, time.Minute)
require.EventuallyWithT(t, func(tt *assert.CollectT) {
_, err := Client.GetSession(ctx, &session.GetSessionRequest{SessionId: id})
if !assert.Error(tt, err) {
return
}
}, retryDuration, tick)
}
func Test_ZITADEL_API_session_expired(t *testing.T) {
@@ -987,8 +923,13 @@ func Test_ZITADEL_API_session_expired(t *testing.T) {
// test session token works
ctx := integration.WithAuthorizationToken(context.Background(), token)
_, err := Client.GetSession(ctx, &session.GetSessionRequest{SessionId: id})
require.NoError(t, err)
retryDuration, tick := integration.WaitForAndTickWithMaxDuration(ctx, time.Minute)
require.EventuallyWithT(t, func(tt *assert.CollectT) {
_, err := Client.GetSession(ctx, &session.GetSessionRequest{SessionId: id})
if !assert.NoError(tt, err) {
return
}
}, retryDuration, tick)
// ensure session expires and does not work anymore
time.Sleep(20 * time.Second)

View File

@@ -0,0 +1,262 @@
package session
import (
"context"
"time"
"github.com/muhlemmer/gu"
"google.golang.org/protobuf/types/known/timestamppb"
"github.com/zitadel/zitadel/internal/api/authz"
"github.com/zitadel/zitadel/internal/api/grpc/object/v2"
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/query"
"github.com/zitadel/zitadel/internal/zerrors"
objpb "github.com/zitadel/zitadel/pkg/grpc/object"
"github.com/zitadel/zitadel/pkg/grpc/session/v2"
)
var (
timestampComparisons = map[objpb.TimestampQueryMethod]query.TimestampComparison{
objpb.TimestampQueryMethod_TIMESTAMP_QUERY_METHOD_EQUALS: query.TimestampEquals,
objpb.TimestampQueryMethod_TIMESTAMP_QUERY_METHOD_GREATER: query.TimestampGreater,
objpb.TimestampQueryMethod_TIMESTAMP_QUERY_METHOD_GREATER_OR_EQUALS: query.TimestampGreaterOrEquals,
objpb.TimestampQueryMethod_TIMESTAMP_QUERY_METHOD_LESS: query.TimestampLess,
objpb.TimestampQueryMethod_TIMESTAMP_QUERY_METHOD_LESS_OR_EQUALS: query.TimestampLessOrEquals,
}
)
func (s *Server) GetSession(ctx context.Context, req *session.GetSessionRequest) (*session.GetSessionResponse, error) {
res, err := s.query.SessionByID(ctx, true, req.GetSessionId(), req.GetSessionToken(), s.checkPermission)
if err != nil {
return nil, err
}
return &session.GetSessionResponse{
Session: sessionToPb(res),
}, nil
}
func (s *Server) ListSessions(ctx context.Context, req *session.ListSessionsRequest) (*session.ListSessionsResponse, error) {
queries, err := listSessionsRequestToQuery(ctx, req)
if err != nil {
return nil, err
}
sessions, err := s.query.SearchSessions(ctx, queries, s.checkPermission)
if err != nil {
return nil, err
}
return &session.ListSessionsResponse{
Details: object.ToListDetails(sessions.SearchResponse),
Sessions: sessionsToPb(sessions.Sessions),
}, nil
}
func listSessionsRequestToQuery(ctx context.Context, req *session.ListSessionsRequest) (*query.SessionsSearchQueries, error) {
offset, limit, asc := object.ListQueryToQuery(req.Query)
queries, err := sessionQueriesToQuery(ctx, req.GetQueries())
if err != nil {
return nil, err
}
return &query.SessionsSearchQueries{
SearchRequest: query.SearchRequest{
Offset: offset,
Limit: limit,
Asc: asc,
SortingColumn: fieldNameToSessionColumn(req.GetSortingColumn()),
},
Queries: queries,
}, nil
}
func sessionQueriesToQuery(ctx context.Context, queries []*session.SearchQuery) (_ []query.SearchQuery, err error) {
q := make([]query.SearchQuery, len(queries))
for i, v := range queries {
q[i], err = sessionQueryToQuery(ctx, v)
if err != nil {
return nil, err
}
}
return q, nil
}
func sessionQueryToQuery(ctx context.Context, sq *session.SearchQuery) (query.SearchQuery, error) {
switch q := sq.Query.(type) {
case *session.SearchQuery_IdsQuery:
return idsQueryToQuery(q.IdsQuery)
case *session.SearchQuery_UserIdQuery:
return query.NewUserIDSearchQuery(q.UserIdQuery.GetId())
case *session.SearchQuery_CreationDateQuery:
return creationDateQueryToQuery(q.CreationDateQuery)
case *session.SearchQuery_CreatorQuery:
if q.CreatorQuery != nil && q.CreatorQuery.Id != nil {
if q.CreatorQuery.GetId() != "" {
return query.NewSessionCreatorSearchQuery(q.CreatorQuery.GetId())
}
} else {
if userID := authz.GetCtxData(ctx).UserID; userID != "" {
return query.NewSessionCreatorSearchQuery(userID)
}
}
return nil, zerrors.ThrowInvalidArgument(nil, "GRPC-x8n24uh", "List.Query.Invalid")
case *session.SearchQuery_UserAgentQuery:
if q.UserAgentQuery != nil && q.UserAgentQuery.FingerprintId != nil {
if *q.UserAgentQuery.FingerprintId != "" {
return query.NewSessionUserAgentFingerprintIDSearchQuery(q.UserAgentQuery.GetFingerprintId())
}
} else {
if agentID := authz.GetCtxData(ctx).AgentID; agentID != "" {
return query.NewSessionUserAgentFingerprintIDSearchQuery(agentID)
}
}
return nil, zerrors.ThrowInvalidArgument(nil, "GRPC-x8n23uh", "List.Query.Invalid")
default:
return nil, zerrors.ThrowInvalidArgument(nil, "GRPC-Sfefs", "List.Query.Invalid")
}
}
func idsQueryToQuery(q *session.IDsQuery) (query.SearchQuery, error) {
return query.NewSessionIDsSearchQuery(q.Ids)
}
func creationDateQueryToQuery(q *session.CreationDateQuery) (query.SearchQuery, error) {
comparison := timestampComparisons[q.GetMethod()]
return query.NewCreationDateQuery(q.GetCreationDate().AsTime(), comparison)
}
func fieldNameToSessionColumn(field session.SessionFieldName) query.Column {
switch field {
case session.SessionFieldName_SESSION_FIELD_NAME_CREATION_DATE:
return query.SessionColumnCreationDate
case session.SessionFieldName_SESSION_FIELD_NAME_UNSPECIFIED:
return query.Column{}
default:
return query.Column{}
}
}
func sessionsToPb(sessions []*query.Session) []*session.Session {
s := make([]*session.Session, len(sessions))
for i, session := range sessions {
s[i] = sessionToPb(session)
}
return s
}
func sessionToPb(s *query.Session) *session.Session {
return &session.Session{
Id: s.ID,
CreationDate: timestamppb.New(s.CreationDate),
ChangeDate: timestamppb.New(s.ChangeDate),
Sequence: s.Sequence,
Factors: factorsToPb(s),
Metadata: s.Metadata,
UserAgent: userAgentToPb(s.UserAgent),
ExpirationDate: expirationToPb(s.Expiration),
}
}
func userAgentToPb(ua domain.UserAgent) *session.UserAgent {
if ua.IsEmpty() {
return nil
}
out := &session.UserAgent{
FingerprintId: ua.FingerprintID,
Description: ua.Description,
}
if ua.IP != nil {
out.Ip = gu.Ptr(ua.IP.String())
}
if ua.Header == nil {
return out
}
out.Header = make(map[string]*session.UserAgent_HeaderValues, len(ua.Header))
for k, v := range ua.Header {
out.Header[k] = &session.UserAgent_HeaderValues{
Values: v,
}
}
return out
}
func expirationToPb(expiration time.Time) *timestamppb.Timestamp {
if expiration.IsZero() {
return nil
}
return timestamppb.New(expiration)
}
func factorsToPb(s *query.Session) *session.Factors {
user := userFactorToPb(s.UserFactor)
if user == nil {
return nil
}
return &session.Factors{
User: user,
Password: passwordFactorToPb(s.PasswordFactor),
WebAuthN: webAuthNFactorToPb(s.WebAuthNFactor),
Intent: intentFactorToPb(s.IntentFactor),
Totp: totpFactorToPb(s.TOTPFactor),
OtpSms: otpFactorToPb(s.OTPSMSFactor),
OtpEmail: otpFactorToPb(s.OTPEmailFactor),
}
}
func passwordFactorToPb(factor query.SessionPasswordFactor) *session.PasswordFactor {
if factor.PasswordCheckedAt.IsZero() {
return nil
}
return &session.PasswordFactor{
VerifiedAt: timestamppb.New(factor.PasswordCheckedAt),
}
}
func intentFactorToPb(factor query.SessionIntentFactor) *session.IntentFactor {
if factor.IntentCheckedAt.IsZero() {
return nil
}
return &session.IntentFactor{
VerifiedAt: timestamppb.New(factor.IntentCheckedAt),
}
}
func webAuthNFactorToPb(factor query.SessionWebAuthNFactor) *session.WebAuthNFactor {
if factor.WebAuthNCheckedAt.IsZero() {
return nil
}
return &session.WebAuthNFactor{
VerifiedAt: timestamppb.New(factor.WebAuthNCheckedAt),
UserVerified: factor.UserVerified,
}
}
func totpFactorToPb(factor query.SessionTOTPFactor) *session.TOTPFactor {
if factor.TOTPCheckedAt.IsZero() {
return nil
}
return &session.TOTPFactor{
VerifiedAt: timestamppb.New(factor.TOTPCheckedAt),
}
}
func otpFactorToPb(factor query.SessionOTPFactor) *session.OTPFactor {
if factor.OTPCheckedAt.IsZero() {
return nil
}
return &session.OTPFactor{
VerifiedAt: timestamppb.New(factor.OTPCheckedAt),
}
}
func userFactorToPb(factor query.SessionUserFactor) *session.UserFactor {
if factor.UserID == "" || factor.UserCheckedAt.IsZero() {
return nil
}
return &session.UserFactor{
VerifiedAt: timestamppb.New(factor.UserCheckedAt),
Id: factor.UserID,
LoginName: factor.LoginName,
DisplayName: factor.DisplayName,
OrganizationId: factor.ResourceOwner,
}
}

View File

@@ -6,6 +6,7 @@ import (
"github.com/zitadel/zitadel/internal/api/authz"
"github.com/zitadel/zitadel/internal/api/grpc/server"
"github.com/zitadel/zitadel/internal/command"
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/query"
"github.com/zitadel/zitadel/pkg/grpc/session/v2"
)
@@ -16,6 +17,8 @@ type Server struct {
session.UnimplementedSessionServiceServer
command *command.Commands
query *query.Queries
checkPermission domain.PermissionCheck
}
type Config struct{}
@@ -23,10 +26,12 @@ type Config struct{}
func CreateServer(
command *command.Commands,
query *query.Queries,
checkPermission domain.PermissionCheck,
) *Server {
return &Server{
command: command,
query: query,
command: command,
query: query,
checkPermission: checkPermission,
}
}

View File

@@ -6,56 +6,17 @@ import (
"net/http"
"time"
"github.com/muhlemmer/gu"
"golang.org/x/text/language"
"google.golang.org/protobuf/types/known/structpb"
"google.golang.org/protobuf/types/known/timestamppb"
"github.com/zitadel/zitadel/internal/api/authz"
"github.com/zitadel/zitadel/internal/api/grpc/object/v2"
"github.com/zitadel/zitadel/internal/command"
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/query"
"github.com/zitadel/zitadel/internal/zerrors"
objpb "github.com/zitadel/zitadel/pkg/grpc/object"
"github.com/zitadel/zitadel/pkg/grpc/session/v2"
)
var (
timestampComparisons = map[objpb.TimestampQueryMethod]query.TimestampComparison{
objpb.TimestampQueryMethod_TIMESTAMP_QUERY_METHOD_EQUALS: query.TimestampEquals,
objpb.TimestampQueryMethod_TIMESTAMP_QUERY_METHOD_GREATER: query.TimestampGreater,
objpb.TimestampQueryMethod_TIMESTAMP_QUERY_METHOD_GREATER_OR_EQUALS: query.TimestampGreaterOrEquals,
objpb.TimestampQueryMethod_TIMESTAMP_QUERY_METHOD_LESS: query.TimestampLess,
objpb.TimestampQueryMethod_TIMESTAMP_QUERY_METHOD_LESS_OR_EQUALS: query.TimestampLessOrEquals,
}
)
func (s *Server) GetSession(ctx context.Context, req *session.GetSessionRequest) (*session.GetSessionResponse, error) {
res, err := s.query.SessionByID(ctx, true, req.GetSessionId(), req.GetSessionToken())
if err != nil {
return nil, err
}
return &session.GetSessionResponse{
Session: sessionToPb(res),
}, nil
}
func (s *Server) ListSessions(ctx context.Context, req *session.ListSessionsRequest) (*session.ListSessionsResponse, error) {
queries, err := listSessionsRequestToQuery(ctx, req)
if err != nil {
return nil, err
}
sessions, err := s.query.SearchSessions(ctx, queries)
if err != nil {
return nil, err
}
return &session.ListSessionsResponse{
Details: object.ToListDetails(sessions.SearchResponse),
Sessions: sessionsToPb(sessions.Sessions),
}, nil
}
func (s *Server) CreateSession(ctx context.Context, req *session.CreateSessionRequest) (*session.CreateSessionResponse, error) {
checks, metadata, userAgent, lifetime, err := s.createSessionRequestToCommand(ctx, req)
if err != nil {
@@ -110,197 +71,6 @@ func (s *Server) DeleteSession(ctx context.Context, req *session.DeleteSessionRe
}, nil
}
func sessionsToPb(sessions []*query.Session) []*session.Session {
s := make([]*session.Session, len(sessions))
for i, session := range sessions {
s[i] = sessionToPb(session)
}
return s
}
func sessionToPb(s *query.Session) *session.Session {
return &session.Session{
Id: s.ID,
CreationDate: timestamppb.New(s.CreationDate),
ChangeDate: timestamppb.New(s.ChangeDate),
Sequence: s.Sequence,
Factors: factorsToPb(s),
Metadata: s.Metadata,
UserAgent: userAgentToPb(s.UserAgent),
ExpirationDate: expirationToPb(s.Expiration),
}
}
func userAgentToPb(ua domain.UserAgent) *session.UserAgent {
if ua.IsEmpty() {
return nil
}
out := &session.UserAgent{
FingerprintId: ua.FingerprintID,
Description: ua.Description,
}
if ua.IP != nil {
out.Ip = gu.Ptr(ua.IP.String())
}
if ua.Header == nil {
return out
}
out.Header = make(map[string]*session.UserAgent_HeaderValues, len(ua.Header))
for k, v := range ua.Header {
out.Header[k] = &session.UserAgent_HeaderValues{
Values: v,
}
}
return out
}
func expirationToPb(expiration time.Time) *timestamppb.Timestamp {
if expiration.IsZero() {
return nil
}
return timestamppb.New(expiration)
}
func factorsToPb(s *query.Session) *session.Factors {
user := userFactorToPb(s.UserFactor)
if user == nil {
return nil
}
return &session.Factors{
User: user,
Password: passwordFactorToPb(s.PasswordFactor),
WebAuthN: webAuthNFactorToPb(s.WebAuthNFactor),
Intent: intentFactorToPb(s.IntentFactor),
Totp: totpFactorToPb(s.TOTPFactor),
OtpSms: otpFactorToPb(s.OTPSMSFactor),
OtpEmail: otpFactorToPb(s.OTPEmailFactor),
}
}
func passwordFactorToPb(factor query.SessionPasswordFactor) *session.PasswordFactor {
if factor.PasswordCheckedAt.IsZero() {
return nil
}
return &session.PasswordFactor{
VerifiedAt: timestamppb.New(factor.PasswordCheckedAt),
}
}
func intentFactorToPb(factor query.SessionIntentFactor) *session.IntentFactor {
if factor.IntentCheckedAt.IsZero() {
return nil
}
return &session.IntentFactor{
VerifiedAt: timestamppb.New(factor.IntentCheckedAt),
}
}
func webAuthNFactorToPb(factor query.SessionWebAuthNFactor) *session.WebAuthNFactor {
if factor.WebAuthNCheckedAt.IsZero() {
return nil
}
return &session.WebAuthNFactor{
VerifiedAt: timestamppb.New(factor.WebAuthNCheckedAt),
UserVerified: factor.UserVerified,
}
}
func totpFactorToPb(factor query.SessionTOTPFactor) *session.TOTPFactor {
if factor.TOTPCheckedAt.IsZero() {
return nil
}
return &session.TOTPFactor{
VerifiedAt: timestamppb.New(factor.TOTPCheckedAt),
}
}
func otpFactorToPb(factor query.SessionOTPFactor) *session.OTPFactor {
if factor.OTPCheckedAt.IsZero() {
return nil
}
return &session.OTPFactor{
VerifiedAt: timestamppb.New(factor.OTPCheckedAt),
}
}
func userFactorToPb(factor query.SessionUserFactor) *session.UserFactor {
if factor.UserID == "" || factor.UserCheckedAt.IsZero() {
return nil
}
return &session.UserFactor{
VerifiedAt: timestamppb.New(factor.UserCheckedAt),
Id: factor.UserID,
LoginName: factor.LoginName,
DisplayName: factor.DisplayName,
OrganizationId: factor.ResourceOwner,
}
}
func listSessionsRequestToQuery(ctx context.Context, req *session.ListSessionsRequest) (*query.SessionsSearchQueries, error) {
offset, limit, asc := object.ListQueryToQuery(req.Query)
queries, err := sessionQueriesToQuery(ctx, req.GetQueries())
if err != nil {
return nil, err
}
return &query.SessionsSearchQueries{
SearchRequest: query.SearchRequest{
Offset: offset,
Limit: limit,
Asc: asc,
SortingColumn: fieldNameToSessionColumn(req.GetSortingColumn()),
},
Queries: queries,
}, nil
}
func sessionQueriesToQuery(ctx context.Context, queries []*session.SearchQuery) (_ []query.SearchQuery, err error) {
q := make([]query.SearchQuery, len(queries)+1)
for i, v := range queries {
q[i], err = sessionQueryToQuery(v)
if err != nil {
return nil, err
}
}
creatorQuery, err := query.NewSessionCreatorSearchQuery(authz.GetCtxData(ctx).UserID)
if err != nil {
return nil, err
}
q[len(queries)] = creatorQuery
return q, nil
}
func sessionQueryToQuery(sq *session.SearchQuery) (query.SearchQuery, error) {
switch q := sq.Query.(type) {
case *session.SearchQuery_IdsQuery:
return idsQueryToQuery(q.IdsQuery)
case *session.SearchQuery_UserIdQuery:
return query.NewUserIDSearchQuery(q.UserIdQuery.GetId())
case *session.SearchQuery_CreationDateQuery:
return creationDateQueryToQuery(q.CreationDateQuery)
default:
return nil, zerrors.ThrowInvalidArgument(nil, "GRPC-Sfefs", "List.Query.Invalid")
}
}
func idsQueryToQuery(q *session.IDsQuery) (query.SearchQuery, error) {
return query.NewSessionIDsSearchQuery(q.Ids)
}
func creationDateQueryToQuery(q *session.CreationDateQuery) (query.SearchQuery, error) {
comparison := timestampComparisons[q.GetMethod()]
return query.NewCreationDateQuery(q.GetCreationDate().AsTime(), comparison)
}
func fieldNameToSessionColumn(field session.SessionFieldName) query.Column {
switch field {
case session.SessionFieldName_SESSION_FIELD_NAME_CREATION_DATE:
return query.SessionColumnCreationDate
default:
return query.Column{}
}
}
func (s *Server) createSessionRequestToCommand(ctx context.Context, req *session.CreateSessionRequest) ([]command.SessionCommand, map[string][]byte, *domain.UserAgent, time.Duration, error) {
checks, err := s.checksToCommand(ctx, req.Checks)
if err != nil {

View File

@@ -339,9 +339,7 @@ func Test_listSessionsRequestToQuery(t *testing.T) {
Limit: 0,
Asc: false,
},
Queries: []query.SearchQuery{
mustNewTextQuery(t, query.SessionColumnCreator, "789", query.TextEquals),
},
Queries: []query.SearchQuery{},
},
},
{
@@ -359,15 +357,13 @@ func Test_listSessionsRequestToQuery(t *testing.T) {
SortingColumn: query.SessionColumnCreationDate,
Asc: false,
},
Queries: []query.SearchQuery{
mustNewTextQuery(t, query.SessionColumnCreator, "789", query.TextEquals),
},
Queries: []query.SearchQuery{},
},
},
{
name: "with list query and sessions",
args: args{
ctx: authz.NewMockContext("123", "456", "789"),
ctx: authz.SetCtxData(context.Background(), authz.CtxData{AgentID: "agent", UserID: "789"}),
req: &session.ListSessionsRequest{
Query: &object.ListQuery{
Offset: 10,
@@ -396,6 +392,12 @@ func Test_listSessionsRequestToQuery(t *testing.T) {
Method: objpb.TimestampQueryMethod_TIMESTAMP_QUERY_METHOD_GREATER,
},
}},
{Query: &session.SearchQuery_CreatorQuery{
CreatorQuery: &session.CreatorQuery{},
}},
{Query: &session.SearchQuery_UserAgentQuery{
UserAgentQuery: &session.UserAgentQuery{},
}},
},
},
},
@@ -411,6 +413,7 @@ func Test_listSessionsRequestToQuery(t *testing.T) {
mustNewTextQuery(t, query.SessionColumnUserID, "10", query.TextEquals),
mustNewTimestampQuery(t, query.SessionColumnCreationDate, creationDate, query.TimestampGreater),
mustNewTextQuery(t, query.SessionColumnCreator, "789", query.TextEquals),
mustNewTextQuery(t, query.SessionColumnUserAgentFingerprintID, "agent", query.TextEquals),
},
},
},
@@ -458,13 +461,11 @@ func Test_sessionQueriesToQuery(t *testing.T) {
wantErr error
}{
{
name: "creator only",
name: "no queries",
args: args{
ctx: authz.NewMockContext("123", "456", "789"),
},
want: []query.SearchQuery{
mustNewTextQuery(t, query.SessionColumnCreator, "789", query.TextEquals),
},
want: []query.SearchQuery{},
},
{
name: "invalid argument",
@@ -491,6 +492,9 @@ func Test_sessionQueriesToQuery(t *testing.T) {
Ids: []string{"4", "5", "6"},
},
}},
{Query: &session.SearchQuery_CreatorQuery{
CreatorQuery: &session.CreatorQuery{},
}},
},
},
want: []query.SearchQuery{
@@ -511,6 +515,7 @@ func Test_sessionQueriesToQuery(t *testing.T) {
func Test_sessionQueryToQuery(t *testing.T) {
type args struct {
ctx context.Context
query *session.SearchQuery
}
tests := []struct {
@@ -521,60 +526,158 @@ func Test_sessionQueryToQuery(t *testing.T) {
}{
{
name: "invalid argument",
args: args{&session.SearchQuery{
Query: nil,
}},
args: args{
context.Background(),
&session.SearchQuery{
Query: nil,
}},
wantErr: zerrors.ThrowInvalidArgument(nil, "GRPC-Sfefs", "List.Query.Invalid"),
},
{
name: "ids query",
args: args{&session.SearchQuery{
Query: &session.SearchQuery_IdsQuery{
IdsQuery: &session.IDsQuery{
Ids: []string{"1", "2", "3"},
args: args{
context.Background(),
&session.SearchQuery{
Query: &session.SearchQuery_IdsQuery{
IdsQuery: &session.IDsQuery{
Ids: []string{"1", "2", "3"},
},
},
},
}},
}},
want: mustNewListQuery(t, query.SessionColumnID, []interface{}{"1", "2", "3"}, query.ListIn),
},
{
name: "user id query",
args: args{&session.SearchQuery{
Query: &session.SearchQuery_UserIdQuery{
UserIdQuery: &session.UserIDQuery{
Id: "10",
args: args{
context.Background(),
&session.SearchQuery{
Query: &session.SearchQuery_UserIdQuery{
UserIdQuery: &session.UserIDQuery{
Id: "10",
},
},
},
}},
}},
want: mustNewTextQuery(t, query.SessionColumnUserID, "10", query.TextEquals),
},
{
name: "creation date query",
args: args{&session.SearchQuery{
Query: &session.SearchQuery_CreationDateQuery{
CreationDateQuery: &session.CreationDateQuery{
CreationDate: timestamppb.New(creationDate),
Method: objpb.TimestampQueryMethod_TIMESTAMP_QUERY_METHOD_LESS,
args: args{
context.Background(),
&session.SearchQuery{
Query: &session.SearchQuery_CreationDateQuery{
CreationDateQuery: &session.CreationDateQuery{
CreationDate: timestamppb.New(creationDate),
Method: objpb.TimestampQueryMethod_TIMESTAMP_QUERY_METHOD_LESS,
},
},
},
}},
}},
want: mustNewTimestampQuery(t, query.SessionColumnCreationDate, creationDate, query.TimestampLess),
},
{
name: "creation date query with default method",
args: args{&session.SearchQuery{
Query: &session.SearchQuery_CreationDateQuery{
CreationDateQuery: &session.CreationDateQuery{
CreationDate: timestamppb.New(creationDate),
args: args{
context.Background(),
&session.SearchQuery{
Query: &session.SearchQuery_CreationDateQuery{
CreationDateQuery: &session.CreationDateQuery{
CreationDate: timestamppb.New(creationDate),
},
},
},
}},
}},
want: mustNewTimestampQuery(t, query.SessionColumnCreationDate, creationDate, query.TimestampEquals),
},
{
name: "own creator",
args: args{
authz.SetCtxData(context.Background(), authz.CtxData{UserID: "creator"}),
&session.SearchQuery{
Query: &session.SearchQuery_CreatorQuery{
CreatorQuery: &session.CreatorQuery{},
},
}},
want: mustNewTextQuery(t, query.SessionColumnCreator, "creator", query.TextEquals),
},
{
name: "empty own creator, error",
args: args{
authz.SetCtxData(context.Background(), authz.CtxData{UserID: ""}),
&session.SearchQuery{
Query: &session.SearchQuery_CreatorQuery{
CreatorQuery: &session.CreatorQuery{},
},
}},
wantErr: zerrors.ThrowInvalidArgument(nil, "GRPC-x8n24uh", "List.Query.Invalid"),
},
{
name: "creator",
args: args{
authz.SetCtxData(context.Background(), authz.CtxData{UserID: "creator1"}),
&session.SearchQuery{
Query: &session.SearchQuery_CreatorQuery{
CreatorQuery: &session.CreatorQuery{Id: gu.Ptr("creator2")},
},
}},
want: mustNewTextQuery(t, query.SessionColumnCreator, "creator2", query.TextEquals),
},
{
name: "empty creator, error",
args: args{
authz.SetCtxData(context.Background(), authz.CtxData{UserID: "creator1"}),
&session.SearchQuery{
Query: &session.SearchQuery_CreatorQuery{
CreatorQuery: &session.CreatorQuery{Id: gu.Ptr("")},
},
}},
wantErr: zerrors.ThrowInvalidArgument(nil, "GRPC-x8n24uh", "List.Query.Invalid"),
},
{
name: "empty own useragent, error",
args: args{
authz.SetCtxData(context.Background(), authz.CtxData{AgentID: ""}),
&session.SearchQuery{
Query: &session.SearchQuery_UserAgentQuery{
UserAgentQuery: &session.UserAgentQuery{},
},
}},
wantErr: zerrors.ThrowInvalidArgument(nil, "GRPC-x8n23uh", "List.Query.Invalid"),
},
{
name: "own useragent",
args: args{
authz.SetCtxData(context.Background(), authz.CtxData{AgentID: "agent"}),
&session.SearchQuery{
Query: &session.SearchQuery_UserAgentQuery{
UserAgentQuery: &session.UserAgentQuery{},
},
}},
want: mustNewTextQuery(t, query.SessionColumnUserAgentFingerprintID, "agent", query.TextEquals),
},
{
name: "empty useragent, error",
args: args{
authz.SetCtxData(context.Background(), authz.CtxData{AgentID: "agent"}),
&session.SearchQuery{
Query: &session.SearchQuery_UserAgentQuery{
UserAgentQuery: &session.UserAgentQuery{FingerprintId: gu.Ptr("")},
},
}},
wantErr: zerrors.ThrowInvalidArgument(nil, "GRPC-x8n23uh", "List.Query.Invalid"),
},
{
name: "useragent",
args: args{
authz.SetCtxData(context.Background(), authz.CtxData{AgentID: "agent1"}),
&session.SearchQuery{
Query: &session.SearchQuery_UserAgentQuery{
UserAgentQuery: &session.UserAgentQuery{FingerprintId: gu.Ptr("agent2")},
},
}},
want: mustNewTextQuery(t, query.SessionColumnUserAgentFingerprintID, "agent2", query.TextEquals),
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := sessionQueryToQuery(tt.args.query)
got, err := sessionQueryToQuery(tt.args.ctx, tt.args.query)
require.ErrorIs(t, err, tt.wantErr)
assert.Equal(t, tt.want, got)
})

View File

@@ -0,0 +1,512 @@
//go:build integration
package session_test
import (
"context"
"testing"
"time"
"github.com/golang/protobuf/ptypes/timestamp"
"github.com/muhlemmer/gu"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"google.golang.org/protobuf/types/known/durationpb"
"google.golang.org/protobuf/types/known/timestamppb"
"github.com/zitadel/zitadel/internal/integration"
object "github.com/zitadel/zitadel/pkg/grpc/object/v2beta"
session "github.com/zitadel/zitadel/pkg/grpc/session/v2beta"
)
func TestServer_GetSession(t *testing.T) {
type args struct {
ctx context.Context
req *session.GetSessionRequest
dep func(ctx context.Context, t *testing.T, request *session.GetSessionRequest) uint64
}
tests := []struct {
name string
args args
want *session.GetSessionResponse
wantFactors []wantFactor
wantExpirationWindow time.Duration
wantErr bool
}{
{
name: "get session, no id provided",
args: args{
CTX,
&session.GetSessionRequest{
SessionId: "",
},
nil,
},
wantErr: true,
},
{
name: "get session, not found",
args: args{
CTX,
&session.GetSessionRequest{
SessionId: "unknown",
},
nil,
},
wantErr: true,
},
{
name: "get session, no permission",
args: args{
UserCTX,
&session.GetSessionRequest{},
func(ctx context.Context, t *testing.T, request *session.GetSessionRequest) uint64 {
resp, err := Client.CreateSession(CTX, &session.CreateSessionRequest{})
require.NoError(t, err)
request.SessionId = resp.SessionId
return resp.GetDetails().GetSequence()
},
},
wantErr: true,
},
{
name: "get session, permission, ok",
args: args{
CTX,
&session.GetSessionRequest{},
func(ctx context.Context, t *testing.T, request *session.GetSessionRequest) uint64 {
resp, err := Client.CreateSession(CTX, &session.CreateSessionRequest{})
require.NoError(t, err)
request.SessionId = resp.SessionId
return resp.GetDetails().GetSequence()
},
},
want: &session.GetSessionResponse{
Session: &session.Session{},
},
},
{
name: "get session, token, ok",
args: args{
UserCTX,
&session.GetSessionRequest{},
func(ctx context.Context, t *testing.T, request *session.GetSessionRequest) uint64 {
resp, err := Client.CreateSession(CTX, &session.CreateSessionRequest{})
require.NoError(t, err)
request.SessionId = resp.SessionId
request.SessionToken = gu.Ptr(resp.SessionToken)
return resp.GetDetails().GetSequence()
},
},
want: &session.GetSessionResponse{
Session: &session.Session{},
},
},
{
name: "get session, user agent, ok",
args: args{
UserCTX,
&session.GetSessionRequest{},
func(ctx context.Context, t *testing.T, request *session.GetSessionRequest) uint64 {
resp, err := Client.CreateSession(CTX, &session.CreateSessionRequest{
UserAgent: &session.UserAgent{
FingerprintId: gu.Ptr("fingerPrintID"),
Ip: gu.Ptr("1.2.3.4"),
Description: gu.Ptr("Description"),
Header: map[string]*session.UserAgent_HeaderValues{
"foo": {Values: []string{"foo", "bar"}},
},
},
},
)
require.NoError(t, err)
request.SessionId = resp.SessionId
request.SessionToken = gu.Ptr(resp.SessionToken)
return resp.GetDetails().GetSequence()
},
},
want: &session.GetSessionResponse{
Session: &session.Session{
UserAgent: &session.UserAgent{
FingerprintId: gu.Ptr("fingerPrintID"),
Ip: gu.Ptr("1.2.3.4"),
Description: gu.Ptr("Description"),
Header: map[string]*session.UserAgent_HeaderValues{
"foo": {Values: []string{"foo", "bar"}},
},
},
},
},
},
{
name: "get session, lifetime, ok",
args: args{
UserCTX,
&session.GetSessionRequest{},
func(ctx context.Context, t *testing.T, request *session.GetSessionRequest) uint64 {
resp, err := Client.CreateSession(CTX, &session.CreateSessionRequest{
Lifetime: durationpb.New(5 * time.Minute),
},
)
require.NoError(t, err)
request.SessionId = resp.SessionId
request.SessionToken = gu.Ptr(resp.SessionToken)
return resp.GetDetails().GetSequence()
},
},
wantExpirationWindow: 5 * time.Minute,
want: &session.GetSessionResponse{
Session: &session.Session{},
},
},
{
name: "get session, metadata, ok",
args: args{
UserCTX,
&session.GetSessionRequest{},
func(ctx context.Context, t *testing.T, request *session.GetSessionRequest) uint64 {
resp, err := Client.CreateSession(CTX, &session.CreateSessionRequest{
Metadata: map[string][]byte{"foo": []byte("bar")},
},
)
require.NoError(t, err)
request.SessionId = resp.SessionId
request.SessionToken = gu.Ptr(resp.SessionToken)
return resp.GetDetails().GetSequence()
},
},
want: &session.GetSessionResponse{
Session: &session.Session{
Metadata: map[string][]byte{"foo": []byte("bar")},
},
},
},
{
name: "get session, user, ok",
args: args{
UserCTX,
&session.GetSessionRequest{},
func(ctx context.Context, t *testing.T, request *session.GetSessionRequest) uint64 {
resp, err := Client.CreateSession(CTX, &session.CreateSessionRequest{
Checks: &session.Checks{
User: &session.CheckUser{
Search: &session.CheckUser_UserId{
UserId: User.GetUserId(),
},
},
},
},
)
require.NoError(t, err)
request.SessionId = resp.SessionId
request.SessionToken = gu.Ptr(resp.SessionToken)
return resp.GetDetails().GetSequence()
},
},
wantFactors: []wantFactor{wantUserFactor},
want: &session.GetSessionResponse{
Session: &session.Session{},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
var sequence uint64
if tt.args.dep != nil {
sequence = tt.args.dep(tt.args.ctx, t, tt.args.req)
}
retryDuration, tick := integration.WaitForAndTickWithMaxDuration(tt.args.ctx, time.Minute)
require.EventuallyWithT(t, func(ttt *assert.CollectT) {
got, err := Client.GetSession(tt.args.ctx, tt.args.req)
if tt.wantErr {
assert.Error(ttt, err)
return
}
if !assert.NoError(ttt, err) {
return
}
tt.want.Session.Id = tt.args.req.SessionId
tt.want.Session.Sequence = sequence
verifySession(ttt, got.GetSession(), tt.want.GetSession(), time.Minute, tt.wantExpirationWindow, User.GetUserId(), tt.wantFactors...)
}, retryDuration, tick)
})
}
}
type sessionAttr struct {
ID string
UserID string
UserAgent string
CreationDate *timestamp.Timestamp
ChangeDate *timestamppb.Timestamp
Details *object.Details
}
type sessionAttrs []*sessionAttr
func (u sessionAttrs) ids() []string {
ids := make([]string, len(u))
for i := range u {
ids[i] = u[i].ID
}
return ids
}
func createSessions(ctx context.Context, t *testing.T, count int, userID string, userAgent string, lifetime *durationpb.Duration, metadata map[string][]byte) sessionAttrs {
infos := make([]*sessionAttr, count)
for i := 0; i < count; i++ {
infos[i] = createSession(ctx, t, userID, userAgent, lifetime, metadata)
}
return infos
}
func createSession(ctx context.Context, t *testing.T, userID string, userAgent string, lifetime *durationpb.Duration, metadata map[string][]byte) *sessionAttr {
req := &session.CreateSessionRequest{}
if userID != "" {
req.Checks = &session.Checks{
User: &session.CheckUser{
Search: &session.CheckUser_UserId{
UserId: userID,
},
},
}
}
if userAgent != "" {
req.UserAgent = &session.UserAgent{
FingerprintId: gu.Ptr(userAgent),
Ip: gu.Ptr("1.2.3.4"),
Description: gu.Ptr("Description"),
Header: map[string]*session.UserAgent_HeaderValues{
"foo": {Values: []string{"foo", "bar"}},
},
}
}
if lifetime != nil {
req.Lifetime = lifetime
}
if metadata != nil {
req.Metadata = metadata
}
resp, err := Client.CreateSession(ctx, req)
require.NoError(t, err)
return &sessionAttr{
resp.GetSessionId(),
userID,
userAgent,
resp.GetDetails().GetChangeDate(),
resp.GetDetails().GetChangeDate(),
resp.GetDetails(),
}
}
func TestServer_ListSessions(t *testing.T) {
type args struct {
ctx context.Context
req *session.ListSessionsRequest
dep func(ctx context.Context, t *testing.T, request *session.ListSessionsRequest) []*sessionAttr
}
tests := []struct {
name string
args args
want *session.ListSessionsResponse
wantFactors []wantFactor
wantExpirationWindow time.Duration
wantErr bool
}{
{
name: "list sessions, not found",
args: args{
CTX,
&session.ListSessionsRequest{
Queries: []*session.SearchQuery{
{Query: &session.SearchQuery_IdsQuery{IdsQuery: &session.IDsQuery{Ids: []string{"unknown"}}}},
},
},
func(ctx context.Context, t *testing.T, request *session.ListSessionsRequest) []*sessionAttr {
return []*sessionAttr{}
},
},
want: &session.ListSessionsResponse{
Details: &object.ListDetails{
TotalResult: 0,
Timestamp: timestamppb.Now(),
},
Sessions: []*session.Session{},
},
},
{
name: "list sessions, wrong creator",
args: args{
UserCTX,
&session.ListSessionsRequest{},
func(ctx context.Context, t *testing.T, request *session.ListSessionsRequest) []*sessionAttr {
info := createSession(ctx, t, "", "", nil, nil)
request.Queries = append(request.Queries, &session.SearchQuery{Query: &session.SearchQuery_IdsQuery{IdsQuery: &session.IDsQuery{Ids: []string{info.ID}}}})
return []*sessionAttr{}
},
},
want: &session.ListSessionsResponse{
Details: &object.ListDetails{
TotalResult: 0,
Timestamp: timestamppb.Now(),
},
Sessions: []*session.Session{},
},
},
{
name: "list sessions, full, ok",
args: args{
CTX,
&session.ListSessionsRequest{},
func(ctx context.Context, t *testing.T, request *session.ListSessionsRequest) []*sessionAttr {
info := createSession(ctx, t, User.GetUserId(), "agent", durationpb.New(time.Minute*5), map[string][]byte{"key": []byte("value")})
request.Queries = append(request.Queries, &session.SearchQuery{Query: &session.SearchQuery_IdsQuery{IdsQuery: &session.IDsQuery{Ids: []string{info.ID}}}})
return []*sessionAttr{info}
},
},
wantExpirationWindow: time.Minute * 5,
wantFactors: []wantFactor{wantUserFactor},
want: &session.ListSessionsResponse{
Details: &object.ListDetails{
TotalResult: 1,
Timestamp: timestamppb.Now(),
},
Sessions: []*session.Session{
{
Metadata: map[string][]byte{"key": []byte("value")},
UserAgent: &session.UserAgent{
FingerprintId: gu.Ptr("agent"),
Ip: gu.Ptr("1.2.3.4"),
Description: gu.Ptr("Description"),
Header: map[string]*session.UserAgent_HeaderValues{
"foo": {Values: []string{"foo", "bar"}},
},
},
},
},
},
},
{
name: "list sessions, multiple, ok",
args: args{
CTX,
&session.ListSessionsRequest{},
func(ctx context.Context, t *testing.T, request *session.ListSessionsRequest) []*sessionAttr {
infos := createSessions(ctx, t, 3, User.GetUserId(), "agent", durationpb.New(time.Minute*5), map[string][]byte{"key": []byte("value")})
request.Queries = append(request.Queries, &session.SearchQuery{Query: &session.SearchQuery_IdsQuery{IdsQuery: &session.IDsQuery{Ids: infos.ids()}}})
return infos
},
},
wantExpirationWindow: time.Minute * 5,
wantFactors: []wantFactor{wantUserFactor},
want: &session.ListSessionsResponse{
Details: &object.ListDetails{
TotalResult: 3,
Timestamp: timestamppb.Now(),
},
Sessions: []*session.Session{
{
Metadata: map[string][]byte{"key": []byte("value")},
UserAgent: &session.UserAgent{
FingerprintId: gu.Ptr("agent"),
Ip: gu.Ptr("1.2.3.4"),
Description: gu.Ptr("Description"),
Header: map[string]*session.UserAgent_HeaderValues{
"foo": {Values: []string{"foo", "bar"}},
},
},
},
{
Metadata: map[string][]byte{"key": []byte("value")},
UserAgent: &session.UserAgent{
FingerprintId: gu.Ptr("agent"),
Ip: gu.Ptr("1.2.3.4"),
Description: gu.Ptr("Description"),
Header: map[string]*session.UserAgent_HeaderValues{
"foo": {Values: []string{"foo", "bar"}},
},
},
},
{
Metadata: map[string][]byte{"key": []byte("value")},
UserAgent: &session.UserAgent{
FingerprintId: gu.Ptr("agent"),
Ip: gu.Ptr("1.2.3.4"),
Description: gu.Ptr("Description"),
Header: map[string]*session.UserAgent_HeaderValues{
"foo": {Values: []string{"foo", "bar"}},
},
},
},
},
},
},
{
name: "list sessions, userid, ok",
args: args{
CTX,
&session.ListSessionsRequest{},
func(ctx context.Context, t *testing.T, request *session.ListSessionsRequest) []*sessionAttr {
createdUser := createFullUser(ctx)
info := createSession(ctx, t, createdUser.GetUserId(), "agent", durationpb.New(time.Minute*5), map[string][]byte{"key": []byte("value")})
request.Queries = append(request.Queries, &session.SearchQuery{Query: &session.SearchQuery_UserIdQuery{UserIdQuery: &session.UserIDQuery{Id: createdUser.GetUserId()}}})
return []*sessionAttr{info}
},
},
wantExpirationWindow: time.Minute * 5,
wantFactors: []wantFactor{wantUserFactor},
want: &session.ListSessionsResponse{
Details: &object.ListDetails{
TotalResult: 1,
Timestamp: timestamppb.Now(),
},
Sessions: []*session.Session{
{
Metadata: map[string][]byte{"key": []byte("value")},
UserAgent: &session.UserAgent{
FingerprintId: gu.Ptr("agent"),
Ip: gu.Ptr("1.2.3.4"),
Description: gu.Ptr("Description"),
Header: map[string]*session.UserAgent_HeaderValues{
"foo": {Values: []string{"foo", "bar"}},
},
},
},
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
infos := tt.args.dep(CTX, t, tt.args.req)
retryDuration, tick := integration.WaitForAndTickWithMaxDuration(tt.args.ctx, time.Minute)
require.EventuallyWithT(t, func(ttt *assert.CollectT) {
got, err := Client.ListSessions(tt.args.ctx, tt.args.req)
if tt.wantErr {
assert.Error(ttt, err)
return
}
if !assert.NoError(ttt, err) {
return
}
if !assert.Equal(ttt, got.Details.TotalResult, tt.want.Details.TotalResult) || !assert.Len(ttt, got.Sessions, len(tt.want.Sessions)) {
return
}
for i := range infos {
tt.want.Sessions[i].Id = infos[i].ID
tt.want.Sessions[i].Sequence = infos[i].Details.GetSequence()
tt.want.Sessions[i].CreationDate = infos[i].Details.GetChangeDate()
tt.want.Sessions[i].ChangeDate = infos[i].Details.GetChangeDate()
verifySession(ttt, got.Sessions[i], tt.want.Sessions[i], time.Minute, tt.wantExpirationWindow, infos[i].UserID, tt.wantFactors...)
}
integration.AssertListDetails(ttt, tt.want, got)
}, retryDuration, tick)
})
}
}

View File

@@ -0,0 +1,74 @@
//go:build integration
package session_test
import (
"context"
"os"
"testing"
"time"
"github.com/zitadel/logging"
"github.com/zitadel/zitadel/internal/integration"
session "github.com/zitadel/zitadel/pkg/grpc/session/v2beta"
"github.com/zitadel/zitadel/pkg/grpc/user/v2"
)
var (
CTX context.Context
IAMOwnerCTX context.Context
UserCTX context.Context
Instance *integration.Instance
Client session.SessionServiceClient
User *user.AddHumanUserResponse
DeactivatedUser *user.AddHumanUserResponse
LockedUser *user.AddHumanUserResponse
)
func TestMain(m *testing.M) {
os.Exit(func() int {
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Minute)
defer cancel()
Instance = integration.NewInstance(ctx)
Client = Instance.Client.SessionV2beta
CTX = Instance.WithAuthorization(ctx, integration.UserTypeOrgOwner)
IAMOwnerCTX = Instance.WithAuthorization(ctx, integration.UserTypeIAMOwner)
UserCTX = Instance.WithAuthorization(ctx, integration.UserTypeNoPermission)
User = createFullUser(CTX)
DeactivatedUser = createDeactivatedUser(CTX)
LockedUser = createLockedUser(CTX)
return m.Run()
}())
}
func createFullUser(ctx context.Context) *user.AddHumanUserResponse {
userResp := Instance.CreateHumanUser(ctx)
Instance.Client.UserV2.VerifyEmail(ctx, &user.VerifyEmailRequest{
UserId: userResp.GetUserId(),
VerificationCode: userResp.GetEmailCode(),
})
Instance.Client.UserV2.VerifyPhone(ctx, &user.VerifyPhoneRequest{
UserId: userResp.GetUserId(),
VerificationCode: userResp.GetPhoneCode(),
})
Instance.SetUserPassword(ctx, userResp.GetUserId(), integration.UserPassword, false)
Instance.RegisterUserPasskey(ctx, userResp.GetUserId())
return userResp
}
func createDeactivatedUser(ctx context.Context) *user.AddHumanUserResponse {
userResp := Instance.CreateHumanUser(ctx)
_, err := Instance.Client.UserV2.DeactivateUser(ctx, &user.DeactivateUserRequest{UserId: userResp.GetUserId()})
logging.OnError(err).Fatal("deactivate human user")
return userResp
}
func createLockedUser(ctx context.Context) *user.AddHumanUserResponse {
userResp := Instance.CreateHumanUser(ctx)
_, err := Instance.Client.UserV2.LockUser(ctx, &user.LockUserRequest{UserId: userResp.GetUserId()})
logging.OnError(err).Fatal("lock human user")
return userResp
}

View File

@@ -5,7 +5,6 @@ package session_test
import (
"context"
"fmt"
"os"
"testing"
"time"
@@ -14,7 +13,6 @@ import (
"github.com/pquerna/otp/totp"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/zitadel/logging"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/metadata"
"google.golang.org/grpc/status"
@@ -29,62 +27,6 @@ import (
"github.com/zitadel/zitadel/pkg/grpc/user/v2"
)
var (
CTX context.Context
IAMOwnerCTX context.Context
Instance *integration.Instance
Client session.SessionServiceClient
User *user.AddHumanUserResponse
DeactivatedUser *user.AddHumanUserResponse
LockedUser *user.AddHumanUserResponse
)
func TestMain(m *testing.M) {
os.Exit(func() int {
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Minute)
defer cancel()
Instance = integration.NewInstance(ctx)
Client = Instance.Client.SessionV2beta
CTX = Instance.WithAuthorization(ctx, integration.UserTypeOrgOwner)
IAMOwnerCTX = Instance.WithAuthorization(ctx, integration.UserTypeIAMOwner)
User = createFullUser(CTX)
DeactivatedUser = createDeactivatedUser(CTX)
LockedUser = createLockedUser(CTX)
return m.Run()
}())
}
func createFullUser(ctx context.Context) *user.AddHumanUserResponse {
userResp := Instance.CreateHumanUser(ctx)
Instance.Client.UserV2.VerifyEmail(ctx, &user.VerifyEmailRequest{
UserId: userResp.GetUserId(),
VerificationCode: userResp.GetEmailCode(),
})
Instance.Client.UserV2.VerifyPhone(ctx, &user.VerifyPhoneRequest{
UserId: userResp.GetUserId(),
VerificationCode: userResp.GetPhoneCode(),
})
Instance.SetUserPassword(ctx, userResp.GetUserId(), integration.UserPassword, false)
Instance.RegisterUserPasskey(ctx, userResp.GetUserId())
return userResp
}
func createDeactivatedUser(ctx context.Context) *user.AddHumanUserResponse {
userResp := Instance.CreateHumanUser(ctx)
_, err := Instance.Client.UserV2.DeactivateUser(ctx, &user.DeactivateUserRequest{UserId: userResp.GetUserId()})
logging.OnError(err).Fatal("deactivate human user")
return userResp
}
func createLockedUser(ctx context.Context) *user.AddHumanUserResponse {
userResp := Instance.CreateHumanUser(ctx)
_, err := Instance.Client.UserV2.LockUser(ctx, &user.LockUserRequest{UserId: userResp.GetUserId()})
logging.OnError(err).Fatal("lock human user")
return userResp
}
func verifyCurrentSession(t testing.TB, id, token string, sequence uint64, window time.Duration, metadata map[string][]byte, userAgent *session.UserAgent, expirationWindow time.Duration, userID string, factors ...wantFactor) *session.Session {
t.Helper()
require.NotEmpty(t, id)
@@ -96,15 +38,25 @@ func verifyCurrentSession(t testing.TB, id, token string, sequence uint64, windo
})
require.NoError(t, err)
s := resp.GetSession()
want := &session.Session{
Id: id,
Sequence: sequence,
Metadata: metadata,
UserAgent: userAgent,
}
verifySession(t, s, want, window, expirationWindow, userID, factors...)
return s
}
assert.Equal(t, id, s.GetId())
func verifySession(t assert.TestingT, s *session.Session, want *session.Session, window time.Duration, expirationWindow time.Duration, userID string, factors ...wantFactor) {
assert.Equal(t, want.Id, s.GetId())
assert.WithinRange(t, s.GetCreationDate().AsTime(), time.Now().Add(-window), time.Now().Add(window))
assert.WithinRange(t, s.GetChangeDate().AsTime(), time.Now().Add(-window), time.Now().Add(window))
assert.Equal(t, sequence, s.GetSequence())
assert.Equal(t, metadata, s.GetMetadata())
assert.Equal(t, want.Sequence, s.GetSequence())
assert.Equal(t, want.Metadata, s.GetMetadata())
if !proto.Equal(userAgent, s.GetUserAgent()) {
t.Errorf("user agent =\n%v\nwant\n%v", s.GetUserAgent(), userAgent)
if !proto.Equal(want.UserAgent, s.GetUserAgent()) {
t.Errorf("user agent =\n%v\nwant\n%v", s.GetUserAgent(), want.UserAgent)
}
if expirationWindow == 0 {
assert.Nil(t, s.GetExpirationDate())
@@ -113,7 +65,6 @@ func verifyCurrentSession(t testing.TB, id, token string, sequence uint64, windo
}
verifyFactors(t, s.GetFactors(), window, userID, factors)
return s
}
type wantFactor int
@@ -129,7 +80,7 @@ const (
wantOTPEmailFactor
)
func verifyFactors(t testing.TB, factors *session.Factors, window time.Duration, userID string, want []wantFactor) {
func verifyFactors(t assert.TestingT, factors *session.Factors, window time.Duration, userID string, want []wantFactor) {
for _, w := range want {
switch w {
case wantUserFactor:
@@ -194,8 +145,15 @@ func TestServer_CreateSession(t *testing.T) {
},
},
{
name: "user agent",
name: "full session",
req: &session.CreateSessionRequest{
Checks: &session.Checks{
User: &session.CheckUser{
Search: &session.CheckUser_UserId{
UserId: User.GetUserId(),
},
},
},
Metadata: map[string][]byte{"foo": []byte("bar")},
UserAgent: &session.UserAgent{
FingerprintId: gu.Ptr("fingerPrintID"),
@@ -205,6 +163,7 @@ func TestServer_CreateSession(t *testing.T) {
"foo": {Values: []string{"foo", "bar"}},
},
},
Lifetime: durationpb.New(5 * time.Minute),
},
want: &session.CreateSessionResponse{
Details: &object.Details{
@@ -212,14 +171,6 @@ func TestServer_CreateSession(t *testing.T) {
ResourceOwner: Instance.ID(),
},
},
wantUserAgent: &session.UserAgent{
FingerprintId: gu.Ptr("fingerPrintID"),
Ip: gu.Ptr("1.2.3.4"),
Description: gu.Ptr("Description"),
Header: map[string]*session.UserAgent_HeaderValues{
"foo": {Values: []string{"foo", "bar"}},
},
},
},
{
name: "negative lifetime",
@@ -229,40 +180,6 @@ func TestServer_CreateSession(t *testing.T) {
},
wantErr: true,
},
{
name: "lifetime",
req: &session.CreateSessionRequest{
Metadata: map[string][]byte{"foo": []byte("bar")},
Lifetime: durationpb.New(5 * time.Minute),
},
want: &session.CreateSessionResponse{
Details: &object.Details{
ChangeDate: timestamppb.Now(),
ResourceOwner: Instance.ID(),
},
},
wantExpirationWindow: 5 * time.Minute,
},
{
name: "with user",
req: &session.CreateSessionRequest{
Checks: &session.Checks{
User: &session.CheckUser{
Search: &session.CheckUser_UserId{
UserId: User.GetUserId(),
},
},
},
Metadata: map[string][]byte{"foo": []byte("bar")},
},
want: &session.CreateSessionResponse{
Details: &object.Details{
ChangeDate: timestamppb.Now(),
ResourceOwner: Instance.ID(),
},
},
wantFactors: []wantFactor{wantUserFactor},
},
{
name: "deactivated user",
req: &session.CreateSessionRequest{
@@ -340,8 +257,6 @@ func TestServer_CreateSession(t *testing.T) {
}
require.NoError(t, err)
integration.AssertDetails(t, tt.want, got)
verifyCurrentSession(t, got.GetSessionId(), got.GetSessionToken(), got.GetDetails().GetSequence(), time.Minute, tt.req.GetMetadata(), tt.wantUserAgent, tt.wantExpirationWindow, User.GetUserId(), tt.wantFactors...)
})
}
}
@@ -946,21 +861,30 @@ func Test_ZITADEL_API_missing_authentication(t *testing.T) {
require.NoError(t, err)
ctx := metadata.AppendToOutgoingContext(context.Background(), "Authorization", fmt.Sprintf("Bearer %s", createResp.GetSessionToken()))
sessionResp, err := Client.GetSession(ctx, &session.GetSessionRequest{SessionId: createResp.GetSessionId()})
require.Error(t, err)
require.Nil(t, sessionResp)
retryDuration, tick := integration.WaitForAndTickWithMaxDuration(ctx, time.Minute)
require.EventuallyWithT(t, func(tt *assert.CollectT) {
sessionResp, err := Client.GetSession(ctx, &session.GetSessionRequest{SessionId: createResp.GetSessionId()})
if !assert.Error(tt, err) {
return
}
assert.Nil(tt, sessionResp)
}, retryDuration, tick)
}
func Test_ZITADEL_API_success(t *testing.T) {
id, token, _, _ := Instance.CreateVerifiedWebAuthNSession(t, CTX, User.GetUserId())
ctx := integration.WithAuthorizationToken(context.Background(), token)
sessionResp, err := Client.GetSession(ctx, &session.GetSessionRequest{SessionId: id})
require.NoError(t, err)
webAuthN := sessionResp.GetSession().GetFactors().GetWebAuthN()
require.NotNil(t, id, webAuthN.GetVerifiedAt().AsTime())
require.True(t, webAuthN.GetUserVerified())
retryDuration, tick := integration.WaitForAndTickWithMaxDuration(ctx, time.Minute)
require.EventuallyWithT(t, func(tt *assert.CollectT) {
sessionResp, err := Client.GetSession(ctx, &session.GetSessionRequest{SessionId: id})
if !assert.NoError(tt, err) {
return
}
webAuthN := sessionResp.GetSession().GetFactors().GetWebAuthN()
assert.NotNil(tt, id, webAuthN.GetVerifiedAt().AsTime())
assert.True(tt, webAuthN.GetUserVerified())
}, retryDuration, tick)
}
func Test_ZITADEL_API_session_not_found(t *testing.T) {
@@ -968,18 +892,30 @@ func Test_ZITADEL_API_session_not_found(t *testing.T) {
// test session token works
ctx := integration.WithAuthorizationToken(context.Background(), token)
_, err := Client.GetSession(ctx, &session.GetSessionRequest{SessionId: id})
require.NoError(t, err)
retryDuration, tick := integration.WaitForAndTickWithMaxDuration(ctx, time.Minute)
require.EventuallyWithT(t, func(tt *assert.CollectT) {
_, err := Client.GetSession(ctx, &session.GetSessionRequest{SessionId: id})
if !assert.NoError(tt, err) {
return
}
}, retryDuration, tick)
//terminate the session and test it does not work anymore
_, err = Client.DeleteSession(CTX, &session.DeleteSessionRequest{
_, err := Client.DeleteSession(CTX, &session.DeleteSessionRequest{
SessionId: id,
SessionToken: gu.Ptr(token),
})
require.NoError(t, err)
ctx = integration.WithAuthorizationToken(context.Background(), token)
_, err = Client.GetSession(ctx, &session.GetSessionRequest{SessionId: id})
require.Error(t, err)
retryDuration, tick = integration.WaitForAndTickWithMaxDuration(ctx, time.Minute)
require.EventuallyWithT(t, func(tt *assert.CollectT) {
_, err := Client.GetSession(ctx, &session.GetSessionRequest{SessionId: id})
if !assert.Error(tt, err) {
return
}
}, retryDuration, tick)
}
func Test_ZITADEL_API_session_expired(t *testing.T) {
@@ -987,8 +923,13 @@ func Test_ZITADEL_API_session_expired(t *testing.T) {
// test session token works
ctx := integration.WithAuthorizationToken(context.Background(), token)
_, err := Client.GetSession(ctx, &session.GetSessionRequest{SessionId: id})
require.NoError(t, err)
retryDuration, tick := integration.WaitForAndTickWithMaxDuration(ctx, time.Minute)
require.EventuallyWithT(t, func(tt *assert.CollectT) {
_, err := Client.GetSession(ctx, &session.GetSessionRequest{SessionId: id})
if !assert.NoError(tt, err) {
return
}
}, retryDuration, tick)
// ensure session expires and does not work anymore
time.Sleep(20 * time.Second)

View File

@@ -6,6 +6,7 @@ import (
"github.com/zitadel/zitadel/internal/api/authz"
"github.com/zitadel/zitadel/internal/api/grpc/server"
"github.com/zitadel/zitadel/internal/command"
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/query"
session "github.com/zitadel/zitadel/pkg/grpc/session/v2beta"
)
@@ -16,6 +17,8 @@ type Server struct {
session.UnimplementedSessionServiceServer
command *command.Commands
query *query.Queries
checkPermission domain.PermissionCheck
}
type Config struct{}
@@ -23,10 +26,12 @@ type Config struct{}
func CreateServer(
command *command.Commands,
query *query.Queries,
checkPermission domain.PermissionCheck,
) *Server {
return &Server{
command: command,
query: query,
command: command,
query: query,
checkPermission: checkPermission,
}
}

View File

@@ -32,7 +32,7 @@ var (
)
func (s *Server) GetSession(ctx context.Context, req *session.GetSessionRequest) (*session.GetSessionResponse, error) {
res, err := s.query.SessionByID(ctx, true, req.GetSessionId(), req.GetSessionToken())
res, err := s.query.SessionByID(ctx, true, req.GetSessionId(), req.GetSessionToken(), s.checkPermission)
if err != nil {
return nil, err
}
@@ -46,7 +46,7 @@ func (s *Server) ListSessions(ctx context.Context, req *session.ListSessionsRequ
if err != nil {
return nil, err
}
sessions, err := s.query.SearchSessions(ctx, queries)
sessions, err := s.query.SearchSessions(ctx, queries, s.checkPermission)
if err != nil {
return nil, err
}