fix(auth): optimise user sessions (#7199)

* fix(auth): start optimise user sessions

* reduce and query user sessions directly without gorm statements

* cleanup

* cleanup

* fix requested changes

---------

Co-authored-by: Livio Spring <livio.a@gmail.com>
This commit is contained in:
Silvan
2024-01-30 16:17:54 +01:00
committed by GitHub
parent c20204d84d
commit aa407c3c3e
8 changed files with 292 additions and 437 deletions

View File

@@ -23,16 +23,20 @@ const (
)
type UserSessionView struct {
CreationDate time.Time `json:"-" gorm:"column:creation_date"`
ChangeDate time.Time `json:"-" gorm:"column:change_date"`
ResourceOwner string `json:"-" gorm:"column:resource_owner"`
State int32 `json:"-" gorm:"column:state"`
UserAgentID string `json:"userAgentID" gorm:"column:user_agent_id;primary_key"`
UserID string `json:"userID" gorm:"column:user_id;primary_key"`
UserName string `json:"-" gorm:"column:user_name"`
LoginName string `json:"-" gorm:"column:login_name"`
DisplayName string `json:"-" gorm:"column:user_display_name"`
AvatarKey string `json:"-" gorm:"column:avatar_key"`
CreationDate time.Time `json:"-" gorm:"column:creation_date"`
ChangeDate time.Time `json:"-" gorm:"column:change_date"`
ResourceOwner string `json:"-" gorm:"column:resource_owner"`
State int32 `json:"-" gorm:"column:state"`
UserAgentID string `json:"userAgentID" gorm:"column:user_agent_id;primary_key"`
UserID string `json:"userID" gorm:"column:user_id;primary_key"`
// As of https://github.com/zitadel/zitadel/pull/7199 the following 4 attributes
// are not projected in the user session handler anymore
// and are therefore annotated with a `gorm:"-"`.
// They will be read from the corresponding projection directly.
UserName string `json:"-" gorm:"-"`
LoginName string `json:"-" gorm:"-"`
DisplayName string `json:"-" gorm:"-"`
AvatarKey string `json:"-" gorm:"-"`
SelectedIDPConfigID string `json:"selectedIDPConfigID" gorm:"column:selected_idp_config_id"`
PasswordVerification time.Time `json:"-" gorm:"column:password_verification"`
PasswordlessVerification time.Time `json:"-" gorm:"column:passwordless_verification"`
@@ -190,14 +194,6 @@ func (v *UserSessionView) AppendEvent(event eventstore.Event) error {
case user.UserIDPLinkRemovedType, user.UserIDPLinkCascadeRemovedType:
v.ExternalLoginVerification = time.Time{}
v.SelectedIDPConfigID = ""
case user.HumanAvatarAddedType:
key, err := avatarKeyFromEvent(event)
if err != nil {
return err
}
v.AvatarKey = key
case user.HumanAvatarRemovedType:
v.AvatarKey = ""
}
return nil
}
@@ -208,15 +204,6 @@ func (v *UserSessionView) setSecondFactorVerification(verificationTime time.Time
v.State = int32(domain.UserSessionStateActive)
}
func avatarKeyFromEvent(event eventstore.Event) (string, error) {
data := make(map[string]string)
if err := event.Unmarshal(&data); err != nil {
logging.Log("EVEN-Sfew2").WithError(err).Error("could not unmarshal event data")
return "", zerrors.ThrowInternal(err, "MODEL-SFw2q", "could not unmarshal event")
}
return data["storeKey"], nil
}
func (v *UserSessionView) EventTypes() []eventstore.EventType {
return []eventstore.EventType{
user.UserV1PasswordCheckSucceededType,
@@ -250,7 +237,5 @@ func (v *UserSessionView) EventTypes() []eventstore.EventType {
user.UserDeactivatedType,
user.UserIDPLinkRemovedType,
user.UserIDPLinkCascadeRemovedType,
user.HumanAvatarAddedType,
user.HumanAvatarRemovedType,
}
}

View File

@@ -0,0 +1,29 @@
SELECT s.creation_date,
s.change_date,
s.resource_owner,
s.state,
s.user_agent_id,
s.user_id,
u.username,
l.login_name,
h.display_name,
h.avatar_key,
s.selected_idp_config_id,
s.password_verification,
s.passwordless_verification,
s.external_login_verification,
s.second_factor_verification,
s.second_factor_verification_type,
s.multi_factor_verification,
s.multi_factor_verification_type,
s.sequence,
s.instance_id
FROM auth.user_sessions s
LEFT JOIN projections.users10 u ON s.user_id = u.id AND s.instance_id = u.instance_id
LEFT JOIN projections.users10_humans h ON s.user_id = h.user_id AND s.instance_id = h.instance_id
LEFT JOIN projections.login_names3 l ON s.user_id = l.user_id AND s.instance_id = l.instance_id AND l.is_primary = true
WHERE (s.user_agent_id = $1)
AND (s.user_id = $2)
AND (s.instance_id = $3)
LIMIT 1
;

View File

@@ -1,123 +1,56 @@
package view
import (
"database/sql"
_ "embed"
"errors"
"github.com/jinzhu/gorm"
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/database"
usr_model "github.com/zitadel/zitadel/internal/user/model"
"github.com/zitadel/zitadel/internal/user/repository/view/model"
"github.com/zitadel/zitadel/internal/view/repository"
"github.com/zitadel/zitadel/internal/zerrors"
)
func UserSessionByIDs(db *gorm.DB, table, agentID, userID, instanceID string) (*model.UserSessionView, error) {
userSession := new(model.UserSessionView)
userAgentQuery := model.UserSessionSearchQuery{
Key: usr_model.UserSessionSearchKeyUserAgentID,
Method: domain.SearchMethodEquals,
Value: agentID,
}
userQuery := model.UserSessionSearchQuery{
Key: usr_model.UserSessionSearchKeyUserID,
Method: domain.SearchMethodEquals,
Value: userID,
}
instanceIDQuery := &model.UserSessionSearchQuery{
Key: usr_model.UserSessionSearchKeyInstanceID,
Method: domain.SearchMethodEquals,
Value: instanceID,
}
query := repository.PrepareGetByQuery(table, userAgentQuery, userQuery, instanceIDQuery)
err := query(db, userSession)
if zerrors.IsNotFound(err) {
return nil, zerrors.ThrowNotFound(nil, "VIEW-NGBs1", "Errors.UserSession.NotFound")
}
//go:embed user_session_by_id.sql
var userSessionByIDQuery string
//go:embed user_sessions_by_user_agent.sql
var userSessionsByUserAgentQuery string
func UserSessionByIDs(db *database.DB, agentID, userID, instanceID string) (userSession *model.UserSessionView, err error) {
err = db.QueryRow(
func(row *sql.Row) error {
userSession, err = scanUserSession(row)
return err
},
userSessionByIDQuery,
agentID,
userID,
instanceID,
)
return userSession, err
}
func UserSessionsByUserID(db *gorm.DB, table, userID, instanceID string) ([]*model.UserSessionView, error) {
userSessions := make([]*model.UserSessionView, 0)
userAgentQuery := &usr_model.UserSessionSearchQuery{
Key: usr_model.UserSessionSearchKeyUserID,
Method: domain.SearchMethodEquals,
Value: userID,
}
instanceIDQuery := &usr_model.UserSessionSearchQuery{
Key: usr_model.UserSessionSearchKeyInstanceID,
Method: domain.SearchMethodEquals,
Value: instanceID,
}
query := repository.PrepareSearchQuery(table, model.UserSessionSearchRequest{
Queries: []*usr_model.UserSessionSearchQuery{userAgentQuery, instanceIDQuery},
})
_, err := query(db, &userSessions)
func UserSessionsByAgentID(db *database.DB, agentID, instanceID string) (userSessions []*model.UserSessionView, err error) {
err = db.Query(
func(rows *sql.Rows) error {
userSessions, err = scanUserSessions(rows)
return err
},
userSessionsByUserAgentQuery,
agentID,
instanceID,
)
return userSessions, err
}
func UserSessionsByAgentID(db *gorm.DB, table, agentID, instanceID string) ([]*model.UserSessionView, error) {
userSessions := make([]*model.UserSessionView, 0)
userAgentQuery := &usr_model.UserSessionSearchQuery{
Key: usr_model.UserSessionSearchKeyUserAgentID,
Method: domain.SearchMethodEquals,
Value: agentID,
}
instanceIDQuery := &usr_model.UserSessionSearchQuery{
Key: usr_model.UserSessionSearchKeyInstanceID,
Method: domain.SearchMethodEquals,
Value: instanceID,
}
query := repository.PrepareSearchQuery(table, model.UserSessionSearchRequest{
Queries: []*usr_model.UserSessionSearchQuery{userAgentQuery, instanceIDQuery},
})
_, err := query(db, &userSessions)
return userSessions, err
}
func UserSessionsByOrgID(db *gorm.DB, table, orgID, instanceID string) ([]*model.UserSessionView, error) {
userSessions := make([]*model.UserSessionView, 0)
userAgentQuery := &usr_model.UserSessionSearchQuery{
Key: usr_model.UserSessionSearchKeyResourceOwner,
Method: domain.SearchMethodEquals,
Value: orgID,
}
instanceIDQuery := &usr_model.UserSessionSearchQuery{
Key: usr_model.UserSessionSearchKeyInstanceID,
Method: domain.SearchMethodEquals,
Value: instanceID,
}
query := repository.PrepareSearchQuery(table, model.UserSessionSearchRequest{
Queries: []*usr_model.UserSessionSearchQuery{userAgentQuery, instanceIDQuery},
})
_, err := query(db, &userSessions)
return userSessions, err
}
func ActiveUserSessions(db *gorm.DB, table string) (uint64, error) {
activeQuery := &usr_model.UserSessionSearchQuery{
Key: usr_model.UserSessionSearchKeyState,
Method: domain.SearchMethodEquals,
Value: domain.UserSessionStateActive,
}
query := repository.PrepareSearchQuery(table, model.UserSessionSearchRequest{
Queries: []*usr_model.UserSessionSearchQuery{activeQuery},
})
return query(db, nil)
}
func PutUserSession(db *gorm.DB, table string, session *model.UserSessionView) error {
save := repository.PrepareSave(table)
return save(db, session)
}
func PutUserSessions(db *gorm.DB, table string, sessions ...*model.UserSessionView) error {
save := repository.PrepareBulkSave(table)
s := make([]interface{}, len(sessions))
for i, session := range sessions {
s[i] = session
}
return save(db, s...)
}
func DeleteUserSessions(db *gorm.DB, table, userID, instanceID string) error {
delete := repository.PrepareDeleteByKeys(table,
repository.Key{Key: model.UserSessionSearchKey(usr_model.UserSessionSearchKeyUserID), Value: userID},
@@ -141,3 +74,81 @@ func DeleteOrgUserSessions(db *gorm.DB, table, instanceID, orgID string) error {
)
return delete(db)
}
func scanUserSession(row *sql.Row) (*model.UserSessionView, error) {
session := new(model.UserSessionView)
var userName, loginName, displayName, avatarKey sql.NullString
err := row.Scan(
&session.CreationDate,
&session.ChangeDate,
&session.ResourceOwner,
&session.State,
&session.UserAgentID,
&session.UserID,
&userName,
&loginName,
&displayName,
&avatarKey,
&session.SelectedIDPConfigID,
&session.PasswordVerification,
&session.PasswordlessVerification,
&session.ExternalLoginVerification,
&session.SecondFactorVerification,
&session.SecondFactorVerificationType,
&session.MultiFactorVerification,
&session.MultiFactorVerificationType,
&session.Sequence,
&session.InstanceID,
)
if errors.Is(err, sql.ErrNoRows) {
return nil, zerrors.ThrowNotFound(nil, "VIEW-NGBs1", "Errors.UserSession.NotFound")
}
session.UserName = userName.String
session.LoginName = loginName.String
session.DisplayName = displayName.String
session.AvatarKey = avatarKey.String
return session, err
}
func scanUserSessions(rows *sql.Rows) ([]*model.UserSessionView, error) {
sessions := make([]*model.UserSessionView, 0)
for rows.Next() {
session := new(model.UserSessionView)
var userName, loginName, displayName, avatarKey sql.NullString
err := rows.Scan(
&session.CreationDate,
&session.ChangeDate,
&session.ResourceOwner,
&session.State,
&session.UserAgentID,
&session.UserID,
&userName,
&loginName,
&displayName,
&avatarKey,
&session.SelectedIDPConfigID,
&session.PasswordVerification,
&session.PasswordlessVerification,
&session.ExternalLoginVerification,
&session.SecondFactorVerification,
&session.SecondFactorVerificationType,
&session.MultiFactorVerification,
&session.MultiFactorVerificationType,
&session.Sequence,
&session.InstanceID,
)
if err != nil {
return nil, err
}
session.UserName = userName.String
session.LoginName = loginName.String
session.DisplayName = displayName.String
session.AvatarKey = avatarKey.String
sessions = append(sessions, session)
}
if err := rows.Close(); err != nil {
return nil, zerrors.ThrowInternal(err, "VIEW-FSF3g", "Errors.Query.CloseRows")
}
return sessions, nil
}

View File

@@ -0,0 +1,27 @@
SELECT s.creation_date,
s.change_date,
s.resource_owner,
s.state,
s.user_agent_id,
s.user_id,
u.username,
l.login_name,
h.display_name,
h.avatar_key,
s.selected_idp_config_id,
s.password_verification,
s.passwordless_verification,
s.external_login_verification,
s.second_factor_verification,
s.second_factor_verification_type,
s.multi_factor_verification,
s.multi_factor_verification_type,
s.sequence,
s.instance_id
FROM auth.user_sessions s
LEFT JOIN projections.users10 u ON s.user_id = u.id AND s.instance_id = u.instance_id
LEFT JOIN projections.users10_humans h ON s.user_id = h.user_id AND s.instance_id = h.instance_id
LEFT JOIN projections.login_names3 l ON s.user_id = l.user_id AND s.instance_id = l.instance_id AND l.is_primary = true
WHERE (s.user_agent_id = $1)
AND (s.instance_id = $2)
;