Files
zitadel/backend/v3/domain/session_delete.go
T
ca4bba3992 feat: add delete session on relation tables (#11334)
# Which Problems Are Solved

As part of moving the session API to the relational database, this PR
adds the functionality to delete sessions through the session API v2 and
the OIDC end_session endpoint.

# How the Problems Are Solved

- added `SessionDeleteCommand` in domain package
- added possibility to pass the session token verifier to the domain
package
- use feature flag to switch API and OIDC usage from CQRS to relation
table
- added a `permissionCheck` condition to let permissions be checked
directly in the repository / sql
- part of it is still a placeholder until actual permission tables are
done
- added a `session_deleted` table to store (recently) deleted sessions
to handle necessary checks / cases like delete my own session, which
needs to be idempotent

# Additional Changes

Fixed `Matches` function of `existsCondition` (as missing an
implementation)

# Additional Context

- closes #11037

---------

Co-authored-by: Stefan Benz <[email protected]>
2026-03-19 08:35:53 +01:00

128 lines
4.0 KiB
Go

package domain
import (
"context"
"errors"
"strings"
"time"
"github.com/zitadel/zitadel/backend/v3/storage/database"
"github.com/zitadel/zitadel/internal/api/authz"
"github.com/zitadel/zitadel/internal/eventstore"
"github.com/zitadel/zitadel/internal/repository/session"
"github.com/zitadel/zitadel/internal/zerrors"
)
type DeleteSessionCommand struct {
ID string `json:"id"`
Token string `json:"token"`
MustCheckPermission bool `json:"mustCheckPermission,omitempty"`
// DeletedAt is set after successful execution.
DeletedAt time.Time `json:"deletedAt,omitempty"`
// deletedRows is set after successful execution, used to determine if the session was deleted or not.
deletedRows int64
}
func NewDeleteSessionCommand(
id string,
token string,
mustCheckPermission bool,
) *DeleteSessionCommand {
return &DeleteSessionCommand{
ID: id,
Token: token,
MustCheckPermission: mustCheckPermission,
}
}
// RequiresTransaction implements [Transactional].
func (cmd *DeleteSessionCommand) RequiresTransaction() {}
// Events implements [Commander].
func (cmd *DeleteSessionCommand) Events(ctx context.Context, opts *InvokeOpts) ([]eventstore.Command, error) {
if cmd.deletedRows == 0 {
return nil, nil
}
return []eventstore.Command{
session.NewTerminateEvent(ctx, &session.NewAggregate(cmd.ID, authz.GetInstance(ctx).InstanceID()).Aggregate),
}, nil
}
const (
ErrSessionTokenInvalid = "ErrSessionTokenInvalid"
ErrMoreThanOneRowAffected = "ErrMoreThanOneRowAffected"
ErrIDMissing = "ErrIDMissing"
ErrMissingPermission = "ErrMissingPermission"
ErrInternal = "ErrInternal"
)
func (cmd *DeleteSessionCommand) sessionDeletePermissionCheckCondition(ctx context.Context, sessionRepo SessionRepository, decryptor SessionTokenDecryptor) (database.Condition, error) {
if !cmd.MustCheckPermission {
return nil, nil
}
if cmd.Token != "" {
sessionID, tokenID, err := decryptor(ctx, cmd.Token)
if err != nil || sessionID != cmd.ID {
return nil, zerrors.ThrowPermissionDenied(err, ErrSessionTokenInvalid, "Errors.Session.Token.Invalid")
}
return database.Or(
sessionRepo.TokenIDCondition(tokenID),
database.PermissionCheck(SessionDeletePermission, true),
), nil
}
return database.Or(
sessionRepo.UserIDCondition(authz.GetCtxData(ctx).UserID),
database.PermissionCheck(SessionDeletePermission, true), // TODO: implement check once permissions are implemented
), nil
}
// Execute implements [Commander].
func (cmd *DeleteSessionCommand) Execute(ctx context.Context, opts *InvokeOpts) (err error) {
sessionRepo := opts.sessionRepo
instance := authz.GetInstance(ctx)
permCheck, err := cmd.sessionDeletePermissionCheckCondition(ctx, sessionRepo, opts.sessionTokenDecryptor)
if err != nil {
return err
}
deletedRows, deletedAt, err := sessionRepo.Delete(ctx, opts.DB(),
sessionRepo.PrimaryKeyCondition(instance.InstanceID(), cmd.ID),
permCheck,
)
if err != nil {
if errors.Is(err, new(database.PermissionError)) {
return zerrors.ThrowPermissionDenied(err, ErrMissingPermission,
"insufficient permissions to delete session, require `session.delete` permission, ownership of the session or current session token")
}
return zerrors.ThrowInternal(err, ErrInternal, "an unexpected error occurred while deleting the session")
}
if deletedRows > 1 {
return zerrors.ThrowInternalf(nil, ErrMoreThanOneRowAffected, "expected 1 session to be deleted, got %d", deletedRows)
}
cmd.DeletedAt = deletedAt
cmd.deletedRows = deletedRows
return nil
}
// String implements [Commander].
func (*DeleteSessionCommand) String() string {
return "DeleteSessionCommand"
}
// Validate implements [Commander].
func (cmd *DeleteSessionCommand) Validate(ctx context.Context, opts *InvokeOpts) (err error) {
if cmd.ID = strings.TrimSpace(cmd.ID); cmd.ID == "" {
return zerrors.ThrowInvalidArgument(nil, ErrIDMissing, "Errors.IDMissing")
}
return nil
}
var (
_ Commander = (*DeleteSessionCommand)(nil)
_ Transactional = (*DeleteSessionCommand)(nil)
)