mirror of
https://github.com/andwati/zitadel.git
synced 2026-11-07 22:55:12 +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 <[email protected]> * remove support for owner bulk limit commands * project limits to instances * migrate instances projection * Revert "migrate instances projection" This reverts commit 214218732a56e6df823beac1972adfcf8beeded5. * 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 <[email protected]> * remove owner * remove owner from reset --------- Co-authored-by: Livio Spring <[email protected]>
This commit is contained in:
co-authored by
Livio Spring
parent
d9d376a275
commit
ed0bc39ea4
@@ -19,10 +19,11 @@ import (
|
||||
)
|
||||
|
||||
type AccessInterceptor struct {
|
||||
svc *logstore.Service[*record.AccessLog]
|
||||
logstoreSvc *logstore.Service[*record.AccessLog]
|
||||
cookieHandler *http_utils.CookieHandler
|
||||
limitConfig *AccessConfig
|
||||
storeOnly bool
|
||||
redirect string
|
||||
}
|
||||
|
||||
type AccessConfig struct {
|
||||
@@ -32,10 +33,10 @@ type AccessConfig struct {
|
||||
|
||||
// NewAccessInterceptor intercepts all requests and stores them to the logstore.
|
||||
// If storeOnly is false, it also checks if requests are exhausted.
|
||||
// If requests are exhausted, it also returns http.StatusTooManyRequests and sets a cookie
|
||||
// If requests are exhausted, it also returns http.StatusTooManyRequests or a redirect to the given path and sets a cookie
|
||||
func NewAccessInterceptor(svc *logstore.Service[*record.AccessLog], cookieHandler *http_utils.CookieHandler, cookieConfig *AccessConfig) *AccessInterceptor {
|
||||
return &AccessInterceptor{
|
||||
svc: svc,
|
||||
logstoreSvc: svc,
|
||||
cookieHandler: cookieHandler,
|
||||
limitConfig: cookieConfig,
|
||||
}
|
||||
@@ -43,24 +44,61 @@ func NewAccessInterceptor(svc *logstore.Service[*record.AccessLog], cookieHandle
|
||||
|
||||
func (a *AccessInterceptor) WithoutLimiting() *AccessInterceptor {
|
||||
return &AccessInterceptor{
|
||||
svc: a.svc,
|
||||
logstoreSvc: a.logstoreSvc,
|
||||
cookieHandler: a.cookieHandler,
|
||||
limitConfig: a.limitConfig,
|
||||
storeOnly: true,
|
||||
redirect: a.redirect,
|
||||
}
|
||||
}
|
||||
|
||||
func (a *AccessInterceptor) WithRedirect(redirect string) *AccessInterceptor {
|
||||
return &AccessInterceptor{
|
||||
logstoreSvc: a.logstoreSvc,
|
||||
cookieHandler: a.cookieHandler,
|
||||
limitConfig: a.limitConfig,
|
||||
storeOnly: a.storeOnly,
|
||||
redirect: redirect,
|
||||
}
|
||||
}
|
||||
|
||||
func (a *AccessInterceptor) AccessService() *logstore.Service[*record.AccessLog] {
|
||||
return a.svc
|
||||
return a.logstoreSvc
|
||||
}
|
||||
|
||||
func (a *AccessInterceptor) Limit(ctx context.Context) bool {
|
||||
if !a.svc.Enabled() || a.storeOnly {
|
||||
func (a *AccessInterceptor) Limit(w http.ResponseWriter, r *http.Request, publicAuthPathPrefixes ...string) bool {
|
||||
if a.storeOnly {
|
||||
return false
|
||||
}
|
||||
ctx := r.Context()
|
||||
instance := authz.GetInstance(ctx)
|
||||
remaining := a.svc.Limit(ctx, instance.InstanceID())
|
||||
return remaining != nil && *remaining <= 0
|
||||
var deleteCookie bool
|
||||
defer func() {
|
||||
if deleteCookie {
|
||||
a.DeleteExhaustedCookie(w)
|
||||
}
|
||||
}()
|
||||
if block := instance.Block(); block != nil {
|
||||
if *block {
|
||||
a.SetExhaustedCookie(w, r)
|
||||
return true
|
||||
}
|
||||
deleteCookie = true
|
||||
}
|
||||
for _, ignoredPathPrefix := range publicAuthPathPrefixes {
|
||||
if strings.HasPrefix(r.RequestURI, ignoredPathPrefix) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
remaining := a.logstoreSvc.Limit(ctx, instance.InstanceID())
|
||||
if remaining != nil {
|
||||
if remaining != nil && *remaining > 0 {
|
||||
a.SetExhaustedCookie(w, r)
|
||||
return true
|
||||
}
|
||||
deleteCookie = true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (a *AccessInterceptor) SetExhaustedCookie(writer http.ResponseWriter, request *http.Request) {
|
||||
@@ -81,42 +119,30 @@ func (a *AccessInterceptor) DeleteExhaustedCookie(writer http.ResponseWriter) {
|
||||
a.cookieHandler.DeleteCookie(writer, a.limitConfig.ExhaustedCookieKey)
|
||||
}
|
||||
|
||||
func (a *AccessInterceptor) HandleIgnorePathPrefixes(ignoredPathPrefixes []string) func(next http.Handler) http.Handler {
|
||||
return a.handle(ignoredPathPrefixes...)
|
||||
func (a *AccessInterceptor) HandleWithPublicAuthPathPrefixes(publicPathPrefixes []string) func(next http.Handler) http.Handler {
|
||||
return a.handle(publicPathPrefixes...)
|
||||
}
|
||||
|
||||
func (a *AccessInterceptor) Handle(next http.Handler) http.Handler {
|
||||
return a.handle()(next)
|
||||
}
|
||||
|
||||
func (a *AccessInterceptor) handle(ignoredPathPrefixes ...string) func(http.Handler) http.Handler {
|
||||
func (a *AccessInterceptor) handle(publicAuthPathPrefixes ...string) func(http.Handler) http.Handler {
|
||||
return func(next http.Handler) http.Handler {
|
||||
if !a.svc.Enabled() {
|
||||
return next
|
||||
}
|
||||
return http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) {
|
||||
ctx := request.Context()
|
||||
tracingCtx, checkSpan := tracing.NewNamedSpan(ctx, "checkAccessQuota")
|
||||
wrappedWriter := &statusRecorder{ResponseWriter: writer, status: 0}
|
||||
for _, ignoredPathPrefix := range ignoredPathPrefixes {
|
||||
if !strings.HasPrefix(request.RequestURI, ignoredPathPrefix) {
|
||||
continue
|
||||
}
|
||||
checkSpan.End()
|
||||
next.ServeHTTP(wrappedWriter, request)
|
||||
a.writeLog(tracingCtx, wrappedWriter, writer, request, true)
|
||||
return
|
||||
}
|
||||
limited := a.Limit(tracingCtx)
|
||||
limited := a.Limit(wrappedWriter, request.WithContext(tracingCtx), publicAuthPathPrefixes...)
|
||||
checkSpan.End()
|
||||
if limited {
|
||||
a.SetExhaustedCookie(wrappedWriter, request)
|
||||
http.Error(wrappedWriter, "quota for authenticated requests is exhausted", http.StatusTooManyRequests)
|
||||
}
|
||||
if !limited && !a.storeOnly {
|
||||
a.DeleteExhaustedCookie(wrappedWriter)
|
||||
}
|
||||
if !limited {
|
||||
if a.redirect != "" {
|
||||
// The console guides the user when the cookie is set
|
||||
http.Redirect(wrappedWriter, request, a.redirect, http.StatusFound)
|
||||
} else {
|
||||
http.Error(wrappedWriter, "Your ZITADEL instance is blocked.", http.StatusTooManyRequests)
|
||||
}
|
||||
} else {
|
||||
next.ServeHTTP(wrappedWriter, request)
|
||||
}
|
||||
a.writeLog(tracingCtx, wrappedWriter, writer, request, a.storeOnly)
|
||||
@@ -125,6 +151,9 @@ func (a *AccessInterceptor) handle(ignoredPathPrefixes ...string) func(http.Hand
|
||||
}
|
||||
|
||||
func (a *AccessInterceptor) writeLog(ctx context.Context, wrappedWriter *statusRecorder, writer http.ResponseWriter, request *http.Request, notCountable bool) {
|
||||
if !a.logstoreSvc.Enabled() {
|
||||
return
|
||||
}
|
||||
ctx, writeSpan := tracing.NewNamedSpan(ctx, "writeAccess")
|
||||
defer writeSpan.End()
|
||||
requestURL := request.RequestURI
|
||||
@@ -133,7 +162,7 @@ func (a *AccessInterceptor) writeLog(ctx context.Context, wrappedWriter *statusR
|
||||
logging.WithError(err).WithField("url", requestURL).Warning("failed to unescape request url")
|
||||
}
|
||||
instance := authz.GetInstance(ctx)
|
||||
a.svc.Handle(ctx, &record.AccessLog{
|
||||
a.logstoreSvc.Handle(ctx, &record.AccessLog{
|
||||
LogDate: time.Now(),
|
||||
Protocol: record.HTTP,
|
||||
RequestURL: unescapedURL,
|
||||
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
"net/http/httptest"
|
||||
"reflect"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"golang.org/x/text/language"
|
||||
@@ -299,6 +300,14 @@ func (m *mockInstanceVerifier) InstanceByID(context.Context) (authz.Instance, er
|
||||
|
||||
type mockInstance struct{}
|
||||
|
||||
func (m *mockInstance) Block() *bool {
|
||||
panic("shouldn't be called here")
|
||||
}
|
||||
|
||||
func (m *mockInstance) AuditLogRetention() *time.Duration {
|
||||
panic("shouldn't be called here")
|
||||
}
|
||||
|
||||
func (m *mockInstance) InstanceID() string {
|
||||
return "instanceID"
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user