Files
zitadel/backend/v3/storage/database/condition.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

416 lines
10 KiB
Go

package database
import (
"go.uber.org/mock/gomock"
)
// Condition represents a SQL condition.
// Its written after the WHERE keyword in a SQL statement.
type Condition interface {
gomock.Matcher
Write(builder *StatementBuilder)
// IsRestrictingColumn is used to check if the condition filters for a specific column.
// It acts as a safeguard database operations that should be specific on the given column.
IsRestrictingColumn(col Column) bool
}
type and struct {
conditions []Condition
}
// Matches implements [Condition].
func (a *and) Matches(x any) bool {
toMatch, ok := x.(*and)
if !ok {
return false
}
if len(a.conditions) != len(toMatch.conditions) {
return false
}
for i, condition := range a.conditions {
if !condition.Matches(toMatch.conditions[i]) {
return false
}
}
return true
}
// String implements [Condition].
func (a *and) String() string {
return "database.and"
}
// Write implements [Condition].
func (a and) Write(builder *StatementBuilder) {
if len(a.conditions) > 1 {
builder.WriteString("(")
defer builder.WriteString(")")
}
for i, condition := range a.conditions {
if i > 0 {
builder.WriteString(" AND ")
}
condition.Write(builder)
}
}
// And combines multiple conditions with AND.
func And(conditions ...Condition) *and {
return &and{conditions: conditions}
}
// IsRestrictingColumn implements [Condition].
func (a and) IsRestrictingColumn(col Column) bool {
for _, condition := range a.conditions {
if condition.IsRestrictingColumn(col) {
return true
}
}
return false
}
var _ Condition = (*and)(nil)
type or struct {
conditions []Condition
}
// Matches implements [Condition].
func (o *or) Matches(x any) bool {
toMatch, ok := x.(*or)
if !ok {
return false
}
if len(o.conditions) != len(toMatch.conditions) {
return false
}
for i, condition := range o.conditions {
if !condition.Matches(toMatch.conditions[i]) {
return false
}
}
return true
}
// String implements [Condition].
func (o *or) String() string {
return "database.or"
}
// Write implements [Condition].
func (o or) Write(builder *StatementBuilder) {
if len(o.conditions) > 1 {
builder.WriteString("(")
defer builder.WriteString(")")
}
for i, condition := range o.conditions {
if i > 0 {
builder.WriteString(" OR ")
}
condition.Write(builder)
}
}
// Or combines multiple conditions with OR.
func Or(conditions ...Condition) *or {
return &or{conditions: conditions}
}
// IsRestrictingColumn implements [Condition].
// It returns true only if all conditions are restricting the given column.
func (o or) IsRestrictingColumn(col Column) bool {
for _, condition := range o.conditions {
if !condition.IsRestrictingColumn(col) {
return false
}
}
return true
}
var _ Condition = (*or)(nil)
type isNull struct {
column Column
}
// Matches implements [Condition].
func (i *isNull) Matches(x any) bool {
toMatch, ok := x.(*isNull)
if !ok {
return false
}
return i.column.Matches(toMatch.column)
}
// String implements [Condition].
func (i *isNull) String() string {
return "database.isNull"
}
// Write implements [Condition].
func (i isNull) Write(builder *StatementBuilder) {
i.column.WriteQualified(builder)
builder.WriteString(" IS NULL")
}
// IsNull creates a condition that checks if a column is NULL.
func IsNull(column Column) *isNull {
return &isNull{column: column}
}
// IsRestrictingColumn implements [Condition].
func (i isNull) IsRestrictingColumn(col Column) bool {
return false
}
var _ Condition = (*isNull)(nil)
type isNotNull struct {
column Column
}
// Matches implements [Condition].
func (i *isNotNull) Matches(x any) bool {
toMatch, ok := x.(*isNotNull)
if !ok {
return false
}
return i.column.Matches(toMatch.column)
}
// String implements [Condition].
func (i *isNotNull) String() string {
return "database.isNotNull"
}
// Write implements [Condition].
func (i isNotNull) Write(builder *StatementBuilder) {
i.column.WriteQualified(builder)
builder.WriteString(" IS NOT NULL")
}
// IsNotNull creates a condition that checks if a column is NOT NULL.
func IsNotNull(column Column) *isNotNull {
return &isNotNull{column: column}
}
// IsRestrictingColumn implements [Condition].
func (i isNotNull) IsRestrictingColumn(col Column) bool {
return false
}
var _ Condition = (*isNotNull)(nil)
type valueCondition struct {
write func(builder *StatementBuilder)
col Column
}
// Matches implements [Condition].
func (c valueCondition) Matches(x any) bool {
toMatch, ok := x.(valueCondition)
if !ok {
return false
}
if !c.col.Matches(toMatch.col) {
return false
}
var expectedStatement, inputStatement StatementBuilder
c.write(&expectedStatement)
toMatch.write(&inputStatement)
return expectedStatement.String() == inputStatement.String()
}
// String implements [Condition].
func (c valueCondition) String() string {
return "database.valueCondition"
}
// NewTextCondition creates a condition that compares a text column with a value.
// if `op.IsIgnoreCaseOperation()` is true the response is created using [NewTextIgnoreCaseCondition]
func NewTextCondition[T Text](col Column, op TextOperation, value T) Condition {
if op.IsIgnoreCaseOperation() {
return NewTextIgnoreCaseCondition(col, op.Value(), value)
}
return valueCondition{
col: col,
write: func(builder *StatementBuilder) {
writeTextOperation[T](builder, col, op, value)
},
}
}
// NewTextIgnoreCaseCondition creates a condition that compares a text column with a value, ignoring case by lowercasing both.
func NewTextIgnoreCaseCondition[T Text](col Column, op TextOperation, value T) Condition {
return valueCondition{
col: col,
write: func(builder *StatementBuilder) {
writeTextOperation[T](builder, LowerColumn(col), op, LowerValue(value))
},
}
}
// NewNumberCondition creates a condition that compares a numeric column with a value.
func NewNumberCondition[V Number](col Column, op NumberOperation, value V) Condition {
return valueCondition{
col: col,
write: func(builder *StatementBuilder) {
writeNumberOperation[V](builder, col, op, value)
},
}
}
// NewBooleanCondition creates a condition that compares a boolean column with a value.
func NewBooleanCondition[V Boolean](col Column, value V) Condition {
return valueCondition{
col: col,
write: func(builder *StatementBuilder) {
writeBooleanOperation[V](builder, col, value)
},
}
}
// NewBytesCondition creates a condition that compares a BYTEA column with a value.
func NewBytesCondition[V Bytes](col Column, op BytesOperation, value any) Condition {
return valueCondition{
col: col,
write: func(builder *StatementBuilder) {
writeBytesOperation[V](builder, col, op, value)
},
}
}
// NewColumnCondition creates a condition that compares two columns on equality.
func NewColumnCondition(col1, col2 Column) Condition {
return valueCondition{
col: col1,
write: func(builder *StatementBuilder) {
col1.WriteQualified(builder)
builder.WriteString(" = ")
col2.WriteQualified(builder)
},
}
}
// Write implements [Condition].
func (c valueCondition) Write(builder *StatementBuilder) {
c.write(builder)
}
// IsRestrictingColumn implements [Condition].
func (i valueCondition) IsRestrictingColumn(col Column) bool {
return i.col.Equals(col)
}
var _ Condition = (*valueCondition)(nil)
// existsCondition is a helper to write an EXISTS (SELECT 1 FROM <table> WHERE <condition>) clause.
// It implements [Condition] so it can be composed with other conditions using And/Or.
type existsCondition struct {
table string
condition Condition
}
// Matches implements [Condition].
func (e *existsCondition) Matches(x any) bool {
toMatch, ok := x.(*existsCondition)
if !ok {
return false
}
if e.table != toMatch.table {
return false
}
return e.condition.Matches(toMatch.condition)
}
// String implements [Condition].
func (e *existsCondition) String() string {
return "existsCondition"
}
// Exists creates a condition that checks for the existence of rows in a subquery.
func Exists(table string, condition Condition) Condition {
return &existsCondition{
table: table,
condition: condition,
}
}
// Write implements [Condition].
func (e existsCondition) Write(builder *StatementBuilder) {
builder.WriteString(" EXISTS (SELECT 1 FROM ")
builder.WriteString(e.table)
builder.WriteString(" WHERE ")
e.condition.Write(builder)
builder.WriteString(")")
}
// IsRestrictingColumn implements [Condition].
func (e existsCondition) IsRestrictingColumn(col Column) bool {
// Forward to the inner condition so safety checks (like instance_id presence) can still work.
return e.condition.IsRestrictingColumn(col)
}
var _ Condition = (*existsCondition)(nil)
type forcedRestrictingColumnCondition struct {
Condition
isRestricting bool
}
func (c forcedRestrictingColumnCondition) IsRestrictingColumn(col Column) bool {
return c.isRestricting
}
func ForceRestrictingColumn(cond Condition, isRestriction bool) Condition {
return forcedRestrictingColumnCondition{
Condition: cond,
isRestricting: isRestriction,
}
}
var _ Condition = (*forcedRestrictingColumnCondition)(nil)
type permissionCheck struct {
requiredPermission string
throwError bool
}
func (p *permissionCheck) Matches(x any) bool {
toMatch, ok := x.(*permissionCheck)
if !ok {
return false
}
if toMatch.requiredPermission != p.requiredPermission {
return false
}
return toMatch.throwError == p.throwError
}
func (p *permissionCheck) String() string {
return "database.permissionCheck"
}
func (p *permissionCheck) Write(builder *StatementBuilder) {
// TODO: implement actual permission check once we have the necessary tables and functions in place.
// For now, we just write a dummy condition that can be used to verify that the permission check is included in the generated SQL.
if p.throwError {
builder.WriteString(" zitadel.throw_not_permitted() ")
return
}
builder.WriteString(" true ")
}
func (p *permissionCheck) IsRestrictingColumn(_ Column) bool {
return false
}
func PermissionCheck(requiredPermission string, throwError bool) *permissionCheck {
return &permissionCheck{
requiredPermission: requiredPermission,
throwError: throwError,
}
}
var _ Condition = (*permissionCheck)(nil)