mirror of
https://github.com/zitadel/zitadel.git
synced 2025-07-13 06:08:31 +00:00

This PR summarizes multiple changes specifically only available with ZITADEL v3: - feat: Web Keys management (https://github.com/zitadel/zitadel/pull/9526) - fix(cmd): ensure proper working of mirror (https://github.com/zitadel/zitadel/pull/9509) - feat(Authz): system user support for permission check v2 (https://github.com/zitadel/zitadel/pull/9640) - chore(license): change from Apache to AGPL (https://github.com/zitadel/zitadel/pull/9597) - feat(console): list v2 sessions (https://github.com/zitadel/zitadel/pull/9539) - fix(console): add loginV2 feature flag (https://github.com/zitadel/zitadel/pull/9682) - fix(feature flags): allow reading "own" flags (https://github.com/zitadel/zitadel/pull/9649) - feat(console): add Actions V2 UI (https://github.com/zitadel/zitadel/pull/9591) BREAKING CHANGE - feat(webkey): migrate to v2beta API (https://github.com/zitadel/zitadel/pull/9445) - chore!: remove CockroachDB Support (https://github.com/zitadel/zitadel/pull/9444) - feat(actions): migrate to v2beta API (https://github.com/zitadel/zitadel/pull/9489) --------- Co-authored-by: Livio Spring <livio.a@gmail.com> Co-authored-by: Stefan Benz <46600784+stebenz@users.noreply.github.com> Co-authored-by: Silvan <27845747+adlerhurst@users.noreply.github.com> Co-authored-by: Ramon <mail@conblem.me> Co-authored-by: Elio Bischof <elio@zitadel.com> Co-authored-by: Kenta Yamaguchi <56732734+KEY60228@users.noreply.github.com> Co-authored-by: Harsha Reddy <harsha.reddy@klaviyo.com> Co-authored-by: Livio Spring <livio@zitadel.com> Co-authored-by: Max Peintner <max@caos.ch> Co-authored-by: Iraq <66622793+kkrime@users.noreply.github.com> Co-authored-by: Florian Forster <florian@zitadel.com> Co-authored-by: Tim Möhlmann <tim+github@zitadel.com> Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Co-authored-by: Max Peintner <peintnerm@gmail.com>
180 lines
6.0 KiB
Go
180 lines
6.0 KiB
Go
package query
|
|
|
|
import (
|
|
"context"
|
|
"database/sql"
|
|
"errors"
|
|
"math"
|
|
"time"
|
|
|
|
sq "github.com/Masterminds/squirrel"
|
|
|
|
"github.com/zitadel/zitadel/internal/query/projection"
|
|
"github.com/zitadel/zitadel/internal/repository/quota"
|
|
"github.com/zitadel/zitadel/internal/telemetry/tracing"
|
|
"github.com/zitadel/zitadel/internal/zerrors"
|
|
)
|
|
|
|
var (
|
|
quotaNotificationsTable = table{
|
|
name: projection.QuotaNotificationsTable,
|
|
instanceIDCol: projection.QuotaNotificationColumnInstanceID,
|
|
}
|
|
QuotaNotificationColumnInstanceID = Column{
|
|
name: projection.QuotaNotificationColumnInstanceID,
|
|
table: quotaNotificationsTable,
|
|
}
|
|
QuotaNotificationColumnUnit = Column{
|
|
name: projection.QuotaNotificationColumnUnit,
|
|
table: quotaNotificationsTable,
|
|
}
|
|
QuotaNotificationColumnID = Column{
|
|
name: projection.QuotaNotificationColumnID,
|
|
table: quotaNotificationsTable,
|
|
}
|
|
QuotaNotificationColumnCallURL = Column{
|
|
name: projection.QuotaNotificationColumnCallURL,
|
|
table: quotaNotificationsTable,
|
|
}
|
|
QuotaNotificationColumnPercent = Column{
|
|
name: projection.QuotaNotificationColumnPercent,
|
|
table: quotaNotificationsTable,
|
|
}
|
|
QuotaNotificationColumnRepeat = Column{
|
|
name: projection.QuotaNotificationColumnRepeat,
|
|
table: quotaNotificationsTable,
|
|
}
|
|
QuotaNotificationColumnLatestDuePeriodStart = Column{
|
|
name: projection.QuotaNotificationColumnLatestDuePeriodStart,
|
|
table: quotaNotificationsTable,
|
|
}
|
|
QuotaNotificationColumnNextDueThreshold = Column{
|
|
name: projection.QuotaNotificationColumnNextDueThreshold,
|
|
table: quotaNotificationsTable,
|
|
}
|
|
)
|
|
|
|
func (q *Queries) GetDueQuotaNotifications(ctx context.Context, instanceID string, unit quota.Unit, qu *Quota, periodStart time.Time, usedAbs uint64) (dueNotifications []*quota.NotificationDueEvent, err error) {
|
|
ctx, span := tracing.NewSpan(ctx)
|
|
defer func() { span.EndWithError(err) }()
|
|
usedRel := uint16(math.Floor(float64(usedAbs*100) / float64(qu.Amount)))
|
|
query, scan := prepareQuotaNotificationsQuery()
|
|
stmt, args, err := query.Where(
|
|
sq.And{
|
|
sq.Eq{
|
|
QuotaNotificationColumnInstanceID.identifier(): instanceID,
|
|
QuotaNotificationColumnUnit.identifier(): unit,
|
|
},
|
|
sq.Or{
|
|
// If the relative usage is greater than the next due threshold in the current period, it's clear we can notify
|
|
sq.And{
|
|
sq.Eq{QuotaNotificationColumnLatestDuePeriodStart.identifier(): periodStart},
|
|
sq.LtOrEq{QuotaNotificationColumnNextDueThreshold.identifier(): usedRel},
|
|
},
|
|
// In case we haven't seen a due notification for this quota period, we compare against the configured percent
|
|
sq.And{
|
|
sq.Or{
|
|
sq.Expr(QuotaNotificationColumnLatestDuePeriodStart.identifier() + " IS NULL"),
|
|
sq.NotEq{QuotaNotificationColumnLatestDuePeriodStart.identifier(): periodStart},
|
|
},
|
|
sq.LtOrEq{QuotaNotificationColumnPercent.identifier(): usedRel},
|
|
},
|
|
},
|
|
},
|
|
).ToSql()
|
|
if err != nil {
|
|
return nil, zerrors.ThrowInternal(err, "QUERY-XmYn9", "Errors.Query.SQLStatement")
|
|
}
|
|
var notifications *QuotaNotifications
|
|
err = q.client.QueryContext(ctx, func(rows *sql.Rows) error {
|
|
notifications, err = scan(rows)
|
|
return err
|
|
}, stmt, args...)
|
|
if errors.Is(err, sql.ErrNoRows) {
|
|
return nil, nil
|
|
}
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
for _, notification := range notifications.Configs {
|
|
reachedThreshold := calculateThreshold(usedRel, notification.Percent)
|
|
if !notification.Repeat && notification.Percent < reachedThreshold {
|
|
continue
|
|
}
|
|
dueNotifications = append(
|
|
dueNotifications,
|
|
quota.NewNotificationDueEvent(
|
|
ctx,
|
|
"a.NewAggregate(qu.ID, instanceID).Aggregate,
|
|
unit,
|
|
notification.ID,
|
|
notification.CallURL,
|
|
periodStart,
|
|
reachedThreshold,
|
|
usedAbs,
|
|
),
|
|
)
|
|
}
|
|
return dueNotifications, nil
|
|
}
|
|
|
|
type QuotaNotification struct {
|
|
ID string
|
|
CallURL string
|
|
Percent uint16
|
|
Repeat bool
|
|
NextDueThreshold uint16
|
|
}
|
|
|
|
type QuotaNotifications struct {
|
|
SearchResponse
|
|
Configs []*QuotaNotification
|
|
}
|
|
|
|
// calculateThreshold calculates the nearest reached threshold.
|
|
// It makes sure that the percent configured on the notification is calculated within the "current" 100%,
|
|
// e.g. when configuring 80%, the thresholds are 80, 180, 280, ...
|
|
// so 170% use is always 70% of the current bucket, with the above config, the reached threshold would be 80.
|
|
func calculateThreshold(usedRel, notificationPercent uint16) uint16 {
|
|
// check how many times we reached 100%
|
|
times := math.Floor(float64(usedRel) / 100)
|
|
// check how many times we reached the percent configured with the "current" 100%
|
|
percent := math.Floor(float64(usedRel%100) / float64(notificationPercent))
|
|
// If neither is reached, directly return 0.
|
|
// This way we don't end up in some wrong uint16 range in the calculation below.
|
|
if times == 0 && percent == 0 {
|
|
return 0
|
|
}
|
|
return uint16(times+percent-1)*100 + notificationPercent
|
|
}
|
|
|
|
func prepareQuotaNotificationsQuery() (sq.SelectBuilder, func(*sql.Rows) (*QuotaNotifications, error)) {
|
|
return sq.Select(
|
|
QuotaNotificationColumnID.identifier(),
|
|
QuotaNotificationColumnCallURL.identifier(),
|
|
QuotaNotificationColumnPercent.identifier(),
|
|
QuotaNotificationColumnRepeat.identifier(),
|
|
QuotaNotificationColumnNextDueThreshold.identifier(),
|
|
).
|
|
From(quotaNotificationsTable.identifier()).
|
|
PlaceholderFormat(sq.Dollar), func(rows *sql.Rows) (*QuotaNotifications, error) {
|
|
cfgs := &QuotaNotifications{Configs: []*QuotaNotification{}}
|
|
for rows.Next() {
|
|
cfg := new(QuotaNotification)
|
|
var nextDueThreshold sql.NullInt16
|
|
err := rows.Scan(&cfg.ID, &cfg.CallURL, &cfg.Percent, &cfg.Repeat, &nextDueThreshold)
|
|
if err != nil {
|
|
if errors.Is(err, sql.ErrNoRows) {
|
|
return nil, zerrors.ThrowNotFound(err, "QUERY-bbqWb", "Errors.QuotaNotification.NotExisting")
|
|
}
|
|
return nil, zerrors.ThrowInternal(err, "QUERY-8copS", "Errors.Internal")
|
|
}
|
|
if nextDueThreshold.Valid {
|
|
cfg.NextDueThreshold = uint16(nextDueThreshold.Int16)
|
|
}
|
|
cfgs.Configs = append(cfgs.Configs, cfg)
|
|
}
|
|
return cfgs, nil
|
|
}
|
|
}
|