mirror of
https://github.com/zitadel/zitadel.git
synced 2024-12-20 23:07:33 +00:00
9a708b1b78
* feat: extend session search service (#6029) add two more searching criteria - human user id and session creation date optional sorting by the session creation date * fix: use correct column identifier * fix: implement Col() * chore: fix unit tests * chore: fix linter warnings --------- Co-authored-by: Fabi <fabienne@zitadel.com>
493 lines
15 KiB
Go
493 lines
15 KiB
Go
package query
|
|
|
|
import (
|
|
"context"
|
|
"database/sql"
|
|
errs "errors"
|
|
"net"
|
|
"net/http"
|
|
"time"
|
|
|
|
sq "github.com/Masterminds/squirrel"
|
|
|
|
"github.com/zitadel/logging"
|
|
|
|
"github.com/zitadel/zitadel/internal/api/authz"
|
|
"github.com/zitadel/zitadel/internal/api/call"
|
|
"github.com/zitadel/zitadel/internal/database"
|
|
"github.com/zitadel/zitadel/internal/domain"
|
|
"github.com/zitadel/zitadel/internal/errors"
|
|
"github.com/zitadel/zitadel/internal/eventstore/handler/v2"
|
|
"github.com/zitadel/zitadel/internal/query/projection"
|
|
"github.com/zitadel/zitadel/internal/telemetry/tracing"
|
|
)
|
|
|
|
type Sessions struct {
|
|
SearchResponse
|
|
Sessions []*Session
|
|
}
|
|
|
|
type Session struct {
|
|
ID string
|
|
CreationDate time.Time
|
|
ChangeDate time.Time
|
|
Sequence uint64
|
|
State domain.SessionState
|
|
ResourceOwner string
|
|
Creator string
|
|
UserFactor SessionUserFactor
|
|
PasswordFactor SessionPasswordFactor
|
|
IntentFactor SessionIntentFactor
|
|
WebAuthNFactor SessionWebAuthNFactor
|
|
TOTPFactor SessionTOTPFactor
|
|
OTPSMSFactor SessionOTPFactor
|
|
OTPEmailFactor SessionOTPFactor
|
|
Metadata map[string][]byte
|
|
UserAgent domain.UserAgent
|
|
Expiration time.Time
|
|
}
|
|
|
|
type SessionUserFactor struct {
|
|
UserID string
|
|
ResourceOwner string
|
|
UserCheckedAt time.Time
|
|
LoginName string
|
|
DisplayName string
|
|
}
|
|
|
|
type SessionPasswordFactor struct {
|
|
PasswordCheckedAt time.Time
|
|
}
|
|
|
|
type SessionIntentFactor struct {
|
|
IntentCheckedAt time.Time
|
|
}
|
|
|
|
type SessionWebAuthNFactor struct {
|
|
WebAuthNCheckedAt time.Time
|
|
UserVerified bool
|
|
}
|
|
|
|
type SessionTOTPFactor struct {
|
|
TOTPCheckedAt time.Time
|
|
}
|
|
|
|
type SessionOTPFactor struct {
|
|
OTPCheckedAt time.Time
|
|
}
|
|
|
|
type SessionsSearchQueries struct {
|
|
SearchRequest
|
|
Queries []SearchQuery
|
|
}
|
|
|
|
func (q *SessionsSearchQueries) toQuery(query sq.SelectBuilder) sq.SelectBuilder {
|
|
query = q.SearchRequest.toQuery(query)
|
|
for _, q := range q.Queries {
|
|
query = q.toQuery(query)
|
|
}
|
|
return query
|
|
}
|
|
|
|
var (
|
|
sessionsTable = table{
|
|
name: projection.SessionsProjectionTable,
|
|
instanceIDCol: projection.SessionColumnInstanceID,
|
|
}
|
|
SessionColumnID = Column{
|
|
name: projection.SessionColumnID,
|
|
table: sessionsTable,
|
|
}
|
|
SessionColumnCreationDate = Column{
|
|
name: projection.SessionColumnCreationDate,
|
|
table: sessionsTable,
|
|
}
|
|
SessionColumnChangeDate = Column{
|
|
name: projection.SessionColumnChangeDate,
|
|
table: sessionsTable,
|
|
}
|
|
SessionColumnSequence = Column{
|
|
name: projection.SessionColumnSequence,
|
|
table: sessionsTable,
|
|
}
|
|
SessionColumnState = Column{
|
|
name: projection.SessionColumnState,
|
|
table: sessionsTable,
|
|
}
|
|
SessionColumnResourceOwner = Column{
|
|
name: projection.SessionColumnResourceOwner,
|
|
table: sessionsTable,
|
|
}
|
|
SessionColumnInstanceID = Column{
|
|
name: projection.SessionColumnInstanceID,
|
|
table: sessionsTable,
|
|
}
|
|
SessionColumnCreator = Column{
|
|
name: projection.SessionColumnCreator,
|
|
table: sessionsTable,
|
|
}
|
|
SessionColumnUserID = Column{
|
|
name: projection.SessionColumnUserID,
|
|
table: sessionsTable,
|
|
}
|
|
SessionColumnUserCheckedAt = Column{
|
|
name: projection.SessionColumnUserCheckedAt,
|
|
table: sessionsTable,
|
|
}
|
|
SessionColumnPasswordCheckedAt = Column{
|
|
name: projection.SessionColumnPasswordCheckedAt,
|
|
table: sessionsTable,
|
|
}
|
|
SessionColumnIntentCheckedAt = Column{
|
|
name: projection.SessionColumnIntentCheckedAt,
|
|
table: sessionsTable,
|
|
}
|
|
SessionColumnWebAuthNCheckedAt = Column{
|
|
name: projection.SessionColumnWebAuthNCheckedAt,
|
|
table: sessionsTable,
|
|
}
|
|
SessionColumnWebAuthNUserVerified = Column{
|
|
name: projection.SessionColumnWebAuthNUserVerified,
|
|
table: sessionsTable,
|
|
}
|
|
SessionColumnTOTPCheckedAt = Column{
|
|
name: projection.SessionColumnTOTPCheckedAt,
|
|
table: sessionsTable,
|
|
}
|
|
SessionColumnOTPSMSCheckedAt = Column{
|
|
name: projection.SessionColumnOTPSMSCheckedAt,
|
|
table: sessionsTable,
|
|
}
|
|
SessionColumnOTPEmailCheckedAt = Column{
|
|
name: projection.SessionColumnOTPEmailCheckedAt,
|
|
table: sessionsTable,
|
|
}
|
|
SessionColumnMetadata = Column{
|
|
name: projection.SessionColumnMetadata,
|
|
table: sessionsTable,
|
|
}
|
|
SessionColumnToken = Column{
|
|
name: projection.SessionColumnTokenID,
|
|
table: sessionsTable,
|
|
}
|
|
SessionColumnUserAgentFingerprintID = Column{
|
|
name: projection.SessionColumnUserAgentFingerprintID,
|
|
table: sessionsTable,
|
|
}
|
|
SessionColumnUserAgentIP = Column{
|
|
name: projection.SessionColumnUserAgentIP,
|
|
table: sessionsTable,
|
|
}
|
|
SessionColumnUserAgentDescription = Column{
|
|
name: projection.SessionColumnUserAgentDescription,
|
|
table: sessionsTable,
|
|
}
|
|
SessionColumnUserAgentHeader = Column{
|
|
name: projection.SessionColumnUserAgentHeader,
|
|
table: sessionsTable,
|
|
}
|
|
SessionColumnExpiration = Column{
|
|
name: projection.SessionColumnExpiration,
|
|
table: sessionsTable,
|
|
}
|
|
)
|
|
|
|
func (q *Queries) SessionByID(ctx context.Context, shouldTriggerBulk bool, id, sessionToken string) (session *Session, err error) {
|
|
ctx, span := tracing.NewSpan(ctx)
|
|
defer func() { span.EndWithError(err) }()
|
|
|
|
if shouldTriggerBulk {
|
|
_, traceSpan := tracing.NewNamedSpan(ctx, "TriggerSessionProjection")
|
|
ctx, err = projection.SessionProjection.Trigger(ctx, handler.WithAwaitRunning())
|
|
logging.OnError(err).Debug("unable to trigger")
|
|
traceSpan.EndWithError(err)
|
|
}
|
|
|
|
query, scan := prepareSessionQuery(ctx, q.client)
|
|
stmt, args, err := query.Where(
|
|
sq.Eq{
|
|
SessionColumnID.identifier(): id,
|
|
SessionColumnInstanceID.identifier(): authz.GetInstance(ctx).InstanceID(),
|
|
},
|
|
).ToSql()
|
|
if err != nil {
|
|
return nil, errors.ThrowInternal(err, "QUERY-dn9JW", "Errors.Query.SQLStatement")
|
|
}
|
|
|
|
var tokenID string
|
|
err = q.client.QueryRowContext(ctx, func(row *sql.Row) error {
|
|
session, tokenID, err = scan(row)
|
|
return err
|
|
}, stmt, args...)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if sessionToken == "" {
|
|
return session, nil
|
|
}
|
|
if err := q.sessionTokenVerifier(ctx, sessionToken, session.ID, tokenID); err != nil {
|
|
return nil, errors.ThrowPermissionDenied(nil, "QUERY-dsfr3", "Errors.PermissionDenied")
|
|
}
|
|
return session, nil
|
|
}
|
|
|
|
func (q *Queries) SearchSessions(ctx context.Context, queries *SessionsSearchQueries) (sessions *Sessions, err error) {
|
|
ctx, span := tracing.NewSpan(ctx)
|
|
defer func() { span.EndWithError(err) }()
|
|
|
|
query, scan := prepareSessionsQuery(ctx, q.client)
|
|
stmt, args, err := queries.toQuery(query).
|
|
Where(sq.Eq{
|
|
SessionColumnInstanceID.identifier(): authz.GetInstance(ctx).InstanceID(),
|
|
}).
|
|
ToSql()
|
|
if err != nil {
|
|
return nil, errors.ThrowInvalidArgument(err, "QUERY-sn9Jf", "Errors.Query.InvalidRequest")
|
|
}
|
|
|
|
err = q.client.QueryContext(ctx, func(rows *sql.Rows) error {
|
|
sessions, err = scan(rows)
|
|
return err
|
|
}, stmt, args...)
|
|
if err != nil {
|
|
return nil, errors.ThrowInternal(err, "QUERY-Sfg42", "Errors.Internal")
|
|
}
|
|
|
|
sessions.State, err = q.latestState(ctx, sessionsTable)
|
|
return sessions, err
|
|
}
|
|
|
|
func NewSessionIDsSearchQuery(ids []string) (SearchQuery, error) {
|
|
list := make([]interface{}, len(ids))
|
|
for i, value := range ids {
|
|
list[i] = value
|
|
}
|
|
return NewListQuery(SessionColumnID, list, ListIn)
|
|
}
|
|
|
|
func NewSessionCreatorSearchQuery(creator string) (SearchQuery, error) {
|
|
return NewTextQuery(SessionColumnCreator, creator, TextEquals)
|
|
}
|
|
|
|
func NewUserIDSearchQuery(id string) (SearchQuery, error) {
|
|
return NewTextQuery(SessionColumnUserID, id, TextEquals)
|
|
}
|
|
|
|
func NewCreationDateQuery(datetime time.Time, compare TimestampComparison) (SearchQuery, error) {
|
|
return NewTimestampQuery(SessionColumnCreationDate, datetime, compare)
|
|
}
|
|
|
|
func prepareSessionQuery(ctx context.Context, db prepareDatabase) (sq.SelectBuilder, func(*sql.Row) (*Session, string, error)) {
|
|
return sq.Select(
|
|
SessionColumnID.identifier(),
|
|
SessionColumnCreationDate.identifier(),
|
|
SessionColumnChangeDate.identifier(),
|
|
SessionColumnSequence.identifier(),
|
|
SessionColumnState.identifier(),
|
|
SessionColumnResourceOwner.identifier(),
|
|
SessionColumnCreator.identifier(),
|
|
SessionColumnUserID.identifier(),
|
|
SessionColumnUserCheckedAt.identifier(),
|
|
LoginNameNameCol.identifier(),
|
|
HumanDisplayNameCol.identifier(),
|
|
UserResourceOwnerCol.identifier(),
|
|
SessionColumnPasswordCheckedAt.identifier(),
|
|
SessionColumnIntentCheckedAt.identifier(),
|
|
SessionColumnWebAuthNCheckedAt.identifier(),
|
|
SessionColumnWebAuthNUserVerified.identifier(),
|
|
SessionColumnTOTPCheckedAt.identifier(),
|
|
SessionColumnOTPSMSCheckedAt.identifier(),
|
|
SessionColumnOTPEmailCheckedAt.identifier(),
|
|
SessionColumnMetadata.identifier(),
|
|
SessionColumnToken.identifier(),
|
|
SessionColumnUserAgentFingerprintID.identifier(),
|
|
SessionColumnUserAgentIP.identifier(),
|
|
SessionColumnUserAgentDescription.identifier(),
|
|
SessionColumnUserAgentHeader.identifier(),
|
|
SessionColumnExpiration.identifier(),
|
|
).From(sessionsTable.identifier()).
|
|
LeftJoin(join(LoginNameUserIDCol, SessionColumnUserID)).
|
|
LeftJoin(join(HumanUserIDCol, SessionColumnUserID)).
|
|
LeftJoin(join(UserIDCol, SessionColumnUserID) + db.Timetravel(call.Took(ctx))).
|
|
PlaceholderFormat(sq.Dollar), func(row *sql.Row) (*Session, string, error) {
|
|
session := new(Session)
|
|
|
|
var (
|
|
userID sql.NullString
|
|
userCheckedAt sql.NullTime
|
|
loginName sql.NullString
|
|
displayName sql.NullString
|
|
userResourceOwner sql.NullString
|
|
passwordCheckedAt sql.NullTime
|
|
intentCheckedAt sql.NullTime
|
|
webAuthNCheckedAt sql.NullTime
|
|
webAuthNUserPresent sql.NullBool
|
|
totpCheckedAt sql.NullTime
|
|
otpSMSCheckedAt sql.NullTime
|
|
otpEmailCheckedAt sql.NullTime
|
|
metadata database.Map[[]byte]
|
|
token sql.NullString
|
|
userAgentIP sql.NullString
|
|
userAgentHeader database.Map[[]string]
|
|
expiration sql.NullTime
|
|
)
|
|
|
|
err := row.Scan(
|
|
&session.ID,
|
|
&session.CreationDate,
|
|
&session.ChangeDate,
|
|
&session.Sequence,
|
|
&session.State,
|
|
&session.ResourceOwner,
|
|
&session.Creator,
|
|
&userID,
|
|
&userCheckedAt,
|
|
&loginName,
|
|
&displayName,
|
|
&userResourceOwner,
|
|
&passwordCheckedAt,
|
|
&intentCheckedAt,
|
|
&webAuthNCheckedAt,
|
|
&webAuthNUserPresent,
|
|
&totpCheckedAt,
|
|
&otpSMSCheckedAt,
|
|
&otpEmailCheckedAt,
|
|
&metadata,
|
|
&token,
|
|
&session.UserAgent.FingerprintID,
|
|
&userAgentIP,
|
|
&session.UserAgent.Description,
|
|
&userAgentHeader,
|
|
&expiration,
|
|
)
|
|
|
|
if err != nil {
|
|
if errs.Is(err, sql.ErrNoRows) {
|
|
return nil, "", errors.ThrowNotFound(err, "QUERY-SFeaa", "Errors.Session.NotExisting")
|
|
}
|
|
return nil, "", errors.ThrowInternal(err, "QUERY-SAder", "Errors.Internal")
|
|
}
|
|
|
|
session.UserFactor.UserID = userID.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
|
|
session.WebAuthNFactor.UserVerified = webAuthNUserPresent.Bool
|
|
session.TOTPFactor.TOTPCheckedAt = totpCheckedAt.Time
|
|
session.OTPSMSFactor.OTPCheckedAt = otpSMSCheckedAt.Time
|
|
session.OTPEmailFactor.OTPCheckedAt = otpEmailCheckedAt.Time
|
|
session.Metadata = metadata
|
|
session.UserAgent.Header = http.Header(userAgentHeader)
|
|
if userAgentIP.Valid {
|
|
session.UserAgent.IP = net.ParseIP(userAgentIP.String)
|
|
}
|
|
session.Expiration = expiration.Time
|
|
return session, token.String, nil
|
|
}
|
|
}
|
|
|
|
func prepareSessionsQuery(ctx context.Context, db prepareDatabase) (sq.SelectBuilder, func(*sql.Rows) (*Sessions, error)) {
|
|
return sq.Select(
|
|
SessionColumnID.identifier(),
|
|
SessionColumnCreationDate.identifier(),
|
|
SessionColumnChangeDate.identifier(),
|
|
SessionColumnSequence.identifier(),
|
|
SessionColumnState.identifier(),
|
|
SessionColumnResourceOwner.identifier(),
|
|
SessionColumnCreator.identifier(),
|
|
SessionColumnUserID.identifier(),
|
|
SessionColumnUserCheckedAt.identifier(),
|
|
LoginNameNameCol.identifier(),
|
|
HumanDisplayNameCol.identifier(),
|
|
UserResourceOwnerCol.identifier(),
|
|
SessionColumnPasswordCheckedAt.identifier(),
|
|
SessionColumnIntentCheckedAt.identifier(),
|
|
SessionColumnWebAuthNCheckedAt.identifier(),
|
|
SessionColumnWebAuthNUserVerified.identifier(),
|
|
SessionColumnTOTPCheckedAt.identifier(),
|
|
SessionColumnOTPSMSCheckedAt.identifier(),
|
|
SessionColumnOTPEmailCheckedAt.identifier(),
|
|
SessionColumnMetadata.identifier(),
|
|
SessionColumnExpiration.identifier(),
|
|
countColumn.identifier(),
|
|
).From(sessionsTable.identifier()).
|
|
LeftJoin(join(LoginNameUserIDCol, SessionColumnUserID)).
|
|
LeftJoin(join(HumanUserIDCol, SessionColumnUserID)).
|
|
LeftJoin(join(UserIDCol, SessionColumnUserID) + db.Timetravel(call.Took(ctx))).
|
|
PlaceholderFormat(sq.Dollar), func(rows *sql.Rows) (*Sessions, error) {
|
|
sessions := &Sessions{Sessions: []*Session{}}
|
|
|
|
for rows.Next() {
|
|
session := new(Session)
|
|
|
|
var (
|
|
userID sql.NullString
|
|
userCheckedAt sql.NullTime
|
|
loginName sql.NullString
|
|
displayName sql.NullString
|
|
userResourceOwner sql.NullString
|
|
passwordCheckedAt sql.NullTime
|
|
intentCheckedAt sql.NullTime
|
|
webAuthNCheckedAt sql.NullTime
|
|
webAuthNUserPresent sql.NullBool
|
|
totpCheckedAt sql.NullTime
|
|
otpSMSCheckedAt sql.NullTime
|
|
otpEmailCheckedAt sql.NullTime
|
|
metadata database.Map[[]byte]
|
|
expiration sql.NullTime
|
|
)
|
|
|
|
err := rows.Scan(
|
|
&session.ID,
|
|
&session.CreationDate,
|
|
&session.ChangeDate,
|
|
&session.Sequence,
|
|
&session.State,
|
|
&session.ResourceOwner,
|
|
&session.Creator,
|
|
&userID,
|
|
&userCheckedAt,
|
|
&loginName,
|
|
&displayName,
|
|
&userResourceOwner,
|
|
&passwordCheckedAt,
|
|
&intentCheckedAt,
|
|
&webAuthNCheckedAt,
|
|
&webAuthNUserPresent,
|
|
&totpCheckedAt,
|
|
&otpSMSCheckedAt,
|
|
&otpEmailCheckedAt,
|
|
&metadata,
|
|
&expiration,
|
|
&sessions.Count,
|
|
)
|
|
|
|
if err != nil {
|
|
return nil, errors.ThrowInternal(err, "QUERY-SAfeg", "Errors.Internal")
|
|
}
|
|
session.UserFactor.UserID = userID.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
|
|
session.WebAuthNFactor.UserVerified = webAuthNUserPresent.Bool
|
|
session.TOTPFactor.TOTPCheckedAt = totpCheckedAt.Time
|
|
session.OTPSMSFactor.OTPCheckedAt = otpSMSCheckedAt.Time
|
|
session.OTPEmailFactor.OTPCheckedAt = otpEmailCheckedAt.Time
|
|
session.Metadata = metadata
|
|
session.Expiration = expiration.Time
|
|
|
|
sessions.Sessions = append(sessions.Sessions, session)
|
|
}
|
|
|
|
return sessions, nil
|
|
}
|
|
}
|