mirror of
https://github.com/zitadel/zitadel.git
synced 2025-08-11 20:57:31 +00:00
feat(api): new session service (#5801)
* backup new protoc plugin * backup * session * backup * initial implementation * change to specific events * implement tests * cleanup * refactor: use new protoc plugin for api v2 * change package * simplify code * cleanup * cleanup * fix merge * start queries * fix tests * improve returned values * add token to projection * tests * test db map * update query * permission checks * fix tests and linting * rework token creation * i18n * refactor token check and fix tests * session to PB test * request to query tests * cleanup proto * test user check * add comment * simplify database map type * Update docs/docs/guides/integrate/access-zitadel-system-api.md Co-authored-by: Tim Möhlmann <tim+github@zitadel.com> * fix test * cleanup * docs --------- Co-authored-by: Tim Möhlmann <tim+github@zitadel.com>
This commit is contained in:
320
internal/query/session.go
Normal file
320
internal/query/session.go
Normal file
@@ -0,0 +1,320 @@
|
||||
package query
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
errs "errors"
|
||||
"time"
|
||||
|
||||
sq "github.com/Masterminds/squirrel"
|
||||
|
||||
"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/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
|
||||
Metadata map[string][]byte
|
||||
}
|
||||
|
||||
type SessionUserFactor struct {
|
||||
UserID string
|
||||
UserCheckedAt time.Time
|
||||
LoginName string
|
||||
DisplayName string
|
||||
}
|
||||
|
||||
type SessionPasswordFactor struct {
|
||||
PasswordCheckedAt 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,
|
||||
}
|
||||
SessionColumnMetadata = Column{
|
||||
name: projection.SessionColumnMetadata,
|
||||
table: sessionsTable,
|
||||
}
|
||||
SessionColumnToken = Column{
|
||||
name: projection.SessionColumnTokenID,
|
||||
table: sessionsTable,
|
||||
}
|
||||
)
|
||||
|
||||
func (q *Queries) SessionByID(ctx context.Context, id, sessionToken string) (_ *Session, err error) {
|
||||
ctx, span := tracing.NewSpan(ctx)
|
||||
defer func() { span.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")
|
||||
}
|
||||
|
||||
row := q.client.QueryRowContext(ctx, stmt, args...)
|
||||
session, tokenID, err := scan(row)
|
||||
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, 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")
|
||||
}
|
||||
|
||||
rows, err := q.client.QueryContext(ctx, stmt, args...)
|
||||
if err != nil || rows.Err() != nil {
|
||||
return nil, errors.ThrowInternal(err, "QUERY-Sfg42", "Errors.Internal")
|
||||
}
|
||||
sessions, err := scan(rows)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
sessions.LatestSequence, err = q.latestSequence(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 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(),
|
||||
SessionColumnPasswordCheckedAt.identifier(),
|
||||
SessionColumnMetadata.identifier(),
|
||||
SessionColumnToken.identifier(),
|
||||
).From(sessionsTable.identifier()).
|
||||
LeftJoin(join(LoginNameUserIDCol, SessionColumnUserID)).
|
||||
LeftJoin(join(HumanUserIDCol, 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
|
||||
passwordCheckedAt sql.NullTime
|
||||
metadata database.Map[[]byte]
|
||||
token sql.NullString
|
||||
)
|
||||
|
||||
err := row.Scan(
|
||||
&session.ID,
|
||||
&session.CreationDate,
|
||||
&session.ChangeDate,
|
||||
&session.Sequence,
|
||||
&session.State,
|
||||
&session.ResourceOwner,
|
||||
&session.Creator,
|
||||
&userID,
|
||||
&userCheckedAt,
|
||||
&loginName,
|
||||
&displayName,
|
||||
&passwordCheckedAt,
|
||||
&metadata,
|
||||
&token,
|
||||
)
|
||||
|
||||
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.PasswordFactor.PasswordCheckedAt = passwordCheckedAt.Time
|
||||
session.Metadata = metadata
|
||||
|
||||
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(),
|
||||
SessionColumnPasswordCheckedAt.identifier(),
|
||||
SessionColumnMetadata.identifier(),
|
||||
countColumn.identifier(),
|
||||
).From(sessionsTable.identifier()).
|
||||
LeftJoin(join(LoginNameUserIDCol, SessionColumnUserID)).
|
||||
LeftJoin(join(HumanUserIDCol, 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
|
||||
passwordCheckedAt sql.NullTime
|
||||
metadata database.Map[[]byte]
|
||||
)
|
||||
|
||||
err := rows.Scan(
|
||||
&session.ID,
|
||||
&session.CreationDate,
|
||||
&session.ChangeDate,
|
||||
&session.Sequence,
|
||||
&session.State,
|
||||
&session.ResourceOwner,
|
||||
&session.Creator,
|
||||
&userID,
|
||||
&userCheckedAt,
|
||||
&loginName,
|
||||
&displayName,
|
||||
&passwordCheckedAt,
|
||||
&metadata,
|
||||
&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.PasswordFactor.PasswordCheckedAt = passwordCheckedAt.Time
|
||||
session.Metadata = metadata
|
||||
|
||||
sessions.Sessions = append(sessions.Sessions, session)
|
||||
}
|
||||
|
||||
return sessions, nil
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user