diff --git a/api/logs/v1/http.go b/api/logs/v1/http.go index 97102cec0..4213dedb9 100644 --- a/api/logs/v1/http.go +++ b/api/logs/v1/http.go @@ -58,7 +58,6 @@ type handlerConfiguration struct { registry *prometheus.Registry instrument handlerInstrumenter spanRoutePrefix string - rulesLabelFilters map[string][]string readMiddlewares []func(http.Handler) http.Handler writeMiddlewares []func(http.Handler) http.Handler rulesReadMiddlewares []func(http.Handler) http.Handler @@ -96,13 +95,6 @@ func WithSpanRoutePrefix(spanRoutePrefix string) HandlerOption { } } -// WithRulesLabelFilters adds the slice of rule labels filters to the handler configuration. -func WithRulesLabelFilters(f map[string][]string) HandlerOption { - return func(h *handlerConfiguration) { - h.rulesLabelFilters = f - } -} - // WithReadMiddleware adds a middleware for all read operations. func WithReadMiddleware(m func(http.Handler) http.Handler) HandlerOption { return func(h *handlerConfiguration) { @@ -239,7 +231,7 @@ func NewHandler(read, tail, write, rules *url.URL, rulesReadOnly bool, tlsOption } if rules != nil { - var proxyReadRules, proxyWriteRules http.Handler + var proxyRules http.Handler { middlewares := proxy.Middlewares( proxy.MiddlewareSetUpstream(rules), @@ -255,14 +247,7 @@ func NewHandler(read, tail, write, rules *url.URL, rulesReadOnly bool, tlsOption TLSClientConfig: tlsOptions.NewClientConfig(), } - proxyReadRules = &httputil.ReverseProxy{ - Director: middlewares, - ErrorLog: proxy.Logger(c.logger), - Transport: otelhttp.NewTransport(t), - ModifyResponse: newModifyResponse(c.logger, c.rulesLabelFilters), - } - - proxyWriteRules = &httputil.ReverseProxy{ + proxyRules = &httputil.ReverseProxy{ Director: middlewares, ErrorLog: proxy.Logger(c.logger), Transport: otelhttp.NewTransport(t), @@ -273,35 +258,35 @@ func NewHandler(read, tail, write, rules *url.URL, rulesReadOnly bool, tlsOption r.Use(c.rulesReadMiddlewares...) r.Get(rulesRoute, c.instrument.NewHandler( prometheus.Labels{"group": "logsv1", "handler": "rules"}, - otelhttp.WithRouteTag(c.spanRoutePrefix+rulesRoute, proxyReadRules), + otelhttp.WithRouteTag(c.spanRoutePrefix+rulesRoute, proxyRules), )) r.Get(rulesPerNamespaceRoute, c.instrument.NewHandler( prometheus.Labels{"group": "logsv1", "handler": "rules"}, - otelhttp.WithRouteTag(c.spanRoutePrefix+rulesPerNamespaceRoute, proxyReadRules), + otelhttp.WithRouteTag(c.spanRoutePrefix+rulesPerNamespaceRoute, proxyRules), )) r.Get(rulesPerGroupNameRoute, c.instrument.NewHandler( prometheus.Labels{"group": "logsv1", "handler": "rules"}, - otelhttp.WithRouteTag(c.spanRoutePrefix+rulesPerGroupNameRoute, proxyReadRules), + otelhttp.WithRouteTag(c.spanRoutePrefix+rulesPerGroupNameRoute, proxyRules), )) r.Get(prometheusRulesRoute, c.instrument.NewHandler( prometheus.Labels{"group": "logsv1", "handler": "rules"}, - otelhttp.WithRouteTag(c.spanRoutePrefix+prometheusRulesRoute, proxyReadRules), + otelhttp.WithRouteTag(c.spanRoutePrefix+prometheusRulesRoute, proxyRules), )) r.Get(prometheusAlertsRoute, c.instrument.NewHandler( prometheus.Labels{"group": "logsv1", "handler": "alerts"}, - otelhttp.WithRouteTag(c.spanRoutePrefix+prometheusAlertsRoute, proxyReadRules), + otelhttp.WithRouteTag(c.spanRoutePrefix+prometheusAlertsRoute, proxyRules), )) r.Get(promRulesRoute, c.instrument.NewHandler( prometheus.Labels{"group": "logsv1", "handler": "rules"}, - otelhttp.WithRouteTag(c.spanRoutePrefix+promRulesRoute, proxyReadRules), + otelhttp.WithRouteTag(c.spanRoutePrefix+promRulesRoute, proxyRules), )) r.Get(promRulesPerNamespaceRoute, c.instrument.NewHandler( prometheus.Labels{"group": "logsv1", "handler": "rules"}, - otelhttp.WithRouteTag(c.spanRoutePrefix+promRulesPerNamespaceRoute, proxyReadRules), + otelhttp.WithRouteTag(c.spanRoutePrefix+promRulesPerNamespaceRoute, proxyRules), )) r.Get(promRulesPerGroupNameRoute, c.instrument.NewHandler( prometheus.Labels{"group": "logsv1", "handler": "rules"}, - otelhttp.WithRouteTag(c.spanRoutePrefix+promRulesPerGroupNameRoute, proxyReadRules), + otelhttp.WithRouteTag(c.spanRoutePrefix+promRulesPerGroupNameRoute, proxyRules), )) }) @@ -311,28 +296,28 @@ func NewHandler(read, tail, write, rules *url.URL, rulesReadOnly bool, tlsOption r.Use(c.rulesWriteMiddlewares...) r.Post(rulesPerNamespaceRoute, c.instrument.NewHandler( prometheus.Labels{"group": "logsv1", "handler": "rules"}, - otelhttp.WithRouteTag(c.spanRoutePrefix+rulesPerNamespaceRoute, proxyWriteRules), + otelhttp.WithRouteTag(c.spanRoutePrefix+rulesPerNamespaceRoute, proxyRules), )) r.Delete(rulesPerNamespaceRoute, c.instrument.NewHandler( prometheus.Labels{"group": "logsv1", "handler": "rules"}, - otelhttp.WithRouteTag(c.spanRoutePrefix+rulesPerNamespaceRoute, proxyWriteRules), + otelhttp.WithRouteTag(c.spanRoutePrefix+rulesPerNamespaceRoute, proxyRules), )) r.Delete(rulesPerGroupNameRoute, c.instrument.NewHandler( prometheus.Labels{"group": "logsv1", "handler": "rules"}, - otelhttp.WithRouteTag(c.spanRoutePrefix+rulesPerGroupNameRoute, proxyWriteRules), + otelhttp.WithRouteTag(c.spanRoutePrefix+rulesPerGroupNameRoute, proxyRules), )) r.Post(promRulesPerNamespaceRoute, c.instrument.NewHandler( prometheus.Labels{"group": "logsv1", "handler": "rules"}, - otelhttp.WithRouteTag(c.spanRoutePrefix+promRulesPerNamespaceRoute, proxyWriteRules), + otelhttp.WithRouteTag(c.spanRoutePrefix+promRulesPerNamespaceRoute, proxyRules), )) r.Delete(promRulesPerNamespaceRoute, c.instrument.NewHandler( prometheus.Labels{"group": "logsv1", "handler": "rules"}, - otelhttp.WithRouteTag(c.spanRoutePrefix+promRulesPerNamespaceRoute, proxyWriteRules), + otelhttp.WithRouteTag(c.spanRoutePrefix+promRulesPerNamespaceRoute, proxyRules), )) r.Delete(promRulesPerGroupNameRoute, c.instrument.NewHandler( prometheus.Labels{"group": "logsv1", "handler": "rules"}, - otelhttp.WithRouteTag(c.spanRoutePrefix+promRulesPerGroupNameRoute, proxyWriteRules), + otelhttp.WithRouteTag(c.spanRoutePrefix+promRulesPerGroupNameRoute, proxyRules), )) }) } diff --git a/api/logs/v1/labels_enforcer.go b/api/logs/v1/labels_enforcer.go index 085c10c5e..4ee0ea694 100644 --- a/api/logs/v1/labels_enforcer.go +++ b/api/logs/v1/labels_enforcer.go @@ -18,7 +18,10 @@ type AuthzResponseData struct { MatcherOp string `json:"matcherOp,omitempty"` } -const logicalOr = "or" +const ( + logicalOr = "or" + queryParam = "query" +) // WithEnforceAuthorizationLabels return a middleware that ensures every query // has a set of labels returned by the OPA authorizer enforced. @@ -60,8 +63,6 @@ func WithEnforceAuthorizationLabels() func(http.Handler) http.Handler { } } -const queryParam = "query" - func enforceValues(mInfo AuthzResponseData, u *url.URL) (values string, err error) { switch { case strings.HasSuffix(u.Path, "/values"): diff --git a/api/logs/v1/rules_labels_enforcer.go b/api/logs/v1/rules_labels_enforcer.go index c11f3ddde..50d619f38 100644 --- a/api/logs/v1/rules_labels_enforcer.go +++ b/api/logs/v1/rules_labels_enforcer.go @@ -1,161 +1,22 @@ package http import ( - "bytes" "encoding/json" - "errors" "fmt" - "io" "net/http" - "strconv" - "time" + "net/url" + "strings" - "github.com/ghodss/yaml" - "github.com/go-kit/log" - "github.com/go-kit/log/level" "github.com/observatorium/api/authentication" "github.com/observatorium/api/authorization" "github.com/observatorium/api/httperr" "github.com/prometheus/prometheus/model/labels" ) -const ( - contentTypeApplicationJSON = "application/json" - contentTypeApplicationYAML = "application/yaml" -) - -var ( - errUnknownTenantKey = errors.New("Unknown tenant key") - errUnknownRulesContentType = errors.New("Unknown rules response content type") -) - -type alert struct { - Labels labels.Labels `json:"labels"` - Annotations labels.Labels `json:"annotations"` - State string `json:"state"` - ActiveAt *time.Time `json:"activeAt,omitempty"` - Value string `json:"value"` -} - -func (a *alert) GetLabels() labels.Labels { return a.Labels } - -type alertingRule struct { - State string `json:"state"` - Name string `json:"name"` - Query string `json:"query"` - Duration float64 `json:"duration"` - Labels labels.Labels `json:"labels"` - Annotations labels.Labels `json:"annotations"` - Alerts []*alert `json:"alerts"` - Health string `json:"health"` - LastError string `json:"lastError"` - LastEvaluation string `json:"lastEvaluation"` - EvaluationTime float64 `json:"evaluationTime"` - // Type of an alertingRule is always "alerting". - Type string `json:"type"` -} - -type recordingRule struct { - Name string `json:"name"` - Query string `json:"query"` - Labels labels.Labels `json:"labels"` - Health string `json:"health"` - LastError string `json:"lastError"` - LastEvaluation string `json:"lastEvaluation"` - EvaluationTime float64 `json:"evaluationTime"` - // Type of a recordingRule is always "recording". - Type string `json:"type"` -} - -type ruleGroup struct { - Name string `json:"name"` - File string `json:"file"` - Rules []rule `json:"rules"` - Interval float64 `json:"interval"` - LastEvaluation string `json:"lastEvaluation"` - EvaluationTime float64 `json:"evaluationTime"` -} - -type rule struct { - *alertingRule - *recordingRule -} - -func (r *rule) GetLabels() labels.Labels { - if r.alertingRule != nil { - return r.alertingRule.Labels - } - return r.recordingRule.Labels -} - -// MarshalJSON implements the json.Marshaler interface for rule. -func (r *rule) MarshalJSON() ([]byte, error) { - if r.alertingRule != nil { - return json.Marshal(r.alertingRule) - } - return json.Marshal(r.recordingRule) -} - -// UnmarshalJSON implements the json.Unmarshaler interface for rule. -func (r *rule) UnmarshalJSON(b []byte) error { - var ruleType struct { - Type string `json:"type"` - } - if err := json.Unmarshal(b, &ruleType); err != nil { - return err - } - switch ruleType.Type { - case "alerting": - var alertingr alertingRule - if err := json.Unmarshal(b, &alertingr); err != nil { - return err - } - r.alertingRule = &alertingr - case "recording": - var recordingr recordingRule - if err := json.Unmarshal(b, &recordingr); err != nil { - return err - } - r.recordingRule = &recordingr - default: - return fmt.Errorf("failed to unmarshal rule: unknown type %q", ruleType.Type) - } - - return nil -} - -type rulesData struct { - RuleGroups []*ruleGroup `json:"groups,omitempty"` - Alerts []*alert `json:"alerts,omitempty"` -} - -type prometheusRulesResponse struct { - Status string `json:"status"` - Data rulesData `json:"data"` - Error string `json:"error"` - ErrorType string `json:"errorType"` -} - -type lokiRule struct { - Alert string `json:"alert,omitempty"` - Record string `json:"record,omitempty"` - Expr string `json:"expr,omitempty"` - For string `json:"for,omitempty"` - Annotations map[string]string `json:"annotations,omitempty"` - Labels map[string]string `json:"labels,omitempty"` -} - -func (r *lokiRule) GetLabels() labels.Labels { return labels.FromMap(r.Labels) } - -type lokiRuleGroup struct { - Name string `json:"name"` - Interval string `json:"interval,omitempty"` - Limit int `json:"limit,omitempty"` - Rules []lokiRule `json:"rules"` -} - -type lokiRulesResponse = map[string][]lokiRuleGroup +const labelsParam = "labels" +// WithEnforceRulesLabelFilters returns a middleware that enforces that every query +// parameter has a matching matcher returned by authorization endpoint. func WithEnforceRulesLabelFilters(labelKeys map[string][]string) func(http.Handler) http.Handler { return func(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { @@ -233,196 +94,83 @@ func WithEnforceRulesLabelFilters(labelKeys map[string][]string) func(http.Handl } } -func newModifyResponse(logger log.Logger, labelKeys map[string][]string) func(*http.Response) error { - return func(res *http.Response) error { - tenant, ok := authentication.GetTenant(res.Request.Context()) - if !ok { - return errUnknownTenantKey - } - - keys, ok := labelKeys[tenant] - if !ok { - level.Debug(logger).Log("msg", "Skip applying rule label filters", "tenant", tenant) - return nil - } - - var ( - matchers = extractMatchers(res.Request, keys) - contentType = res.Header.Get("Content-Type") - ) - - data, ok := authorization.GetData(res.Request.Context()) +// WithParametersAsLabelsFilterRules returns a middleware that transforms query parameters +// that match the CLI arg labelKeys to the native Loki labels query parameters. +func WithParametersAsLabelsFilterRules(labelKeys map[string][]string) func(http.Handler) http.Handler { + return func(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + tenant, ok := authentication.GetTenant(r.Context()) + if !ok { + httperr.PrometheusAPIError(w, "missing tenant id", http.StatusBadRequest) - var matchersInfo AuthzResponseData - if ok && data != "" { - if err := json.Unmarshal([]byte(data), &matchersInfo); err != nil { - return nil + return } - } - - strictMode := len(matchersInfo.Matchers) != 0 - - matcherStr := fmt.Sprintf("%s", matchers) - level.Debug(logger).Log("msg", "filtering using matchers", "tenant", tenant, "matchers", matcherStr) - - body, err := io.ReadAll(res.Body) - if err != nil { - level.Error(logger).Log("msg", err) - return err - } - res.Body.Close() - - b, err := filterRules(body, contentType, matchers, strictMode) - if err != nil { - level.Error(logger).Log("msg", err) - return err - } - - res.Body = io.NopCloser(bytes.NewReader(b)) - res.ContentLength = int64(len(b)) - res.Header.Set("Content-Length", strconv.FormatInt(res.ContentLength, 10)) - - return nil - } -} - -func extractMatchers(r *http.Request, l []string) map[string]string { - queryParams := r.URL.Query() - matchers := map[string]string{} - for _, name := range l { - value := queryParams.Get(name) - if value != "" { - matchers[name] = value - } - } - - return matchers -} - -func filterRules(body []byte, contentType string, matchers map[string]string, strictMode bool) ([]byte, error) { - switch contentType { - case contentTypeApplicationJSON: - var res prometheusRulesResponse - err := json.Unmarshal(body, &res) - if err != nil { - return nil, err - } - - return json.Marshal(filterPrometheusResponse(res, matchers, strictMode)) - - case contentTypeApplicationYAML: - var res lokiRulesResponse - if err := yaml.Unmarshal(body, &res); err != nil { - return nil, err - } - - return yaml.Marshal(filterLokiRules(res, matchers, strictMode)) - default: - return nil, errUnknownRulesContentType - } -} + keys, ok := labelKeys[tenant] + if !ok || len(keys) == 0 { + next.ServeHTTP(w, r) -func filterPrometheusResponse(res prometheusRulesResponse, matchers map[string]string, strictEnforce bool) prometheusRulesResponse { - if len(matchers) == 0 { - if strictEnforce { - res.Data = rulesData{} - } + return + } - return res - } + data, ok := authorization.GetData(r.Context()) + if !ok { + httperr.PrometheusAPIError(w, "error finding authorization label matcher", http.StatusInternalServerError) - if len(res.Data.RuleGroups) > 0 { - filtered := filterPrometheusRuleGroups(res.Data.RuleGroups, matchers) - res.Data = rulesData{RuleGroups: filtered} - } + return + } - if len(res.Data.Alerts) > 0 { - filtered := filterPrometheusAlerts(res.Data.Alerts, matchers) - res.Data = rulesData{Alerts: filtered} - } + // Early pass to the next if no authz label enforcement configured. + if data == "" { + next.ServeHTTP(w, r) - return res -} + return + } -type labeledRule interface { - GetLabels() labels.Labels -} + var matchersInfo AuthzResponseData + if err := json.Unmarshal([]byte(data), &matchersInfo); err != nil { + httperr.PrometheusAPIError(w, "error parsing authorization label matchers", http.StatusInternalServerError) -func hasMatchingLabels(rule labeledRule, matchers map[string]string) bool { - for key, value := range matchers { - labels := rule.GetLabels().Map() - val, ok := labels[key] - if !ok || val != value { - return false - } - } - return true -} + return + } -func filterPrometheusRuleGroups(groups []*ruleGroup, matchers map[string]string) []*ruleGroup { - var filtered []*ruleGroup + matchers, err := initAuthzMatchers(matchersInfo.Matchers) + if err != nil { + httperr.PrometheusAPIError(w, "error initializing authorization label matchers", http.StatusInternalServerError) - for _, group := range groups { - var filteredRules []rule - for _, rule := range group.Rules { - if hasMatchingLabels(&rule, matchers) { - filteredRules = append(filteredRules, rule) + return } - } - if len(filteredRules) > 0 { - group.Rules = filteredRules - filtered = append(filtered, group) - } - } + r.URL.RawQuery = transformParametersInLabelFilter(keys, matchers, r.URL.Query()) - return filtered -} + next.ServeHTTP(w, r) -func filterPrometheusAlerts(alerts []*alert, matchers map[string]string) []*alert { - var filtered []*alert - for _, alert := range alerts { - if hasMatchingLabels(alert, matchers) { - filtered = append(filtered, alert) - } + }) } - - return filtered } -func filterLokiRules(res lokiRulesResponse, matchers map[string]string, strictEnforce bool) lokiRulesResponse { - if len(matchers) == 0 { - if strictEnforce { - return nil - } +func transformParametersInLabelFilter(keys []string, matchers []*labels.Matcher, queryParams url.Values) string { + var labelFilter []string + for _, key := range keys { + val := queryParams.Get(key) - return res - } - - filtered := lokiRulesResponse{} - - for name, groups := range res { - var filteredGroups []lokiRuleGroup - - for _, group := range groups { - var filteredRules []lokiRule - for _, rule := range group.Rules { - if hasMatchingLabels(&rule, matchers) { - filteredRules = append(filteredRules, rule) - } + for _, matcher := range matchers { + if matcher == nil { + continue } - if len(filteredRules) > 0 { - group.Rules = filteredRules - filteredGroups = append(filteredGroups, group) + if matcher.Name == key && matcher.Matches(val) { + labelFilter = append(labelFilter, fmt.Sprintf("%s:%s", key, val)) + queryParams.Del(key) + break } } + } - if len(filteredGroups) > 0 { - filtered[name] = filteredGroups - } + if len(labelFilter) == 0 { + return queryParams.Encode() } - return filtered + queryParams.Set(labelsParam, strings.Join(labelFilter, ",")) + return queryParams.Encode() } diff --git a/api/logs/v1/rules_labels_enforcer_test.go b/api/logs/v1/rules_labels_enforcer_test.go index a076bffaf..1e728a4e8 100644 --- a/api/logs/v1/rules_labels_enforcer_test.go +++ b/api/logs/v1/rules_labels_enforcer_test.go @@ -1,420 +1,145 @@ package http import ( - "bytes" - "encoding/json" - "errors" - "io" - "net/http" - "net/http/httptest" - "net/http/httputil" - "os" + "net/url" "testing" - "github.com/ghodss/yaml" - "github.com/go-chi/chi" - "github.com/go-kit/log" - "github.com/observatorium/api/authentication" + "github.com/efficientgo/core/testutil" "github.com/prometheus/prometheus/model/labels" ) -func TestFilterRules_WithPrometheusAPIRulesResponseBody(t *testing.T) { - contentType := "application/json" - - body, err := os.ReadFile("testdata/rules.json") - if err != nil { - t.Fatal(err) - } - - matchers := map[string]string{ - "namespace": "log-test-0", - } - - b, err := filterRules(body, contentType, matchers, true) - if err != nil { - t.Fatal(err) - } - - var got prometheusRulesResponse - if err := json.Unmarshal(b, &got); err != nil { - t.Fatal(err) - } - - for _, group := range got.Data.RuleGroups { - for _, rule := range group.Rules { - if val := rule.GetLabels().Get("namespace"); val != "log-test-0" { - t.Errorf("invalid rule for label: %s and value: %s", "namespace", val) - } - } - } -} - -func TestFilterRules_WithPrometheusAPIAlertsResponseBody(t *testing.T) { - contentType := "application/json" - - body, err := os.ReadFile("testdata/alerts.json") - if err != nil { - t.Fatal(err) - } - - matchers := map[string]string{ - "namespace": "log-test-0", - } - - b, err := filterRules(body, contentType, matchers, true) - if err != nil { - t.Fatal(err) - } - - var got prometheusRulesResponse - if err := json.Unmarshal(b, &got); err != nil { - t.Fatal(err) - } - - for _, alert := range got.Data.Alerts { - if val := alert.Labels.Get("namespace"); val != "log-test-0" { - t.Errorf("invalid rule for label: %s and value: %s", "namespace", val) - } - } -} - -func TestFilterRules_WithPrometheusAPIResponseBody_ReturnNothingOnParseError(t *testing.T) { - contentType := "application/json" - body := []byte(`{`) - matchers := map[string]string{ - "key": "value", - } - - b, err := filterRules(body, contentType, matchers, true) - if err == nil { - t.Error("missing parse error") - } - - if b != nil { - t.Errorf("want nil, got: %s", b) - } -} - -func TestFilterRules_WithLokiAPIResponseBody(t *testing.T) { - contentType := "application/yaml" - - body, err := os.ReadFile("testdata/rules.yaml") - if err != nil { - t.Fatal(err) - } - - matchers := map[string]string{ - "namespace": "log-test-0", - } - - b, err := filterRules(body, contentType, matchers, true) - if err != nil { - t.Error(err) - } - - var got lokiRulesResponse - if err := yaml.Unmarshal(b, &got); err != nil { - t.Error(err) - } - - for _, groups := range got { - for _, group := range groups { - for _, rule := range group.Rules { - if val := rule.Labels["namespace"]; val != "log-test-0" { - t.Errorf("invalid rule for label: %s and value: %s", "namespace", val) - } - } - } - } -} - -func TestFilterRules_WithokiAPIResponseBody_ReturnNothingOnParseError(t *testing.T) { - contentType := "application/yaml" - body := []byte(`invalid`) - matchers := map[string]string{ - "key": "value", - } - - b, err := filterRules(body, contentType, matchers, true) - if err == nil { - t.Error("missing parse error") - } - - if b != nil { - t.Errorf("want nil, got: %s", b) - } -} - -func TestFilterRules_WithUnknownContentType_ReturnsError(t *testing.T) { - contentType := "invalid/content" - - var ( - body []byte - matchers map[string]string - ) - - b, err := filterRules(body, contentType, matchers, true) - if !errors.Is(err, errUnknownRulesContentType) { - t.Errorf("want %s, got: %s", errUnknownRulesContentType, err) - } - - if b != nil { - t.Errorf("want nil, got: %s", b) - } -} - -func TestFilterPrometheusRules(t *testing.T) { +func TestEnforceNamespaceLabels(t *testing.T) { tt := []struct { - desc string - matchers map[string]string - strictEnforce bool - res prometheusRulesResponse - want prometheusRulesResponse + desc string + keys []string + accessMatchers []*labels.Matcher + namespaceLabels string + expectedQuery string }{ { - desc: "without matchers returns empty", - strictEnforce: true, - res: prometheusRulesResponse{ - Data: rulesData{ - RuleGroups: []*ruleGroup{ - {Name: "group-a"}, - }, - }, - }, - want: prometheusRulesResponse{}, + desc: "empty_keys", + namespaceLabels: "kubernetes_namespace_name=last-ns-name", + expectedQuery: "kubernetes_namespace_name=last-ns-name", }, { - desc: "without matchers returns original response", - res: prometheusRulesResponse{ - Data: rulesData{ - RuleGroups: []*ruleGroup{ - {Name: "group-a"}, - }, - }, - }, - want: prometheusRulesResponse{ - Data: rulesData{ - RuleGroups: []*ruleGroup{ - {Name: "group-a"}, - }, - }, - }, + desc: "single_key_no_matcher", + keys: []string{"kubernetes_namespace_name"}, + namespaceLabels: "kubernetes_namespace_name=last-ns-name", + expectedQuery: "kubernetes_namespace_name=last-ns-name", }, { - desc: "only matching", - matchers: map[string]string{"label": "value"}, - res: prometheusRulesResponse{ - Data: rulesData{ - RuleGroups: []*ruleGroup{ - { - Name: "group-a", - Rules: []rule{ - { - alertingRule: &alertingRule{ - Labels: labels.FromMap(map[string]string{"label": "value"}), - }, - }, - { - alertingRule: &alertingRule{ - Labels: labels.FromMap(map[string]string{"other": "not"}), - }, - }, - }, - }, - }, - }, - }, - want: prometheusRulesResponse{ - Data: rulesData{ - RuleGroups: []*ruleGroup{ - { - Name: "group-a", - Rules: []rule{ - { - alertingRule: &alertingRule{ - Labels: labels.FromMap(map[string]string{"label": "value"}), - }, - }, - }, - }, - }, + desc: "single_key_wrong_matcher", + keys: []string{"kubernetes_namespace_name"}, + accessMatchers: []*labels.Matcher{ + { + Type: labels.MatchEqual, + Name: "kubernetes_pod_name", + Value: "pod-name-.*", }, }, + namespaceLabels: "kubernetes_namespace_name=last-ns-name", + expectedQuery: "kubernetes_namespace_name=last-ns-name", }, { - desc: "nothing matching", - matchers: map[string]string{"label": "value"}, - res: prometheusRulesResponse{ - Data: rulesData{ - RuleGroups: []*ruleGroup{ - { - Name: "group-a", - Rules: []rule{ - { - alertingRule: &alertingRule{ - Labels: labels.FromMap(map[string]string{"not": "other"}), - }, - }, - }, - }, - }, + desc: "single_key_matching_matcher_wrong_value", + keys: []string{"kubernetes_namespace_name"}, + accessMatchers: []*labels.Matcher{ + { + Type: labels.MatchEqual, + Name: "kubernetes_namespace_name", + Value: "first-ns-name", }, }, - want: prometheusRulesResponse{}, + namespaceLabels: "kubernetes_namespace_name=last-ns-name", + expectedQuery: "kubernetes_namespace_name=last-ns-name", }, - } - - for _, tc := range tt { - tc := tc - t.Run(tc.desc, func(t *testing.T) { - t.Parallel() - - got := filterPrometheusResponse(tc.res, tc.matchers, tc.strictEnforce) - - wantJSON, err := json.MarshalIndent(tc.want, "", " ") - if err != nil { - t.Errorf(err.Error()) - } - - gotJSON, err := json.MarshalIndent(got, "", " ") - if err != nil { - t.Errorf(err.Error()) - } - - if string(wantJSON) != string(gotJSON) { - t.Errorf("\nwant: %s\ngot: %s", wantJSON, gotJSON) - } - }) - } -} - -func TestFilterLokiRules(t *testing.T) { - tt := []struct { - desc string - matchers map[string]string - strictEnforce bool - res lokiRulesResponse - want lokiRulesResponse - }{ { - desc: "without matchers returns empty", - strictEnforce: true, - res: lokiRulesResponse{ - "ns-1": []lokiRuleGroup{ - {Name: "group-a"}, - }, - "ns-2": []lokiRuleGroup{ - {Name: "group-b"}, + desc: "single_key_matching_matcher_matching_value_wrong_type", + keys: []string{"kubernetes_namespace_name"}, + accessMatchers: []*labels.Matcher{ + { + Type: labels.MatchNotEqual, + Name: "kubernetes_namespace_name", + Value: "last-ns-name", }, }, + namespaceLabels: "kubernetes_namespace_name=last-ns-name", + expectedQuery: "kubernetes_namespace_name=last-ns-name", }, { - desc: "without matchers returns original response", - res: lokiRulesResponse{ - "ns-1": []lokiRuleGroup{ - {Name: "group-a"}, - }, - "ns-2": []lokiRuleGroup{ - {Name: "group-b"}, + desc: "single_key_matching_matcher_matching_value", + keys: []string{"kubernetes_namespace_name"}, + accessMatchers: []*labels.Matcher{ + { + Type: labels.MatchEqual, + Name: "kubernetes_namespace_name", + Value: "last-ns-name", }, }, - want: lokiRulesResponse{ - "ns-1": []lokiRuleGroup{ - {Name: "group-a"}, - }, - "ns-2": []lokiRuleGroup{ - {Name: "group-b"}, + namespaceLabels: "kubernetes_namespace_name=last-ns-name", + expectedQuery: "labels=kubernetes_namespace_name:last-ns-name", + }, + { + desc: "query with a single key with multiple occurrences", + keys: []string{"kubernetes_namespace_name"}, + accessMatchers: []*labels.Matcher{ + { + Type: labels.MatchRegexp, + Name: "kubernetes_namespace_name", + Value: "ns-name|another-ns-name", }, }, + namespaceLabels: "kubernetes_namespace_name=ns-name&kubernetes_namespace_name=another-ns-name", + expectedQuery: "labels=kubernetes_namespace_name:ns-name", }, { - desc: "only matching", - matchers: map[string]string{"label": "value"}, - res: lokiRulesResponse{ - "ns-1": []lokiRuleGroup{ - { - Name: "group-a", - Rules: []lokiRule{ - { - Alert: "group-a-alert-1", - Labels: map[string]string{"label": "value"}, - }, - { - Alert: "group-a-alert-2", - Labels: map[string]string{"other": "not"}, - }, - }, - }, - }, - "ns-2": []lokiRuleGroup{ - { - Name: "group-b", - Rules: []lokiRule{ - { - Alert: "group-b-alert-1", - Labels: map[string]string{"label": "value"}, - }, - { - Alert: "group-b-alert-2", - Labels: map[string]string{"other": "not"}, - }, - }, - }, + desc: "query_with_multiple_keys_with_single_occurrences", + keys: []string{"kubernetes_namespace_name", "kubernetes_pod_name"}, + accessMatchers: []*labels.Matcher{ + { + Type: labels.MatchEqual, + Name: "kubernetes_namespace_name", + Value: "ns-name", + }, + { + Type: labels.MatchEqual, + Name: "kubernetes_pod_name", + Value: "my-pod", }, }, - want: lokiRulesResponse{ - "ns-1": []lokiRuleGroup{ - { - Name: "group-a", - Rules: []lokiRule{ - { - Alert: "group-a-alert-1", - Labels: map[string]string{"label": "value"}, - }, - }, - }, - }, - "ns-2": []lokiRuleGroup{ - { - Name: "group-b", - Rules: []lokiRule{ - { - Alert: "group-b-alert-1", - Labels: map[string]string{"label": "value"}, - }, - }, - }, + namespaceLabels: "kubernetes_namespace_name=ns-name&kubernetes_pod_name=my-pod", + expectedQuery: "labels=kubernetes_namespace_name:ns-name,kubernetes_pod_name:my-pod", + }, + { + desc: "query_with_multiple_keys_with_single_occurrences_but_only_one_matcher", + keys: []string{"kubernetes_namespace_name", "kubernetes_pod_name"}, + accessMatchers: []*labels.Matcher{ + { + Type: labels.MatchEqual, + Name: "kubernetes_namespace_name", + Value: "ns-name", }, }, + namespaceLabels: "kubernetes_namespace_name=ns-name&kubernetes_pod_name=my-pod", + expectedQuery: "kubernetes_pod_name=my-pod&labels=kubernetes_namespace_name:ns-name", }, { - desc: "nothing matching", - matchers: map[string]string{"label": "value"}, - res: lokiRulesResponse{ - "ns-1": []lokiRuleGroup{ - { - Name: "group-a", - Rules: []lokiRule{ - { - Alert: "group-a-alert", - Labels: map[string]string{"other": "not"}, - }, - }, - }, - }, - "ns-2": []lokiRuleGroup{ - { - Name: "group-b", - Rules: []lokiRule{ - { - Alert: "group-b-alert", - Labels: map[string]string{"other": "not"}, - }, - }, - }, + desc: "query_with_multiple_keys_with_multiple_occurrences", + keys: []string{"kubernetes_namespace_name", "kubernetes_pod_name"}, + accessMatchers: []*labels.Matcher{ + { + Type: labels.MatchRegexp, + Name: "kubernetes_namespace_name", + Value: "ns-name|ns-new-name", + }, + { + Type: labels.MatchRegexp, + Name: "kubernetes_pod_name", + Value: "my-pod|my-new-pod", }, }, - want: lokiRulesResponse{}, + namespaceLabels: "kubernetes_namespace_name=ns-name&kubernetes_pod_name=my-pod&kubernetes_namespace_name=ns-new-name&kubernetes_pod_name=my-new-pod", + expectedQuery: "labels=kubernetes_namespace_name:ns-name,kubernetes_pod_name:my-pod", }, } @@ -423,80 +148,17 @@ func TestFilterLokiRules(t *testing.T) { t.Run(tc.desc, func(t *testing.T) { t.Parallel() - got := filterLokiRules(tc.res, tc.matchers, tc.strictEnforce) + matchers, err := initAuthzMatchers(tc.accessMatchers) + testutil.Ok(t, err) - wantJSON, err := json.MarshalIndent(tc.want, "", " ") - if err != nil { - t.Errorf(err.Error()) - } + queryValues, err := url.ParseQuery(tc.namespaceLabels) + testutil.Ok(t, err) - gotJSON, err := json.MarshalIndent(got, "", " ") - if err != nil { - t.Errorf(err.Error()) - } + v := transformParametersInLabelFilter(tc.keys, matchers, queryValues) - if string(wantJSON) != string(gotJSON) { - t.Errorf("\nwant: %s\ngot: %s", wantJSON, gotJSON) - } + ac, err := url.QueryUnescape(v) + testutil.Ok(t, err) + testutil.Equals(t, tc.expectedQuery, ac) }) } } - -func TestModifyResponse(t *testing.T) { - l := log.NewNopLogger() - lk := map[string][]string{ - "fake": {"namespace"}, - } - - rules, err := os.ReadFile("testdata/rules.json") - if err != nil { - t.Fatal(err) - } - originanLen := int64(len(rules)) - - filtered, err := os.ReadFile("testdata/rules-log-test-0.json") - if err != nil { - t.Fatal(err) - } - filteredLen := int64(len(filtered)) - - headers := make(http.Header) - headers.Add("Content-Type", "application/json") - - res := &http.Response{ - StatusCode: http.StatusOK, - Header: headers, - Body: io.NopCloser(bytes.NewReader(rules)), - ContentLength: originanLen, - } - - proxy := &httputil.ReverseProxy{ - Director: func(r *http.Request) {}, - Transport: staticResponseRoundTripper{res}, - ModifyResponse: newModifyResponse(l, lk), - } - - r := chi.NewRouter() - r.Handle("/rules/{tenant}", authentication.WithTenant(proxy)) - - rr := httptest.NewRecorder() - r.ServeHTTP(rr, httptest.NewRequest("GET", "/rules/fake?namespace=log-test-0", nil)) - - result := rr.Result() - if result.StatusCode != http.StatusOK { - t.Errorf("Broken routing: %s", rr.Result().Status) - } - - if result.ContentLength == originanLen || result.ContentLength != filteredLen { - t.Errorf("failed to filter rules, original len: %d, want: %d, got: %d", originanLen, filteredLen, result.ContentLength) - } -} - -type staticResponseRoundTripper struct { - res *http.Response -} - -func (rt staticResponseRoundTripper) RoundTrip(r *http.Request) (*http.Response, error) { - rt.res.Request = r - return rt.res, nil -} diff --git a/main.go b/main.go index 3274b09e7..18e3fc40f 100644 --- a/main.go +++ b/main.go @@ -751,9 +751,9 @@ func main() { logsv1.WithReadMiddleware(authorization.WithAuthorizers(authorizers, rbac.Read, "logs")), logsv1.WithReadMiddleware(logsv1.WithEnforceAuthorizationLabels()), logsv1.WithWriteMiddleware(authorization.WithAuthorizers(authorizers, rbac.Write, "logs")), - logsv1.WithRulesLabelFilters(cfg.logs.rulesLabelFilters), logsv1.WithRulesReadMiddleware(logsv1.WithEnforceTenantAsRuleNamespace()), logsv1.WithRulesReadMiddleware(logsv1.WithEnforceRulesLabelFilters(cfg.logs.rulesLabelFilters)), + logsv1.WithRulesReadMiddleware(logsv1.WithParametersAsLabelsFilterRules(cfg.logs.rulesLabelFilters)), logsv1.WithRulesWriteMiddleware(logsv1.WithEnforceTenantAsRuleNamespace()), logsv1.WithRulesWriteMiddleware(logsv1.WithEnforceRuleLabels(cfg.logs.tenantLabel)), ),