Files
zitadel/backend/v3/domain/session_delete.go
T
a2bf528ca0 feat(domain): introduce error slugs (#12030)
# Which Problems Are Solved

As part of https://github.com/zitadel/zitadel/issues/11917 we want to
introduce error slugs so (API) clients can rely on stable,
machine-readable errors and act accordingly.

# How the Problems Are Solved

- Added a `NewSlug` helper function in the domain package.
- Added `ErrorDetails` to the `ZitadelError`
- Added an `zitadel.error.v2.ErrorDetail` proto message
- Updated the connectRPC error interceptor to map new slug based errors
to the new `ErrorDetail`
- Defined some common slugs and error functions like internal errors
- Defined (session) specific slugs used in the `DeleteSession` and
`CheckUser` functions and replaced old implementations
- Updated integration tests to check specific errors if the relation
database feature is enabled
- Updated doc and guideline to reflect the latest changes and decisions
- Updated DeleteSession endpoint API to list possible slugs

# Additional Changes

None

# Additional Context

- closes #11957

---------

Co-authored-by: Wim Van Laer <[email protected]>
2026-04-13 09:47:04 +02:00

119 lines
3.6 KiB
Go

package domain
import (
"context"
"errors"
"fmt"
"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"
)
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
}
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, ErrSessionTokenInvalid(err)
}
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 ErrAuthMissingPermission(err, "insufficient permissions to delete session, require `session.delete` permission, ownership of the session or current session token", SessionDeletePermission)
}
return ErrInternal(err, "an unexpected error occurred while deleting the session")
}
if deletedRows > 1 {
return ErrMoreThanOneRowAffected(fmt.Sprintf("expected 1 session to be deleted, got %d", deletedRows), 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 ErrIDMissing()
}
return nil
}
var (
_ Commander = (*DeleteSessionCommand)(nil)
_ Transactional = (*DeleteSessionCommand)(nil)
)