2023-02-15 01:52:11 +00:00
|
|
|
package middleware
|
|
|
|
|
|
|
|
import (
|
2023-05-15 06:51:02 +00:00
|
|
|
"context"
|
2023-05-11 07:24:44 +00:00
|
|
|
"net"
|
2023-02-15 01:52:11 +00:00
|
|
|
"net/http"
|
|
|
|
"net/url"
|
2023-05-11 07:24:44 +00:00
|
|
|
"strings"
|
2023-02-15 01:52:11 +00:00
|
|
|
"time"
|
|
|
|
|
|
|
|
"github.com/zitadel/logging"
|
|
|
|
|
|
|
|
"github.com/zitadel/zitadel/internal/api/authz"
|
2023-05-11 07:24:44 +00:00
|
|
|
"github.com/zitadel/zitadel/internal/api/grpc/server/middleware"
|
2023-02-15 01:52:11 +00:00
|
|
|
http_utils "github.com/zitadel/zitadel/internal/api/http"
|
|
|
|
"github.com/zitadel/zitadel/internal/logstore"
|
2023-09-15 14:58:45 +00:00
|
|
|
"github.com/zitadel/zitadel/internal/logstore/record"
|
2023-02-15 01:52:11 +00:00
|
|
|
"github.com/zitadel/zitadel/internal/telemetry/tracing"
|
|
|
|
)
|
|
|
|
|
|
|
|
type AccessInterceptor struct {
|
2024-01-17 10:16:48 +00:00
|
|
|
logstoreSvc *logstore.Service[*record.AccessLog]
|
2023-02-15 01:52:11 +00:00
|
|
|
cookieHandler *http_utils.CookieHandler
|
|
|
|
limitConfig *AccessConfig
|
2023-05-11 07:24:44 +00:00
|
|
|
storeOnly bool
|
2024-01-17 10:16:48 +00:00
|
|
|
redirect string
|
2023-02-15 01:52:11 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
type AccessConfig struct {
|
|
|
|
ExhaustedCookieKey string
|
|
|
|
ExhaustedCookieMaxAge time.Duration
|
|
|
|
}
|
|
|
|
|
2023-05-11 07:24:44 +00:00
|
|
|
// NewAccessInterceptor intercepts all requests and stores them to the logstore.
|
|
|
|
// If storeOnly is false, it also checks if requests are exhausted.
|
2024-01-17 10:16:48 +00:00
|
|
|
// If requests are exhausted, it also returns http.StatusTooManyRequests or a redirect to the given path and sets a cookie
|
2023-09-15 14:58:45 +00:00
|
|
|
func NewAccessInterceptor(svc *logstore.Service[*record.AccessLog], cookieHandler *http_utils.CookieHandler, cookieConfig *AccessConfig) *AccessInterceptor {
|
2023-02-15 01:52:11 +00:00
|
|
|
return &AccessInterceptor{
|
2024-01-17 10:16:48 +00:00
|
|
|
logstoreSvc: svc,
|
2023-05-11 07:24:44 +00:00
|
|
|
cookieHandler: cookieHandler,
|
|
|
|
limitConfig: cookieConfig,
|
2023-02-15 01:52:11 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-05-15 06:51:02 +00:00
|
|
|
func (a *AccessInterceptor) WithoutLimiting() *AccessInterceptor {
|
|
|
|
return &AccessInterceptor{
|
2024-01-17 10:16:48 +00:00
|
|
|
logstoreSvc: a.logstoreSvc,
|
2023-05-15 06:51:02 +00:00
|
|
|
cookieHandler: a.cookieHandler,
|
|
|
|
limitConfig: a.limitConfig,
|
|
|
|
storeOnly: true,
|
2024-01-17 10:16:48 +00:00
|
|
|
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,
|
2023-05-15 06:51:02 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-09-15 14:58:45 +00:00
|
|
|
func (a *AccessInterceptor) AccessService() *logstore.Service[*record.AccessLog] {
|
2024-01-17 10:16:48 +00:00
|
|
|
return a.logstoreSvc
|
2023-05-15 06:51:02 +00:00
|
|
|
}
|
|
|
|
|
2024-01-17 10:16:48 +00:00
|
|
|
func (a *AccessInterceptor) Limit(w http.ResponseWriter, r *http.Request, publicAuthPathPrefixes ...string) bool {
|
|
|
|
if a.storeOnly {
|
2023-05-15 06:51:02 +00:00
|
|
|
return false
|
|
|
|
}
|
2024-01-17 10:16:48 +00:00
|
|
|
ctx := r.Context()
|
2023-05-15 06:51:02 +00:00
|
|
|
instance := authz.GetInstance(ctx)
|
2024-01-17 10:16:48 +00:00
|
|
|
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 {
|
2024-01-25 16:28:20 +00:00
|
|
|
deleteCookie = true
|
|
|
|
return false
|
2024-01-17 10:16:48 +00:00
|
|
|
}
|
2024-01-25 16:28:20 +00:00
|
|
|
a.SetExhaustedCookie(w, r)
|
|
|
|
return true
|
2024-01-17 10:16:48 +00:00
|
|
|
}
|
|
|
|
return false
|
2023-05-15 06:51:02 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
func (a *AccessInterceptor) SetExhaustedCookie(writer http.ResponseWriter, request *http.Request) {
|
|
|
|
cookieValue := "true"
|
|
|
|
host := request.Header.Get(middleware.HTTP1Host)
|
|
|
|
domain := host
|
|
|
|
if strings.ContainsAny(host, ":") {
|
|
|
|
var err error
|
|
|
|
domain, _, err = net.SplitHostPort(host)
|
|
|
|
if err != nil {
|
|
|
|
logging.WithError(err).WithField("host", host).Warning("failed to extract cookie domain from request host")
|
|
|
|
}
|
|
|
|
}
|
|
|
|
a.cookieHandler.SetCookie(writer, a.limitConfig.ExhaustedCookieKey, domain, cookieValue)
|
|
|
|
}
|
|
|
|
|
2023-05-19 05:12:31 +00:00
|
|
|
func (a *AccessInterceptor) DeleteExhaustedCookie(writer http.ResponseWriter) {
|
|
|
|
a.cookieHandler.DeleteCookie(writer, a.limitConfig.ExhaustedCookieKey)
|
2023-05-15 06:51:02 +00:00
|
|
|
}
|
|
|
|
|
2024-01-17 10:16:48 +00:00
|
|
|
func (a *AccessInterceptor) HandleWithPublicAuthPathPrefixes(publicPathPrefixes []string) func(next http.Handler) http.Handler {
|
|
|
|
return a.handle(publicPathPrefixes...)
|
2023-09-15 14:58:45 +00:00
|
|
|
}
|
|
|
|
|
2023-02-15 01:52:11 +00:00
|
|
|
func (a *AccessInterceptor) Handle(next http.Handler) http.Handler {
|
2023-09-15 14:58:45 +00:00
|
|
|
return a.handle()(next)
|
|
|
|
}
|
|
|
|
|
2024-01-17 10:16:48 +00:00
|
|
|
func (a *AccessInterceptor) handle(publicAuthPathPrefixes ...string) func(http.Handler) http.Handler {
|
2023-09-15 14:58:45 +00:00
|
|
|
return func(next http.Handler) http.Handler {
|
|
|
|
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}
|
2024-01-17 10:16:48 +00:00
|
|
|
limited := a.Limit(wrappedWriter, request.WithContext(tracingCtx), publicAuthPathPrefixes...)
|
2023-09-15 14:58:45 +00:00
|
|
|
checkSpan.End()
|
|
|
|
if limited {
|
2024-01-17 10:16:48 +00:00
|
|
|
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 {
|
2023-09-15 14:58:45 +00:00
|
|
|
next.ServeHTTP(wrappedWriter, request)
|
|
|
|
}
|
|
|
|
a.writeLog(tracingCtx, wrappedWriter, writer, request, a.storeOnly)
|
2023-02-15 01:52:11 +00:00
|
|
|
})
|
2023-09-15 14:58:45 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func (a *AccessInterceptor) writeLog(ctx context.Context, wrappedWriter *statusRecorder, writer http.ResponseWriter, request *http.Request, notCountable bool) {
|
2024-01-17 10:16:48 +00:00
|
|
|
if !a.logstoreSvc.Enabled() {
|
|
|
|
return
|
|
|
|
}
|
2023-09-15 14:58:45 +00:00
|
|
|
ctx, writeSpan := tracing.NewNamedSpan(ctx, "writeAccess")
|
|
|
|
defer writeSpan.End()
|
|
|
|
requestURL := request.RequestURI
|
|
|
|
unescapedURL, err := url.QueryUnescape(requestURL)
|
|
|
|
if err != nil {
|
|
|
|
logging.WithError(err).WithField("url", requestURL).Warning("failed to unescape request url")
|
|
|
|
}
|
|
|
|
instance := authz.GetInstance(ctx)
|
2024-01-17 10:16:48 +00:00
|
|
|
a.logstoreSvc.Handle(ctx, &record.AccessLog{
|
2023-09-15 14:58:45 +00:00
|
|
|
LogDate: time.Now(),
|
|
|
|
Protocol: record.HTTP,
|
|
|
|
RequestURL: unescapedURL,
|
|
|
|
ResponseStatus: uint32(wrappedWriter.status),
|
|
|
|
RequestHeaders: request.Header,
|
|
|
|
ResponseHeaders: writer.Header(),
|
|
|
|
InstanceID: instance.InstanceID(),
|
|
|
|
ProjectID: instance.ProjectID(),
|
|
|
|
RequestedDomain: instance.RequestedDomain(),
|
|
|
|
RequestedHost: instance.RequestedHost(),
|
|
|
|
NotCountable: notCountable,
|
2023-02-15 01:52:11 +00:00
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
type statusRecorder struct {
|
|
|
|
http.ResponseWriter
|
|
|
|
status int
|
|
|
|
ignoreWrites bool
|
|
|
|
}
|
|
|
|
|
|
|
|
func (r *statusRecorder) WriteHeader(status int) {
|
|
|
|
if r.ignoreWrites {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
r.status = status
|
|
|
|
r.ResponseWriter.WriteHeader(status)
|
|
|
|
}
|