mirror of
https://github.com/zitadel/zitadel.git
synced 2025-08-12 04:57:33 +00:00
feat: block instances (#7129)
* docs: fix init description typos
* feat: block instances using limits
* translate
* unit tests
* fix translations
* redirect /ui/login
* fix http interceptor
* cleanup
* fix http interceptor
* fix: delete cookies on gateway 200
* add integration tests
* add command test
* docs
* fix integration tests
* add bulk api and integration test
* optimize bulk set limits
* unit test bulk limits
* fix broken link
* fix assets middleware
* fix broken link
* validate instance id format
* Update internal/eventstore/search_query.go
Co-authored-by: Livio Spring <livio.a@gmail.com>
* remove support for owner bulk limit commands
* project limits to instances
* migrate instances projection
* Revert "migrate instances projection"
This reverts commit 214218732a
.
* join limits, remove owner
* remove todo
* use optional bool
* normally validate instance ids
* use 302
* cleanup
* cleanup
* Update internal/api/grpc/system/limits_converter.go
Co-authored-by: Livio Spring <livio.a@gmail.com>
* remove owner
* remove owner from reset
---------
Co-authored-by: Livio Spring <livio.a@gmail.com>
This commit is contained in:
@@ -8,7 +8,6 @@ import (
|
||||
"github.com/zitadel/zitadel/internal/api/call"
|
||||
"github.com/zitadel/zitadel/internal/eventstore"
|
||||
"github.com/zitadel/zitadel/internal/telemetry/tracing"
|
||||
"github.com/zitadel/zitadel/internal/zerrors"
|
||||
)
|
||||
|
||||
type Event struct {
|
||||
@@ -44,12 +43,8 @@ func (q *Queries) SearchEvents(ctx context.Context, query *eventstore.SearchQuer
|
||||
ctx, span := tracing.NewSpan(ctx)
|
||||
defer func() { span.EndWithError(err) }()
|
||||
auditLogRetention := q.defaultAuditLogRetention
|
||||
instanceLimits, err := q.Limits(ctx, authz.GetInstance(ctx).InstanceID())
|
||||
if err != nil && !zerrors.IsNotFound(err) {
|
||||
return nil, err
|
||||
}
|
||||
if instanceLimits != nil && instanceLimits.AuditLogRetention != nil {
|
||||
auditLogRetention = *instanceLimits.AuditLogRetention
|
||||
if instanceAuditLogRetention := authz.GetInstance(ctx).AuditLogRetention(); instanceAuditLogRetention != nil {
|
||||
auditLogRetention = *instanceAuditLogRetention
|
||||
}
|
||||
if auditLogRetention != 0 {
|
||||
query = filterAuditLogRetention(ctx, auditLogRetention, query)
|
||||
|
@@ -29,6 +29,10 @@ var (
|
||||
name: projection.InstanceProjectionTable,
|
||||
instanceIDCol: projection.InstanceColumnID,
|
||||
}
|
||||
limitsTable = table{
|
||||
name: projection.LimitsProjectionTable,
|
||||
instanceIDCol: projection.LimitsColumnInstanceID,
|
||||
}
|
||||
InstanceColumnID = Column{
|
||||
name: projection.InstanceColumnID,
|
||||
table: instanceTable,
|
||||
@@ -69,6 +73,18 @@ var (
|
||||
name: projection.InstanceColumnDefaultLanguage,
|
||||
table: instanceTable,
|
||||
}
|
||||
LimitsColumnInstanceID = Column{
|
||||
name: projection.LimitsColumnInstanceID,
|
||||
table: limitsTable,
|
||||
}
|
||||
LimitsColumnAuditLogRetention = Column{
|
||||
name: projection.LimitsColumnAuditLogRetention,
|
||||
table: limitsTable,
|
||||
}
|
||||
LimitsColumnBlock = Column{
|
||||
name: projection.LimitsColumnBlock,
|
||||
table: limitsTable,
|
||||
}
|
||||
)
|
||||
|
||||
type Instance struct {
|
||||
@@ -78,14 +94,16 @@ type Instance struct {
|
||||
Sequence uint64
|
||||
Name string
|
||||
|
||||
DefaultOrgID string
|
||||
IAMProjectID string
|
||||
ConsoleID string
|
||||
ConsoleAppID string
|
||||
DefaultLang language.Tag
|
||||
Domains []*InstanceDomain
|
||||
host string
|
||||
csp csp
|
||||
DefaultOrgID string
|
||||
IAMProjectID string
|
||||
ConsoleID string
|
||||
ConsoleAppID string
|
||||
DefaultLang language.Tag
|
||||
Domains []*InstanceDomain
|
||||
host string
|
||||
csp csp
|
||||
block *bool
|
||||
auditLogRetention *time.Duration
|
||||
}
|
||||
|
||||
type csp struct {
|
||||
@@ -137,6 +155,14 @@ func (i *Instance) SecurityPolicyAllowedOrigins() []string {
|
||||
return i.csp.allowedOrigins
|
||||
}
|
||||
|
||||
func (i *Instance) Block() *bool {
|
||||
return i.block
|
||||
}
|
||||
|
||||
func (i *Instance) AuditLogRetention() *time.Duration {
|
||||
return i.auditLogRetention
|
||||
}
|
||||
|
||||
type InstanceSearchQueries struct {
|
||||
SearchRequest
|
||||
Queries []SearchQuery
|
||||
@@ -260,8 +286,10 @@ func prepareInstanceQuery(ctx context.Context, db prepareDatabase, host string)
|
||||
From(instanceTable.identifier() + db.Timetravel(call.Took(ctx))).
|
||||
PlaceholderFormat(sq.Dollar),
|
||||
func(row *sql.Row) (*Instance, error) {
|
||||
instance := &Instance{host: host}
|
||||
lang := ""
|
||||
var (
|
||||
instance = &Instance{host: host}
|
||||
lang = ""
|
||||
)
|
||||
err := row.Scan(
|
||||
&instance.ID,
|
||||
&instance.CreationDate,
|
||||
@@ -491,10 +519,13 @@ func prepareAuthzInstanceQuery(ctx context.Context, db prepareDatabase, host str
|
||||
InstanceDomainSequenceCol.identifier(),
|
||||
SecurityPolicyColumnEnabled.identifier(),
|
||||
SecurityPolicyColumnAllowedOrigins.identifier(),
|
||||
LimitsColumnAuditLogRetention.identifier(),
|
||||
LimitsColumnBlock.identifier(),
|
||||
).
|
||||
From(instanceTable.identifier()).
|
||||
LeftJoin(join(InstanceDomainInstanceIDCol, InstanceColumnID)).
|
||||
LeftJoin(join(SecurityPolicyColumnInstanceID, InstanceColumnID) + db.Timetravel(call.Took(ctx))).
|
||||
LeftJoin(join(SecurityPolicyColumnInstanceID, InstanceColumnID)).
|
||||
LeftJoin(join(LimitsColumnInstanceID, InstanceColumnID) + db.Timetravel(call.Took(ctx))).
|
||||
PlaceholderFormat(sq.Dollar),
|
||||
func(rows *sql.Rows) (*Instance, error) {
|
||||
instance := &Instance{
|
||||
@@ -511,6 +542,8 @@ func prepareAuthzInstanceQuery(ctx context.Context, db prepareDatabase, host str
|
||||
creationDate sql.NullTime
|
||||
sequence sql.NullInt64
|
||||
securityPolicyEnabled sql.NullBool
|
||||
auditLogRetention database.NullDuration
|
||||
block sql.NullBool
|
||||
)
|
||||
err := rows.Scan(
|
||||
&instance.ID,
|
||||
@@ -531,6 +564,8 @@ func prepareAuthzInstanceQuery(ctx context.Context, db prepareDatabase, host str
|
||||
&sequence,
|
||||
&securityPolicyEnabled,
|
||||
&instance.csp.allowedOrigins,
|
||||
&auditLogRetention,
|
||||
&block,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, zerrors.ThrowInternal(err, "QUERY-d3fas", "Errors.Internal")
|
||||
@@ -547,6 +582,12 @@ func prepareAuthzInstanceQuery(ctx context.Context, db prepareDatabase, host str
|
||||
IsGenerated: isGenerated.Bool,
|
||||
InstanceID: instance.ID,
|
||||
})
|
||||
if auditLogRetention.Valid {
|
||||
instance.auditLogRetention = &auditLogRetention.Duration
|
||||
}
|
||||
if block.Valid {
|
||||
instance.block = &block.Bool
|
||||
}
|
||||
instance.csp.enabled = securityPolicyEnabled.Bool
|
||||
}
|
||||
if instance.ID == "" {
|
||||
|
@@ -1,119 +0,0 @@
|
||||
package query
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"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/query/projection"
|
||||
"github.com/zitadel/zitadel/internal/telemetry/tracing"
|
||||
"github.com/zitadel/zitadel/internal/zerrors"
|
||||
)
|
||||
|
||||
var (
|
||||
limitSettingsTable = table{
|
||||
name: projection.LimitsProjectionTable,
|
||||
instanceIDCol: projection.LimitsColumnInstanceID,
|
||||
}
|
||||
LimitsColumnAggregateID = Column{
|
||||
name: projection.LimitsColumnAggregateID,
|
||||
table: limitSettingsTable,
|
||||
}
|
||||
LimitsColumnCreationDate = Column{
|
||||
name: projection.LimitsColumnCreationDate,
|
||||
table: limitSettingsTable,
|
||||
}
|
||||
LimitsColumnChangeDate = Column{
|
||||
name: projection.LimitsColumnChangeDate,
|
||||
table: limitSettingsTable,
|
||||
}
|
||||
LimitsColumnResourceOwner = Column{
|
||||
name: projection.LimitsColumnResourceOwner,
|
||||
table: limitSettingsTable,
|
||||
}
|
||||
LimitsColumnInstanceID = Column{
|
||||
name: projection.LimitsColumnInstanceID,
|
||||
table: limitSettingsTable,
|
||||
}
|
||||
LimitsColumnSequence = Column{
|
||||
name: projection.LimitsColumnSequence,
|
||||
table: limitSettingsTable,
|
||||
}
|
||||
LimitsColumnAuditLogRetention = Column{
|
||||
name: projection.LimitsColumnAuditLogRetention,
|
||||
table: limitSettingsTable,
|
||||
}
|
||||
)
|
||||
|
||||
type Limits struct {
|
||||
AggregateID string
|
||||
CreationDate time.Time
|
||||
ChangeDate time.Time
|
||||
ResourceOwner string
|
||||
Sequence uint64
|
||||
|
||||
AuditLogRetention *time.Duration
|
||||
}
|
||||
|
||||
func (q *Queries) Limits(ctx context.Context, resourceOwner string) (limits *Limits, err error) {
|
||||
ctx, span := tracing.NewSpan(ctx)
|
||||
defer func() { span.EndWithError(err) }()
|
||||
|
||||
stmt, scan := prepareLimitsQuery(ctx, q.client)
|
||||
query, args, err := stmt.Where(sq.Eq{
|
||||
LimitsColumnInstanceID.identifier(): authz.GetInstance(ctx).InstanceID(),
|
||||
LimitsColumnResourceOwner.identifier(): resourceOwner,
|
||||
}).ToSql()
|
||||
if err != nil {
|
||||
return nil, zerrors.ThrowInternal(err, "QUERY-jJe80", "Errors.Query.SQLStatment")
|
||||
}
|
||||
|
||||
err = q.client.QueryRowContext(ctx, func(row *sql.Row) error {
|
||||
limits, err = scan(row)
|
||||
return err
|
||||
}, query, args...)
|
||||
return limits, err
|
||||
}
|
||||
|
||||
func prepareLimitsQuery(ctx context.Context, db prepareDatabase) (sq.SelectBuilder, func(*sql.Row) (*Limits, error)) {
|
||||
return sq.Select(
|
||||
LimitsColumnAggregateID.identifier(),
|
||||
LimitsColumnCreationDate.identifier(),
|
||||
LimitsColumnChangeDate.identifier(),
|
||||
LimitsColumnResourceOwner.identifier(),
|
||||
LimitsColumnSequence.identifier(),
|
||||
LimitsColumnAuditLogRetention.identifier(),
|
||||
).
|
||||
From(limitSettingsTable.identifier() + db.Timetravel(call.Took(ctx))).
|
||||
PlaceholderFormat(sq.Dollar),
|
||||
func(row *sql.Row) (*Limits, error) {
|
||||
var (
|
||||
limits = new(Limits)
|
||||
auditLogRetention database.NullDuration
|
||||
)
|
||||
err := row.Scan(
|
||||
&limits.AggregateID,
|
||||
&limits.CreationDate,
|
||||
&limits.ChangeDate,
|
||||
&limits.ResourceOwner,
|
||||
&limits.Sequence,
|
||||
&auditLogRetention,
|
||||
)
|
||||
if err != nil {
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
return nil, zerrors.ThrowNotFound(err, "QUERY-GU1em", "Errors.Limits.NotFound")
|
||||
}
|
||||
return nil, zerrors.ThrowInternal(err, "QUERY-00jgy", "Errors.Internal")
|
||||
}
|
||||
if auditLogRetention.Valid {
|
||||
limits.AuditLogRetention = &auditLogRetention.Duration
|
||||
}
|
||||
return limits, nil
|
||||
}
|
||||
}
|
@@ -1,116 +0,0 @@
|
||||
package query
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"database/sql/driver"
|
||||
"errors"
|
||||
"fmt"
|
||||
"regexp"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/muhlemmer/gu"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/zerrors"
|
||||
)
|
||||
|
||||
var (
|
||||
expectedLimitsQuery = regexp.QuoteMeta("SELECT projections.limits.aggregate_id," +
|
||||
" projections.limits.creation_date," +
|
||||
" projections.limits.change_date," +
|
||||
" projections.limits.resource_owner," +
|
||||
" projections.limits.sequence," +
|
||||
" projections.limits.audit_log_retention" +
|
||||
" FROM projections.limits" +
|
||||
" AS OF SYSTEM TIME '-1 ms'",
|
||||
)
|
||||
|
||||
limitsCols = []string{
|
||||
"aggregate_id",
|
||||
"creation_date",
|
||||
"change_date",
|
||||
"resource_owner",
|
||||
"sequence",
|
||||
"audit_log_retention",
|
||||
}
|
||||
)
|
||||
|
||||
func Test_LimitsPrepare(t *testing.T) {
|
||||
type want struct {
|
||||
sqlExpectations sqlExpectation
|
||||
err checkErr
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
prepare interface{}
|
||||
want want
|
||||
object interface{}
|
||||
}{
|
||||
{
|
||||
name: "prepareLimitsQuery no result",
|
||||
prepare: prepareLimitsQuery,
|
||||
want: want{
|
||||
sqlExpectations: mockQueriesScanErr(
|
||||
expectedLimitsQuery,
|
||||
nil,
|
||||
nil,
|
||||
),
|
||||
err: func(err error) (error, bool) {
|
||||
if !zerrors.IsNotFound(err) {
|
||||
return fmt.Errorf("err should be zitadel.NotFoundError got: %w", err), false
|
||||
}
|
||||
return nil, true
|
||||
},
|
||||
},
|
||||
object: (*Limits)(nil),
|
||||
},
|
||||
{
|
||||
name: "prepareLimitsQuery",
|
||||
prepare: prepareLimitsQuery,
|
||||
want: want{
|
||||
sqlExpectations: mockQuery(
|
||||
expectedLimitsQuery,
|
||||
limitsCols,
|
||||
[]driver.Value{
|
||||
"limits1",
|
||||
testNow,
|
||||
testNow,
|
||||
"instance1",
|
||||
0,
|
||||
intervalDriverValue(t, time.Hour),
|
||||
},
|
||||
),
|
||||
},
|
||||
object: &Limits{
|
||||
AggregateID: "limits1",
|
||||
CreationDate: testNow,
|
||||
ChangeDate: testNow,
|
||||
ResourceOwner: "instance1",
|
||||
Sequence: 0,
|
||||
AuditLogRetention: gu.Ptr(time.Hour),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "prepareLimitsQuery sql err",
|
||||
prepare: prepareLimitsQuery,
|
||||
want: want{
|
||||
sqlExpectations: mockQueryErr(
|
||||
expectedLimitsQuery,
|
||||
sql.ErrConnDone,
|
||||
),
|
||||
err: func(err error) (error, bool) {
|
||||
if !errors.Is(err, sql.ErrConnDone) {
|
||||
return fmt.Errorf("err should be sql.ErrConnDone got: %w", err), false
|
||||
}
|
||||
return nil, true
|
||||
},
|
||||
},
|
||||
object: (*Limits)(nil),
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
assertPrepare(t, tt.prepare, tt.object, tt.want.sqlExpectations, tt.want.err, defaultPrepareArgs...)
|
||||
})
|
||||
}
|
||||
}
|
@@ -21,6 +21,7 @@ const (
|
||||
LimitsColumnSequence = "sequence"
|
||||
|
||||
LimitsColumnAuditLogRetention = "audit_log_retention"
|
||||
LimitsColumnBlock = "block"
|
||||
)
|
||||
|
||||
type limitsProjection struct{}
|
||||
@@ -43,6 +44,7 @@ func (*limitsProjection) Init() *old_handler.Check {
|
||||
handler.NewColumn(LimitsColumnInstanceID, handler.ColumnTypeText),
|
||||
handler.NewColumn(LimitsColumnSequence, handler.ColumnTypeInt64),
|
||||
handler.NewColumn(LimitsColumnAuditLogRetention, handler.ColumnTypeInterval, handler.Nullable()),
|
||||
handler.NewColumn(LimitsColumnBlock, handler.ColumnTypeBool, handler.Nullable()),
|
||||
},
|
||||
handler.NewPrimaryKey(LimitsColumnInstanceID, LimitsColumnResourceOwner),
|
||||
),
|
||||
@@ -96,6 +98,9 @@ func (p *limitsProjection) reduceLimitsSet(event eventstore.Event) (*handler.Sta
|
||||
if e.AuditLogRetention != nil {
|
||||
updateCols = append(updateCols, handler.NewCol(LimitsColumnAuditLogRetention, *e.AuditLogRetention))
|
||||
}
|
||||
if e.Block != nil {
|
||||
updateCols = append(updateCols, handler.NewCol(LimitsColumnBlock, *e.Block))
|
||||
}
|
||||
return handler.NewUpsertStatement(e, conflictCols, updateCols), nil
|
||||
}
|
||||
|
||||
|
@@ -21,7 +21,7 @@ func TestLimitsProjection_reduces(t *testing.T) {
|
||||
want wantReduce
|
||||
}{
|
||||
{
|
||||
name: "reduceLimitsSet",
|
||||
name: "reduceLimitsSet auditLogRetention",
|
||||
args: args{
|
||||
event: getEvent(testEvent(
|
||||
limits.SetEventType,
|
||||
@@ -53,7 +53,107 @@ func TestLimitsProjection_reduces(t *testing.T) {
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
name: "reduceLimitsSet block true",
|
||||
args: args{
|
||||
event: getEvent(testEvent(
|
||||
limits.SetEventType,
|
||||
limits.AggregateType,
|
||||
[]byte(`{
|
||||
"block": true
|
||||
}`),
|
||||
), limits.SetEventMapper),
|
||||
},
|
||||
reduce: (&limitsProjection{}).reduceLimitsSet,
|
||||
want: wantReduce{
|
||||
aggregateType: eventstore.AggregateType("limits"),
|
||||
sequence: 15,
|
||||
executer: &testExecuter{
|
||||
executions: []execution{
|
||||
{
|
||||
expectedStmt: "INSERT INTO projections.limits (instance_id, resource_owner, creation_date, change_date, sequence, aggregate_id, block) VALUES ($1, $2, $3, $4, $5, $6, $7) ON CONFLICT (instance_id, resource_owner) DO UPDATE SET (creation_date, change_date, sequence, aggregate_id, block) = (EXCLUDED.creation_date, EXCLUDED.change_date, EXCLUDED.sequence, EXCLUDED.aggregate_id, EXCLUDED.block)",
|
||||
expectedArgs: []interface{}{
|
||||
"instance-id",
|
||||
"ro-id",
|
||||
anyArg{},
|
||||
anyArg{},
|
||||
uint64(15),
|
||||
"agg-id",
|
||||
true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "reduceLimitsSet block false",
|
||||
args: args{
|
||||
event: getEvent(testEvent(
|
||||
limits.SetEventType,
|
||||
limits.AggregateType,
|
||||
[]byte(`{
|
||||
"block": false
|
||||
}`),
|
||||
), limits.SetEventMapper),
|
||||
},
|
||||
reduce: (&limitsProjection{}).reduceLimitsSet,
|
||||
want: wantReduce{
|
||||
aggregateType: eventstore.AggregateType("limits"),
|
||||
sequence: 15,
|
||||
executer: &testExecuter{
|
||||
executions: []execution{
|
||||
{
|
||||
expectedStmt: "INSERT INTO projections.limits (instance_id, resource_owner, creation_date, change_date, sequence, aggregate_id, block) VALUES ($1, $2, $3, $4, $5, $6, $7) ON CONFLICT (instance_id, resource_owner) DO UPDATE SET (creation_date, change_date, sequence, aggregate_id, block) = (EXCLUDED.creation_date, EXCLUDED.change_date, EXCLUDED.sequence, EXCLUDED.aggregate_id, EXCLUDED.block)",
|
||||
expectedArgs: []interface{}{
|
||||
"instance-id",
|
||||
"ro-id",
|
||||
anyArg{},
|
||||
anyArg{},
|
||||
uint64(15),
|
||||
"agg-id",
|
||||
false,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "reduceLimitsSet all",
|
||||
args: args{
|
||||
event: getEvent(testEvent(
|
||||
limits.SetEventType,
|
||||
limits.AggregateType,
|
||||
[]byte(`{
|
||||
"auditLogRetention": 300000000000,
|
||||
"block": true
|
||||
}`),
|
||||
), limits.SetEventMapper),
|
||||
},
|
||||
reduce: (&limitsProjection{}).reduceLimitsSet,
|
||||
want: wantReduce{
|
||||
aggregateType: eventstore.AggregateType("limits"),
|
||||
sequence: 15,
|
||||
executer: &testExecuter{
|
||||
executions: []execution{
|
||||
{
|
||||
expectedStmt: "INSERT INTO projections.limits (instance_id, resource_owner, creation_date, change_date, sequence, aggregate_id, audit_log_retention, block) VALUES ($1, $2, $3, $4, $5, $6, $7, $8) ON CONFLICT (instance_id, resource_owner) DO UPDATE SET (creation_date, change_date, sequence, aggregate_id, audit_log_retention, block) = (EXCLUDED.creation_date, EXCLUDED.change_date, EXCLUDED.sequence, EXCLUDED.aggregate_id, EXCLUDED.audit_log_retention, EXCLUDED.block)",
|
||||
expectedArgs: []interface{}{
|
||||
"instance-id",
|
||||
"ro-id",
|
||||
anyArg{},
|
||||
anyArg{},
|
||||
uint64(15),
|
||||
"agg-id",
|
||||
time.Minute * 5,
|
||||
true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "reduceLimitsReset",
|
||||
args: args{
|
||||
|
Reference in New Issue
Block a user