From 2e8c3b5a53f0bfa97fb1e0b20610ed20a8df0e12 Mon Sep 17 00:00:00 2001 From: Livio Spring Date: Thu, 16 Nov 2023 08:35:50 +0200 Subject: [PATCH] feat: allow session deletion without session token (#6889) * fix: add resource owner of user and change the one of session to instance * use user resource owner from session projection * fix session permission check * integration tests and fixes * update api docs --- cmd/defaults.yaml | 5 + internal/api/grpc/session/v2/session.go | 2 +- .../session/v2/session_integration_test.go | 79 ++++- internal/command/auth_request.go | 4 +- internal/command/auth_request_test.go | 123 ++------ internal/command/main_test.go | 11 + internal/command/oidc_session.go | 2 +- internal/command/oidc_session_test.go | 8 +- internal/command/session.go | 37 +-- internal/command/session_model.go | 8 +- internal/command/session_test.go | 272 ++++++++++++------ internal/command/session_webauhtn.go | 4 +- internal/query/projection/session.go | 6 +- internal/query/projection/session_test.go | 27 +- internal/query/session.go | 20 +- internal/query/sessions_test.go | 114 ++++---- internal/repository/session/session.go | 13 +- .../session/v2beta/session_service.proto | 14 +- 18 files changed, 448 insertions(+), 301 deletions(-) diff --git a/cmd/defaults.yaml b/cmd/defaults.yaml index 82fef45409..ea4c1fc5bf 100644 --- a/cmd/defaults.yaml +++ b/cmd/defaults.yaml @@ -953,6 +953,7 @@ InternalAuthZ: - "project.grant.member.delete" - "events.read" - "milestones.read" + - "session.delete" - Role: "IAM_OWNER_VIEWER" Permissions: - "iam.read" @@ -1030,6 +1031,7 @@ InternalAuthZ: - "project.grant.member.read" - "project.grant.member.write" - "project.grant.member.delete" + - "session.delete" - Role: "IAM_USER_MANAGER" Permissions: - "org.read" @@ -1053,6 +1055,7 @@ InternalAuthZ: - "project.grant.write" - "project.grant.delete" - "project.grant.member.read" + - "session.delete" - Role: "ORG_OWNER" Permissions: - "org.read" @@ -1102,6 +1105,7 @@ InternalAuthZ: - "project.grant.member.read" - "project.grant.member.write" - "project.grant.member.delete" + - "session.delete" - Role: "ORG_USER_MANAGER" Permissions: - "org.read" @@ -1116,6 +1120,7 @@ InternalAuthZ: - "policy.read" - "project.read" - "project.role.read" + - "session.delete" - Role: "ORG_OWNER_VIEWER" Permissions: - "org.read" diff --git a/internal/api/grpc/session/v2/session.go b/internal/api/grpc/session/v2/session.go index a3aa481010..0d85aabddb 100644 --- a/internal/api/grpc/session/v2/session.go +++ b/internal/api/grpc/session/v2/session.go @@ -355,7 +355,7 @@ func (s *Server) checksToCommand(ctx context.Context, checks *session.Checks) ([ // trigger activity log for session for user activity.Trigger(ctx, user.ResourceOwner, user.ID, activity.SessionAPI) - sessionChecks = append(sessionChecks, command.CheckUser(user.ID)) + sessionChecks = append(sessionChecks, command.CheckUser(user.ID, user.ResourceOwner)) } if password := checks.GetPassword(); password != nil { sessionChecks = append(sessionChecks, command.CheckPassword(password.GetPassword())) diff --git a/internal/api/grpc/session/v2/session_integration_test.go b/internal/api/grpc/session/v2/session_integration_test.go index abd49ac8c1..d632cfa7bd 100644 --- a/internal/api/grpc/session/v2/session_integration_test.go +++ b/internal/api/grpc/session/v2/session_integration_test.go @@ -158,7 +158,7 @@ func TestServer_CreateSession(t *testing.T) { }, want: &session.CreateSessionResponse{ Details: &object.Details{ - ResourceOwner: Tester.Organisation.ID, + ResourceOwner: Tester.Instance.InstanceID(), }, }, }, @@ -177,7 +177,7 @@ func TestServer_CreateSession(t *testing.T) { }, want: &session.CreateSessionResponse{ Details: &object.Details{ - ResourceOwner: Tester.Organisation.ID, + ResourceOwner: Tester.Instance.InstanceID(), }, }, wantUserAgent: &session.UserAgent{ @@ -205,7 +205,7 @@ func TestServer_CreateSession(t *testing.T) { }, want: &session.CreateSessionResponse{ Details: &object.Details{ - ResourceOwner: Tester.Organisation.ID, + ResourceOwner: Tester.Instance.InstanceID(), }, }, wantExpirationWindow: 5 * time.Minute, @@ -224,7 +224,7 @@ func TestServer_CreateSession(t *testing.T) { }, want: &session.CreateSessionResponse{ Details: &object.Details{ - ResourceOwner: Tester.Organisation.ID, + ResourceOwner: Tester.Instance.InstanceID(), }, }, wantFactors: []wantFactor{wantUserFactor}, @@ -660,6 +660,77 @@ func TestServer_SetSession_expired(t *testing.T) { require.Error(t, err) } +func TestServer_DeleteSession_token(t *testing.T) { + createResp, err := Client.CreateSession(CTX, &session.CreateSessionRequest{}) + require.NoError(t, err) + + _, err = Client.DeleteSession(CTX, &session.DeleteSessionRequest{ + SessionId: createResp.GetSessionId(), + SessionToken: gu.Ptr("invalid"), + }) + require.Error(t, err) + + _, err = Client.DeleteSession(CTX, &session.DeleteSessionRequest{ + SessionId: createResp.GetSessionId(), + SessionToken: gu.Ptr(createResp.GetSessionToken()), + }) + require.NoError(t, err) +} + +func TestServer_DeleteSession_own_session(t *testing.T) { + // create two users for the test and a session each to get tokens for authorization + user1 := Tester.CreateHumanUser(CTX) + Tester.SetUserPassword(CTX, user1.GetUserId(), integration.UserPassword) + _, token1, _, _ := Tester.CreatePasswordSession(t, CTX, user1.GetUserId(), integration.UserPassword) + + user2 := Tester.CreateHumanUser(CTX) + Tester.SetUserPassword(CTX, user2.GetUserId(), integration.UserPassword) + _, token2, _, _ := Tester.CreatePasswordSession(t, CTX, user2.GetUserId(), integration.UserPassword) + + // create a new session for the first user + createResp, err := Client.CreateSession(CTX, &session.CreateSessionRequest{ + Checks: &session.Checks{ + User: &session.CheckUser{ + Search: &session.CheckUser_UserId{ + UserId: user1.GetUserId(), + }, + }, + }, + }) + require.NoError(t, err) + + // delete the new (user1) session must not be possible with user (has no permission) + _, err = Client.DeleteSession(Tester.WithAuthorizationToken(context.Background(), token2), &session.DeleteSessionRequest{ + SessionId: createResp.GetSessionId(), + }) + require.Error(t, err) + + // delete the new (user1) session by himself + _, err = Client.DeleteSession(Tester.WithAuthorizationToken(context.Background(), token1), &session.DeleteSessionRequest{ + SessionId: createResp.GetSessionId(), + }) + require.NoError(t, err) +} + +func TestServer_DeleteSession_with_permission(t *testing.T) { + createResp, err := Client.CreateSession(CTX, &session.CreateSessionRequest{ + Checks: &session.Checks{ + User: &session.CheckUser{ + Search: &session.CheckUser_UserId{ + UserId: User.GetUserId(), + }, + }, + }, + }) + require.NoError(t, err) + + // delete the new session by ORG_OWNER + _, err = Client.DeleteSession(Tester.WithAuthorization(context.Background(), integration.OrgOwner), &session.DeleteSessionRequest{ + SessionId: createResp.GetSessionId(), + }) + require.NoError(t, err) +} + func Test_ZITADEL_API_missing_authentication(t *testing.T) { // create new, empty session createResp, err := Client.CreateSession(CTX, &session.CreateSessionRequest{}) diff --git a/internal/command/auth_request.go b/internal/command/auth_request.go index e54f0d65f4..b0b0226100 100644 --- a/internal/command/auth_request.go +++ b/internal/command/auth_request.go @@ -90,7 +90,7 @@ func (c *Commands) LinkSessionToAuthRequest(ctx context.Context, id, sessionID, if checkLoginClient && authz.GetCtxData(ctx).UserID != writeModel.LoginClient { return nil, nil, errors.ThrowPermissionDenied(nil, "COMMAND-rai9Y", "Errors.AuthRequest.WrongLoginClient") } - sessionWriteModel := NewSessionWriteModel(sessionID, authz.GetCtxData(ctx).OrgID) + sessionWriteModel := NewSessionWriteModel(sessionID, authz.GetInstance(ctx).InstanceID()) err = c.eventstore.FilterToQueryReducer(ctx, sessionWriteModel) if err != nil { return nil, nil, err @@ -98,7 +98,7 @@ func (c *Commands) LinkSessionToAuthRequest(ctx context.Context, id, sessionID, if err = sessionWriteModel.CheckIsActive(); err != nil { return nil, nil, err } - if err := c.sessionPermission(ctx, sessionWriteModel, sessionToken, domain.PermissionSessionWrite); err != nil { + if err := c.sessionTokenVerifier(ctx, sessionToken, sessionWriteModel.AggregateID, sessionWriteModel.TokenID); err != nil { return nil, nil, err } diff --git a/internal/command/auth_request_test.go b/internal/command/auth_request_test.go index 2a390a9227..e077c90f97 100644 --- a/internal/command/auth_request_test.go +++ b/internal/command/auth_request_test.go @@ -164,9 +164,8 @@ func TestCommands_AddAuthRequest(t *testing.T) { func TestCommands_LinkSessionToAuthRequest(t *testing.T) { mockCtx := authz.NewMockContext("instanceID", "orgID", "loginClient") type fields struct { - eventstore *eventstore.Eventstore - tokenVerifier func(ctx context.Context, sessionToken, sessionID, tokenID string) (err error) - checkPermission domain.PermissionCheck + eventstore *eventstore.Eventstore + tokenVerifier func(ctx context.Context, sessionToken, sessionID, tokenID string) (err error) } type args struct { ctx context.Context @@ -192,10 +191,7 @@ func TestCommands_LinkSessionToAuthRequest(t *testing.T) { eventstore: eventstoreExpect(t, expectFilter(), ), - tokenVerifier: func(ctx context.Context, sessionToken, sessionID, tokenID string) (err error) { - return nil - }, - checkPermission: newMockPermissionCheckNotAllowed(), + tokenVerifier: newMockTokenVerifierValid(), }, args{ ctx: mockCtx, @@ -235,10 +231,7 @@ func TestCommands_LinkSessionToAuthRequest(t *testing.T) { ), ), ), - tokenVerifier: func(ctx context.Context, sessionToken, sessionID, tokenID string) (err error) { - return nil - }, - checkPermission: newMockPermissionCheckAllowed(), + tokenVerifier: newMockTokenVerifierValid(), }, args{ ctx: mockCtx, @@ -274,10 +267,7 @@ func TestCommands_LinkSessionToAuthRequest(t *testing.T) { ), ), ), - tokenVerifier: func(ctx context.Context, sessionToken, sessionID, tokenID string) (err error) { - return nil - }, - checkPermission: newMockPermissionCheckAllowed(), + tokenVerifier: newMockTokenVerifierValid(), }, args{ ctx: authz.NewMockContext("instanceID", "orgID", "wrongLoginClient"), @@ -316,10 +306,7 @@ func TestCommands_LinkSessionToAuthRequest(t *testing.T) { ), expectFilter(), ), - tokenVerifier: func(ctx context.Context, sessionToken, sessionID, tokenID string) (err error) { - return nil - }, - checkPermission: newMockPermissionCheckNotAllowed(), + tokenVerifier: newMockTokenVerifierValid(), }, args{ ctx: mockCtx, @@ -357,7 +344,7 @@ func TestCommands_LinkSessionToAuthRequest(t *testing.T) { expectFilter( eventFromEventPusher( session.NewAddedEvent(mockCtx, - &session.NewAggregate("sessionID", "org1").Aggregate, + &session.NewAggregate("sessionID", "instance1").Aggregate, &domain.UserAgent{ FingerprintID: gu.Ptr("fp1"), IP: net.ParseIP("1.2.3.4"), @@ -366,15 +353,15 @@ func TestCommands_LinkSessionToAuthRequest(t *testing.T) { }, )), eventFromEventPusher( - session.NewUserCheckedEvent(mockCtx, &session.NewAggregate("sessionID", "org1").Aggregate, - "userID", testNow.Add(-5*time.Minute)), + session.NewUserCheckedEvent(mockCtx, &session.NewAggregate("sessionID", "instance1").Aggregate, + "userID", "org1", testNow.Add(-5*time.Minute)), ), eventFromEventPusher( - session.NewPasswordCheckedEvent(mockCtx, &session.NewAggregate("sessionID", "org1").Aggregate, + session.NewPasswordCheckedEvent(mockCtx, &session.NewAggregate("sessionID", "instance1").Aggregate, testNow.Add(-5*time.Minute)), ), eventFromEventPusher( - session.NewLifetimeSetEvent(mockCtx, &session.NewAggregate("sessionID", "org1").Aggregate, + session.NewLifetimeSetEvent(mockCtx, &session.NewAggregate("sessionID", "instance1").Aggregate, 2*time.Minute), ), ), @@ -390,57 +377,6 @@ func TestCommands_LinkSessionToAuthRequest(t *testing.T) { wantErr: caos_errs.ThrowPreconditionFailed(nil, "COMMAND-Hkl3d", "Errors.Session.Expired"), }, }, - { - "missing permission", - fields{ - eventstore: eventstoreExpect(t, - expectFilter( - eventFromEventPusher( - authrequest.NewAddedEvent(mockCtx, &authrequest.NewAggregate("V2_id", "instanceID").Aggregate, - "loginClient", - "clientID", - "redirectURI", - "state", - "nonce", - []string{"openid"}, - []string{"audience"}, - domain.OIDCResponseTypeCode, - nil, - nil, - nil, - nil, - nil, - nil, - ), - ), - ), - expectFilter( - eventFromEventPusher( - session.NewAddedEvent(mockCtx, - &session.NewAggregate("sessionID", "org1").Aggregate, - &domain.UserAgent{ - FingerprintID: gu.Ptr("fp1"), - IP: net.ParseIP("1.2.3.4"), - Description: gu.Ptr("firefox"), - Header: http.Header{"foo": []string{"bar"}}, - }, - )), - ), - ), - tokenVerifier: func(ctx context.Context, sessionToken, sessionID, tokenID string) (err error) { - return nil - }, - checkPermission: newMockPermissionCheckNotAllowed(), - }, - args{ - ctx: mockCtx, - id: "V2_id", - sessionID: "sessionID", - }, - res{ - wantErr: caos_errs.ThrowPermissionDenied(nil, "AUTHZ-HKJD33", "Errors.PermissionDenied"), - }, - }, { "invalid session token", fields{ @@ -468,7 +404,7 @@ func TestCommands_LinkSessionToAuthRequest(t *testing.T) { expectFilter( eventFromEventPusher( session.NewAddedEvent(mockCtx, - &session.NewAggregate("sessionID", "org1").Aggregate, + &session.NewAggregate("sessionID", "instance1").Aggregate, &domain.UserAgent{ FingerprintID: gu.Ptr("fp1"), IP: net.ParseIP("1.2.3.4"), @@ -478,9 +414,7 @@ func TestCommands_LinkSessionToAuthRequest(t *testing.T) { )), ), ), - tokenVerifier: func(ctx context.Context, sessionToken, sessionID, tokenID string) (err error) { - return caos_errs.ThrowPermissionDenied(nil, "COMMAND-sGr42", "Errors.Session.Token.Invalid") - }, + tokenVerifier: newMockTokenVerifierInvalid(), }, args{ ctx: mockCtx, @@ -519,7 +453,7 @@ func TestCommands_LinkSessionToAuthRequest(t *testing.T) { expectFilter( eventFromEventPusher( session.NewAddedEvent(mockCtx, - &session.NewAggregate("sessionID", "org1").Aggregate, + &session.NewAggregate("sessionID", "instance1").Aggregate, &domain.UserAgent{ FingerprintID: gu.Ptr("fp1"), IP: net.ParseIP("1.2.3.4"), @@ -528,15 +462,15 @@ func TestCommands_LinkSessionToAuthRequest(t *testing.T) { }, )), eventFromEventPusher( - session.NewUserCheckedEvent(mockCtx, &session.NewAggregate("sessionID", "org1").Aggregate, - "userID", testNow), + session.NewUserCheckedEvent(mockCtx, &session.NewAggregate("sessionID", "instance1").Aggregate, + "userID", "org1", testNow), ), eventFromEventPusher( - session.NewPasswordCheckedEvent(mockCtx, &session.NewAggregate("sessionID", "org1").Aggregate, + session.NewPasswordCheckedEvent(mockCtx, &session.NewAggregate("sessionID", "instance1").Aggregate, testNow), ), eventFromEventPusherWithCreationDateNow( - session.NewLifetimeSetEvent(mockCtx, &session.NewAggregate("sessionID", "org1").Aggregate, + session.NewLifetimeSetEvent(mockCtx, &session.NewAggregate("sessionID", "instance1").Aggregate, 2*time.Minute), ), ), @@ -549,10 +483,7 @@ func TestCommands_LinkSessionToAuthRequest(t *testing.T) { ), ), ), - tokenVerifier: func(ctx context.Context, sessionToken, sessionID, tokenID string) (err error) { - return nil - }, - checkPermission: newMockPermissionCheckAllowed(), + tokenVerifier: newMockTokenVerifierValid(), }, args{ ctx: mockCtx, @@ -607,7 +538,7 @@ func TestCommands_LinkSessionToAuthRequest(t *testing.T) { expectFilter( eventFromEventPusher( session.NewAddedEvent(mockCtx, - &session.NewAggregate("sessionID", "org1").Aggregate, + &session.NewAggregate("sessionID", "instance1").Aggregate, &domain.UserAgent{ FingerprintID: gu.Ptr("fp1"), IP: net.ParseIP("1.2.3.4"), @@ -616,15 +547,15 @@ func TestCommands_LinkSessionToAuthRequest(t *testing.T) { }, )), eventFromEventPusher( - session.NewUserCheckedEvent(mockCtx, &session.NewAggregate("sessionID", "org1").Aggregate, - "userID", testNow), + session.NewUserCheckedEvent(mockCtx, &session.NewAggregate("sessionID", "instance1").Aggregate, + "userID", "org1", testNow), ), eventFromEventPusher( - session.NewPasswordCheckedEvent(mockCtx, &session.NewAggregate("sessionID", "org1").Aggregate, + session.NewPasswordCheckedEvent(mockCtx, &session.NewAggregate("sessionID", "instance1").Aggregate, testNow), ), eventFromEventPusherWithCreationDateNow( - session.NewLifetimeSetEvent(mockCtx, &session.NewAggregate("sessionID", "org1").Aggregate, + session.NewLifetimeSetEvent(mockCtx, &session.NewAggregate("sessionID", "instance1").Aggregate, 2*time.Minute), ), ), @@ -637,10 +568,7 @@ func TestCommands_LinkSessionToAuthRequest(t *testing.T) { ), ), ), - tokenVerifier: func(ctx context.Context, sessionToken, sessionID, tokenID string) (err error) { - return nil - }, - checkPermission: newMockPermissionCheckAllowed(), + tokenVerifier: newMockTokenVerifierValid(), }, args{ ctx: authz.NewMockContext("instanceID", "orgID", "loginClient"), @@ -675,7 +603,6 @@ func TestCommands_LinkSessionToAuthRequest(t *testing.T) { c := &Commands{ eventstore: tt.fields.eventstore, sessionTokenVerifier: tt.fields.tokenVerifier, - checkPermission: tt.fields.checkPermission, } details, got, err := c.LinkSessionToAuthRequest(tt.args.ctx, tt.args.id, tt.args.sessionID, tt.args.sessionToken, tt.args.checkLoginClient) require.ErrorIs(t, err, tt.res.wantErr) diff --git a/internal/command/main_test.go b/internal/command/main_test.go index 5458811eb7..341665de5a 100644 --- a/internal/command/main_test.go +++ b/internal/command/main_test.go @@ -241,6 +241,17 @@ func newMockPermissionCheckNotAllowed() domain.PermissionCheck { } } +func newMockTokenVerifierValid() func(ctx context.Context, sessionToken, sessionID, tokenID string) (err error) { + return func(ctx context.Context, sessionToken, sessionID, tokenID string) (err error) { + return nil + } +} +func newMockTokenVerifierInvalid() func(ctx context.Context, sessionToken, sessionID, tokenID string) (err error) { + return func(ctx context.Context, sessionToken, sessionID, tokenID string) (err error) { + return errors.ThrowPermissionDenied(nil, "COMMAND-sGr42", "Errors.Session.Token.Invalid") + } +} + type plainHasher struct { x string // arbitrary info that triggers update when different from encoding } diff --git a/internal/command/oidc_session.go b/internal/command/oidc_session.go index bcefbefd03..5854423110 100644 --- a/internal/command/oidc_session.go +++ b/internal/command/oidc_session.go @@ -153,7 +153,7 @@ func (c *Commands) newOIDCSessionAddEvents(ctx context.Context, authRequestID st if err = authRequestWriteModel.CheckAuthenticated(); err != nil { return nil, err } - sessionWriteModel := NewSessionWriteModel(authRequestWriteModel.SessionID, authz.GetCtxData(ctx).OrgID) + sessionWriteModel := NewSessionWriteModel(authRequestWriteModel.SessionID, authz.GetInstance(ctx).InstanceID()) err = c.eventstore.FilterToQueryReducer(ctx, sessionWriteModel) if err != nil { return nil, err diff --git a/internal/command/oidc_session_test.go b/internal/command/oidc_session_test.go index 7a8e7b4027..e797fcda4b 100644 --- a/internal/command/oidc_session_test.go +++ b/internal/command/oidc_session_test.go @@ -166,7 +166,7 @@ func TestCommands_AddOIDCSessionAccessToken(t *testing.T) { expectFilter( eventFromEventPusher( session.NewAddedEvent(context.Background(), - &session.NewAggregate("sessionID", "org1").Aggregate, + &session.NewAggregate("sessionID", "instance1").Aggregate, &domain.UserAgent{ FingerprintID: gu.Ptr("fp1"), IP: net.ParseIP("1.2.3.4"), @@ -177,7 +177,7 @@ func TestCommands_AddOIDCSessionAccessToken(t *testing.T) { ), eventFromEventPusher( session.NewUserCheckedEvent(context.Background(), &session.NewAggregate("sessionID", "instanceID").Aggregate, - "userID", testNow), + "userID", "org1", testNow), ), eventFromEventPusher( session.NewPasswordCheckedEvent(context.Background(), &session.NewAggregate("sessionID", "instanceID").Aggregate, @@ -367,7 +367,7 @@ func TestCommands_AddOIDCSessionRefreshAndAccessToken(t *testing.T) { expectFilter( eventFromEventPusher( session.NewAddedEvent(context.Background(), - &session.NewAggregate("sessionID", "org1").Aggregate, + &session.NewAggregate("sessionID", "instance1").Aggregate, &domain.UserAgent{ FingerprintID: gu.Ptr("fp1"), IP: net.ParseIP("1.2.3.4"), @@ -378,7 +378,7 @@ func TestCommands_AddOIDCSessionRefreshAndAccessToken(t *testing.T) { ), eventFromEventPusher( session.NewUserCheckedEvent(context.Background(), &session.NewAggregate("sessionID", "instanceID").Aggregate, - "userID", testNow), + "userID", "org1", testNow), ), eventFromEventPusher( session.NewPasswordCheckedEvent(context.Background(), &session.NewAggregate("sessionID", "instanceID").Aggregate, diff --git a/internal/command/session.go b/internal/command/session.go index ec434818cf..c62b6ccee8 100644 --- a/internal/command/session.go +++ b/internal/command/session.go @@ -55,12 +55,12 @@ func (c *Commands) NewSessionCommands(cmds []SessionCommand, session *SessionWri } // CheckUser defines a user check to be executed for a session update -func CheckUser(id string) SessionCommand { +func CheckUser(id string, resourceOwner string) SessionCommand { return func(ctx context.Context, cmd *SessionCommands) error { if cmd.sessionWriteModel.UserID != "" && id != "" && cmd.sessionWriteModel.UserID != id { return caos_errs.ThrowInvalidArgument(nil, "", "user change not possible") } - return cmd.UserChecked(ctx, id, cmd.now()) + return cmd.UserChecked(ctx, id, resourceOwner, cmd.now()) } } @@ -170,10 +170,11 @@ func (s *SessionCommands) Start(ctx context.Context, userAgent *domain.UserAgent s.eventCommands = append(s.eventCommands, session.NewAddedEvent(ctx, s.sessionWriteModel.aggregate, userAgent)) } -func (s *SessionCommands) UserChecked(ctx context.Context, userID string, checkedAt time.Time) error { - s.eventCommands = append(s.eventCommands, session.NewUserCheckedEvent(ctx, s.sessionWriteModel.aggregate, userID, checkedAt)) +func (s *SessionCommands) UserChecked(ctx context.Context, userID, resourceOwner string, checkedAt time.Time) error { + s.eventCommands = append(s.eventCommands, session.NewUserCheckedEvent(ctx, s.sessionWriteModel.aggregate, userID, resourceOwner, checkedAt)) // set the userID so other checks can use it s.sessionWriteModel.UserID = userID + s.sessionWriteModel.UserResourceOwner = resourceOwner return nil } @@ -267,7 +268,7 @@ func (s *SessionCommands) gethumanWriteModel(ctx context.Context) (*HumanWriteMo if s.sessionWriteModel.UserID == "" { return nil, caos_errs.ThrowPreconditionFailed(nil, "COMMAND-eeR2e", "Errors.User.UserIDMissing") } - humanWriteModel := NewHumanWriteModel(s.sessionWriteModel.UserID, "") + humanWriteModel := NewHumanWriteModel(s.sessionWriteModel.UserID, s.sessionWriteModel.UserResourceOwner) err := s.eventstore.FilterToQueryReducer(ctx, humanWriteModel) if err != nil { return nil, err @@ -296,7 +297,7 @@ func (c *Commands) CreateSession(ctx context.Context, cmds []SessionCommand, met if err != nil { return nil, err } - sessionWriteModel := NewSessionWriteModel(sessionID, authz.GetCtxData(ctx).OrgID) + sessionWriteModel := NewSessionWriteModel(sessionID, authz.GetInstance(ctx).InstanceID()) err = c.eventstore.FilterToQueryReducer(ctx, sessionWriteModel) if err != nil { return nil, err @@ -307,12 +308,12 @@ func (c *Commands) CreateSession(ctx context.Context, cmds []SessionCommand, met } func (c *Commands) UpdateSession(ctx context.Context, sessionID, sessionToken string, cmds []SessionCommand, metadata map[string][]byte, lifetime time.Duration) (set *SessionChanged, err error) { - sessionWriteModel := NewSessionWriteModel(sessionID, authz.GetCtxData(ctx).OrgID) + sessionWriteModel := NewSessionWriteModel(sessionID, authz.GetInstance(ctx).InstanceID()) err = c.eventstore.FilterToQueryReducer(ctx, sessionWriteModel) if err != nil { return nil, err } - if err := c.sessionPermission(ctx, sessionWriteModel, sessionToken, domain.PermissionSessionWrite); err != nil { + if err := c.sessionTokenVerifier(ctx, sessionToken, sessionWriteModel.AggregateID, sessionWriteModel.TokenID); err != nil { return nil, err } cmd := c.NewSessionCommands(cmds, sessionWriteModel) @@ -328,12 +329,12 @@ func (c *Commands) TerminateSessionWithoutTokenCheck(ctx context.Context, sessio } func (c *Commands) terminateSession(ctx context.Context, sessionID, sessionToken string, mustCheckToken bool) (*domain.ObjectDetails, error) { - sessionWriteModel := NewSessionWriteModel(sessionID, "") + sessionWriteModel := NewSessionWriteModel(sessionID, authz.GetInstance(ctx).InstanceID()) if err := c.eventstore.FilterToQueryReducer(ctx, sessionWriteModel); err != nil { return nil, err } if mustCheckToken { - if err := c.sessionPermission(ctx, sessionWriteModel, sessionToken, domain.PermissionSessionDelete); err != nil { + if err := c.checkSessionTerminationPermission(ctx, sessionWriteModel, sessionToken); err != nil { return nil, err } } @@ -386,13 +387,17 @@ func (c *Commands) updateSession(ctx context.Context, checks *SessionCommands, m return changed, nil } -// sessionPermission will check that the provided sessionToken is correct or -// if empty, check that the caller is granted the necessary permission -func (c *Commands) sessionPermission(ctx context.Context, sessionWriteModel *SessionWriteModel, sessionToken, permission string) (err error) { - if sessionToken == "" { - return c.checkPermission(ctx, permission, authz.GetCtxData(ctx).OrgID, sessionWriteModel.AggregateID) +// checkSessionTerminationPermission will check that the provided sessionToken is correct or +// if empty, check that the caller is either terminating the own session or +// is granted the "session.delete" permission on the resource owner of the authenticated user. +func (c *Commands) checkSessionTerminationPermission(ctx context.Context, model *SessionWriteModel, token string) error { + if token != "" { + return c.sessionTokenVerifier(ctx, token, model.AggregateID, model.TokenID) } - return c.sessionTokenVerifier(ctx, sessionToken, sessionWriteModel.AggregateID, sessionWriteModel.TokenID) + if model.UserID != "" && model.UserID == authz.GetCtxData(ctx).UserID { + return nil + } + return c.checkPermission(ctx, domain.PermissionSessionDelete, model.UserResourceOwner, model.UserID) } func sessionTokenCreator(idGenerator id.Generator, sessionAlg crypto.EncryptionAlgorithm) func(sessionID string) (id string, token string, err error) { diff --git a/internal/command/session_model.go b/internal/command/session_model.go index 4df4b6b558..4929904576 100644 --- a/internal/command/session_model.go +++ b/internal/command/session_model.go @@ -39,6 +39,7 @@ type SessionWriteModel struct { TokenID string UserID string + UserResourceOwner string UserCheckedAt time.Time PasswordCheckedAt time.Time IntentCheckedAt time.Time @@ -58,14 +59,14 @@ type SessionWriteModel struct { aggregate *eventstore.Aggregate } -func NewSessionWriteModel(sessionID string, resourceOwner string) *SessionWriteModel { +func NewSessionWriteModel(sessionID string, instanceID string) *SessionWriteModel { return &SessionWriteModel{ WriteModel: eventstore.WriteModel{ AggregateID: sessionID, - ResourceOwner: resourceOwner, + ResourceOwner: instanceID, }, Metadata: make(map[string][]byte), - aggregate: &session.NewAggregate(sessionID, resourceOwner).Aggregate, + aggregate: &session.NewAggregate(sessionID, instanceID).Aggregate, } } @@ -141,6 +142,7 @@ func (wm *SessionWriteModel) reduceAdded(e *session.AddedEvent) { func (wm *SessionWriteModel) reduceUserChecked(e *session.UserCheckedEvent) { wm.UserID = e.UserID + wm.UserResourceOwner = e.UserResourceOwner wm.UserCheckedAt = e.CheckedAt } diff --git a/internal/command/session_test.go b/internal/command/session_test.go index 429e376823..ba6827f52d 100644 --- a/internal/command/session_test.go +++ b/internal/command/session_test.go @@ -204,7 +204,7 @@ func TestCommands_CreateSession(t *testing.T) { }, }, args{ - ctx: authz.NewMockContext("", "org1", ""), + ctx: authz.NewMockContext("instance1", "", ""), userAgent: &domain.UserAgent{ FingerprintID: gu.Ptr("fp1"), IP: net.ParseIP("1.2.3.4"), @@ -231,7 +231,7 @@ func TestCommands_CreateSession(t *testing.T) { }, }, args{ - ctx: authz.NewMockContext("", "org1", ""), + ctx: authz.NewMockContext("instance1", "", ""), userAgent: &domain.UserAgent{ FingerprintID: gu.Ptr("fp1"), IP: net.ParseIP("1.2.3.4"), @@ -244,7 +244,7 @@ func TestCommands_CreateSession(t *testing.T) { expectFilter(), expectPush( session.NewAddedEvent(context.Background(), - &session.NewAggregate("sessionID", "org1").Aggregate, + &session.NewAggregate("sessionID", "instance1").Aggregate, &domain.UserAgent{ FingerprintID: gu.Ptr("fp1"), IP: net.ParseIP("1.2.3.4"), @@ -252,15 +252,15 @@ func TestCommands_CreateSession(t *testing.T) { Header: http.Header{"foo": []string{"bar"}}, }, ), - session.NewLifetimeSetEvent(context.Background(), &session.NewAggregate("sessionID", "org1").Aggregate, 10*time.Minute), - session.NewTokenSetEvent(context.Background(), &session.NewAggregate("sessionID", "org1").Aggregate, + session.NewLifetimeSetEvent(context.Background(), &session.NewAggregate("sessionID", "instance1").Aggregate, 10*time.Minute), + session.NewTokenSetEvent(context.Background(), &session.NewAggregate("sessionID", "instance1").Aggregate, "tokenID", ), ), }, res{ want: &SessionChanged{ - ObjectDetails: &domain.ObjectDetails{ResourceOwner: "org1"}, + ObjectDetails: &domain.ObjectDetails{ResourceOwner: "instance1"}, ID: "sessionID", NewToken: "token", }, @@ -326,7 +326,7 @@ func TestCommands_UpdateSession(t *testing.T) { expectFilter( eventFromEventPusher( session.NewAddedEvent(context.Background(), - &session.NewAggregate("sessionID", "org1").Aggregate, + &session.NewAggregate("sessionID", "instance1").Aggregate, &domain.UserAgent{ FingerprintID: gu.Ptr("fp1"), IP: net.ParseIP("1.2.3.4"), @@ -335,13 +335,11 @@ func TestCommands_UpdateSession(t *testing.T) { }, )), eventFromEventPusher( - session.NewTokenSetEvent(context.Background(), &session.NewAggregate("sessionID", "org1").Aggregate, + session.NewTokenSetEvent(context.Background(), &session.NewAggregate("sessionID", "instance1").Aggregate, "tokenID")), ), ), - tokenVerifier: func(ctx context.Context, sessionToken, sessionID, tokenID string) (err error) { - return caos_errs.ThrowPermissionDenied(nil, "COMMAND-sGr42", "Errors.Session.Token.Invalid") - }, + tokenVerifier: newMockTokenVerifierInvalid(), }, args{ ctx: context.Background(), @@ -359,7 +357,7 @@ func TestCommands_UpdateSession(t *testing.T) { expectFilter( eventFromEventPusher( session.NewAddedEvent(context.Background(), - &session.NewAggregate("sessionID", "org1").Aggregate, + &session.NewAggregate("sessionID", "instance1").Aggregate, &domain.UserAgent{ FingerprintID: gu.Ptr("fp1"), IP: net.ParseIP("1.2.3.4"), @@ -368,7 +366,7 @@ func TestCommands_UpdateSession(t *testing.T) { }, )), eventFromEventPusher( - session.NewTokenSetEvent(context.Background(), &session.NewAggregate("sessionID", "org1").Aggregate, + session.NewTokenSetEvent(context.Background(), &session.NewAggregate("sessionID", "instance1").Aggregate, "tokenID")), ), ), @@ -384,7 +382,7 @@ func TestCommands_UpdateSession(t *testing.T) { res{ want: &SessionChanged{ ObjectDetails: &domain.ObjectDetails{ - ResourceOwner: "org1", + ResourceOwner: "instance1", }, ID: "sessionID", NewToken: "", @@ -463,7 +461,7 @@ func TestCommands_updateSession(t *testing.T) { args{ ctx: context.Background(), checks: &SessionCommands{ - sessionWriteModel: NewSessionWriteModel("sessionID", "org1"), + sessionWriteModel: NewSessionWriteModel("sessionID", "instance1"), sessionCommands: []SessionCommand{ func(ctx context.Context, cmd *SessionCommands) error { return caos_errs.ThrowInternal(nil, "id", "check failed") @@ -481,16 +479,16 @@ func TestCommands_updateSession(t *testing.T) { eventstore: eventstoreExpect(t), }, args{ - ctx: context.Background(), + ctx: authz.NewMockContext("instance1", "", ""), checks: &SessionCommands{ - sessionWriteModel: NewSessionWriteModel("sessionID", "org1"), + sessionWriteModel: NewSessionWriteModel("sessionID", "instance1"), sessionCommands: []SessionCommand{}, }, }, res{ want: &SessionChanged{ ObjectDetails: &domain.ObjectDetails{ - ResourceOwner: "org1", + ResourceOwner: "instance1", }, ID: "sessionID", NewToken: "", @@ -503,9 +501,9 @@ func TestCommands_updateSession(t *testing.T) { eventstore: eventstoreExpect(t), }, args{ - ctx: context.Background(), + ctx: authz.NewMockContext("instance1", "", ""), checks: &SessionCommands{ - sessionWriteModel: NewSessionWriteModel("sessionID", "org1"), + sessionWriteModel: NewSessionWriteModel("sessionID", "instance1"), sessionCommands: []SessionCommand{}, eventstore: eventstoreExpect(t), createToken: func(sessionID string) (string, string, error) { @@ -528,19 +526,19 @@ func TestCommands_updateSession(t *testing.T) { fields{ eventstore: eventstoreExpect(t, expectPush( - session.NewLifetimeSetEvent(context.Background(), &session.NewAggregate("sessionID", "org1").Aggregate, + session.NewLifetimeSetEvent(context.Background(), &session.NewAggregate("sessionID", "instance1").Aggregate, 10*time.Minute, ), - session.NewTokenSetEvent(context.Background(), &session.NewAggregate("sessionID", "org1").Aggregate, + session.NewTokenSetEvent(context.Background(), &session.NewAggregate("sessionID", "instance1").Aggregate, "tokenID", ), ), ), }, args{ - ctx: context.Background(), + ctx: authz.NewMockContext("instance1", "", ""), checks: &SessionCommands{ - sessionWriteModel: NewSessionWriteModel("sessionID", "org1"), + sessionWriteModel: NewSessionWriteModel("sessionID", "instance1"), sessionCommands: []SessionCommand{}, eventstore: eventstoreExpect(t), createToken: func(sessionID string) (string, string, error) { @@ -557,7 +555,7 @@ func TestCommands_updateSession(t *testing.T) { res{ want: &SessionChanged{ ObjectDetails: &domain.ObjectDetails{ - ResourceOwner: "org1", + ResourceOwner: "instance1", }, ID: "sessionID", NewToken: "token", @@ -569,27 +567,27 @@ func TestCommands_updateSession(t *testing.T) { fields{ eventstore: eventstoreExpect(t, expectPush( - session.NewUserCheckedEvent(context.Background(), &session.NewAggregate("sessionID", "org1").Aggregate, - "userID", testNow, + session.NewUserCheckedEvent(context.Background(), &session.NewAggregate("sessionID", "instance1").Aggregate, + "userID", "org1", testNow, ), - session.NewPasswordCheckedEvent(context.Background(), &session.NewAggregate("sessionID", "org1").Aggregate, + session.NewPasswordCheckedEvent(context.Background(), &session.NewAggregate("sessionID", "instance1").Aggregate, testNow, ), - session.NewMetadataSetEvent(context.Background(), &session.NewAggregate("sessionID", "org1").Aggregate, + session.NewMetadataSetEvent(context.Background(), &session.NewAggregate("sessionID", "instance1").Aggregate, map[string][]byte{"key": []byte("value")}, ), - session.NewTokenSetEvent(context.Background(), &session.NewAggregate("sessionID", "org1").Aggregate, + session.NewTokenSetEvent(context.Background(), &session.NewAggregate("sessionID", "instance1").Aggregate, "tokenID", ), ), ), }, args{ - ctx: context.Background(), + ctx: authz.NewMockContext("instance1", "", ""), checks: &SessionCommands{ - sessionWriteModel: NewSessionWriteModel("sessionID", "org1"), + sessionWriteModel: NewSessionWriteModel("sessionID", "instance1"), sessionCommands: []SessionCommand{ - CheckUser("userID"), + CheckUser("userID", "org1"), CheckPassword("password"), }, eventstore: eventstoreExpect(t, @@ -621,7 +619,7 @@ func TestCommands_updateSession(t *testing.T) { res{ want: &SessionChanged{ ObjectDetails: &domain.ObjectDetails{ - ResourceOwner: "org1", + ResourceOwner: "instance1", }, ID: "sessionID", NewToken: "token", @@ -634,11 +632,11 @@ func TestCommands_updateSession(t *testing.T) { eventstore: eventstoreExpect(t), }, args{ - ctx: context.Background(), + ctx: authz.NewMockContext("instance1", "", ""), checks: &SessionCommands{ - sessionWriteModel: NewSessionWriteModel("sessionID", "org1"), + sessionWriteModel: NewSessionWriteModel("sessionID", "instance1"), sessionCommands: []SessionCommand{ - CheckUser("userID"), + CheckUser("userID", "org1"), CheckIntent("intent", "aW50ZW50"), }, eventstore: eventstoreExpect(t, @@ -673,11 +671,11 @@ func TestCommands_updateSession(t *testing.T) { eventstore: eventstoreExpect(t), }, args{ - ctx: context.Background(), + ctx: authz.NewMockContext("instance1", "", ""), checks: &SessionCommands{ - sessionWriteModel: NewSessionWriteModel("sessionID", "org1"), + sessionWriteModel: NewSessionWriteModel("sessionID", "instance1"), sessionCommands: []SessionCommand{ - CheckUser("userID"), + CheckUser("userID", "org1"), CheckIntent("intent", "aW50ZW50"), }, eventstore: eventstoreExpect(t, @@ -722,11 +720,11 @@ func TestCommands_updateSession(t *testing.T) { eventstore: eventstoreExpect(t), }, args{ - ctx: context.Background(), + ctx: authz.NewMockContext("instance1", "", ""), checks: &SessionCommands{ - sessionWriteModel: NewSessionWriteModel("sessionID", "org1"), + sessionWriteModel: NewSessionWriteModel("sessionID", "instance1"), sessionCommands: []SessionCommand{ - CheckUser("userID"), + CheckUser("userID", "org1"), CheckIntent("intent2", "aW50ZW50"), }, eventstore: eventstoreExpect(t), @@ -753,23 +751,23 @@ func TestCommands_updateSession(t *testing.T) { fields{ eventstore: eventstoreExpect(t, expectPush( - session.NewUserCheckedEvent(context.Background(), &session.NewAggregate("sessionID", "org1").Aggregate, - "userID", testNow), - session.NewIntentCheckedEvent(context.Background(), &session.NewAggregate("sessionID", "org1").Aggregate, + session.NewUserCheckedEvent(context.Background(), &session.NewAggregate("sessionID", "instance1").Aggregate, + "userID", "org1", testNow), + session.NewIntentCheckedEvent(context.Background(), &session.NewAggregate("sessionID", "instance1").Aggregate, testNow), - session.NewMetadataSetEvent(context.Background(), &session.NewAggregate("sessionID", "org1").Aggregate, + session.NewMetadataSetEvent(context.Background(), &session.NewAggregate("sessionID", "instance1").Aggregate, map[string][]byte{"key": []byte("value")}), - session.NewTokenSetEvent(context.Background(), &session.NewAggregate("sessionID", "org1").Aggregate, + session.NewTokenSetEvent(context.Background(), &session.NewAggregate("sessionID", "instance1").Aggregate, "tokenID"), ), ), }, args{ - ctx: context.Background(), + ctx: authz.NewMockContext("instance1", "", ""), checks: &SessionCommands{ - sessionWriteModel: NewSessionWriteModel("sessionID", "org1"), + sessionWriteModel: NewSessionWriteModel("sessionID", "instance1"), sessionCommands: []SessionCommand{ - CheckUser("userID"), + CheckUser("userID", "org1"), CheckIntent("intent", "aW50ZW50"), }, eventstore: eventstoreExpect(t, @@ -807,7 +805,7 @@ func TestCommands_updateSession(t *testing.T) { res{ want: &SessionChanged{ ObjectDetails: &domain.ObjectDetails{ - ResourceOwner: "org1", + ResourceOwner: "instance1", }, ID: "sessionID", NewToken: "token", @@ -828,13 +826,13 @@ func TestCommands_updateSession(t *testing.T) { } func TestCheckTOTP(t *testing.T) { - ctx := authz.NewMockContext("", "org1", "user1") + ctx := authz.NewMockContext("instance1", "org1", "user1") cryptoAlg := crypto.CreateMockEncryptionAlg(gomock.NewController(t)) key, secret, err := domain.NewTOTPKey("example.com", "user1", cryptoAlg) require.NoError(t, err) - sessAgg := &session.NewAggregate("session1", "org1").Aggregate + sessAgg := &session.NewAggregate("session1", "instance1").Aggregate userAgg := &user.NewAggregate("user1", "org1").Aggregate code, err := totp.GenerateCode(key.Secret(), testNow) @@ -961,8 +959,9 @@ func TestCheckTOTP(t *testing.T) { func TestCommands_TerminateSession(t *testing.T) { type fields struct { - eventstore *eventstore.Eventstore - tokenVerifier func(ctx context.Context, sessionToken, sessionID, tokenID string) (err error) + eventstore func(t *testing.T) *eventstore.Eventstore + tokenVerifier func(ctx context.Context, sessionToken, sessionID, tokenID string) (err error) + checkPermission domain.PermissionCheck } type args struct { ctx context.Context @@ -982,7 +981,7 @@ func TestCommands_TerminateSession(t *testing.T) { { "eventstore failed", fields{ - eventstore: eventstoreExpect(t, + eventstore: expectEventstore( expectFilterError(caos_errs.ThrowInternal(nil, "id", "filter failed")), ), }, @@ -996,11 +995,11 @@ func TestCommands_TerminateSession(t *testing.T) { { "invalid session token", fields{ - eventstore: eventstoreExpect(t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( session.NewAddedEvent(context.Background(), - &session.NewAggregate("sessionID", "org1").Aggregate, + &session.NewAggregate("sessionID", "instance1").Aggregate, &domain.UserAgent{ FingerprintID: gu.Ptr("fp1"), IP: net.ParseIP("1.2.3.4"), @@ -1009,13 +1008,11 @@ func TestCommands_TerminateSession(t *testing.T) { }, )), eventFromEventPusher( - session.NewTokenSetEvent(context.Background(), &session.NewAggregate("sessionID", "org1").Aggregate, + session.NewTokenSetEvent(context.Background(), &session.NewAggregate("sessionID", "instance1").Aggregate, "tokenID")), ), ), - tokenVerifier: func(ctx context.Context, sessionToken, sessionID, tokenID string) (err error) { - return caos_errs.ThrowPermissionDenied(nil, "COMMAND-sGr42", "Errors.Session.Token.Invalid") - }, + tokenVerifier: newMockTokenVerifierInvalid(), }, args{ ctx: context.Background(), @@ -1027,13 +1024,13 @@ func TestCommands_TerminateSession(t *testing.T) { }, }, { - "not active", + "missing permission", fields{ - eventstore: eventstoreExpect(t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( session.NewAddedEvent(context.Background(), - &session.NewAggregate("sessionID", "org1").Aggregate, + &session.NewAggregate("sessionID", "instance1").Aggregate, &domain.UserAgent{ FingerprintID: gu.Ptr("fp1"), IP: net.ParseIP("1.2.3.4"), @@ -1042,10 +1039,41 @@ func TestCommands_TerminateSession(t *testing.T) { }, )), eventFromEventPusher( - session.NewTokenSetEvent(context.Background(), &session.NewAggregate("sessionID", "org1").Aggregate, + session.NewTokenSetEvent(context.Background(), &session.NewAggregate("sessionID", "instance1").Aggregate, + "tokenID")), + ), + ), + checkPermission: newMockPermissionCheckNotAllowed(), + }, + args{ + ctx: context.Background(), + sessionID: "sessionID", + sessionToken: "", + }, + res{ + err: caos_errs.ThrowPermissionDenied(nil, "AUTHZ-HKJD33", "Errors.PermissionDenied"), + }, + }, + { + "not active", + fields{ + eventstore: expectEventstore( + expectFilter( + eventFromEventPusher( + session.NewAddedEvent(context.Background(), + &session.NewAggregate("sessionID", "instance1").Aggregate, + &domain.UserAgent{ + FingerprintID: gu.Ptr("fp1"), + IP: net.ParseIP("1.2.3.4"), + Description: gu.Ptr("firefox"), + Header: http.Header{"foo": []string{"bar"}}, + }, + )), + eventFromEventPusher( + session.NewTokenSetEvent(context.Background(), &session.NewAggregate("sessionID", "instance1").Aggregate, "tokenID")), eventFromEventPusher( - session.NewTerminateEvent(context.Background(), &session.NewAggregate("sessionID", "org1").Aggregate)), + session.NewTerminateEvent(context.Background(), &session.NewAggregate("sessionID", "instance1").Aggregate)), ), ), tokenVerifier: func(ctx context.Context, sessionToken, sessionID, tokenID string) (err error) { @@ -1059,18 +1087,18 @@ func TestCommands_TerminateSession(t *testing.T) { }, res{ want: &domain.ObjectDetails{ - ResourceOwner: "org1", + ResourceOwner: "instance1", }, }, }, { "push failed", fields{ - eventstore: eventstoreExpect(t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( session.NewAddedEvent(context.Background(), - &session.NewAggregate("sessionID", "org1").Aggregate, + &session.NewAggregate("sessionID", "instance1").Aggregate, &domain.UserAgent{ FingerprintID: gu.Ptr("fp1"), IP: net.ParseIP("1.2.3.4"), @@ -1079,13 +1107,13 @@ func TestCommands_TerminateSession(t *testing.T) { }, )), eventFromEventPusher( - session.NewTokenSetEvent(context.Background(), &session.NewAggregate("sessionID", "org1").Aggregate, + session.NewTokenSetEvent(context.Background(), &session.NewAggregate("sessionID", "instance1").Aggregate, "tokenID"), ), ), expectPushFailed( caos_errs.ThrowInternal(nil, "id", "pushed failed"), - session.NewTerminateEvent(context.Background(), &session.NewAggregate("sessionID", "org1").Aggregate), + session.NewTerminateEvent(context.Background(), &session.NewAggregate("sessionID", "instance1").Aggregate), ), ), tokenVerifier: func(ctx context.Context, sessionToken, sessionID, tokenID string) (err error) { @@ -1102,13 +1130,13 @@ func TestCommands_TerminateSession(t *testing.T) { }, }, { - "terminate", + "terminate with token", fields{ - eventstore: eventstoreExpect(t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( session.NewAddedEvent(context.Background(), - &session.NewAggregate("sessionID", "org1").Aggregate, + &session.NewAggregate("sessionID", "instance1").Aggregate, &domain.UserAgent{ FingerprintID: gu.Ptr("fp1"), IP: net.ParseIP("1.2.3.4"), @@ -1117,12 +1145,12 @@ func TestCommands_TerminateSession(t *testing.T) { }, )), eventFromEventPusher( - session.NewTokenSetEvent(context.Background(), &session.NewAggregate("sessionID", "org1").Aggregate, + session.NewTokenSetEvent(context.Background(), &session.NewAggregate("sessionID", "instance1").Aggregate, "tokenID"), ), ), expectPush( - session.NewTerminateEvent(context.Background(), &session.NewAggregate("sessionID", "org1").Aggregate), + session.NewTerminateEvent(context.Background(), &session.NewAggregate("sessionID", "instance1").Aggregate), ), ), tokenVerifier: func(ctx context.Context, sessionToken, sessionID, tokenID string) (err error) { @@ -1136,7 +1164,90 @@ func TestCommands_TerminateSession(t *testing.T) { }, res{ want: &domain.ObjectDetails{ - ResourceOwner: "org1", + ResourceOwner: "instance1", + }, + }, + }, + { + "terminate own session", + fields{ + eventstore: expectEventstore( + expectFilter( + eventFromEventPusher( + session.NewAddedEvent(context.Background(), + &session.NewAggregate("sessionID", "instance1").Aggregate, + &domain.UserAgent{ + FingerprintID: gu.Ptr("fp1"), + IP: net.ParseIP("1.2.3.4"), + Description: gu.Ptr("firefox"), + Header: http.Header{"foo": []string{"bar"}}, + }, + ), + ), + eventFromEventPusher( + session.NewUserCheckedEvent(context.Background(), &session.NewAggregate("sessionID", "instance1").Aggregate, + "user1", "org1", testNow), + ), + eventFromEventPusher( + session.NewTokenSetEvent(context.Background(), &session.NewAggregate("sessionID", "instance1").Aggregate, + "tokenID"), + ), + ), + expectPush( + session.NewTerminateEvent(authz.NewMockContext("instance1", "org1", "user1"), &session.NewAggregate("sessionID", "instance1").Aggregate), + ), + ), + }, + args{ + ctx: authz.NewMockContext("instance1", "org1", "user1"), + sessionID: "sessionID", + sessionToken: "", + }, + res{ + want: &domain.ObjectDetails{ + ResourceOwner: "instance1", + }, + }, + }, + { + "terminate with permission", + fields{ + eventstore: expectEventstore( + expectFilter( + eventFromEventPusher( + session.NewAddedEvent(context.Background(), + &session.NewAggregate("sessionID", "instance1").Aggregate, + &domain.UserAgent{ + FingerprintID: gu.Ptr("fp1"), + IP: net.ParseIP("1.2.3.4"), + Description: gu.Ptr("firefox"), + Header: http.Header{"foo": []string{"bar"}}, + }, + ), + ), + eventFromEventPusher( + session.NewUserCheckedEvent(context.Background(), &session.NewAggregate("sessionID", "instance1").Aggregate, + "userID", "org1", testNow), + ), + eventFromEventPusher( + session.NewTokenSetEvent(context.Background(), &session.NewAggregate("sessionID", "instance1").Aggregate, + "tokenID"), + ), + ), + expectPush( + session.NewTerminateEvent(authz.NewMockContext("instance1", "org1", "admin1"), &session.NewAggregate("sessionID", "instance1").Aggregate), + ), + ), + checkPermission: newMockPermissionCheckAllowed(), + }, + args{ + ctx: authz.NewMockContext("instance1", "org1", "admin1"), + sessionID: "sessionID", + sessionToken: "", + }, + res{ + want: &domain.ObjectDetails{ + ResourceOwner: "instance1", }, }, }, @@ -1144,8 +1255,9 @@ func TestCommands_TerminateSession(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { c := &Commands{ - eventstore: tt.fields.eventstore, + eventstore: tt.fields.eventstore(t), sessionTokenVerifier: tt.fields.tokenVerifier, + checkPermission: tt.fields.checkPermission, } got, err := c.TerminateSession(tt.args.ctx, tt.args.sessionID, tt.args.sessionToken) require.ErrorIs(t, err, tt.res.err) diff --git a/internal/command/session_webauhtn.go b/internal/command/session_webauhtn.go index dd690a9e9b..991a519886 100644 --- a/internal/command/session_webauhtn.go +++ b/internal/command/session_webauhtn.go @@ -29,9 +29,9 @@ func (s *SessionCommands) getHumanWebAuthNTokens(ctx context.Context, userVerifi } func (s *SessionCommands) getHumanWebAuthNTokenReadModel(ctx context.Context, userVerification domain.UserVerificationRequirement) (readModel HumanWebAuthNTokensReadModel, err error) { - readModel = NewHumanU2FTokensReadModel(s.sessionWriteModel.UserID, "") + readModel = NewHumanU2FTokensReadModel(s.sessionWriteModel.UserID, s.sessionWriteModel.UserResourceOwner) if userVerification == domain.UserVerificationRequirementRequired { - readModel = NewHumanPasswordlessTokensReadModel(s.sessionWriteModel.UserID, "") + readModel = NewHumanPasswordlessTokensReadModel(s.sessionWriteModel.UserID, s.sessionWriteModel.UserResourceOwner) } err = s.eventstore.FilterToQueryReducer(ctx, readModel) if err != nil { diff --git a/internal/query/projection/session.go b/internal/query/projection/session.go index f8c181ea14..90736523f3 100644 --- a/internal/query/projection/session.go +++ b/internal/query/projection/session.go @@ -14,7 +14,7 @@ import ( ) const ( - SessionsProjectionTable = "projections.sessions7" + SessionsProjectionTable = "projections.sessions8" SessionColumnID = "id" SessionColumnCreationDate = "creation_date" @@ -25,6 +25,7 @@ const ( SessionColumnInstanceID = "instance_id" SessionColumnCreator = "creator" SessionColumnUserID = "user_id" + SessionColumnUserResourceOwner = "user_resource_owner" SessionColumnUserCheckedAt = "user_checked_at" SessionColumnPasswordCheckedAt = "password_checked_at" SessionColumnIntentCheckedAt = "intent_checked_at" @@ -64,6 +65,7 @@ func (*sessionProjection) Init() *old_handler.Check { handler.NewColumn(SessionColumnInstanceID, handler.ColumnTypeText), handler.NewColumn(SessionColumnCreator, handler.ColumnTypeText), handler.NewColumn(SessionColumnUserID, handler.ColumnTypeText, handler.Nullable()), + handler.NewColumn(SessionColumnUserResourceOwner, handler.ColumnTypeText, handler.Nullable()), handler.NewColumn(SessionColumnUserCheckedAt, handler.ColumnTypeTimestamp, handler.Nullable()), handler.NewColumn(SessionColumnPasswordCheckedAt, handler.ColumnTypeTimestamp, handler.Nullable()), handler.NewColumn(SessionColumnIntentCheckedAt, handler.ColumnTypeTimestamp, handler.Nullable()), @@ -213,6 +215,7 @@ func (p *sessionProjection) reduceUserChecked(event eventstore.Event) (*handler. handler.NewCol(SessionColumnChangeDate, e.CreationDate()), handler.NewCol(SessionColumnSequence, e.Sequence()), handler.NewCol(SessionColumnUserID, e.UserID), + handler.NewCol(SessionColumnUserResourceOwner, e.UserResourceOwner), handler.NewCol(SessionColumnUserCheckedAt, e.CheckedAt), }, []handler.Condition{ @@ -430,6 +433,7 @@ func (p *sessionProjection) reducePasswordChanged(event eventstore.Event) (*hand }, []handler.Condition{ handler.NewCond(SessionColumnUserID, e.Aggregate().ID), + handler.NewCond(SessionColumnInstanceID, e.Aggregate().InstanceID), handler.NewLessThanCond(SessionColumnPasswordCheckedAt, e.CreationDate()), }, ), nil diff --git a/internal/query/projection/session_test.go b/internal/query/projection/session_test.go index 7fcb5391b2..78218c88ab 100644 --- a/internal/query/projection/session_test.go +++ b/internal/query/projection/session_test.go @@ -51,7 +51,7 @@ func TestSessionProjection_reduces(t *testing.T) { executer: &testExecuter{ executions: []execution{ { - expectedStmt: "INSERT INTO projections.sessions7 (id, instance_id, creation_date, change_date, resource_owner, state, sequence, creator, user_agent_fingerprint_id, user_agent_description, user_agent_ip, user_agent_header) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12)", + expectedStmt: "INSERT INTO projections.sessions8 (id, instance_id, creation_date, change_date, resource_owner, state, sequence, creator, user_agent_fingerprint_id, user_agent_description, user_agent_ip, user_agent_header) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12)", expectedArgs: []interface{}{ "agg-id", "instance-id", @@ -79,6 +79,7 @@ func TestSessionProjection_reduces(t *testing.T) { session.AggregateType, []byte(`{ "userId": "user-id", + "userResourceOwner": "org-id", "checkedAt": "2023-05-04T00:00:00Z" }`), ), session.UserCheckedEventMapper), @@ -90,11 +91,12 @@ func TestSessionProjection_reduces(t *testing.T) { executer: &testExecuter{ executions: []execution{ { - expectedStmt: "UPDATE projections.sessions7 SET (change_date, sequence, user_id, user_checked_at) = ($1, $2, $3, $4) WHERE (id = $5) AND (instance_id = $6)", + expectedStmt: "UPDATE projections.sessions8 SET (change_date, sequence, user_id, user_resource_owner, user_checked_at) = ($1, $2, $3, $4, $5) WHERE (id = $6) AND (instance_id = $7)", expectedArgs: []interface{}{ anyArg{}, anyArg{}, "user-id", + "org-id", time.Date(2023, time.May, 4, 0, 0, 0, 0, time.UTC), "agg-id", "instance-id", @@ -122,7 +124,7 @@ func TestSessionProjection_reduces(t *testing.T) { executer: &testExecuter{ executions: []execution{ { - expectedStmt: "UPDATE projections.sessions7 SET (change_date, sequence, password_checked_at) = ($1, $2, $3) WHERE (id = $4) AND (instance_id = $5)", + expectedStmt: "UPDATE projections.sessions8 SET (change_date, sequence, password_checked_at) = ($1, $2, $3) WHERE (id = $4) AND (instance_id = $5)", expectedArgs: []interface{}{ anyArg{}, anyArg{}, @@ -154,7 +156,7 @@ func TestSessionProjection_reduces(t *testing.T) { executer: &testExecuter{ executions: []execution{ { - expectedStmt: "UPDATE projections.sessions7 SET (change_date, sequence, webauthn_checked_at, webauthn_user_verified) = ($1, $2, $3, $4) WHERE (id = $5) AND (instance_id = $6)", + expectedStmt: "UPDATE projections.sessions8 SET (change_date, sequence, webauthn_checked_at, webauthn_user_verified) = ($1, $2, $3, $4) WHERE (id = $5) AND (instance_id = $6)", expectedArgs: []interface{}{ anyArg{}, anyArg{}, @@ -186,7 +188,7 @@ func TestSessionProjection_reduces(t *testing.T) { executer: &testExecuter{ executions: []execution{ { - expectedStmt: "UPDATE projections.sessions7 SET (change_date, sequence, intent_checked_at) = ($1, $2, $3) WHERE (id = $4) AND (instance_id = $5)", + expectedStmt: "UPDATE projections.sessions8 SET (change_date, sequence, intent_checked_at) = ($1, $2, $3) WHERE (id = $4) AND (instance_id = $5)", expectedArgs: []interface{}{ anyArg{}, anyArg{}, @@ -217,7 +219,7 @@ func TestSessionProjection_reduces(t *testing.T) { executer: &testExecuter{ executions: []execution{ { - expectedStmt: "UPDATE projections.sessions7 SET (change_date, sequence, totp_checked_at) = ($1, $2, $3) WHERE (id = $4) AND (instance_id = $5)", + expectedStmt: "UPDATE projections.sessions8 SET (change_date, sequence, totp_checked_at) = ($1, $2, $3) WHERE (id = $4) AND (instance_id = $5)", expectedArgs: []interface{}{ anyArg{}, anyArg{}, @@ -248,7 +250,7 @@ func TestSessionProjection_reduces(t *testing.T) { executer: &testExecuter{ executions: []execution{ { - expectedStmt: "UPDATE projections.sessions7 SET (change_date, sequence, token_id) = ($1, $2, $3) WHERE (id = $4) AND (instance_id = $5)", + expectedStmt: "UPDATE projections.sessions8 SET (change_date, sequence, token_id) = ($1, $2, $3) WHERE (id = $4) AND (instance_id = $5)", expectedArgs: []interface{}{ anyArg{}, anyArg{}, @@ -281,7 +283,7 @@ func TestSessionProjection_reduces(t *testing.T) { executer: &testExecuter{ executions: []execution{ { - expectedStmt: "UPDATE projections.sessions7 SET (change_date, sequence, metadata) = ($1, $2, $3) WHERE (id = $4) AND (instance_id = $5)", + expectedStmt: "UPDATE projections.sessions8 SET (change_date, sequence, metadata) = ($1, $2, $3) WHERE (id = $4) AND (instance_id = $5)", expectedArgs: []interface{}{ anyArg{}, anyArg{}, @@ -314,7 +316,7 @@ func TestSessionProjection_reduces(t *testing.T) { executer: &testExecuter{ executions: []execution{ { - expectedStmt: "UPDATE projections.sessions7 SET (change_date, sequence, expiration) = ($1, $2, $3) WHERE (id = $4) AND (instance_id = $5)", + expectedStmt: "UPDATE projections.sessions8 SET (change_date, sequence, expiration) = ($1, $2, $3) WHERE (id = $4) AND (instance_id = $5)", expectedArgs: []interface{}{ anyArg{}, anyArg{}, @@ -343,7 +345,7 @@ func TestSessionProjection_reduces(t *testing.T) { executer: &testExecuter{ executions: []execution{ { - expectedStmt: "DELETE FROM projections.sessions7 WHERE (id = $1) AND (instance_id = $2)", + expectedStmt: "DELETE FROM projections.sessions8 WHERE (id = $1) AND (instance_id = $2)", expectedArgs: []interface{}{ "agg-id", "instance-id", @@ -370,7 +372,7 @@ func TestSessionProjection_reduces(t *testing.T) { executer: &testExecuter{ executions: []execution{ { - expectedStmt: "DELETE FROM projections.sessions7 WHERE (instance_id = $1)", + expectedStmt: "DELETE FROM projections.sessions8 WHERE (instance_id = $1)", expectedArgs: []interface{}{ "agg-id", }, @@ -400,10 +402,11 @@ func TestSessionProjection_reduces(t *testing.T) { executer: &testExecuter{ executions: []execution{ { - expectedStmt: "UPDATE projections.sessions7 SET password_checked_at = $1 WHERE (user_id = $2) AND (password_checked_at < $3)", + expectedStmt: "UPDATE projections.sessions8 SET password_checked_at = $1 WHERE (user_id = $2) AND (instance_id = $3) AND (password_checked_at < $4)", expectedArgs: []interface{}{ nil, "agg-id", + "instance-id", anyArg{}, }, }, diff --git a/internal/query/session.go b/internal/query/session.go index acf5ac3bd5..4507320abe 100644 --- a/internal/query/session.go +++ b/internal/query/session.go @@ -130,6 +130,10 @@ var ( name: projection.SessionColumnUserID, table: sessionsTable, } + SessionColumnUserResourceOwner = Column{ + name: projection.SessionColumnUserResourceOwner, + table: sessionsTable, + } SessionColumnUserCheckedAt = Column{ name: projection.SessionColumnUserCheckedAt, table: sessionsTable, @@ -287,10 +291,10 @@ func prepareSessionQuery(ctx context.Context, db prepareDatabase) (sq.SelectBuil SessionColumnResourceOwner.identifier(), SessionColumnCreator.identifier(), SessionColumnUserID.identifier(), + SessionColumnUserResourceOwner.identifier(), SessionColumnUserCheckedAt.identifier(), LoginNameNameCol.identifier(), HumanDisplayNameCol.identifier(), - UserResourceOwnerCol.identifier(), SessionColumnPasswordCheckedAt.identifier(), SessionColumnIntentCheckedAt.identifier(), SessionColumnWebAuthNCheckedAt.identifier(), @@ -314,10 +318,10 @@ func prepareSessionQuery(ctx context.Context, db prepareDatabase) (sq.SelectBuil var ( userID sql.NullString + userResourceOwner sql.NullString userCheckedAt sql.NullTime loginName sql.NullString displayName sql.NullString - userResourceOwner sql.NullString passwordCheckedAt sql.NullTime intentCheckedAt sql.NullTime webAuthNCheckedAt sql.NullTime @@ -341,10 +345,10 @@ func prepareSessionQuery(ctx context.Context, db prepareDatabase) (sq.SelectBuil &session.ResourceOwner, &session.Creator, &userID, + &userResourceOwner, &userCheckedAt, &loginName, &displayName, - &userResourceOwner, &passwordCheckedAt, &intentCheckedAt, &webAuthNCheckedAt, @@ -369,10 +373,10 @@ func prepareSessionQuery(ctx context.Context, db prepareDatabase) (sq.SelectBuil } session.UserFactor.UserID = userID.String + session.UserFactor.ResourceOwner = userResourceOwner.String session.UserFactor.UserCheckedAt = userCheckedAt.Time session.UserFactor.LoginName = loginName.String session.UserFactor.DisplayName = displayName.String - session.UserFactor.ResourceOwner = userResourceOwner.String session.PasswordFactor.PasswordCheckedAt = passwordCheckedAt.Time session.IntentFactor.IntentCheckedAt = intentCheckedAt.Time session.WebAuthNFactor.WebAuthNCheckedAt = webAuthNCheckedAt.Time @@ -400,10 +404,10 @@ func prepareSessionsQuery(ctx context.Context, db prepareDatabase) (sq.SelectBui SessionColumnResourceOwner.identifier(), SessionColumnCreator.identifier(), SessionColumnUserID.identifier(), + SessionColumnUserResourceOwner.identifier(), SessionColumnUserCheckedAt.identifier(), LoginNameNameCol.identifier(), HumanDisplayNameCol.identifier(), - UserResourceOwnerCol.identifier(), SessionColumnPasswordCheckedAt.identifier(), SessionColumnIntentCheckedAt.identifier(), SessionColumnWebAuthNCheckedAt.identifier(), @@ -426,10 +430,10 @@ func prepareSessionsQuery(ctx context.Context, db prepareDatabase) (sq.SelectBui var ( userID sql.NullString + userResourceOwner sql.NullString userCheckedAt sql.NullTime loginName sql.NullString displayName sql.NullString - userResourceOwner sql.NullString passwordCheckedAt sql.NullTime intentCheckedAt sql.NullTime webAuthNCheckedAt sql.NullTime @@ -450,10 +454,10 @@ func prepareSessionsQuery(ctx context.Context, db prepareDatabase) (sq.SelectBui &session.ResourceOwner, &session.Creator, &userID, + &userResourceOwner, &userCheckedAt, &loginName, &displayName, - &userResourceOwner, &passwordCheckedAt, &intentCheckedAt, &webAuthNCheckedAt, @@ -470,10 +474,10 @@ func prepareSessionsQuery(ctx context.Context, db prepareDatabase) (sq.SelectBui return nil, errors.ThrowInternal(err, "QUERY-SAfeg", "Errors.Internal") } session.UserFactor.UserID = userID.String + session.UserFactor.ResourceOwner = userResourceOwner.String session.UserFactor.UserCheckedAt = userCheckedAt.Time session.UserFactor.LoginName = loginName.String session.UserFactor.DisplayName = displayName.String - session.UserFactor.ResourceOwner = userResourceOwner.String session.PasswordFactor.PasswordCheckedAt = passwordCheckedAt.Time session.IntentFactor.IntentCheckedAt = intentCheckedAt.Time session.WebAuthNFactor.WebAuthNCheckedAt = webAuthNCheckedAt.Time diff --git a/internal/query/sessions_test.go b/internal/query/sessions_test.go index d78bedbbd5..ada4ef38f6 100644 --- a/internal/query/sessions_test.go +++ b/internal/query/sessions_test.go @@ -20,63 +20,63 @@ import ( ) var ( - expectedSessionQuery = regexp.QuoteMeta(`SELECT projections.sessions7.id,` + - ` projections.sessions7.creation_date,` + - ` projections.sessions7.change_date,` + - ` projections.sessions7.sequence,` + - ` projections.sessions7.state,` + - ` projections.sessions7.resource_owner,` + - ` projections.sessions7.creator,` + - ` projections.sessions7.user_id,` + - ` projections.sessions7.user_checked_at,` + + expectedSessionQuery = regexp.QuoteMeta(`SELECT projections.sessions8.id,` + + ` projections.sessions8.creation_date,` + + ` projections.sessions8.change_date,` + + ` projections.sessions8.sequence,` + + ` projections.sessions8.state,` + + ` projections.sessions8.resource_owner,` + + ` projections.sessions8.creator,` + + ` projections.sessions8.user_id,` + + ` projections.sessions8.user_resource_owner,` + + ` projections.sessions8.user_checked_at,` + ` projections.login_names2.login_name,` + ` projections.users8_humans.display_name,` + - ` projections.users8.resource_owner,` + - ` projections.sessions7.password_checked_at,` + - ` projections.sessions7.intent_checked_at,` + - ` projections.sessions7.webauthn_checked_at,` + - ` projections.sessions7.webauthn_user_verified,` + - ` projections.sessions7.totp_checked_at,` + - ` projections.sessions7.otp_sms_checked_at,` + - ` projections.sessions7.otp_email_checked_at,` + - ` projections.sessions7.metadata,` + - ` projections.sessions7.token_id,` + - ` projections.sessions7.user_agent_fingerprint_id,` + - ` projections.sessions7.user_agent_ip,` + - ` projections.sessions7.user_agent_description,` + - ` projections.sessions7.user_agent_header,` + - ` projections.sessions7.expiration` + - ` FROM projections.sessions7` + - ` LEFT JOIN projections.login_names2 ON projections.sessions7.user_id = projections.login_names2.user_id AND projections.sessions7.instance_id = projections.login_names2.instance_id` + - ` LEFT JOIN projections.users8_humans ON projections.sessions7.user_id = projections.users8_humans.user_id AND projections.sessions7.instance_id = projections.users8_humans.instance_id` + - ` LEFT JOIN projections.users8 ON projections.sessions7.user_id = projections.users8.id AND projections.sessions7.instance_id = projections.users8.instance_id` + + ` projections.sessions8.password_checked_at,` + + ` projections.sessions8.intent_checked_at,` + + ` projections.sessions8.webauthn_checked_at,` + + ` projections.sessions8.webauthn_user_verified,` + + ` projections.sessions8.totp_checked_at,` + + ` projections.sessions8.otp_sms_checked_at,` + + ` projections.sessions8.otp_email_checked_at,` + + ` projections.sessions8.metadata,` + + ` projections.sessions8.token_id,` + + ` projections.sessions8.user_agent_fingerprint_id,` + + ` projections.sessions8.user_agent_ip,` + + ` projections.sessions8.user_agent_description,` + + ` projections.sessions8.user_agent_header,` + + ` projections.sessions8.expiration` + + ` FROM projections.sessions8` + + ` LEFT JOIN projections.login_names2 ON projections.sessions8.user_id = projections.login_names2.user_id AND projections.sessions8.instance_id = projections.login_names2.instance_id` + + ` LEFT JOIN projections.users8_humans ON projections.sessions8.user_id = projections.users8_humans.user_id AND projections.sessions8.instance_id = projections.users8_humans.instance_id` + + ` LEFT JOIN projections.users8 ON projections.sessions8.user_id = projections.users8.id AND projections.sessions8.instance_id = projections.users8.instance_id` + ` AS OF SYSTEM TIME '-1 ms'`) - expectedSessionsQuery = regexp.QuoteMeta(`SELECT projections.sessions7.id,` + - ` projections.sessions7.creation_date,` + - ` projections.sessions7.change_date,` + - ` projections.sessions7.sequence,` + - ` projections.sessions7.state,` + - ` projections.sessions7.resource_owner,` + - ` projections.sessions7.creator,` + - ` projections.sessions7.user_id,` + - ` projections.sessions7.user_checked_at,` + + expectedSessionsQuery = regexp.QuoteMeta(`SELECT projections.sessions8.id,` + + ` projections.sessions8.creation_date,` + + ` projections.sessions8.change_date,` + + ` projections.sessions8.sequence,` + + ` projections.sessions8.state,` + + ` projections.sessions8.resource_owner,` + + ` projections.sessions8.creator,` + + ` projections.sessions8.user_id,` + + ` projections.sessions8.user_resource_owner,` + + ` projections.sessions8.user_checked_at,` + ` projections.login_names2.login_name,` + ` projections.users8_humans.display_name,` + - ` projections.users8.resource_owner,` + - ` projections.sessions7.password_checked_at,` + - ` projections.sessions7.intent_checked_at,` + - ` projections.sessions7.webauthn_checked_at,` + - ` projections.sessions7.webauthn_user_verified,` + - ` projections.sessions7.totp_checked_at,` + - ` projections.sessions7.otp_sms_checked_at,` + - ` projections.sessions7.otp_email_checked_at,` + - ` projections.sessions7.metadata,` + - ` projections.sessions7.expiration,` + + ` projections.sessions8.password_checked_at,` + + ` projections.sessions8.intent_checked_at,` + + ` projections.sessions8.webauthn_checked_at,` + + ` projections.sessions8.webauthn_user_verified,` + + ` projections.sessions8.totp_checked_at,` + + ` projections.sessions8.otp_sms_checked_at,` + + ` projections.sessions8.otp_email_checked_at,` + + ` projections.sessions8.metadata,` + + ` projections.sessions8.expiration,` + ` COUNT(*) OVER ()` + - ` FROM projections.sessions7` + - ` LEFT JOIN projections.login_names2 ON projections.sessions7.user_id = projections.login_names2.user_id AND projections.sessions7.instance_id = projections.login_names2.instance_id` + - ` LEFT JOIN projections.users8_humans ON projections.sessions7.user_id = projections.users8_humans.user_id AND projections.sessions7.instance_id = projections.users8_humans.instance_id` + - ` LEFT JOIN projections.users8 ON projections.sessions7.user_id = projections.users8.id AND projections.sessions7.instance_id = projections.users8.instance_id` + + ` FROM projections.sessions8` + + ` LEFT JOIN projections.login_names2 ON projections.sessions8.user_id = projections.login_names2.user_id AND projections.sessions8.instance_id = projections.login_names2.instance_id` + + ` LEFT JOIN projections.users8_humans ON projections.sessions8.user_id = projections.users8_humans.user_id AND projections.sessions8.instance_id = projections.users8_humans.instance_id` + + ` LEFT JOIN projections.users8 ON projections.sessions8.user_id = projections.users8.id AND projections.sessions8.instance_id = projections.users8.instance_id` + ` AS OF SYSTEM TIME '-1 ms'`) sessionCols = []string{ @@ -88,10 +88,10 @@ var ( "resource_owner", "creator", "user_id", + "user_resource_owner", "user_checked_at", "login_name", "display_name", - "user_resource_owner", "password_checked_at", "intent_checked_at", "webauthn_checked_at", @@ -117,10 +117,10 @@ var ( "resource_owner", "creator", "user_id", + "user_resource_owner", "user_checked_at", "login_name", "display_name", - "user_resource_owner", "password_checked_at", "intent_checked_at", "webauthn_checked_at", @@ -174,10 +174,10 @@ func Test_SessionsPrepare(t *testing.T) { "ro", "creator", "user-id", + "resourceOwner", testNow, "login-name", "display-name", - "resourceOwner", testNow, testNow, testNow, @@ -255,10 +255,10 @@ func Test_SessionsPrepare(t *testing.T) { "ro", "creator", "user-id", + "resourceOwner", testNow, "login-name", "display-name", - "resourceOwner", testNow, testNow, testNow, @@ -278,10 +278,10 @@ func Test_SessionsPrepare(t *testing.T) { "ro", "creator2", "user-id2", + "resourceOwner", testNow, "login-name2", "display-name2", - "resourceOwner", testNow, testNow, testNow, @@ -451,10 +451,10 @@ func Test_SessionPrepare(t *testing.T) { "ro", "creator", "user-id", + "resourceOwner", testNow, "login-name", "display-name", - "resourceOwner", testNow, testNow, testNow, diff --git a/internal/repository/session/session.go b/internal/repository/session/session.go index a63cb8cfb0..464c72971c 100644 --- a/internal/repository/session/session.go +++ b/internal/repository/session/session.go @@ -75,8 +75,9 @@ func AddedEventMapper(event eventstore.Event) (eventstore.Event, error) { type UserCheckedEvent struct { eventstore.BaseEvent `json:"-"` - UserID string `json:"userID"` - CheckedAt time.Time `json:"checkedAt"` + UserID string `json:"userID"` + UserResourceOwner string `json:"userResourceOwner"` + CheckedAt time.Time `json:"checkedAt"` } func (e *UserCheckedEvent) Payload() interface{} { @@ -90,7 +91,8 @@ func (e *UserCheckedEvent) UniqueConstraints() []*eventstore.UniqueConstraint { func NewUserCheckedEvent( ctx context.Context, aggregate *eventstore.Aggregate, - userID string, + userID, + userResourceOwner string, checkedAt time.Time, ) *UserCheckedEvent { return &UserCheckedEvent{ @@ -99,8 +101,9 @@ func NewUserCheckedEvent( aggregate, UserCheckedType, ), - UserID: userID, - CheckedAt: checkedAt, + UserID: userID, + UserResourceOwner: userResourceOwner, + CheckedAt: checkedAt, } } diff --git a/proto/zitadel/session/v2beta/session_service.proto b/proto/zitadel/session/v2beta/session_service.proto index 7a3a42a400..ef1108e2c7 100644 --- a/proto/zitadel/session/v2beta/session_service.proto +++ b/proto/zitadel/session/v2beta/session_service.proto @@ -290,12 +290,12 @@ message CreateSessionResponse{ string session_id = 2 [ (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { description: "\"id of the session\""; - example: "\"d654e6ba-70a3-48ef-a95d-37c8d8a7901a\""; + example: "\"222430354126975533\""; } ]; string session_token = 3 [ (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { - description: "\"token of the session, which is required for further updates of the session or the request other resources\""; + description: "\"The current token of the session, which is required for further updates of the session or the request other resources.\""; } ]; Challenges challenges = 4; @@ -308,7 +308,7 @@ message SetSessionRequest{ min_length: 1; max_length: 200; description: "\"id of the session to update\""; - example: "\"d654e6ba-70a3-48ef-a95d-37c8d8a7901a\""; + example: "\"222430354126975533\""; } ]; string session_token = 2 [ @@ -316,7 +316,7 @@ message SetSessionRequest{ (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { min_length: 1; max_length: 200; - description: "\"token of the session, previously returned on the create / update request\""; + description: "\"The current token of the session, previously returned on the create / update request.\""; } ]; Checks checks = 3[ @@ -342,7 +342,7 @@ message SetSessionResponse{ zitadel.object.v2beta.Details details = 1; string session_token = 2 [ (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { - description: "\"token of the session, which is required for further updates of the session or the request other resources\""; + description: "\"The current token of the session, which is required for further updates of the session or to request other resources.\""; } ]; Challenges challenges = 3; @@ -355,12 +355,12 @@ message DeleteSessionRequest{ min_length: 1; max_length: 200; description: "\"id of the session to terminate\""; - example: "\"d654e6ba-70a3-48ef-a95d-37c8d8a7901a\""; + example: "\"222430354126975533\""; } ]; optional string session_token = 2 [ (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { - description: "\"token of the session, previously returned on the create / update request\""; + description: "\"The current token of the session, previously returned on the create / update request. The token is required unless the authenticated user terminates the own session or is granted the `session.delete` permission.\""; } ]; }