From 918f1d943ae00d6714db7877cb0365cba97f7577 Mon Sep 17 00:00:00 2001 From: "Grot (@grafanabot)" <43478413+grafanabot@users.noreply.github.com> Date: Wed, 2 Feb 2022 19:07:37 +0100 Subject: [PATCH 001/113] Instrumentation: Fix HTTP request instrumentation of authentication failures (#44234) (#44793) Moves the request tracing middleware earlier in the chain, just after the tracing middleware and before the log middleware. With these changes we'll be able to track authentication/authorization status failures that currently exits early and don't execute the request tracing middleware. In addition, there might be some other routes now being tracked with this that we didn't do before. Fixes #39590 (cherry picked from commit 0092d107649c2dfac03e3ab7411f1067a98ddad3) Co-authored-by: Marcus Efraimsson --- pkg/api/http_server.go | 1 + pkg/api/routing/route_register.go | 5 +- pkg/middleware/request_metrics.go | 86 +++++++++++++++++-------------- 3 files changed, 51 insertions(+), 41 deletions(-) diff --git a/pkg/api/http_server.go b/pkg/api/http_server.go index 5a33efa7706..70bf0c4c26f 100644 --- a/pkg/api/http_server.go +++ b/pkg/api/http_server.go @@ -432,6 +432,7 @@ func (hs *HTTPServer) addMiddlewaresAndStaticRoutes() { m := hs.web m.Use(middleware.RequestTracing(hs.tracer)) + m.Use(middleware.RequestMetrics(hs.Features)) m.Use(middleware.Logger(hs.Cfg)) diff --git a/pkg/api/routing/route_register.go b/pkg/api/routing/route_register.go index 4c2a59eee25..f9751972951 100644 --- a/pkg/api/routing/route_register.go +++ b/pkg/api/routing/route_register.go @@ -5,7 +5,6 @@ import ( "strings" "github.com/grafana/grafana/pkg/middleware" - "github.com/grafana/grafana/pkg/services/featuremgmt" "github.com/grafana/grafana/pkg/web" ) @@ -52,8 +51,8 @@ type RouteRegister interface { type RegisterNamedMiddleware func(name string) web.Handler -func ProvideRegister(features featuremgmt.FeatureToggles) *RouteRegisterImpl { - return NewRouteRegister(middleware.ProvideRouteOperationName, middleware.RequestMetrics(features)) +func ProvideRegister() *RouteRegisterImpl { + return NewRouteRegister(middleware.ProvideRouteOperationName) } // NewRouteRegister creates a new RouteRegister with all middlewares sent as params diff --git a/pkg/middleware/request_metrics.go b/pkg/middleware/request_metrics.go index 6429f95f318..d4923f233a6 100644 --- a/pkg/middleware/request_metrics.go +++ b/pkg/middleware/request_metrics.go @@ -45,50 +45,60 @@ func init() { } // RequestMetrics is a middleware handler that instruments the request. -func RequestMetrics(features featuremgmt.FeatureToggles) func(handler string) web.Handler { - return func(handler string) web.Handler { - return func(res http.ResponseWriter, req *http.Request, c *web.Context) { - rw := res.(web.ResponseWriter) - now := time.Now() - httpRequestsInFlight.Inc() - defer httpRequestsInFlight.Dec() +func RequestMetrics(features featuremgmt.FeatureToggles) web.Handler { + return func(res http.ResponseWriter, req *http.Request, c *web.Context) { + if strings.HasPrefix(c.Req.URL.Path, "/public/") || c.Req.URL.Path == "robots.txt" || c.Req.URL.Path == "/metrics" { c.Next() + return + } - status := rw.Status() + rw := res.(web.ResponseWriter) + now := time.Now() + httpRequestsInFlight.Inc() + defer httpRequestsInFlight.Dec() + c.Map(c.Req) + c.Next() - code := sanitizeCode(status) - method := sanitizeMethod(req.Method) + handler := "unknown" - // enable histogram and disable summaries + counters for http requests. - if features.IsEnabled(featuremgmt.FlagDisableHttpRequestHistogram) { - duration := time.Since(now).Nanoseconds() / int64(time.Millisecond) - metrics.MHttpRequestTotal.WithLabelValues(handler, code, method).Inc() - metrics.MHttpRequestSummary.WithLabelValues(handler, code, method).Observe(float64(duration)) - } else { - // avoiding the sanitize functions for in the new instrumentation - // since they dont make much sense. We should remove them later. - histogram := httpRequestDurationHistogram. - WithLabelValues(handler, strconv.Itoa(rw.Status()), req.Method) - if traceID, ok := cw.ExtractSampledTraceID(c.Req.Context()); ok { - // Need to type-convert the Observer to an - // ExemplarObserver. This will always work for a - // HistogramVec. - histogram.(prometheus.ExemplarObserver).ObserveWithExemplar( - time.Since(now).Seconds(), prometheus.Labels{"traceID": traceID}, - ) - return - } - histogram.Observe(time.Since(now).Seconds()) + if routeOperation, exists := RouteOperationNameFromContext(c.Req.Context()); exists { + handler = routeOperation + } + + status := rw.Status() + + code := sanitizeCode(status) + method := sanitizeMethod(req.Method) + + // enable histogram and disable summaries + counters for http requests. + if features.IsEnabled(featuremgmt.FlagDisableHttpRequestHistogram) { + duration := time.Since(now).Nanoseconds() / int64(time.Millisecond) + metrics.MHttpRequestTotal.WithLabelValues(handler, code, method).Inc() + metrics.MHttpRequestSummary.WithLabelValues(handler, code, method).Observe(float64(duration)) + } else { + // avoiding the sanitize functions for in the new instrumentation + // since they dont make much sense. We should remove them later. + histogram := httpRequestDurationHistogram. + WithLabelValues(handler, code, req.Method) + if traceID, ok := cw.ExtractSampledTraceID(c.Req.Context()); ok { + // Need to type-convert the Observer to an + // ExemplarObserver. This will always work for a + // HistogramVec. + histogram.(prometheus.ExemplarObserver).ObserveWithExemplar( + time.Since(now).Seconds(), prometheus.Labels{"traceID": traceID}, + ) + return } + histogram.Observe(time.Since(now).Seconds()) + } - switch { - case strings.HasPrefix(req.RequestURI, "/api/datasources/proxy"): - countProxyRequests(status) - case strings.HasPrefix(req.RequestURI, "/api/"): - countApiRequests(status) - default: - countPageRequests(status) - } + switch { + case strings.HasPrefix(req.RequestURI, "/api/datasources/proxy"): + countProxyRequests(status) + case strings.HasPrefix(req.RequestURI, "/api/"): + countApiRequests(status) + default: + countPageRequests(status) } } } From e031568aa4e1376d08e3fd6eba012296be0c3aca Mon Sep 17 00:00:00 2001 From: "Grot (@grafanabot)" <43478413+grafanabot@users.noreply.github.com> Date: Wed, 2 Feb 2022 19:29:13 +0100 Subject: [PATCH 002/113] Do not store EvaluationString in Evaluation. (#44606) (#44795) * do not store evaluation string in Evaluation. * reduce number of buckets to store for a single state (cherry picked from commit 984c95de6349a16679f56b889118297ab44f5956) Co-authored-by: Yuriy Tseretyan --- pkg/services/ngalert/api/api_prometheus.go | 10 +++--- pkg/services/ngalert/schedule/compat.go | 4 +-- pkg/services/ngalert/schedule/compat_test.go | 6 +--- pkg/services/ngalert/state/manager.go | 28 +++++++-------- pkg/services/ngalert/state/manager_test.go | 30 ---------------- pkg/services/ngalert/state/state.go | 36 ++++++++++---------- 6 files changed, 40 insertions(+), 74 deletions(-) diff --git a/pkg/services/ngalert/api/api_prometheus.go b/pkg/services/ngalert/api/api_prometheus.go index 36d8dc1d5b3..e8344434860 100644 --- a/pkg/services/ngalert/api/api_prometheus.go +++ b/pkg/services/ngalert/api/api_prometheus.go @@ -9,9 +9,9 @@ import ( "strings" "time" - "github.com/grafana/grafana/pkg/services/ngalert/eval" apiv1 "github.com/prometheus/client_golang/api/prometheus/v1" + "github.com/grafana/grafana/pkg/services/ngalert/eval" ngmodels "github.com/grafana/grafana/pkg/services/ngalert/models" "github.com/grafana/grafana/pkg/services/ngalert/store" @@ -40,8 +40,8 @@ func (srv PrometheusSrv) RouteGetAlertStatuses(c *models.ReqContext) response.Re for _, alertState := range srv.manager.GetAll(c.OrgId) { startsAt := alertState.StartsAt valString := "" - if len(alertState.Results) > 0 && alertState.State == eval.Alerting { - valString = alertState.Results[0].EvaluationString + if alertState.State == eval.Alerting { + valString = alertState.LastEvaluationString } alertResponse.Data.Alerts = append(alertResponse.Data.Alerts, &apimodels.Alert{ Labels: map[string]string(alertState.Labels), @@ -173,8 +173,8 @@ func (srv PrometheusSrv) RouteGetRuleStatuses(c *models.ReqContext) response.Res for _, alertState := range srv.manager.GetStatesForRuleUID(c.OrgId, rule.UID) { activeAt := alertState.StartsAt valString := "" - if len(alertState.Results) > 0 && alertState.State == eval.Alerting { - valString = alertState.Results[0].EvaluationString + if alertState.State == eval.Alerting { + valString = alertState.LastEvaluationString } alert := &apimodels.Alert{ Labels: map[string]string(alertState.Labels), diff --git a/pkg/services/ngalert/schedule/compat.go b/pkg/services/ngalert/schedule/compat.go index 17dc9057f6a..fda9b1da948 100644 --- a/pkg/services/ngalert/schedule/compat.go +++ b/pkg/services/ngalert/schedule/compat.go @@ -35,8 +35,8 @@ func stateToPostableAlert(alertState *state.State, appURL *url.URL) *models.Post nL := alertState.Labels.Copy() nA := data.Labels(alertState.Annotations).Copy() - if len(alertState.Results) > 0 { - nA["__value_string__"] = alertState.Results[0].EvaluationString + if alertState.LastEvaluationString != "" { + nA["__value_string__"] = alertState.LastEvaluationString } var urlStr string diff --git a/pkg/services/ngalert/schedule/compat_test.go b/pkg/services/ngalert/schedule/compat_test.go index 704fbaff730..6c67cc04b23 100644 --- a/pkg/services/ngalert/schedule/compat_test.go +++ b/pkg/services/ngalert/schedule/compat_test.go @@ -100,11 +100,7 @@ func Test_stateToPostableAlert(t *testing.T) { alertState := randomState(tc.state) alertState.Annotations = randomMapOfStrings() expectedValueString := util.GenerateShortUID() - alertState.Results = []state.Evaluation{ - { - EvaluationString: expectedValueString, - }, - } + alertState.LastEvaluationString = expectedValueString result := stateToPostableAlert(alertState, appURL) diff --git a/pkg/services/ngalert/state/manager.go b/pkg/services/ngalert/state/manager.go index 5a3d2e03cb3..a1db3b23ef2 100644 --- a/pkg/services/ngalert/state/manager.go +++ b/pkg/services/ngalert/state/manager.go @@ -96,16 +96,16 @@ func (st *Manager) Warm() { st.log.Error("error getting cacheId for entry", "msg", err.Error()) } stateForEntry := &State{ - AlertRuleUID: entry.RuleUID, - OrgID: entry.RuleOrgID, - CacheId: cacheId, - Labels: lbs, - State: translateInstanceState(entry.CurrentState), - Results: []Evaluation{}, - StartsAt: entry.CurrentStateSince, - EndsAt: entry.CurrentStateEnd, - LastEvaluationTime: entry.LastEvalTime, - Annotations: ruleForEntry.Annotations, + AlertRuleUID: entry.RuleUID, + OrgID: entry.RuleOrgID, + CacheId: cacheId, + Labels: lbs, + State: translateInstanceState(entry.CurrentState), + LastEvaluationString: "", + StartsAt: entry.CurrentStateSince, + EndsAt: entry.CurrentStateEnd, + LastEvaluationTime: entry.LastEvalTime, + Annotations: ruleForEntry.Annotations, } states = append(states, stateForEntry) } @@ -158,11 +158,11 @@ func (st *Manager) setNextState(ctx context.Context, alertRule *ngModels.AlertRu currentState.LastEvaluationTime = result.EvaluatedAt currentState.EvaluationDuration = result.EvaluationDuration currentState.Results = append(currentState.Results, Evaluation{ - EvaluationTime: result.EvaluatedAt, - EvaluationState: result.State, - EvaluationString: result.EvaluationString, - Values: NewEvaluationValues(result.Values), + EvaluationTime: result.EvaluatedAt, + EvaluationState: result.State, + Values: NewEvaluationValues(result.Values), }) + currentState.LastEvaluationString = result.EvaluationString currentState.TrimResults(alertRule) oldState := currentState.State diff --git a/pkg/services/ngalert/state/manager_test.go b/pkg/services/ngalert/state/manager_test.go index 9149bc31bb6..21ec276a9d8 100644 --- a/pkg/services/ngalert/state/manager_test.go +++ b/pkg/services/ngalert/state/manager_test.go @@ -437,16 +437,6 @@ func TestProcessEvalResults(t *testing.T) { }, State: eval.Pending, Results: []state.Evaluation{ - { - EvaluationTime: evaluationTime.Add(10 * time.Second), - EvaluationState: eval.Alerting, - Values: make(map[string]*float64), - }, - { - EvaluationTime: evaluationTime.Add(20 * time.Second), - EvaluationState: eval.NoData, - Values: make(map[string]*float64), - }, { EvaluationTime: evaluationTime.Add(30 * time.Second), EvaluationState: eval.Alerting, @@ -528,16 +518,6 @@ func TestProcessEvalResults(t *testing.T) { }, State: eval.NoData, Results: []state.Evaluation{ - { - EvaluationTime: evaluationTime, - EvaluationState: eval.Alerting, - Values: make(map[string]*float64), - }, - { - EvaluationTime: evaluationTime.Add(10 * time.Second), - EvaluationState: eval.Alerting, - Values: make(map[string]*float64), - }, { EvaluationTime: evaluationTime.Add(20 * time.Second), EvaluationState: eval.Alerting, @@ -1336,11 +1316,6 @@ func TestProcessEvalResults(t *testing.T) { }, State: eval.Alerting, Results: []state.Evaluation{ - { - EvaluationTime: evaluationTime, - EvaluationState: eval.Normal, - Values: make(map[string]*float64), - }, { EvaluationTime: evaluationTime.Add(30 * time.Second), EvaluationState: eval.Alerting, @@ -1428,11 +1403,6 @@ func TestProcessEvalResults(t *testing.T) { }, State: eval.NoData, Results: []state.Evaluation{ - { - EvaluationTime: evaluationTime, - EvaluationState: eval.Normal, - Values: make(map[string]*float64), - }, { EvaluationTime: evaluationTime.Add(30 * time.Second), EvaluationState: eval.Alerting, diff --git a/pkg/services/ngalert/state/state.go b/pkg/services/ngalert/state/state.go index 02f1f41440c..2947f789761 100644 --- a/pkg/services/ngalert/state/state.go +++ b/pkg/services/ngalert/state/state.go @@ -12,26 +12,26 @@ import ( ) type State struct { - AlertRuleUID string - OrgID int64 - CacheId string - State eval.State - Resolved bool - Results []Evaluation - StartsAt time.Time - EndsAt time.Time - LastEvaluationTime time.Time - EvaluationDuration time.Duration - LastSentAt time.Time - Annotations map[string]string - Labels data.Labels - Error error + AlertRuleUID string + OrgID int64 + CacheId string + State eval.State + Resolved bool + Results []Evaluation + LastEvaluationString string + StartsAt time.Time + EndsAt time.Time + LastEvaluationTime time.Time + EvaluationDuration time.Duration + LastSentAt time.Time + Annotations map[string]string + Labels data.Labels + Error error } type Evaluation struct { - EvaluationTime time.Time - EvaluationState eval.State - EvaluationString string + EvaluationTime time.Time + EvaluationState eval.State // Values contains the RefID and value of reduce and math expressions. // It does not contain values for classic conditions as the values // in classic conditions do not have a RefID. @@ -152,7 +152,7 @@ func (a *State) Equals(b *State) bool { } func (a *State) TrimResults(alertRule *ngModels.AlertRule) { - numBuckets := 2 * (int64(alertRule.For.Seconds()) / alertRule.IntervalSeconds) + numBuckets := int64(alertRule.For.Seconds()) / alertRule.IntervalSeconds if numBuckets == 0 { numBuckets = 10 // keep at least 10 evaluations in the event For is set to 0 } From b4579334d4178ab3d7ae1fb5c842df2458cc25b1 Mon Sep 17 00:00:00 2001 From: "Grot (@grafanabot)" <43478413+grafanabot@users.noreply.github.com> Date: Wed, 2 Feb 2022 21:05:14 +0100 Subject: [PATCH 003/113] DataFrame: insert null values along interval (#44622) (#44792) (cherry picked from commit 3504844ad702b9b308640decb3e7820f0e3a103e) Co-authored-by: Leon Sorokin --- packages/grafana-data/src/types/dataFrame.ts | 6 + .../GraphNG/nullInsertThreshold.test.ts | 215 ++++++++++++++++++ .../components/GraphNG/nullInsertThreshold.ts | 115 ++++++++++ .../src/components/GraphNG/utils.ts | 3 +- .../src/components/Sparkline/utils.ts | 5 +- .../plugins/panel/state-timeline/timeline.ts | 13 +- .../panel/state-timeline/utils.test.ts | 6 +- .../app/plugins/panel/state-timeline/utils.ts | 31 +-- 8 files changed, 365 insertions(+), 29 deletions(-) create mode 100644 packages/grafana-ui/src/components/GraphNG/nullInsertThreshold.test.ts create mode 100644 packages/grafana-ui/src/components/GraphNG/nullInsertThreshold.ts diff --git a/packages/grafana-data/src/types/dataFrame.ts b/packages/grafana-data/src/types/dataFrame.ts index bb4e9784e34..36c27d1b845 100644 --- a/packages/grafana-data/src/types/dataFrame.ts +++ b/packages/grafana-data/src/types/dataFrame.ts @@ -67,6 +67,12 @@ export interface FieldConfig { min?: number | null; max?: number | null; + // Interval indicates the expected regular step between values in the series. + // When an interval exists, consumers can identify "missing" values when the expected value is not present. + // The grafana timeseries visualization will render disconnected values when missing values are found it the time field. + // The interval uses the same units as the values. For time.Time, this is defined in milliseconds. + interval?: number | null; + // Convert input values into a display string mappings?: ValueMapping[]; diff --git a/packages/grafana-ui/src/components/GraphNG/nullInsertThreshold.test.ts b/packages/grafana-ui/src/components/GraphNG/nullInsertThreshold.test.ts new file mode 100644 index 00000000000..c74fb32dbf5 --- /dev/null +++ b/packages/grafana-ui/src/components/GraphNG/nullInsertThreshold.test.ts @@ -0,0 +1,215 @@ +import { ArrayVector, FieldType, MutableDataFrame } from '@grafana/data'; +import { applyNullInsertThreshold } from './nullInsertThreshold'; + +function randInt(min: number, max: number) { + return Math.floor(Math.random() * (max - min + 1) + min); +} + +function genFrame() { + let fieldCount = 10; + let valueCount = 3000; + let step = 1000; + let skipProb = 0.5; + let skipSteps = [1, 5]; // min, max + + let allValues = Array(fieldCount); + + allValues[0] = Array(valueCount); + + for (let i = 0, curStep = Date.now(); i < valueCount; i++) { + curStep = allValues[0][i] = curStep + step * (Math.random() < skipProb ? randInt(skipSteps[0], skipSteps[1]) : 1); + } + + for (let fi = 1; fi < fieldCount; fi++) { + let values = Array(valueCount); + + for (let i = 0; i < valueCount; i++) { + values[i] = Math.random() * 100; + } + + allValues[fi] = values; + } + + return { + length: valueCount, + fields: allValues.map((values, i) => { + return { + name: 'A-' + i, + type: i === 0 ? FieldType.time : FieldType.number, + config: { + interval: i === 0 ? step : null, + }, + values: new ArrayVector(values), + }; + }), + }; +} + +describe('nullInsertThreshold Transformer', () => { + test('should insert nulls at +threshold between adjacent > threshold: 1', () => { + const df = new MutableDataFrame({ + refId: 'A', + fields: [ + { name: 'Time', type: FieldType.time, values: [1, 3, 10] }, + { name: 'One', type: FieldType.number, config: { custom: { insertNulls: 1 } }, values: [4, 6, 8] }, + { name: 'Two', type: FieldType.string, config: { custom: { insertNulls: 1 } }, values: ['a', 'b', 'c'] }, + ], + }); + + const result = applyNullInsertThreshold(df); + + expect(result.fields[0].values.toArray()).toStrictEqual([1, 2, 3, 4, 10]); + expect(result.fields[1].values.toArray()).toStrictEqual([4, null, 6, null, 8]); + expect(result.fields[2].values.toArray()).toStrictEqual(['a', null, 'b', null, 'c']); + }); + + test('should insert nulls at +threshold between adjacent > threshold: 2', () => { + const df = new MutableDataFrame({ + refId: 'A', + fields: [ + { name: 'Time', type: FieldType.time, values: [5, 7, 11] }, + { name: 'One', type: FieldType.number, config: { custom: { insertNulls: 2 } }, values: [4, 6, 8] }, + { name: 'Two', type: FieldType.string, config: { custom: { insertNulls: 2 } }, values: ['a', 'b', 'c'] }, + ], + }); + + const result = applyNullInsertThreshold(df); + + expect(result.fields[0].values.toArray()).toStrictEqual([5, 7, 9, 11]); + expect(result.fields[1].values.toArray()).toStrictEqual([4, 6, null, 8]); + expect(result.fields[2].values.toArray()).toStrictEqual(['a', 'b', null, 'c']); + }); + + test('should insert nulls at +interval between adjacent > interval: 1', () => { + const df = new MutableDataFrame({ + refId: 'A', + fields: [ + { name: 'Time', type: FieldType.time, config: { interval: 1 }, values: [1, 3, 10] }, + { name: 'One', type: FieldType.number, values: [4, 6, 8] }, + { name: 'Two', type: FieldType.string, values: ['a', 'b', 'c'] }, + ], + }); + + const result = applyNullInsertThreshold(df); + + expect(result.fields[0].values.toArray()).toStrictEqual([1, 2, 3, 4, 10]); + expect(result.fields[1].values.toArray()).toStrictEqual([4, null, 6, null, 8]); + expect(result.fields[2].values.toArray()).toStrictEqual(['a', null, 'b', null, 'c']); + }); + + // TODO: make this work + test.skip('should insert nulls at +threshold (when defined) instead of +interval', () => { + const df = new MutableDataFrame({ + refId: 'A', + fields: [ + { name: 'Time', type: FieldType.time, config: { interval: 2 }, values: [5, 7, 11] }, + { name: 'One', type: FieldType.number, config: { custom: { insertNulls: 1 } }, values: [4, 6, 8] }, + { name: 'Two', type: FieldType.string, config: { custom: { insertNulls: 1 } }, values: ['a', 'b', 'c'] }, + ], + }); + + const result = applyNullInsertThreshold(df); + + expect(result.fields[0].values.toArray()).toStrictEqual([5, 6, 7, 8, 11]); + expect(result.fields[1].values.toArray()).toStrictEqual([4, null, 6, null, 8]); + expect(result.fields[2].values.toArray()).toStrictEqual(['a', null, 'b', null, 'c']); + }); + + test('should insert nulls at midpoints between adjacent > interval: 2', () => { + const df = new MutableDataFrame({ + refId: 'A', + fields: [ + { name: 'Time', type: FieldType.time, config: { interval: 2 }, values: [5, 7, 11] }, + { name: 'One', type: FieldType.number, values: [4, 6, 8] }, + { name: 'Two', type: FieldType.string, values: ['a', 'b', 'c'] }, + ], + }); + + const result = applyNullInsertThreshold(df); + + expect(result.fields[0].values.toArray()).toStrictEqual([5, 7, 9, 11]); + expect(result.fields[1].values.toArray()).toStrictEqual([4, 6, null, 8]); + expect(result.fields[2].values.toArray()).toStrictEqual(['a', 'b', null, 'c']); + }); + + test('should noop on fewer than two values', () => { + const df = new MutableDataFrame({ + refId: 'A', + fields: [ + { name: 'Time', type: FieldType.time, config: { interval: 1 }, values: [1] }, + { name: 'Value', type: FieldType.number, values: [1] }, + ], + }); + + const result = applyNullInsertThreshold(df); + + expect(result).toBe(df); + }); + + test('should noop on invalid threshold', () => { + const df = new MutableDataFrame({ + refId: 'A', + fields: [ + { name: 'Time', type: FieldType.time, values: [1, 2, 4] }, + { name: 'Value', type: FieldType.number, config: { custom: { insertNulls: -1 } }, values: [1, 1, 1] }, + ], + }); + + const result = applyNullInsertThreshold(df); + + expect(result).toBe(df); + }); + + test('should noop on invalid interval', () => { + const df = new MutableDataFrame({ + refId: 'A', + fields: [ + { name: 'Time', type: FieldType.time, config: { interval: -1 }, values: [1, 2, 4] }, + { name: 'Value', type: FieldType.number, values: [1, 1, 1] }, + ], + }); + + const result = applyNullInsertThreshold(df); + + expect(result).toBe(df); + }); + + test('should noop when no missing steps', () => { + const df = new MutableDataFrame({ + refId: 'A', + fields: [ + { name: 'Time', type: FieldType.time, config: { interval: 1 }, values: [1, 2, 3] }, + { name: 'Value', type: FieldType.number, values: [1, 1, 1] }, + ], + }); + + const result = applyNullInsertThreshold(df); + + expect(result).toBe(df); + }); + + test('should noop when refFieldName not found', () => { + const df = new MutableDataFrame({ + refId: 'A', + fields: [ + { name: 'Time', type: FieldType.time, config: { interval: 1 }, values: [1, 2, 5] }, + { name: 'Value', type: FieldType.number, values: [1, 1, 1] }, + ], + }); + + const result = applyNullInsertThreshold(df, 'Time2'); + + expect(result).toBe(df); + }); + + test('perf stress test should be <= 10ms', () => { + // 10 fields x 3,000 values with 50% skip (output = 10 fields x 6,000 values) + let bigFrameA = genFrame(); + + // eslint-disable-next-line no-console + console.time('insertValues-10x3k'); + applyNullInsertThreshold(bigFrameA); + // eslint-disable-next-line no-console + console.timeEnd('insertValues-10x3k'); + }); +}); diff --git a/packages/grafana-ui/src/components/GraphNG/nullInsertThreshold.ts b/packages/grafana-ui/src/components/GraphNG/nullInsertThreshold.ts new file mode 100644 index 00000000000..4e22d0af2b1 --- /dev/null +++ b/packages/grafana-ui/src/components/GraphNG/nullInsertThreshold.ts @@ -0,0 +1,115 @@ +import { ArrayVector, DataFrame, FieldType } from '@grafana/data'; + +type InsertMode = (prev: number, next: number, threshold: number) => number; + +const INSERT_MODES = { + threshold: (prev: number, next: number, threshold: number) => prev + threshold, + midpoint: (prev: number, next: number, threshold: number) => (prev + next) / 2, + // previous time + 1ms to prevent StateTimeline from forward-interpolating prior state + plusone: (prev: number, next: number, threshold: number) => prev + 1, +}; + +export function applyNullInsertThreshold( + frame: DataFrame, + refFieldName?: string | null, + insertMode: InsertMode = INSERT_MODES.threshold +): DataFrame { + if (frame.length < 2) { + return frame; + } + + const refField = frame.fields.find((field) => { + // note: getFieldDisplayName() would require full DF[] + return refFieldName != null ? field.name === refFieldName : field.type === FieldType.time; + }); + + if (refField == null) { + return frame; + } + + const thresholds = frame.fields.map((field) => field.config.custom?.insertNulls ?? refField.config.interval ?? null); + + const uniqueThresholds = new Set(thresholds); + + uniqueThresholds.delete(null as any); + + if (uniqueThresholds.size === 0) { + return frame; + } + + if (uniqueThresholds.size === 1) { + const threshold = uniqueThresholds.values().next().value; + + if (threshold <= 0) { + return frame; + } + + const refValues = refField.values.toArray(); + + const frameValues = frame.fields.map((field) => field.values.toArray()); + + const filledFieldValues = nullInsertThreshold(refValues, frameValues, threshold, insertMode); + + if (filledFieldValues === frameValues) { + return frame; + } + + return { + ...frame, + length: filledFieldValues[0].length, + fields: frame.fields.map((field, i) => ({ + ...field, + values: new ArrayVector(filledFieldValues[i]), + })), + }; + } + + // TODO: unique threshold-per-field (via overrides) is unimplemented + // should be done by processing each (refField + thresholdA-field1 + thresholdA-field2...) + // as a separate nullInsertThreshold() dataset, then re-join into single dataset via join() + return frame; +} + +function nullInsertThreshold(refValues: number[], frameValues: any[][], threshold: number, getInsertValue: InsertMode) { + const len = refValues.length; + let prevValue: number = refValues[0]; + const refValuesNew: number[] = [prevValue]; + + for (let i = 1; i < len; i++) { + const curValue = refValues[i]; + + if (curValue - prevValue > threshold) { + refValuesNew.push(getInsertValue(prevValue, curValue, threshold)); + } + + refValuesNew.push(curValue); + + prevValue = curValue; + } + + const filledLen = refValuesNew.length; + + if (filledLen === len) { + return frameValues; + } + + const filledFieldValues: any[][] = []; + + for (let fieldValues of frameValues) { + let filledValues; + + if (fieldValues !== refValues) { + filledValues = Array(filledLen); + + for (let i = 0, j = 0; i < filledLen; i++) { + filledValues[i] = refValues[j] === refValuesNew[i] ? fieldValues[j++] : null; + } + } else { + filledValues = refValuesNew; + } + + filledFieldValues.push(filledValues); + } + + return filledFieldValues; +} diff --git a/packages/grafana-ui/src/components/GraphNG/utils.ts b/packages/grafana-ui/src/components/GraphNG/utils.ts index fe733543086..427408064a0 100644 --- a/packages/grafana-ui/src/components/GraphNG/utils.ts +++ b/packages/grafana-ui/src/components/GraphNG/utils.ts @@ -1,6 +1,7 @@ import { XYFieldMatchers } from './types'; import { ArrayVector, DataFrame, FieldConfig, FieldType, outerJoinDataFrames } from '@grafana/data'; import { nullToUndefThreshold } from './nullToUndefThreshold'; +import { applyNullInsertThreshold } from './nullInsertThreshold'; import { AxisPlacement, GraphFieldConfig, ScaleDistribution, ScaleDistributionConfig } from '@grafana/schema'; import { FIXED_UNIT } from './GraphNG'; @@ -32,7 +33,7 @@ function applySpanNullsThresholds(frame: DataFrame) { export function preparePlotFrame(frames: DataFrame[], dimFields: XYFieldMatchers) { let alignedFrame = outerJoinDataFrames({ - frames: frames, + frames: frames.map((frame) => applyNullInsertThreshold(frame)), joinBy: dimFields.x, keep: dimFields.y, keepOriginIndices: true, diff --git a/packages/grafana-ui/src/components/Sparkline/utils.ts b/packages/grafana-ui/src/components/Sparkline/utils.ts index d9ec460f3b3..9f57db07152 100644 --- a/packages/grafana-ui/src/components/Sparkline/utils.ts +++ b/packages/grafana-ui/src/components/Sparkline/utils.ts @@ -1,5 +1,6 @@ import { DataFrame, FieldConfig, FieldSparkline, IndexVector } from '@grafana/data'; import { GraphFieldConfig } from '@grafana/schema'; +import { applyNullInsertThreshold } from '../GraphNG/nullInsertThreshold'; /** @internal * Given a sparkline config returns a DataFrame ready to be turned into Plot data set @@ -11,7 +12,7 @@ export function preparePlotFrame(sparkline: FieldSparkline, config?: FieldConfig ...config, }; - return { + return applyNullInsertThreshold({ refId: 'sparkline', fields: [ sparkline.x ?? IndexVector.newField(length), @@ -21,5 +22,5 @@ export function preparePlotFrame(sparkline: FieldSparkline, config?: FieldConfig }, ], length, - }; + }); } diff --git a/public/app/plugins/panel/state-timeline/timeline.ts b/public/app/plugins/panel/state-timeline/timeline.ts index b82ca170a22..d81d2384db8 100644 --- a/public/app/plugins/panel/state-timeline/timeline.ts +++ b/public/app/plugins/panel/state-timeline/timeline.ts @@ -41,6 +41,7 @@ export interface TimelineCoreOptions { colWidth?: number; theme: GrafanaTheme2; showValue: VisibilityMode; + mergeValues?: boolean; isDiscrete: (seriesIdx: number) => boolean; getValueColor: (seriesIdx: number, value: any) => string; label: (seriesIdx: number) => string; @@ -62,6 +63,7 @@ export function getConfig(opts: TimelineCoreOptions) { rowHeight = 0, colWidth = 0, showValue, + mergeValues = false, theme, label, formatValue, @@ -212,11 +214,16 @@ export function getConfig(opts: TimelineCoreOptions) { walk(rowHeight, sidx - 1, numSeries, yDim, (iy, y0, height) => { if (mode === TimelineMode.Changes) { for (let ix = 0; ix < dataY.length; ix++) { - if (dataY[ix] != null) { + let yVal = dataY[ix]; + + if (yVal != null) { let left = Math.round(valToPosX(dataX[ix], scaleX, xDim, xOff)); let nextIx = ix; - while (dataY[++nextIx] === undefined && nextIx < dataY.length) {} + while ( + ++nextIx < dataY.length && + (dataY[nextIx] === undefined || (mergeValues && dataY[nextIx] === yVal)) + ) {} // to now (not to end of chart) let right = @@ -236,7 +243,7 @@ export function getConfig(opts: TimelineCoreOptions) { strokeWidth, iy, ix, - dataY[ix], + yVal, discrete ); diff --git a/public/app/plugins/panel/state-timeline/utils.test.ts b/public/app/plugins/panel/state-timeline/utils.test.ts index 8e7fb9f4a90..0382bf21bd0 100644 --- a/public/app/plugins/panel/state-timeline/utils.test.ts +++ b/public/app/plugins/panel/state-timeline/utils.test.ts @@ -54,12 +54,12 @@ describe('prepare timeline graph', () => { const field = out.fields.find((f) => f.name === 'b'); expect(field?.values.toArray()).toMatchInlineSnapshot(` Array [ + 1, 1, undefined, - undefined, - undefined, + 1, + 2, 2, - undefined, null, 2, 3, diff --git a/public/app/plugins/panel/state-timeline/utils.ts b/public/app/plugins/panel/state-timeline/utils.ts index f5cbefd4e67..74209a04412 100644 --- a/public/app/plugins/panel/state-timeline/utils.ts +++ b/public/app/plugins/panel/state-timeline/utils.ts @@ -69,6 +69,7 @@ export const preparePlotConfigBuilder: UPlotConfigPrepFn = ({ colWidth, showValue, alignValue, + mergeValues, }) => { const builder = new UPlotConfigBuilder(timeZone); @@ -98,6 +99,7 @@ export const preparePlotConfigBuilder: UPlotConfigPrepFn = ({ mode: mode!, numSeries: frame.fields.length - 1, isDiscrete: (seriesIdx) => isDiscrete(frame.fields[seriesIdx]), + mergeValues, rowHeight: rowHeight!, colWidth: colWidth, showValue: showValue!, @@ -329,7 +331,6 @@ export function mergeThresholdValues(field: Field, theme: GrafanaTheme2): Field textToColor.set(items[i].label, items[i].color!); } - let prev: Threshold | undefined = undefined; let input = field.values.toArray(); const vals = new Array(field.values.length); if (thresholds.mode === ThresholdsMode.Percentage) { @@ -347,19 +348,21 @@ export function mergeThresholdValues(field: Field, theme: GrafanaTheme2): Field const v = input[i]; if (v == null) { vals[i] = v; - prev = undefined; - } - const active = getActiveThreshold(v, thresholds.steps); - if (active === prev) { - vals[i] = undefined; } else { - vals[i] = thresholdToText.get(active); + vals[i] = thresholdToText.get(getActiveThreshold(v, thresholds.steps)); } - prev = active; } return { ...field, + config: { + ...field.config, + custom: { + ...field.config.custom, + // magic value for join() to leave nulls alone + spanNulls: -1, + }, + }, type: FieldType.string, values: new ArrayVector(vals), display: (value: string) => ({ @@ -415,18 +418,6 @@ export function prepareTimelineFields( }, }, }; - - if (mergeValues) { - let merged = unsetSameFutureValues(field.values.toArray()); - if (merged) { - fields.push({ - ...field, - values: new ArrayVector(merged), - }); - changed = true; - continue; - } - } fields.push(field); break; default: From 71a0ae79314071bbe052f4c62b7816be4b448b4a Mon Sep 17 00:00:00 2001 From: "Grot (@grafanabot)" <43478413+grafanabot@users.noreply.github.com> Date: Thu, 3 Feb 2022 07:54:57 +0100 Subject: [PATCH 004/113] Encryption: CLI rollback command (#43935) (#44810) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Encryption: CLI rollback command * Update flag reference to 'featuremgmt' pkg * Update feature toggles usage * Clean up data keys table after envelope encryption rollback (cherry picked from commit f8105efff322c4c4b1f69513227bfce3151e45f3) Co-authored-by: Joan López de la Franca Beltran <5459617+joanlopez@users.noreply.github.com> --- pkg/cmd/grafana-cli/commands/commands.go | 5 + .../secretsmigrations/reencrypt_secrets.go | 12 - .../secretsmigrations/rollback_secrets.go | 220 ++++++++++++++++++ .../commands/secretsmigrations/types.go | 13 ++ 4 files changed, 238 insertions(+), 12 deletions(-) create mode 100644 pkg/cmd/grafana-cli/commands/secretsmigrations/rollback_secrets.go create mode 100644 pkg/cmd/grafana-cli/commands/secretsmigrations/types.go diff --git a/pkg/cmd/grafana-cli/commands/commands.go b/pkg/cmd/grafana-cli/commands/commands.go index 9ae1f6e7f52..53730ec033d 100644 --- a/pkg/cmd/grafana-cli/commands/commands.go +++ b/pkg/cmd/grafana-cli/commands/commands.go @@ -183,6 +183,11 @@ var adminCommands = []*cli.Command{ Usage: "Re-encrypts secrets by decrypting and re-encrypting them with the currently configured encryption. Returns ok unless there is an error. Safe to execute multiple times.", Action: runRunnerCommand(secretsmigrations.ReEncryptSecrets), }, + { + Name: "rollback", + Usage: "Rolls back secrets to legacy encryption. Returns ok unless there is an error. Safe to execute multiple times.", + Action: runRunnerCommand(secretsmigrations.RollBackSecrets), + }, }, }, } diff --git a/pkg/cmd/grafana-cli/commands/secretsmigrations/reencrypt_secrets.go b/pkg/cmd/grafana-cli/commands/secretsmigrations/reencrypt_secrets.go index f175d4464d9..067a5d10d36 100644 --- a/pkg/cmd/grafana-cli/commands/secretsmigrations/reencrypt_secrets.go +++ b/pkg/cmd/grafana-cli/commands/secretsmigrations/reencrypt_secrets.go @@ -17,12 +17,6 @@ import ( "xorm.io/xorm" ) -type simpleSecret struct { - tableName string - columnName string - isBase64Encoded bool -} - func (s simpleSecret) reencrypt(secretsSrv *manager.SecretsService, sess *xorm.Session) error { var rows []struct { Id int @@ -76,10 +70,6 @@ func (s simpleSecret) reencrypt(secretsSrv *manager.SecretsService, sess *xorm.S return nil } -type jsonSecret struct { - tableName string -} - func (s jsonSecret) reencrypt(secretsSrv *manager.SecretsService, sess *xorm.Session) error { var rows []struct { Id int @@ -119,8 +109,6 @@ func (s jsonSecret) reencrypt(secretsSrv *manager.SecretsService, sess *xorm.Ses return nil } -type alertingSecret struct{} - func (s alertingSecret) reencrypt(secretsSrv *manager.SecretsService, sess *xorm.Session) error { var results []struct { Id int diff --git a/pkg/cmd/grafana-cli/commands/secretsmigrations/rollback_secrets.go b/pkg/cmd/grafana-cli/commands/secretsmigrations/rollback_secrets.go new file mode 100644 index 00000000000..619cbfb8f6e --- /dev/null +++ b/pkg/cmd/grafana-cli/commands/secretsmigrations/rollback_secrets.go @@ -0,0 +1,220 @@ +package secretsmigrations + +import ( + "context" + "encoding/base64" + "encoding/json" + "fmt" + + "github.com/grafana/grafana/pkg/services/featuremgmt" + + "github.com/grafana/grafana/pkg/cmd/grafana-cli/logger" + "github.com/grafana/grafana/pkg/cmd/grafana-cli/runner" + "github.com/grafana/grafana/pkg/cmd/grafana-cli/utils" + "github.com/grafana/grafana/pkg/services/encryption" + "github.com/grafana/grafana/pkg/services/ngalert/notifier" + "github.com/grafana/grafana/pkg/services/secrets/manager" + "github.com/grafana/grafana/pkg/services/sqlstore" + "xorm.io/xorm" +) + +func (s simpleSecret) rollback( + secretsSrv *manager.SecretsService, + encryptionSrv encryption.Internal, + sess *xorm.Session, + secretKey string, +) error { + var rows []struct { + Id int + Secret string + } + + if err := sess.Table(s.tableName).Select(fmt.Sprintf("id, %s as secret", s.columnName)).Find(&rows); err != nil { + return err + } + + for _, row := range rows { + if len(row.Secret) == 0 { + continue + } + + var ( + err error + decoded = []byte(row.Secret) + ) + + if s.isBase64Encoded { + decoded, err = base64.StdEncoding.DecodeString(row.Secret) + if err != nil { + return err + } + } + + decrypted, err := secretsSrv.Decrypt(context.Background(), decoded) + if err != nil { + return err + } + + encrypted, err := encryptionSrv.Encrypt(context.Background(), decrypted, secretKey) + if err != nil { + return err + } + + encoded := string(encrypted) + if s.isBase64Encoded { + encoded = base64.StdEncoding.EncodeToString(encrypted) + } + + updateSQL := fmt.Sprintf("UPDATE %s SET %s = ? WHERE id = ?", s.tableName, s.columnName) + if _, err := sess.Exec(updateSQL, encoded, row.Id); err != nil { + return err + } + } + + logger.Infof("Column %s from %s have been rolled back successfully\n", s.columnName, s.tableName) + + return nil +} + +func (s jsonSecret) rollback( + secretsSrv *manager.SecretsService, + encryptionSrv encryption.Internal, + sess *xorm.Session, + secretKey string, +) error { + var rows []struct { + Id int + SecureJsonData map[string][]byte + } + + if err := sess.Table(s.tableName).Cols("id", "secure_json_data").Find(&rows); err != nil { + return err + } + + for _, row := range rows { + if len(row.SecureJsonData) == 0 { + continue + } + + decrypted, err := secretsSrv.DecryptJsonData(context.Background(), row.SecureJsonData) + if err != nil { + return err + } + + var toUpdate struct { + SecureJsonData map[string][]byte + } + + toUpdate.SecureJsonData, err = encryptionSrv.EncryptJsonData(context.Background(), decrypted, secretKey) + if err != nil { + return err + } + + if _, err := sess.Table(s.tableName).Where("id = ?", row.Id).Update(toUpdate); err != nil { + return err + } + } + + logger.Infof("Secure json data from %s have been rolled back successfully\n", s.tableName) + + return nil +} + +func (s alertingSecret) rollback( + secretsSrv *manager.SecretsService, + encryptionSrv encryption.Internal, + sess *xorm.Session, + secretKey string, +) error { + var results []struct { + Id int + AlertmanagerConfiguration string + } + + selectSQL := "SELECT id, alertmanager_configuration FROM alert_configuration" + if err := sess.SQL(selectSQL).Find(&results); err != nil { + return err + } + + for _, result := range results { + result := result + postableUserConfig, err := notifier.Load([]byte(result.AlertmanagerConfiguration)) + if err != nil { + return err + } + + for _, receiver := range postableUserConfig.AlertmanagerConfig.Receivers { + for _, gmr := range receiver.GrafanaManagedReceivers { + for k, v := range gmr.SecureSettings { + decoded, err := base64.StdEncoding.DecodeString(v) + if err != nil { + return err + } + + decrypted, err := secretsSrv.Decrypt(context.Background(), decoded) + if err != nil { + return err + } + + reencrypted, err := encryptionSrv.Encrypt(context.Background(), decrypted, secretKey) + if err != nil { + return err + } + + gmr.SecureSettings[k] = base64.StdEncoding.EncodeToString(reencrypted) + } + } + } + + marshalled, err := json.Marshal(postableUserConfig) + if err != nil { + return err + } + + result.AlertmanagerConfiguration = string(marshalled) + if _, err := sess.Table("alert_configuration").Where("id = ?", result.Id).Update(&result); err != nil { + return err + } + } + + logger.Info("Alerting secrets have rolled re-encrypted successfully\n") + + return nil +} + +func RollBackSecrets(_ utils.CommandLine, runner runner.Runner) error { + if !runner.Features.IsEnabled(featuremgmt.FlagEnvelopeEncryption) { + logger.Warn("Envelope encryption is not enabled, quitting...") + return nil + } + + toMigrate := []interface { + rollback(*manager.SecretsService, encryption.Internal, *xorm.Session, string) error + }{ + simpleSecret{tableName: "dashboard_snapshot", columnName: "dashboard_encrypted", isBase64Encoded: false}, + simpleSecret{tableName: "user_auth", columnName: "o_auth_access_token", isBase64Encoded: true}, + simpleSecret{tableName: "user_auth", columnName: "o_auth_refresh_token", isBase64Encoded: true}, + simpleSecret{tableName: "user_auth", columnName: "o_auth_token_type", isBase64Encoded: true}, + jsonSecret{tableName: "data_source"}, + jsonSecret{tableName: "plugin_setting"}, + alertingSecret{}, + } + + return runner.SQLStore.WithDbSession(context.Background(), func(sess *sqlstore.DBSession) error { + for _, m := range toMigrate { + if err := m.rollback( + runner.SecretsService, + runner.EncryptionService, + sess.Session, + runner.Cfg.SecretKey); err != nil { + return err + } + } + + if _, err := sess.Exec("DELETE FROM data_keys"); err != nil { + logger.Warn("Error while cleaning up data keys table...", "err", err) + } + + return nil + }) +} diff --git a/pkg/cmd/grafana-cli/commands/secretsmigrations/types.go b/pkg/cmd/grafana-cli/commands/secretsmigrations/types.go new file mode 100644 index 00000000000..b272983f26d --- /dev/null +++ b/pkg/cmd/grafana-cli/commands/secretsmigrations/types.go @@ -0,0 +1,13 @@ +package secretsmigrations + +type simpleSecret struct { + tableName string + columnName string + isBase64Encoded bool +} + +type jsonSecret struct { + tableName string +} + +type alertingSecret struct{} From a7ad5ee55ffef558d5f123fb571ffefe2abff580 Mon Sep 17 00:00:00 2001 From: "Grot (@grafanabot)" <43478413+grafanabot@users.noreply.github.com> Date: Thu, 3 Feb 2022 09:58:12 +0100 Subject: [PATCH 005/113] Encryption: Add support for data keys re-encryption (#43548) (#44814) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Encryption: Add support for data keys re-encryption * Add tests for data keys re-encryption * Update code after refactorings Co-authored-by: Leonard Gram (cherry picked from commit b2655750e8218574ae8867d1dfd081bc8f1dff0b) Co-authored-by: Joan López de la Franca Beltran <5459617+joanlopez@users.noreply.github.com> --- pkg/cmd/grafana-cli/commands/commands.go | 5 +++ .../secretsmigrations/reencrypt_deks.go | 19 +++++++++ pkg/services/secrets/database/database.go | 39 +++++++++++++++++++ pkg/services/secrets/fakes/fake_service.go | 4 ++ pkg/services/secrets/fakes/fake_store.go | 4 ++ pkg/services/secrets/manager/manager.go | 11 ++++++ pkg/services/secrets/manager/manager_test.go | 39 +++++++++++++++++++ pkg/services/secrets/secrets.go | 3 ++ 8 files changed, 124 insertions(+) create mode 100644 pkg/cmd/grafana-cli/commands/secretsmigrations/reencrypt_deks.go diff --git a/pkg/cmd/grafana-cli/commands/commands.go b/pkg/cmd/grafana-cli/commands/commands.go index 53730ec033d..55f381ebec3 100644 --- a/pkg/cmd/grafana-cli/commands/commands.go +++ b/pkg/cmd/grafana-cli/commands/commands.go @@ -188,6 +188,11 @@ var adminCommands = []*cli.Command{ Usage: "Rolls back secrets to legacy encryption. Returns ok unless there is an error. Safe to execute multiple times.", Action: runRunnerCommand(secretsmigrations.RollBackSecrets), }, + { + Name: "re-encrypt-data-keys", + Usage: "Rotates persisted data encryption keys. Returns ok unless there is an error. Safe to execute multiple times.", + Action: runRunnerCommand(secretsmigrations.ReEncryptDEKS), + }, }, }, } diff --git a/pkg/cmd/grafana-cli/commands/secretsmigrations/reencrypt_deks.go b/pkg/cmd/grafana-cli/commands/secretsmigrations/reencrypt_deks.go new file mode 100644 index 00000000000..0d97599c491 --- /dev/null +++ b/pkg/cmd/grafana-cli/commands/secretsmigrations/reencrypt_deks.go @@ -0,0 +1,19 @@ +package secretsmigrations + +import ( + "context" + + "github.com/grafana/grafana/pkg/cmd/grafana-cli/logger" + "github.com/grafana/grafana/pkg/cmd/grafana-cli/runner" + "github.com/grafana/grafana/pkg/cmd/grafana-cli/utils" + "github.com/grafana/grafana/pkg/services/featuremgmt" +) + +func ReEncryptDEKS(_ utils.CommandLine, runner runner.Runner) error { + if !runner.Features.IsEnabled(featuremgmt.FlagEnvelopeEncryption) { + logger.Warn("Envelope encryption is not enabled, quitting...") + return nil + } + + return runner.SecretsService.ReEncryptDataKeys(context.Background()) +} diff --git a/pkg/services/secrets/database/database.go b/pkg/services/secrets/database/database.go index b9e0ac81863..19a5e509e93 100644 --- a/pkg/services/secrets/database/database.go +++ b/pkg/services/secrets/database/database.go @@ -87,3 +87,42 @@ func (ss *SecretsStoreImpl) DeleteDataKey(ctx context.Context, name string) erro return err }) } + +func (ss *SecretsStoreImpl) ReEncryptDataKeys( + ctx context.Context, + providers map[secrets.ProviderID]secrets.Provider, + currProvider secrets.ProviderID, +) error { + return ss.sqlStore.WithTransactionalDbSession(ctx, func(sess *sqlstore.DBSession) error { + keys := make([]*secrets.DataKey, 0) + if err := sess.Table(dataKeysTable).Find(&keys); err != nil { + return err + } + + for _, k := range keys { + provider, ok := providers[k.Provider] + if !ok { + return fmt.Errorf("could not find encryption provider '%s'", k.Provider) + } + + decrypted, err := provider.Decrypt(ctx, k.EncryptedData) + if err != nil { + return err + } + + // Updating current data key by re-encrypting it with current provider. + // Accessing the current provider within providers map should be safe. + k.Provider = currProvider + k.EncryptedData, err = providers[currProvider].Encrypt(ctx, decrypted) + if err != nil { + return err + } + + if _, err := sess.Table(dataKeysTable).Where("name = ?", k.Name).Update(k); err != nil { + return err + } + } + + return nil + }) +} diff --git a/pkg/services/secrets/fakes/fake_service.go b/pkg/services/secrets/fakes/fake_service.go index b743c3af7ec..9a9b2c1c35a 100644 --- a/pkg/services/secrets/fakes/fake_service.go +++ b/pkg/services/secrets/fakes/fake_service.go @@ -40,6 +40,10 @@ func (f FakeSecretsService) GetDecryptedValue(_ context.Context, sjd map[string] return fallback } +func (f FakeSecretsService) ReEncryptDataKeys(_ context.Context) error { + return nil +} + func (f FakeSecretsService) CurrentProviderID() string { return "fakeProvider" } diff --git a/pkg/services/secrets/fakes/fake_store.go b/pkg/services/secrets/fakes/fake_store.go index 6ac990f6d1f..53a89956bcd 100644 --- a/pkg/services/secrets/fakes/fake_store.go +++ b/pkg/services/secrets/fakes/fake_store.go @@ -45,3 +45,7 @@ func (f FakeSecretsStore) DeleteDataKey(_ context.Context, name string) error { delete(f.store, name) return nil } + +func (f FakeSecretsStore) ReEncryptDataKeys(_ context.Context, _ map[secrets.ProviderID]secrets.Provider, _ secrets.ProviderID) error { + return nil +} diff --git a/pkg/services/secrets/manager/manager.go b/pkg/services/secrets/manager/manager.go index 14cf179537c..b31f0c7f73b 100644 --- a/pkg/services/secrets/manager/manager.go +++ b/pkg/services/secrets/manager/manager.go @@ -353,6 +353,17 @@ func (s *SecretsService) GetProviders() map[secrets.ProviderID]secrets.Provider return s.providers } +func (s *SecretsService) ReEncryptDataKeys(ctx context.Context) error { + err := s.store.ReEncryptDataKeys(ctx, s.providers, s.currentProviderID) + if err != nil { + return nil + } + + // Invalidate cache + s.dataKeyCache = make(map[string]dataKeyCacheItem) + return err +} + // These variables are used to test the code // responsible for periodically cleaning up // data encryption keys cache. diff --git a/pkg/services/secrets/manager/manager_test.go b/pkg/services/secrets/manager/manager_test.go index 7080c008dcf..ada2608c712 100644 --- a/pkg/services/secrets/manager/manager_test.go +++ b/pkg/services/secrets/manager/manager_test.go @@ -319,3 +319,42 @@ func TestSecretsService_Run(t *testing.T) { assert.True(t, svc.dataKeyCache[dataKeyID].expiry.After(time.Now().Add(dekTTL))) }) } + +func TestSecretsService_ReEncryptDataKeys(t *testing.T) { + ctx := context.Background() + sql := sqlstore.InitTestDB(t) + store := database.ProvideSecretsStore(sql) + svc := SetupTestService(t, store) + + // Encrypt to generate data encryption key + withoutScope := secrets.WithoutScope() + ciphertext, err := svc.Encrypt(ctx, []byte("grafana"), withoutScope) + require.NoError(t, err) + + t.Run("existing key should be re-encrypted", func(t *testing.T) { + prevDataKeys, err := store.GetAllDataKeys(ctx) + require.NoError(t, err) + require.Len(t, prevDataKeys, 1) + + err = svc.ReEncryptDataKeys(ctx) + require.NoError(t, err) + + reEncryptedDataKeys, err := store.GetAllDataKeys(ctx) + require.NoError(t, err) + require.Len(t, reEncryptedDataKeys, 1) + + assert.NotEqual(t, prevDataKeys[0].EncryptedData, reEncryptedDataKeys[0].EncryptedData) + }) + + t.Run("data keys cache should be invalidated", func(t *testing.T) { + // Decrypt to ensure data key is cached + _, err := svc.Decrypt(ctx, ciphertext) + require.NoError(t, err) + require.NotEmpty(t, svc.dataKeyCache) + + err = svc.ReEncryptDataKeys(ctx) + require.NoError(t, err) + + assert.Empty(t, svc.dataKeyCache) + }) +} diff --git a/pkg/services/secrets/secrets.go b/pkg/services/secrets/secrets.go index d1d3ee3cf68..f4b2fa7cc8f 100644 --- a/pkg/services/secrets/secrets.go +++ b/pkg/services/secrets/secrets.go @@ -24,6 +24,8 @@ type Service interface { DecryptJsonData(ctx context.Context, sjd map[string][]byte) (map[string]string, error) GetDecryptedValue(ctx context.Context, sjd map[string][]byte, key, fallback string) string + + ReEncryptDataKeys(ctx context.Context) error } // Store defines methods to interact with secrets storage @@ -33,6 +35,7 @@ type Store interface { CreateDataKey(ctx context.Context, dataKey DataKey) error CreateDataKeyWithDBSession(ctx context.Context, dataKey DataKey, sess *xorm.Session) error DeleteDataKey(ctx context.Context, name string) error + ReEncryptDataKeys(ctx context.Context, providers map[ProviderID]Provider, currProvider ProviderID) error } // Provider is a key encryption key provider for envelope encryption From d7ba4dd29348d59c7207bb9ce26394fdee20f236 Mon Sep 17 00:00:00 2001 From: "Grot (@grafanabot)" <43478413+grafanabot@users.noreply.github.com> Date: Thu, 3 Feb 2022 10:49:12 +0100 Subject: [PATCH 006/113] grafana/ui: Fix RelativeTimeRange supported formats (#44535) (#44819) * remove link to docs site * extract tooltip to component * text and formatting * use div instead of p (cherry picked from commit 3314178a0afce7860466cde66737a227c820f87e) Co-authored-by: Peter Holmberg --- .../RelativeTimeRangePicker.tsx | 48 +++++++++++++++---- 1 file changed, 40 insertions(+), 8 deletions(-) diff --git a/packages/grafana-ui/src/components/DateTimePickers/RelativeTimeRangePicker/RelativeTimeRangePicker.tsx b/packages/grafana-ui/src/components/DateTimePickers/RelativeTimeRangePicker/RelativeTimeRangePicker.tsx index 939ac33c704..02f7945ceb5 100644 --- a/packages/grafana-ui/src/components/DateTimePickers/RelativeTimeRangePicker/RelativeTimeRangePicker.tsx +++ b/packages/grafana-ui/src/components/DateTimePickers/RelativeTimeRangePicker/RelativeTimeRangePicker.tsx @@ -18,6 +18,7 @@ import { import { Field } from '../../Forms/Field'; import { getInputStyles, Input } from '../../Input/Input'; import { Icon } from '../../Icon/Icon'; +import { Tooltip } from '../../Tooltip/Tooltip'; /** * @internal @@ -115,14 +116,13 @@ export function RelativeTimeRangePicker(props: RelativeTimeRangePickerProps): Re
- Specify time range -
- Specify a relative time range, for more information see{' '} - - docs - - . -
+ + } placement="bottom" theme="info"> +
+ Specify time range +
+
+
{ + const styles = useStyles2(toolTipStyles); + return ( + <> +
+ Supported formats: now-[digit]s/m/h/d/w +
+
Example: to select a time range from 10 minutes ago to now
+ From: now-10m To: now +
+ For more information see{' '} + + docs + + . +
+ + ); +}; + +const toolTipStyles = (theme: GrafanaTheme2) => ({ + supported: css` + margin-bottom: ${theme.spacing(1)}; + `, + tooltip: css` + margin: 0; + `, + link: css` + margin-top: ${theme.spacing(1)}; + `, +}); + const getStyles = (fromError?: string, toError?: string) => (theme: GrafanaTheme2) => { const inputStyles = getInputStyles({ theme, invalid: false }); const bodyMinimumHeight = 250; From 629274901462beb9119af4c33e2a24819639c092 Mon Sep 17 00:00:00 2001 From: "Grot (@grafanabot)" <43478413+grafanabot@users.noreply.github.com> Date: Thu, 3 Feb 2022 11:14:18 +0100 Subject: [PATCH 007/113] Release: Bump version to 8.4.0-beta.1 (#44821) Co-authored-by: Andres Martinez Gotor --- lerna.json | 6 ++- package.json | 2 +- packages/grafana-data/package.json | 4 +- packages/grafana-e2e-selectors/package.json | 2 +- packages/grafana-e2e/package.json | 4 +- packages/grafana-runtime/package.json | 8 ++-- packages/grafana-schema/package.json | 2 +- packages/grafana-toolkit/package.json | 6 +-- packages/grafana-ui/package.json | 8 ++-- packages/jaeger-ui-components/package.json | 6 +-- .../internal/input-datasource/package.json | 8 ++-- yarn.lock | 40 +++++++++---------- 12 files changed, 49 insertions(+), 47 deletions(-) diff --git a/lerna.json b/lerna.json index ea302e8c442..3e7c3834156 100644 --- a/lerna.json +++ b/lerna.json @@ -1,6 +1,8 @@ { "npmClient": "yarn", "useWorkspaces": true, - "packages": ["packages/*"], - "version": "8.4.0-pre" + "packages": [ + "packages/*" + ], + "version": "8.4.0-beta.1" } diff --git a/package.json b/package.json index e421573e495..2698218ff20 100644 --- a/package.json +++ b/package.json @@ -3,7 +3,7 @@ "license": "AGPL-3.0-only", "private": true, "name": "grafana", - "version": "8.4.0-pre", + "version": "8.4.0-beta.1", "repository": "github:grafana/grafana", "scripts": { "api-tests": "jest --notify --watch --config=devenv/e2e-api-tests/jest.js", diff --git a/packages/grafana-data/package.json b/packages/grafana-data/package.json index 1360df99d8d..cb11f6558d7 100644 --- a/packages/grafana-data/package.json +++ b/packages/grafana-data/package.json @@ -2,7 +2,7 @@ "author": "Grafana Labs", "license": "Apache-2.0", "name": "@grafana/data", - "version": "8.4.0-pre", + "version": "8.4.0-beta.1", "description": "Grafana Data Library", "keywords": [ "typescript" @@ -22,7 +22,7 @@ }, "dependencies": { "@braintree/sanitize-url": "5.0.2", - "@grafana/schema": "8.4.0-pre", + "@grafana/schema": "8.4.0-beta.1", "@types/d3-interpolate": "^1.4.0", "d3-interpolate": "1.4.0", "date-fns": "2.28.0", diff --git a/packages/grafana-e2e-selectors/package.json b/packages/grafana-e2e-selectors/package.json index 195c0b8d964..f248469db50 100644 --- a/packages/grafana-e2e-selectors/package.json +++ b/packages/grafana-e2e-selectors/package.json @@ -2,7 +2,7 @@ "author": "Grafana Labs", "license": "Apache-2.0", "name": "@grafana/e2e-selectors", - "version": "8.4.0-pre", + "version": "8.4.0-beta.1", "description": "Grafana End-to-End Test Selectors Library", "keywords": [ "cli", diff --git a/packages/grafana-e2e/package.json b/packages/grafana-e2e/package.json index 337a953666e..06b887ec540 100644 --- a/packages/grafana-e2e/package.json +++ b/packages/grafana-e2e/package.json @@ -2,7 +2,7 @@ "author": "Grafana Labs", "license": "Apache-2.0", "name": "@grafana/e2e", - "version": "8.4.0-pre", + "version": "8.4.0-beta.1", "description": "Grafana End-to-End Test Library", "keywords": [ "cli", @@ -48,7 +48,7 @@ "@babel/core": "7.16.7", "@babel/preset-env": "7.16.7", "@cypress/webpack-preprocessor": "5.11.0", - "@grafana/e2e-selectors": "8.4.0-pre", + "@grafana/e2e-selectors": "8.4.0-beta.1", "@grafana/tsconfig": "^1.0.0-rc1", "@mochajs/json-file-reporter": "^1.2.0", "babel-loader": "8.2.3", diff --git a/packages/grafana-runtime/package.json b/packages/grafana-runtime/package.json index 8de4dbe08e2..836e2439ca6 100644 --- a/packages/grafana-runtime/package.json +++ b/packages/grafana-runtime/package.json @@ -2,7 +2,7 @@ "author": "Grafana Labs", "license": "Apache-2.0", "name": "@grafana/runtime", - "version": "8.4.0-pre", + "version": "8.4.0-beta.1", "description": "Grafana Runtime Library", "keywords": [ "grafana", @@ -22,9 +22,9 @@ "typecheck": "tsc --noEmit" }, "dependencies": { - "@grafana/data": "8.4.0-pre", - "@grafana/e2e-selectors": "8.4.0-pre", - "@grafana/ui": "8.4.0-pre", + "@grafana/data": "8.4.0-beta.1", + "@grafana/e2e-selectors": "8.4.0-beta.1", + "@grafana/ui": "8.4.0-beta.1", "@sentry/browser": "6.17.2", "history": "4.10.1", "lodash": "4.17.21", diff --git a/packages/grafana-schema/package.json b/packages/grafana-schema/package.json index 204550b89ed..c8d2b7caea3 100644 --- a/packages/grafana-schema/package.json +++ b/packages/grafana-schema/package.json @@ -2,7 +2,7 @@ "author": "Grafana Labs", "license": "Apache-2.0", "name": "@grafana/schema", - "version": "8.4.0-pre", + "version": "8.4.0-beta.1", "description": "Grafana Schema Library", "keywords": [ "typescript" diff --git a/packages/grafana-toolkit/package.json b/packages/grafana-toolkit/package.json index 061a034b8c8..1d3b1649e4c 100644 --- a/packages/grafana-toolkit/package.json +++ b/packages/grafana-toolkit/package.json @@ -2,7 +2,7 @@ "author": "Grafana Labs", "license": "Apache-2.0", "name": "@grafana/toolkit", - "version": "8.4.0-pre", + "version": "8.4.0-beta.1", "description": "Grafana Toolkit", "keywords": [ "grafana", @@ -28,10 +28,10 @@ "dependencies": { "@babel/core": "7.13.14", "@babel/preset-env": "7.13.12", - "@grafana/data": "8.4.0-pre", + "@grafana/data": "8.4.0-beta.1", "@grafana/eslint-config": "2.5.2", "@grafana/tsconfig": "^1.0.0-rc1", - "@grafana/ui": "8.4.0-pre", + "@grafana/ui": "8.4.0-beta.1", "@jest/core": "26.6.3", "@rushstack/eslint-patch": "1.0.6", "@types/command-exists": "^1.2.0", diff --git a/packages/grafana-ui/package.json b/packages/grafana-ui/package.json index 6f51f50ec0e..d74d6fcdfe1 100644 --- a/packages/grafana-ui/package.json +++ b/packages/grafana-ui/package.json @@ -2,7 +2,7 @@ "author": "Grafana Labs", "license": "Apache-2.0", "name": "@grafana/ui", - "version": "8.4.0-pre", + "version": "8.4.0-beta.1", "description": "Grafana Components Library", "keywords": [ "grafana", @@ -33,9 +33,9 @@ "@emotion/css": "11.7.1", "@emotion/react": "11.7.1", "@grafana/aws-sdk": "0.0.31", - "@grafana/data": "8.4.0-pre", - "@grafana/e2e-selectors": "8.4.0-pre", - "@grafana/schema": "8.4.0-pre", + "@grafana/data": "8.4.0-beta.1", + "@grafana/e2e-selectors": "8.4.0-beta.1", + "@grafana/schema": "8.4.0-beta.1", "@grafana/slate-react": "0.22.10-grafana", "@monaco-editor/react": "4.3.1", "@popperjs/core": "2.11.2", diff --git a/packages/jaeger-ui-components/package.json b/packages/jaeger-ui-components/package.json index 5de63be37d2..9326d12125e 100644 --- a/packages/jaeger-ui-components/package.json +++ b/packages/jaeger-ui-components/package.json @@ -1,6 +1,6 @@ { "name": "@jaegertracing/jaeger-ui-components", - "version": "8.4.0-pre", + "version": "8.4.0-beta.1", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -27,8 +27,8 @@ }, "dependencies": { "@emotion/css": "11.7.1", - "@grafana/data": "8.4.0-pre", - "@grafana/ui": "8.4.0-pre", + "@grafana/data": "8.4.0-beta.1", + "@grafana/ui": "8.4.0-beta.1", "chance": "^1.0.10", "classnames": "^2.2.5", "combokeys": "^3.0.0", diff --git a/plugins-bundled/internal/input-datasource/package.json b/plugins-bundled/internal/input-datasource/package.json index 67bc2ced531..691c674e1e2 100644 --- a/plugins-bundled/internal/input-datasource/package.json +++ b/plugins-bundled/internal/input-datasource/package.json @@ -1,6 +1,6 @@ { "name": "@grafana-plugins/input-datasource", - "version": "8.4.0-pre", + "version": "8.4.0-beta.1", "description": "Input Datasource", "private": true, "repository": { @@ -24,9 +24,9 @@ "webpack": "5.58.1" }, "dependencies": { - "@grafana/data": "8.4.0-pre", - "@grafana/toolkit": "8.4.0-pre", - "@grafana/ui": "8.4.0-pre", + "@grafana/data": "8.4.0-beta.1", + "@grafana/toolkit": "8.4.0-beta.1", + "@grafana/ui": "8.4.0-beta.1", "jquery": "3.5.1", "react": "17.0.1", "react-dom": "17.0.1", diff --git a/yarn.lock b/yarn.lock index e5dcf69b0f4..63b7649b880 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3642,9 +3642,9 @@ __metadata: version: 0.0.0-use.local resolution: "@grafana-plugins/input-datasource@workspace:plugins-bundled/internal/input-datasource" dependencies: - "@grafana/data": 8.4.0-pre - "@grafana/toolkit": 8.4.0-pre - "@grafana/ui": 8.4.0-pre + "@grafana/data": 8.4.0-beta.1 + "@grafana/toolkit": 8.4.0-beta.1 + "@grafana/ui": 8.4.0-beta.1 "@types/jest": 26.0.15 "@types/lodash": 4.14.149 "@types/react": 17.0.30 @@ -3685,12 +3685,12 @@ __metadata: languageName: node linkType: hard -"@grafana/data@8.4.0-pre, @grafana/data@workspace:*, @grafana/data@workspace:packages/grafana-data": +"@grafana/data@8.4.0-beta.1, @grafana/data@workspace:*, @grafana/data@workspace:packages/grafana-data": version: 0.0.0-use.local resolution: "@grafana/data@workspace:packages/grafana-data" dependencies: "@braintree/sanitize-url": 5.0.2 - "@grafana/schema": 8.4.0-pre + "@grafana/schema": 8.4.0-beta.1 "@grafana/tsconfig": ^1.0.0-rc1 "@rollup/plugin-commonjs": 21.0.1 "@rollup/plugin-json": 4.1.0 @@ -3742,7 +3742,7 @@ __metadata: languageName: unknown linkType: soft -"@grafana/e2e-selectors@8.4.0-pre, @grafana/e2e-selectors@workspace:*, @grafana/e2e-selectors@workspace:packages/grafana-e2e-selectors": +"@grafana/e2e-selectors@8.4.0-beta.1, @grafana/e2e-selectors@workspace:*, @grafana/e2e-selectors@workspace:packages/grafana-e2e-selectors": version: 0.0.0-use.local resolution: "@grafana/e2e-selectors@workspace:packages/grafana-e2e-selectors" dependencies: @@ -3766,7 +3766,7 @@ __metadata: "@babel/core": 7.16.7 "@babel/preset-env": 7.16.7 "@cypress/webpack-preprocessor": 5.11.0 - "@grafana/e2e-selectors": 8.4.0-pre + "@grafana/e2e-selectors": 8.4.0-beta.1 "@grafana/tsconfig": ^1.0.0-rc1 "@mochajs/json-file-reporter": ^1.2.0 "@rollup/plugin-commonjs": 21.0.1 @@ -3846,10 +3846,10 @@ __metadata: version: 0.0.0-use.local resolution: "@grafana/runtime@workspace:packages/grafana-runtime" dependencies: - "@grafana/data": 8.4.0-pre - "@grafana/e2e-selectors": 8.4.0-pre + "@grafana/data": 8.4.0-beta.1 + "@grafana/e2e-selectors": 8.4.0-beta.1 "@grafana/tsconfig": ^1.0.0-rc1 - "@grafana/ui": 8.4.0-pre + "@grafana/ui": 8.4.0-beta.1 "@rollup/plugin-commonjs": 21.0.1 "@rollup/plugin-node-resolve": 13.1.3 "@sentry/browser": 6.17.2 @@ -3878,7 +3878,7 @@ __metadata: languageName: unknown linkType: soft -"@grafana/schema@8.4.0-pre, @grafana/schema@workspace:*, @grafana/schema@workspace:packages/grafana-schema": +"@grafana/schema@8.4.0-beta.1, @grafana/schema@workspace:*, @grafana/schema@workspace:packages/grafana-schema": version: 0.0.0-use.local resolution: "@grafana/schema@workspace:packages/grafana-schema" dependencies: @@ -3925,16 +3925,16 @@ __metadata: languageName: node linkType: hard -"@grafana/toolkit@8.4.0-pre, @grafana/toolkit@workspace:*, @grafana/toolkit@workspace:packages/grafana-toolkit": +"@grafana/toolkit@8.4.0-beta.1, @grafana/toolkit@workspace:*, @grafana/toolkit@workspace:packages/grafana-toolkit": version: 0.0.0-use.local resolution: "@grafana/toolkit@workspace:packages/grafana-toolkit" dependencies: "@babel/core": 7.13.14 "@babel/preset-env": 7.13.12 - "@grafana/data": 8.4.0-pre + "@grafana/data": 8.4.0-beta.1 "@grafana/eslint-config": 2.5.2 "@grafana/tsconfig": ^1.0.0-rc1 - "@grafana/ui": 8.4.0-pre + "@grafana/ui": 8.4.0-beta.1 "@jest/core": 26.6.3 "@rushstack/eslint-patch": 1.0.6 "@types/command-exists": ^1.2.0 @@ -4025,7 +4025,7 @@ __metadata: languageName: node linkType: hard -"@grafana/ui@8.4.0-pre, @grafana/ui@workspace:*, @grafana/ui@workspace:packages/grafana-ui": +"@grafana/ui@8.4.0-beta.1, @grafana/ui@workspace:*, @grafana/ui@workspace:packages/grafana-ui": version: 0.0.0-use.local resolution: "@grafana/ui@workspace:packages/grafana-ui" dependencies: @@ -4033,9 +4033,9 @@ __metadata: "@emotion/css": 11.7.1 "@emotion/react": 11.7.1 "@grafana/aws-sdk": 0.0.31 - "@grafana/data": 8.4.0-pre - "@grafana/e2e-selectors": 8.4.0-pre - "@grafana/schema": 8.4.0-pre + "@grafana/data": 8.4.0-beta.1 + "@grafana/e2e-selectors": 8.4.0-beta.1 + "@grafana/schema": 8.4.0-beta.1 "@grafana/slate-react": 0.22.10-grafana "@grafana/tsconfig": ^1.0.0-rc1 "@mdx-js/react": 1.6.22 @@ -4251,9 +4251,9 @@ __metadata: resolution: "@jaegertracing/jaeger-ui-components@workspace:packages/jaeger-ui-components" dependencies: "@emotion/css": 11.7.1 - "@grafana/data": 8.4.0-pre + "@grafana/data": 8.4.0-beta.1 "@grafana/tsconfig": ^1.0.0-rc1 - "@grafana/ui": 8.4.0-pre + "@grafana/ui": 8.4.0-beta.1 "@types/classnames": ^2.2.7 "@types/deep-freeze": ^0.1.1 "@types/grafana__slate-react": "npm:@types/slate-react@0.22.5" From 60fcb87a6b75a84645489fe55af0e75a7f9598be Mon Sep 17 00:00:00 2001 From: "Grot (@grafanabot)" <43478413+grafanabot@users.noreply.github.com> Date: Thu, 3 Feb 2022 13:54:42 +0100 Subject: [PATCH 008/113] Feature Highlights: move setting to a feature toggle (#44780) (#44827) * Add toggle * Use the toggle * Cleanup (cherry picked from commit a79c048344bddff7a868b040d9a08953917480f9) Co-authored-by: Alex Khomenko --- docs/sources/enterprise/enterprise-configuration.md | 6 ------ packages/grafana-data/src/types/featureToggles.gen.ts | 1 + pkg/api/frontendsettings.go | 3 --- pkg/services/featuremgmt/registry.go | 5 +++++ pkg/services/featuremgmt/toggles_gen.go | 4 ++++ public/app/features/datasources/state/navModel.ts | 8 ++++---- public/app/features/teams/TeamPages.tsx | 2 +- public/app/features/teams/state/navModel.ts | 2 +- 8 files changed, 16 insertions(+), 15 deletions(-) diff --git a/docs/sources/enterprise/enterprise-configuration.md b/docs/sources/enterprise/enterprise-configuration.md index ebee7b16687..1639e9e2abf 100644 --- a/docs/sources/enterprise/enterprise-configuration.md +++ b/docs/sources/enterprise/enterprise-configuration.md @@ -501,9 +501,3 @@ The org id of the datasource where the query data will be written. If all `default_remote_write_*` properties are set, this information will be populated at startup. If a remote write target has already been configured, nothing will happen. - -## [feature_highlights] - -### enabled - -Whether the feature highlights feature is enabled diff --git a/packages/grafana-data/src/types/featureToggles.gen.ts b/packages/grafana-data/src/types/featureToggles.gen.ts index 971014422c9..a3f86909bee 100644 --- a/packages/grafana-data/src/types/featureToggles.gen.ts +++ b/packages/grafana-data/src/types/featureToggles.gen.ts @@ -36,4 +36,5 @@ export interface FeatureToggles { showFeatureFlagsInUI?: boolean; disable_http_request_histogram?: boolean; validatedQueries?: boolean; + featureHighlights?: boolean; } diff --git a/pkg/api/frontendsettings.go b/pkg/api/frontendsettings.go index 0e45d41c620..43d38af0b77 100644 --- a/pkg/api/frontendsettings.go +++ b/pkg/api/frontendsettings.go @@ -273,9 +273,6 @@ func (hs *HTTPServer) getFrontendSettingsMap(c *models.ReqContext) (map[string]i "enabled": hs.Cfg.SectionWithEnvOverrides("recorded_queries").Key("enabled").MustBool(true), }, "unifiedAlertingEnabled": hs.Cfg.UnifiedAlerting.Enabled, - "featureHighlights": map[string]bool{ - "enabled": hs.SettingsProvider.Section("feature_highlights").KeyValue("enabled").MustBool(false), - }, } if hs.Cfg.GeomapDefaultBaseLayerConfig != nil { diff --git a/pkg/services/featuremgmt/registry.go b/pkg/services/featuremgmt/registry.go index 3763a03f094..9a5a487cbf7 100644 --- a/pkg/services/featuremgmt/registry.go +++ b/pkg/services/featuremgmt/registry.go @@ -110,5 +110,10 @@ var ( State: FeatureStateAlpha, RequiresDevMode: true, }, + { + Name: "featureHighlights", + Description: "Highlight Enterprise features", + State: FeatureStateStable, + }, } ) diff --git a/pkg/services/featuremgmt/toggles_gen.go b/pkg/services/featuremgmt/toggles_gen.go index 430b9fbda56..49faad2bd17 100644 --- a/pkg/services/featuremgmt/toggles_gen.go +++ b/pkg/services/featuremgmt/toggles_gen.go @@ -82,4 +82,8 @@ const ( // FlagValidatedQueries // only execute the query saved in a panel FlagValidatedQueries = "validatedQueries" + + // FlagFeatureHighlights + // Highlight Enterprise features + FlagFeatureHighlights = "featureHighlights" ) diff --git a/public/app/features/datasources/state/navModel.ts b/public/app/features/datasources/state/navModel.ts index 25c88ae6935..4b6e635ed6e 100644 --- a/public/app/features/datasources/state/navModel.ts +++ b/public/app/features/datasources/state/navModel.ts @@ -8,7 +8,7 @@ import { GenericDataSourcePlugin } from '../settings/PluginSettings'; export function buildNavModel(dataSource: DataSourceSettings, plugin: GenericDataSourcePlugin): NavModelItem { const pluginMeta = plugin.meta; - + const highlightsEnabled = config.featureToggles.featureHighlights; const navModel: NavModelItem = { img: pluginMeta.info.logos.large, id: 'datasource-' + dataSource.uid, @@ -61,7 +61,7 @@ export function buildNavModel(dataSource: DataSourceSettings, plugin: GenericDat if (contextSrv.hasPermission(AccessControlAction.DataSourcesPermissionsRead)) { navModel.children!.push(dsPermissions); } - } else if (config.featureHighlights.enabled) { + } else if (highlightsEnabled) { navModel.children!.push({ ...dsPermissions, url: dsPermissions.url + '/upgrade', @@ -79,7 +79,7 @@ export function buildNavModel(dataSource: DataSourceSettings, plugin: GenericDat if (featureEnabled('analytics')) { navModel.children!.push(analytics); - } else if (config.featureHighlights.enabled) { + } else if (highlightsEnabled) { navModel.children!.push({ ...analytics, url: analytics.url + '/upgrade', @@ -98,7 +98,7 @@ export function buildNavModel(dataSource: DataSourceSettings, plugin: GenericDat if (featureEnabled('caching')) { navModel.children!.push(caching); - } else if (config.featureHighlights.enabled) { + } else if (highlightsEnabled) { navModel.children!.push({ ...caching, url: caching.url + '/upgrade', diff --git a/public/app/features/teams/TeamPages.tsx b/public/app/features/teams/TeamPages.tsx index 400dc0079c5..9817ddefe5b 100644 --- a/public/app/features/teams/TeamPages.tsx +++ b/public/app/features/teams/TeamPages.tsx @@ -130,7 +130,7 @@ export class TeamPages extends PureComponent { case PageTypes.GroupSync: if (isSignedInUserTeamAdmin && isSyncEnabled) { return ; - } else if (config.featureHighlights.enabled) { + } else if (config.featureToggles.featureHighlights) { return ( Date: Thu, 3 Feb 2022 14:08:57 +0100 Subject: [PATCH 009/113] Alerting: load correct unified alerting tab (#44794) (#44832) (cherry picked from commit 29b97361f70e8991069601a88842080d686dce9f) Co-authored-by: Gilles De Mey --- .../components/PanelEditor/PanelEditorTabs.tsx | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/public/app/features/dashboard/components/PanelEditor/PanelEditorTabs.tsx b/public/app/features/dashboard/components/PanelEditor/PanelEditorTabs.tsx index 1c04cfc2473..8c903e3fbfa 100644 --- a/public/app/features/dashboard/components/PanelEditor/PanelEditorTabs.tsx +++ b/public/app/features/dashboard/components/PanelEditor/PanelEditorTabs.tsx @@ -41,7 +41,7 @@ export const PanelEditorTabs: FC = React.memo(({ panel, da {tabs.map((tab) => { if (tab.id === PanelEditorTabId.Alert) { - renderAlertTab(tab, panel, dashboard, onChangeTab); + return renderAlertTab(tab, panel, dashboard, onChangeTab); } return ( void ) { - if (!config.alertingEnabled || !config.unifiedAlertingEnabled) { + const alertingDisabled = !config.alertingEnabled && !config.unifiedAlertingEnabled; + + if (alertingDisabled) { return null; - } else if (config.unifiedAlertingEnabled) { + } + + if (config.unifiedAlertingEnabled) { return ( ); - } else if (config.alertingEnabled) { + } + + if (config.alertingEnabled) { return ( Date: Thu, 3 Feb 2022 14:32:48 +0100 Subject: [PATCH 010/113] Alerting: Fix alert notification template (#44761) (#44839) (cherry picked from commit bb88cf683cefc750004032c5c518501269a01694) Co-authored-by: Konrad Lalik --- emails/templates/ng_alert_notification.html | 116 ++++++++--------- public/emails/ng_alert_notification.html | 133 ++++++++++---------- 2 files changed, 128 insertions(+), 121 deletions(-) diff --git a/emails/templates/ng_alert_notification.html b/emails/templates/ng_alert_notification.html index 1f967e53353..b7c7f6156dd 100644 --- a/emails/templates/ng_alert_notification.html +++ b/emails/templates/ng_alert_notification.html @@ -1,11 +1,14 @@ + +
+ [[Subject .Subject "[[.Title]]"]] [[ define "alert" ]] - - - Value: [[ .ValueString ]] - - + + + Value: [[ .ValueString ]] + + [[ if gt (len .Annotations.SortedPairs) 0 ]] @@ -187,62 +190,63 @@ - [[ if gt (len .Alerts.Firing) 0 ]] - - - - [[ range .Alerts.Firing ]] + [[ if gt (len .Alerts.Firing) 0 ]] + + + + [[ range .Alerts.Firing ]] + + + + + [[ template "alert" . ]] + [[ end ]] + [[ end ]] + [[ if gt (len .Alerts.Resolved) 0 ]] + + + + [[ range .Alerts.Resolved ]] + + + + + [[ template "alert" . ]] + [[ end ]] + [[ end ]] - - - [[ template "alert" . ]] - [[ end ]] - [[ end ]] - [[ if gt (len .Alerts.Resolved) 0 ]] - - - - [[ range .Alerts.Resolved ]] - - - - - [[ template "alert" . ]] - [[ end ]] - [[ end ]] - - - -
- Firing: [[ .Alerts.Firing | len ]] alert[[ if gt (len .Alerts.Firing) 1 ]]s[[ end ]][[ if gt (len .GroupLabels.SortedPairs) 1 ]] for - [[ range .GroupLabels.SortedPairs ]] - [[ .Name ]]=[[ .Value ]] - [[ end ]][[ end ]] -
+ Firing: [[ .Alerts.Firing | len ]] alert[[ if gt (len .Alerts.Firing) 1 ]]s[[ end ]][[ if gt (len .GroupLabels.SortedPairs) 1 ]] for + [[ range .GroupLabels.SortedPairs ]] + [[ .Name ]]=[[ .Value ]] + [[ end ]][[ end ]] +
+ Firing + + [[ .Labels.alertname ]] +
+ Resolved: [[ .Alerts.Resolved | len ]] alert[[ if gt (len .Alerts.Resolved) 1 ]]s[[ end ]][[ if gt (len .GroupLabels.SortedPairs) 1 ]] for + [[ range .GroupLabels.SortedPairs ]] + [[ .Name ]]=[[ .Value ]] + [[ end ]][[ end ]] +
+ Resolved + + [[ .Labels.alertname ]] +
- Firing - - [[ .Labels.alertname ]] + + Go to alerts page
- Resolved: [[ .Alerts.Resolved | len ]] alert[[ if gt (len .Alerts.Resolved) 1 ]]s[[ end ]][[ if gt (len .GroupLabels.SortedPairs) 1 ]] for - [[ range .GroupLabels.SortedPairs ]] - [[ .Name ]]=[[ .Value ]] - [[ end ]][[ end ]] -
- Resolved - - [[ .Labels.alertname ]] -
- Go to alerts page -
[[ end ]] + +
diff --git a/public/emails/ng_alert_notification.html b/public/emails/ng_alert_notification.html index a873e1299b7..8f0f2828b8a 100644 --- a/public/emails/ng_alert_notification.html +++ b/public/emails/ng_alert_notification.html @@ -183,7 +183,7 @@ text-decoration: underline; @@ -200,18 +200,21 @@ text-decoration: underline;
- +
- + - - + + + {{ if gt (len .Annotations.SortedPairs) 0 }}
- {{Subject .Subject "{{.Title}}"}} + +
+ +{{Subject .Subject "{{.Title}}"}} {{ define "alert" }} -
- Value: {{ .ValueString }} -
+ Value: {{ .ValueString }} +
@@ -233,25 +236,25 @@ text-decoration: underline; {{ if .SilenceURL }} - + Silence {{ end }} {{ if .Annotations.runbook_url }} - + View Runbook {{ end }} {{ if .DashboardURL}} - + Go to Dashboard {{ end }} {{ if .PanelURL}} - + Go to Panel {{ end }} @@ -277,62 +280,62 @@ text-decoration: underline;
- {{ if gt (len .Alerts.Firing) 0 }} - - - - {{ range .Alerts.Firing }} + {{ if gt (len .Alerts.Firing) 0 }} + + + + {{ range .Alerts.Firing }} + + + + + {{ template "alert" . }} + {{ end }} + {{ end }} + {{ if gt (len .Alerts.Resolved) 0 }} + + + + {{ range .Alerts.Resolved }} + + + + + {{ template "alert" . }} + {{ end }} + {{ end }} - - - {{ template "alert" . }} - {{ end }} - {{ end }} - {{ if gt (len .Alerts.Resolved) 0 }} - - - - {{ range .Alerts.Resolved }} - - - - - {{ template "alert" . }} - {{ end }} - {{ end }} - - -
- Firing: {{ .Alerts.Firing | len }} alert{{ if gt (len .Alerts.Firing) 1 }}s{{ end }}{{ if gt (len .GroupLabels.SortedPairs) 1 }} for - {{ range .GroupLabels.SortedPairs }} - {{ .Name }}={{ .Value }} - {{ end }}{{ end }} -
+ Firing: {{ .Alerts.Firing | len }} alert{{ if gt (len .Alerts.Firing) 1 }}s{{ end }}{{ if gt (len .GroupLabels.SortedPairs) 1 }} for + {{ range .GroupLabels.SortedPairs }} + {{ .Name }}={{ .Value }} + {{ end }}{{ end }} +
+ Firing + + {{ .Labels.alertname }} +
+ Resolved: {{ .Alerts.Resolved | len }} alert{{ if gt (len .Alerts.Resolved) 1 }}s{{ end }}{{ if gt (len .GroupLabels.SortedPairs) 1 }} for + {{ range .GroupLabels.SortedPairs }} + {{ .Name }}={{ .Value }} + {{ end }}{{ end }} +
+ Resolved + + {{ .Labels.alertname }} +
- Firing - - {{ .Labels.alertname }} + + Go to alerts page
- Resolved: {{ .Alerts.Resolved | len }} alert{{ if gt (len .Alerts.Resolved) 1 }}s{{ end }}{{ if gt (len .GroupLabels.SortedPairs) 1 }} for - {{ range .GroupLabels.SortedPairs }} - {{ .Name }}={{ .Value }} - {{ end }}{{ end }} -
- Resolved - - {{ .Labels.alertname }} -
- Go to alerts page -
-
- - + + {{ end }} +
- + @@ -341,7 +344,7 @@ text-decoration: underline; -
+

Sent by Grafana v{{.BuildVersion}} @@ -355,9 +358,9 @@ text-decoration: underline;

- - - - + + + + From d20ac041201768eec24831b52dc87f57a2bbd2aa Mon Sep 17 00:00:00 2001 From: "Grot (@grafanabot)" <43478413+grafanabot@users.noreply.github.com> Date: Thu, 3 Feb 2022 15:57:01 +0100 Subject: [PATCH 011/113] Loki: use generic grafana null-insertion mechanism (#44826) (#44846) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * loki: refactor: return dataframes instead of timeseries * fixed unit test * removed unused import (cherry picked from commit 0c2ba819a7a0772fdec7358c80f2e31e2246eac1) Co-authored-by: Gábor Farkas --- .../datasource/loki/datasource.test.ts | 19 ++++--- .../loki/result_transformer.test.ts | 6 +-- .../datasource/loki/result_transformer.ts | 50 ++++++++++++------- 3 files changed, 46 insertions(+), 29 deletions(-) diff --git a/public/app/plugins/datasource/loki/datasource.test.ts b/public/app/plugins/datasource/loki/datasource.test.ts index da697baefa1..2c75f36b5da 100644 --- a/public/app/plugins/datasource/loki/datasource.test.ts +++ b/public/app/plugins/datasource/loki/datasource.test.ts @@ -10,7 +10,6 @@ import { FieldType, LogRowModel, MutableDataFrame, - TimeSeries, toUtc, } from '@grafana/data'; import { BackendSrvRequest, FetchResponse, config } from '@grafana/runtime'; @@ -334,7 +333,7 @@ describe('LokiDatasource', () => { expect(ds.runRangeQuery).toBeCalled(); }); - it('should return series data for metrics range queries', async () => { + it('should return dataframe data for metrics range queries', async () => { const ds = createLokiDSForTests(); const options = getQueryOptions({ targets: [{ expr: metricsQuery, refId: 'B', range: true }], @@ -345,11 +344,19 @@ describe('LokiDatasource', () => { await expect(ds.query(options)).toEmitValuesWith((received) => { const result = received[0]; - const timeSeries = result.data[0] as TimeSeries; + const frame = result.data[0] as DataFrame; - expect(timeSeries.meta?.preferredVisualisationType).toBe('graph'); - expect(timeSeries.refId).toBe('B'); - expect(timeSeries.datapoints[0]).toEqual([1.1, 1605715380000]); + expect(frame.meta?.preferredVisualisationType).toBe('graph'); + expect(frame.refId).toBe('B'); + frame.fields.forEach((field) => { + const value = field.values.get(0); + + if (field.type === FieldType.time) { + expect(value).toBe(1605715380000); + } else { + expect(value).toBe(1.1); + } + }); }); }); diff --git a/public/app/plugins/datasource/loki/result_transformer.test.ts b/public/app/plugins/datasource/loki/result_transformer.test.ts index b2fb3682d4e..d9d0e0b99b8 100644 --- a/public/app/plugins/datasource/loki/result_transformer.test.ts +++ b/public/app/plugins/datasource/loki/result_transformer.test.ts @@ -290,8 +290,6 @@ describe('enhanceDataFrame', () => { * NOTE on time parameters: * - Input time series data has timestamps in sec (like Prometheus) * - Output time series has timestamps in ms (as expected for the chart lib) - * - Start/end parameters are in ns (as expected for Loki) - * - Step is in sec (like in Prometheus) */ const data: Array<[number, string]> = [ [1, '1'], @@ -300,12 +298,10 @@ describe('enhanceDataFrame', () => { ]; it('returns data as is if step, start, and end align', () => { - const options: Partial = { start: 1 * 1e9, end: 4 * 1e9, step: 1 }; - const result = ResultTransformer.lokiPointsToTimeseriesPoints(data, options as TransformerOptions); + const result = ResultTransformer.lokiPointsToTimeseriesPoints(data); expect(result).toEqual([ [1, 1000], [0, 2000], - [null, 3000], [1, 4000], ]); }); diff --git a/public/app/plugins/datasource/loki/result_transformer.ts b/public/app/plugins/datasource/loki/result_transformer.ts index 970cb1ddc45..ae7ef9ce46c 100644 --- a/public/app/plugins/datasource/loki/result_transformer.ts +++ b/public/app/plugins/datasource/loki/result_transformer.ts @@ -17,6 +17,7 @@ import { QueryResultMeta, TimeSeriesValue, ScopedVars, + toDataFrame, } from '@grafana/data'; import { getTemplateSrv, getDataSourceSrv } from '@grafana/runtime'; @@ -184,21 +185,16 @@ function lokiMatrixToTimeSeries(matrixResult: LokiMatrixResult, options: Transfo return { target: name, title: name, - datapoints: lokiPointsToTimeseriesPoints(matrixResult.values, options), + datapoints: lokiPointsToTimeseriesPoints(matrixResult.values), tags: matrixResult.metric, meta: options.meta, refId: options.refId, }; } -export function lokiPointsToTimeseriesPoints( - data: Array<[number, string]>, - options: TransformerOptions -): TimeSeriesValue[][] { - const stepMs = options.step * 1000; +export function lokiPointsToTimeseriesPoints(data: Array<[number, string]>): TimeSeriesValue[][] { const datapoints: TimeSeriesValue[][] = []; - let baseTimestampMs = options.start / 1e6; for (const [time, value] of data) { let datapointValue: TimeSeriesValue = parseFloat(value); @@ -207,19 +203,10 @@ export function lokiPointsToTimeseriesPoints( } const timestamp = time * 1000; - for (let t = baseTimestampMs; t < timestamp; t += stepMs) { - datapoints.push([null, t]); - } - baseTimestampMs = timestamp + stepMs; datapoints.push([datapointValue, timestamp]); } - const endTimestamp = options.end / 1e6; - for (let t = baseTimestampMs; t <= endTimestamp; t += stepMs) { - datapoints.push([null, t]); - } - return datapoints; } @@ -454,7 +441,7 @@ function fieldFromDerivedFieldConfig(derivedFieldConfigs: DerivedFieldConfig[]): }; } -export function rangeQueryResponseToTimeSeries( +function rangeQueryResponseToTimeSeries( response: LokiResponse, query: LokiRangeQueryRequest, target: LokiQuery, @@ -491,6 +478,33 @@ export function rangeQueryResponseToTimeSeries( } } +export function rangeQueryResponseToDataFrames( + response: LokiResponse, + query: LokiRangeQueryRequest, + target: LokiQuery, + responseListLength: number, + scopedVars: ScopedVars +): DataFrame[] { + const series = rangeQueryResponseToTimeSeries(response, query, target, responseListLength, scopedVars); + const frames = series.map((s) => toDataFrame(s)); + + const { step } = query; + + if (step != null) { + const intervalMs = step * 1000; + + frames.forEach((frame) => { + frame.fields.forEach((field) => { + if (field.type === FieldType.time) { + field.config.interval = intervalMs; + } + }); + }); + } + + return frames; +} + export function processRangeQueryResponse( response: LokiResponse, target: LokiQuery, @@ -511,7 +525,7 @@ export function processRangeQueryResponse( case LokiResultType.Vector: case LokiResultType.Matrix: return of({ - data: rangeQueryResponseToTimeSeries( + data: rangeQueryResponseToDataFrames( response, query, { From c5aaf9526e655e78d474a7c867e71829c02c680f Mon Sep 17 00:00:00 2001 From: "Grot (@grafanabot)" <43478413+grafanabot@users.noreply.github.com> Date: Thu, 3 Feb 2022 16:02:05 +0100 Subject: [PATCH 012/113] Rename build-e2e-publish pipelines (#44836) (#44847) (cherry picked from commit 5f16e4cedc39678ec80dadd6dd7c3be258ef6d9b) (cherry picked from commit 37ac299853b27ff819d16085630f425fc30c794e) Co-authored-by: Dimitris Sotirakis --- .drone.yml | 10 +++++----- scripts/drone/pipelines/release.star | 4 ++-- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/.drone.yml b/.drone.yml index f8d744be837..2b826efa4a0 100644 --- a/.drone.yml +++ b/.drone.yml @@ -1094,7 +1094,7 @@ type: docker --- depends_on: [] kind: pipeline -name: oss-build-publish-e2e-release +name: oss-build-e2e-publish-release node: type: no-parallel platform: @@ -1599,7 +1599,7 @@ volumes: medium: memory --- depends_on: -- oss-build-publish-e2e-release +- oss-build-e2e-publish-release - oss-test-release - oss-integration-tests-release kind: pipeline @@ -2858,7 +2858,7 @@ volumes: --- depends_on: [] kind: pipeline -name: oss-build-publish-e2e-release-branch +name: oss-build-e2e-publish-release-branch node: type: no-parallel platform: @@ -3305,7 +3305,7 @@ volumes: medium: memory --- depends_on: -- oss-build-publish-e2e-release-branch +- oss-build-e2e-publish-release-branch - oss-test-release-branch - oss-integration-tests-release-branch kind: pipeline @@ -4191,6 +4191,6 @@ kind: secret name: gcp_upload_artifacts_key --- kind: signature -hmac: 4d1a5696bf1e510fb51a021c07e240c50cb913724ce08ed52cce037ff02dd8de +hmac: f26fc6de1d7ec3cf5608b70c851c8cf2b998e07abad8c52714e72ad072492387 ... diff --git a/scripts/drone/pipelines/release.star b/scripts/drone/pipelines/release.star index 1c592e9d730..88be5a4a675 100644 --- a/scripts/drone/pipelines/release.star +++ b/scripts/drone/pipelines/release.star @@ -285,7 +285,7 @@ def get_oss_pipelines(trigger, ver_mode): ) pipelines = [ pipeline( - name='oss-build-publish{}-{}'.format(get_e2e_suffix(), ver_mode), edition=edition, trigger=trigger, services=[], + name='oss-build{}-publish-{}'.format(get_e2e_suffix(), ver_mode), edition=edition, trigger=trigger, services=[], steps=[download_grabpl_step()] + initialize_step(edition, platform='linux', ver_mode=ver_mode) + build_steps + package_steps + publish_steps, volumes=volumes, @@ -308,7 +308,7 @@ def get_oss_pipelines(trigger, ver_mode): ]) deps = { 'depends_on': [ - 'oss-build-publish{}-{}'.format(get_e2e_suffix(), ver_mode), + 'oss-build{}-publish-{}'.format(get_e2e_suffix(), ver_mode), 'oss-test-{}'.format(ver_mode), 'oss-integration-tests-{}'.format(ver_mode) ] From 5a1fc33eb43e560562be63ff8ac1c665def44615 Mon Sep 17 00:00:00 2001 From: "Grot (@grafanabot)" <43478413+grafanabot@users.noreply.github.com> Date: Thu, 3 Feb 2022 16:02:47 +0100 Subject: [PATCH 013/113] ReleaseNotes: Updated changelog and release notes for 8.4.0-beta1 (#44822) (#44845) (cherry picked from commit afac7701cb15e323312e16f201039aabe6e2b660) --- CHANGELOG.md | 20 ++++++++++++++++ docs/sources/release-notes/_index.md | 1 + .../release-notes-8-4-0-beta1.md | 23 +++++++++++++++++++ 3 files changed, 44 insertions(+) create mode 100644 docs/sources/release-notes/release-notes-8-4-0-beta1.md diff --git a/CHANGELOG.md b/CHANGELOG.md index 94e10774dee..a2b75759278 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,23 @@ + + +# 8.4.0-beta1 (2022-02-02) + +### Features and enhancements + +- **Alerting:** Support WeCom as a contact point type. [#40975](https://github.com/grafana/grafana/pull/40975), [@smallpath](https://github.com/smallpath) +- **Alerting:** UI for mute timings. [#41578](https://github.com/grafana/grafana/pull/41578), [@nathanrodman](https://github.com/nathanrodman) +- **Alerting:** add settings for peer reconnection in HA mode. [#42300](https://github.com/grafana/grafana/pull/42300), [@JohnnyQQQQ](https://github.com/JohnnyQQQQ) +- **Auth:** implement auto_sign_up for auth.jwt. [#37040](https://github.com/grafana/grafana/pull/37040), [@Roguelazer](https://github.com/Roguelazer) +- **Dashboard:** Add Show unknown variables toggle to dashboard settings. [#41854](https://github.com/grafana/grafana/pull/41854), [@hugohaggmark](https://github.com/hugohaggmark) +- **Instrumentation:** Logger migration from log15 to gokit/log. [#41636](https://github.com/grafana/grafana/pull/41636), [@ying-jeanne](https://github.com/ying-jeanne) +- **MSSQL:** Change regex to validate Provider connection string. [#40248](https://github.com/grafana/grafana/pull/40248), [@ianselmi](https://github.com/ianselmi) +- **MSSQL:** Configuration of certificate verification for TLS connection. [#31865](https://github.com/grafana/grafana/pull/31865), [@mortenaa](https://github.com/mortenaa) +- **Middleware:** Don't require HTTPS for HSTS headers to be emitted. [#35147](https://github.com/grafana/grafana/pull/35147), [@alexmv](https://github.com/alexmv) +- **Navigation:** Implement Keyboard Navigation. [#41618](https://github.com/grafana/grafana/pull/41618), [@axelavargas](https://github.com/axelavargas) +- **News:** Reload feed when changing the time range or refreshing. [#42217](https://github.com/grafana/grafana/pull/42217), [@ashharrison90](https://github.com/ashharrison90) +- **UI/Plot:** Implement keyboard controls for plot cursor. [#42244](https://github.com/grafana/grafana/pull/42244), [@kaydelaney](https://github.com/kaydelaney) + + # 8.3.4 (2022-01-17) diff --git a/docs/sources/release-notes/_index.md b/docs/sources/release-notes/_index.md index 2ae655180d9..907d9f47afd 100644 --- a/docs/sources/release-notes/_index.md +++ b/docs/sources/release-notes/_index.md @@ -8,6 +8,7 @@ weight = 10000 Here you can find detailed release notes that list everything that is included in every release as well as notices about deprecations, breaking changes as well as changes that relate to plugin development. +- [Release notes for 8.4.0-beta1]({{< relref "release-notes-8-4-0-beta1" >}}) - [Release notes for 8.3.4]({{< relref "release-notes-8-3-4" >}}) - [Release notes for 8.3.3]({{< relref "release-notes-8-3-3" >}}) - [Release notes for 8.3.2]({{< relref "release-notes-8-3-2" >}}) diff --git a/docs/sources/release-notes/release-notes-8-4-0-beta1.md b/docs/sources/release-notes/release-notes-8-4-0-beta1.md new file mode 100644 index 00000000000..1d9aaf557bb --- /dev/null +++ b/docs/sources/release-notes/release-notes-8-4-0-beta1.md @@ -0,0 +1,23 @@ ++++ +title = "Release notes for Grafana 8.4.0-beta1" +hide_menu = true ++++ + + + +# Release notes for Grafana 8.4.0-beta1 + +### Features and enhancements + +- **Alerting:** Support WeCom as a contact point type. [#40975](https://github.com/grafana/grafana/pull/40975), [@smallpath](https://github.com/smallpath) +- **Alerting:** UI for mute timings. [#41578](https://github.com/grafana/grafana/pull/41578), [@nathanrodman](https://github.com/nathanrodman) +- **Alerting:** add settings for peer reconnection in HA mode. [#42300](https://github.com/grafana/grafana/pull/42300), [@JohnnyQQQQ](https://github.com/JohnnyQQQQ) +- **Auth:** implement auto_sign_up for auth.jwt. [#37040](https://github.com/grafana/grafana/pull/37040), [@Roguelazer](https://github.com/Roguelazer) +- **Dashboard:** Add Show unknown variables toggle to dashboard settings. [#41854](https://github.com/grafana/grafana/pull/41854), [@hugohaggmark](https://github.com/hugohaggmark) +- **Instrumentation:** Logger migration from log15 to gokit/log. [#41636](https://github.com/grafana/grafana/pull/41636), [@ying-jeanne](https://github.com/ying-jeanne) +- **MSSQL:** Change regex to validate Provider connection string. [#40248](https://github.com/grafana/grafana/pull/40248), [@ianselmi](https://github.com/ianselmi) +- **MSSQL:** Configuration of certificate verification for TLS connection. [#31865](https://github.com/grafana/grafana/pull/31865), [@mortenaa](https://github.com/mortenaa) +- **Middleware:** Don't require HTTPS for HSTS headers to be emitted. [#35147](https://github.com/grafana/grafana/pull/35147), [@alexmv](https://github.com/alexmv) +- **Navigation:** Implement Keyboard Navigation. [#41618](https://github.com/grafana/grafana/pull/41618), [@axelavargas](https://github.com/axelavargas) +- **News:** Reload feed when changing the time range or refreshing. [#42217](https://github.com/grafana/grafana/pull/42217), [@ashharrison90](https://github.com/ashharrison90) +- **UI/Plot:** Implement keyboard controls for plot cursor. [#42244](https://github.com/grafana/grafana/pull/42244), [@kaydelaney](https://github.com/kaydelaney) From 9e0acc9ecef07f5f3b3e06aebfdb0f3d01219d54 Mon Sep 17 00:00:00 2001 From: "Grot (@grafanabot)" <43478413+grafanabot@users.noreply.github.com> Date: Thu, 3 Feb 2022 16:34:18 +0100 Subject: [PATCH 014/113] Chore: Fix log filters (#44681) (#44853) (cherry picked from commit bc7e55d99bb43f3f870c3ebe7b960e147462b9dd) Co-authored-by: Marcus Efraimsson --- pkg/api/frontend_logging_test.go | 2 +- pkg/infra/log/composite_logger.go | 25 ++ pkg/infra/log/interface.go | 2 +- pkg/infra/log/log.go | 335 ++++++++++++------ pkg/infra/log/syslog.go | 2 +- pkg/infra/usagestats/service/service.go | 2 +- pkg/login/social/generic_oauth_test.go | 2 +- pkg/middleware/recovery.go | 2 +- .../loader/initializer/initializer_test.go | 6 +- pkg/plugins/manager/loader/loader_test.go | 4 +- .../login/loginservice/loginservice_test.go | 2 +- pkg/setting/setting.go | 2 + 12 files changed, 261 insertions(+), 125 deletions(-) create mode 100644 pkg/infra/log/composite_logger.go diff --git a/pkg/api/frontend_logging_test.go b/pkg/api/frontend_logging_test.go index a8e204fdd25..84d8953f7aa 100644 --- a/pkg/api/frontend_logging_test.go +++ b/pkg/api/frontend_logging_test.go @@ -42,7 +42,7 @@ func logSentryEventScenario(t *testing.T, desc string, event frontendlogging.Fro })) origHandler := frontendLogger.GetLogger() - frontendLogger.AddLogger(newfrontendLogger, "info", map[string]level.Option{}) + frontendLogger.SetLogger(level.NewFilter(newfrontendLogger, level.AllowInfo())) sourceMapReads := []SourceMapReadRecord{} t.Cleanup(func() { diff --git a/pkg/infra/log/composite_logger.go b/pkg/infra/log/composite_logger.go new file mode 100644 index 00000000000..5c238673347 --- /dev/null +++ b/pkg/infra/log/composite_logger.go @@ -0,0 +1,25 @@ +package log + +import gokitlog "github.com/go-kit/log" + +type compositeLogger struct { + loggers []gokitlog.Logger +} + +func newCompositeLogger(loggers ...gokitlog.Logger) *compositeLogger { + if len(loggers) == 0 { + loggers = []gokitlog.Logger{} + } + + return &compositeLogger{loggers: loggers} +} + +func (l *compositeLogger) Log(keyvals ...interface{}) error { + for _, logger := range l.loggers { + if err := logger.Log(keyvals...); err != nil { + return err + } + } + + return nil +} diff --git a/pkg/infra/log/interface.go b/pkg/infra/log/interface.go index 523f9a3bc33..c0909d682cb 100644 --- a/pkg/infra/log/interface.go +++ b/pkg/infra/log/interface.go @@ -12,7 +12,7 @@ const ( type Logger interface { // New returns a new Logger that has this logger's context plus the given context - New(ctx ...interface{}) MultiLoggers + New(ctx ...interface{}) *ConcreteLogger Log(keyvals ...interface{}) error diff --git a/pkg/infra/log/log.go b/pkg/infra/log/log.go index ba531ca633f..a4d38d84256 100644 --- a/pkg/infra/log/log.go +++ b/pkg/infra/log/log.go @@ -7,9 +7,12 @@ package log import ( "fmt" "io" + "log" "os" "path/filepath" + "sort" "strings" + "sync" "time" gokitlog "github.com/go-kit/log" @@ -25,8 +28,7 @@ import ( var loggersToClose []DisposableHandler var loggersToReload []ReloadableHandler -var filters map[string]level.Option -var Root MultiLoggers +var root *logManager const ( // top 7 calls in the stack are within logger @@ -37,11 +39,212 @@ const ( func init() { loggersToClose = make([]DisposableHandler, 0) loggersToReload = make([]ReloadableHandler, 0) - filters = map[string]level.Option{} // Use console by default format := getLogFormat("console") - Root.AddLogger(format(os.Stderr), "info", filters) + logger := level.NewFilter(format(os.Stderr), level.AllowInfo()) + root = newManager(logger) +} + +// logManager manage loggers +type logManager struct { + *ConcreteLogger + loggersByName map[string]*ConcreteLogger + logFilters []LogWithFilters + mutex sync.RWMutex +} + +func newManager(logger gokitlog.Logger) *logManager { + return &logManager{ + ConcreteLogger: newConcreteLogger(logger), + loggersByName: map[string]*ConcreteLogger{}, + } +} + +func (lm *logManager) initialize(loggers []LogWithFilters) { + lm.mutex.Lock() + defer lm.mutex.Unlock() + + defaultLoggers := make([]gokitlog.Logger, len(loggers)) + for index, logger := range loggers { + defaultLoggers[index] = level.NewFilter(logger.val, logger.maxLevel) + } + + lm.ConcreteLogger.SetLogger(&compositeLogger{loggers: defaultLoggers}) + lm.logFilters = loggers + + loggersByName := []string{} + for k := range lm.loggersByName { + loggersByName = append(loggersByName, k) + } + sort.Strings(loggersByName) + + for _, name := range loggersByName { + ctxLoggers := make([]gokitlog.Logger, len(loggers)) + + for index, logger := range loggers { + if filterLevel, exists := logger.filters[name]; !exists { + ctxLoggers[index] = level.NewFilter(logger.val, logger.maxLevel) + } else { + ctxLoggers[index] = level.NewFilter(logger.val, filterLevel) + } + } + + lm.loggersByName[name].SetLogger(&compositeLogger{loggers: ctxLoggers}) + } +} + +func (lm *logManager) SetLogger(logger gokitlog.Logger) { + lm.ConcreteLogger.SetLogger(logger) +} + +func (lm *logManager) GetLogger() gokitlog.Logger { + return lm.ConcreteLogger.GetLogger() +} + +func (lm *logManager) Log(args ...interface{}) error { + lm.mutex.RLock() + defer lm.mutex.RUnlock() + if err := lm.ConcreteLogger.Log(args...); err != nil { + log.Println("Logging error", "error", err) + } + + return nil +} + +func (lm *logManager) New(ctx ...interface{}) *ConcreteLogger { + lm.mutex.Lock() + defer lm.mutex.Unlock() + if len(ctx) == 0 { + return lm.ConcreteLogger + } + + loggerName, ok := ctx[0].(string) + if !ok { + return lm.ConcreteLogger + } + + if logger, exists := lm.loggersByName[loggerName]; exists { + return logger + } + + ctx = append([]interface{}{"logger"}, ctx...) + + if len(lm.logFilters) == 0 { + ctxLogger := newConcreteLogger(lm.logger, ctx...) + lm.loggersByName[loggerName] = ctxLogger + return ctxLogger + } + + compositeLogger := newCompositeLogger() + for _, logWithFilter := range lm.logFilters { + filterLevel, ok := logWithFilter.filters[loggerName] + if ok { + logWithFilter.val = level.NewFilter(logWithFilter.val, filterLevel) + } else { + logWithFilter.val = level.NewFilter(logWithFilter.val, logWithFilter.maxLevel) + } + + compositeLogger.loggers = append(compositeLogger.loggers, logWithFilter.val) + } + + ctxLogger := newConcreteLogger(compositeLogger, ctx...) + lm.loggersByName[loggerName] = ctxLogger + return ctxLogger +} + +type ConcreteLogger struct { + ctx []interface{} + logger gokitlog.Logger + mutex sync.RWMutex +} + +func newConcreteLogger(logger gokitlog.Logger, ctx ...interface{}) *ConcreteLogger { + if len(ctx) == 0 { + ctx = []interface{}{} + } else { + logger = gokitlog.With(logger, ctx...) + } + + return &ConcreteLogger{ + ctx: ctx, + logger: logger, + } +} + +func (cl *ConcreteLogger) SetLogger(logger gokitlog.Logger) { + cl.mutex.Lock() + cl.logger = gokitlog.With(logger, cl.ctx...) + cl.mutex.Unlock() +} + +func (cl *ConcreteLogger) GetLogger() gokitlog.Logger { + cl.mutex.Lock() + defer cl.mutex.Unlock() + return cl.logger +} + +func (cl *ConcreteLogger) Warn(msg string, args ...interface{}) { + _ = cl.log(msg, level.WarnValue(), args...) +} + +func (cl *ConcreteLogger) Debug(msg string, args ...interface{}) { + // args = append([]interface{}{level.Key(), level.DebugValue(), "msg", msg}, args...) + _ = cl.log(msg, level.DebugValue(), args...) +} + +func (cl *ConcreteLogger) Error(msg string, args ...interface{}) { + _ = cl.log(msg, level.ErrorValue(), args...) +} + +func (cl *ConcreteLogger) Info(msg string, args ...interface{}) { + _ = cl.log(msg, level.InfoValue(), args...) +} + +func (cl *ConcreteLogger) log(msg string, logLevel level.Value, args ...interface{}) error { + cl.mutex.RLock() + logger := gokitlog.With(cl.logger, "t", gokitlog.TimestampFormat(time.Now, "2006-01-02T15:04:05.99-0700")) + cl.mutex.RUnlock() + + args = append([]interface{}{level.Key(), logLevel, "msg", msg}, args...) + + return logger.Log(args...) +} + +func (cl *ConcreteLogger) Log(keyvals ...interface{}) error { + cl.mutex.RLock() + defer cl.mutex.RUnlock() + return cl.logger.Log(keyvals...) +} + +func (cl *ConcreteLogger) New(ctx ...interface{}) *ConcreteLogger { + if len(ctx) == 0 { + root.New() + } + + keyvals := []interface{}{} + + if len(cl.ctx)%2 == 1 { + cl.ctx = append(cl.ctx, nil) + } + + for i := 0; i < len(cl.ctx); i += 2 { + k, v := cl.ctx[i], cl.ctx[i+1] + + if k == "logger" { + continue + } + + keyvals = append(keyvals, k, v) + } + + keyvals = append(keyvals, ctx...) + + return root.New(keyvals...) +} + +func New(ctx ...interface{}) *ConcreteLogger { + return root.New(ctx...) } type LogWithFilters struct { @@ -50,111 +253,23 @@ type LogWithFilters struct { maxLevel level.Option } -type MultiLoggers struct { - loggers []LogWithFilters -} - -func (ml *MultiLoggers) AddLogger(val gokitlog.Logger, levelName string, filters map[string]level.Option) { - logger := LogWithFilters{val: val, filters: filters, maxLevel: getLogLevelFromString(levelName)} - ml.loggers = append(ml.loggers, logger) -} - -func (ml *MultiLoggers) SetLogger(des MultiLoggers) { - ml.loggers = des.loggers -} - -func (ml *MultiLoggers) GetLogger() MultiLoggers { - return *ml -} - -func (ml MultiLoggers) Warn(msg string, args ...interface{}) { - args = append([]interface{}{level.Key(), level.WarnValue(), "msg", msg}, args...) - err := ml.Log(args...) - if err != nil { - _ = level.Error(Root).Log("Logging error", "error", err) - } -} - -func (ml MultiLoggers) Debug(msg string, args ...interface{}) { - args = append([]interface{}{level.Key(), level.DebugValue(), "msg", msg}, args...) - err := ml.Log(args...) - if err != nil { - _ = level.Error(Root).Log("Logging error", "error", err) - } -} - -func (ml MultiLoggers) Error(msg string, args ...interface{}) { - args = append([]interface{}{level.Key(), level.ErrorValue(), "msg", msg}, args...) - err := ml.Log(args...) - if err != nil { - _ = level.Error(Root).Log("Logging error", "error", err) - } -} - -func (ml MultiLoggers) Info(msg string, args ...interface{}) { - args = append([]interface{}{level.Key(), level.InfoValue(), "msg", msg}, args...) - err := ml.Log(args...) - if err != nil { - _ = level.Error(Root).Log("Logging error", "error", err) - } -} - -func (ml MultiLoggers) Log(keyvals ...interface{}) error { - for _, multilogger := range ml.loggers { - multilogger.val = gokitlog.With(multilogger.val, "t", gokitlog.TimestampFormat(time.Now, "2006-01-02T15:04:05.99-0700")) - if err := multilogger.val.Log(keyvals...); err != nil { - return err - } - } - return nil -} - -// New creates a new logger from the existing one with additional context -func (ml MultiLoggers) New(ctx ...interface{}) MultiLoggers { - return with(ml, gokitlog.With, ctx) -} - -// New creates MultiLoggers with the provided context and caller that is added as a suffix. -// The first element of the context must be the logger name -func New(ctx ...interface{}) MultiLoggers { +func with(ctxLogger *ConcreteLogger, withFunc func(gokitlog.Logger, ...interface{}) gokitlog.Logger, ctx []interface{}) *ConcreteLogger { if len(ctx) == 0 { - return Root + return ctxLogger } - var newloger MultiLoggers - ctx = append([]interface{}{"logger"}, ctx...) - for _, logWithFilter := range Root.loggers { - logWithFilter.val = gokitlog.With(logWithFilter.val, ctx...) - v, ok := logWithFilter.filters[ctx[0].(string)] - if ok { - logWithFilter.val = level.NewFilter(logWithFilter.val, v) - } else { - logWithFilter.val = level.NewFilter(logWithFilter.val, logWithFilter.maxLevel) - } - newloger.loggers = append(newloger.loggers, logWithFilter) - } - return newloger -} -func with(loggers MultiLoggers, withFunc func(gokitlog.Logger, ...interface{}) gokitlog.Logger, ctx []interface{}) MultiLoggers { - if len(ctx) == 0 { - return loggers - } - var newloger MultiLoggers - for _, l := range loggers.loggers { - l.val = withFunc(l.val, ctx...) - newloger.loggers = append(newloger.loggers, l) - } - return newloger + ctxLogger.logger = withFunc(ctxLogger.logger, ctx...) + return ctxLogger } // WithPrefix adds context that will be added to the log message -func WithPrefix(loggers MultiLoggers, ctx ...interface{}) MultiLoggers { - return with(loggers, gokitlog.WithPrefix, ctx) +func WithPrefix(ctxLogger *ConcreteLogger, ctx ...interface{}) *ConcreteLogger { + return with(ctxLogger, gokitlog.WithPrefix, ctx) } // WithSuffix adds context that will be appended at the end of the log message -func WithSuffix(loggers MultiLoggers, ctx ...interface{}) MultiLoggers { - return with(loggers, gokitlog.WithSuffix, ctx) +func WithSuffix(ctxLogger *ConcreteLogger, ctx ...interface{}) *ConcreteLogger { + return with(ctxLogger, gokitlog.WithSuffix, ctx) } var logLevels = map[string]level.Option{ @@ -177,7 +292,7 @@ func getLogLevelFromString(levelName string) level.Option { loglevel, ok := logLevels[levelName] if !ok { - _ = level.Error(Root).Log("Unknown log level", "level", levelName) + _ = level.Error(root).Log("Unknown log level", "level", levelName) return level.AllowError() } @@ -282,7 +397,7 @@ func ReadLoggingConfig(modes []string, logsPath string, cfg *ini.File) error { mode = strings.TrimSpace(mode) sec, err := cfg.GetSection("log." + mode) if err != nil { - _ = level.Error(Root).Log("Unknown log mode", "mode", mode) + _ = level.Error(root).Log("Unknown log mode", "mode", mode) return errutil.Wrapf(err, "failed to get config section log.%s", mode) } @@ -301,7 +416,7 @@ func ReadLoggingConfig(modes []string, logsPath string, cfg *ini.File) error { fileName := sec.Key("file_name").MustString(filepath.Join(logsPath, "grafana.log")) dpath := filepath.Dir(fileName) if err := os.MkdirAll(dpath, os.ModePerm); err != nil { - _ = level.Error(Root).Log("Failed to create directory", "dpath", dpath, "err", err) + _ = level.Error(root).Log("Failed to create directory", "dpath", dpath, "err", err) return errutil.Wrapf(err, "failed to create log directory %q", dpath) } fileHandler := NewFileWriter() @@ -313,7 +428,7 @@ func ReadLoggingConfig(modes []string, logsPath string, cfg *ini.File) error { fileHandler.Daily = sec.Key("daily_rotate").MustBool(true) fileHandler.Maxdays = sec.Key("max_days").MustInt64(7) if err := fileHandler.Init(); err != nil { - _ = level.Error(Root).Log("Failed to initialize file handler", "dpath", dpath, "err", err) + _ = level.Error(root).Log("Failed to initialize file handler", "dpath", dpath, "err", err) return errutil.Wrapf(err, "failed to initialize file handler") } @@ -336,20 +451,14 @@ func ReadLoggingConfig(modes []string, logsPath string, cfg *ini.File) error { } } - // copy joined default + mode filters into filters - for key, value := range modeFilters { - if _, exist := filters[key]; !exist { - filters[key] = value - } - } - handler.filters = modeFilters handler.maxLevel = leveloption - // handler = LogFilterHandler(leveloption, modeFilters, handler) configLoggers = append(configLoggers, handler) } + if len(configLoggers) > 0 { - Root.loggers = configLoggers + root.initialize(configLoggers) } + return nil } diff --git a/pkg/infra/log/syslog.go b/pkg/infra/log/syslog.go index b1d818f5d58..150a8c6fae3 100644 --- a/pkg/infra/log/syslog.go +++ b/pkg/infra/log/syslog.go @@ -62,7 +62,7 @@ func NewSyslog(sec *ini.Section, format Formatedlogger) *SysLogHandler { handler.Tag = sec.Key("tag").MustString("") if err := handler.Init(); err != nil { - _ = level.Error(Root).Log("Failed to init syslog log handler", "error", err) + _ = level.Error(root).Log("Failed to init syslog log handler", "error", err) os.Exit(1) } handler.logger = gokitsyslog.NewSyslogLogger(handler.syslog, format, gokitsyslog.PrioritySelectorOption(selector)) diff --git a/pkg/infra/usagestats/service/service.go b/pkg/infra/usagestats/service/service.go index 13ac15b2be1..bea01a403b4 100644 --- a/pkg/infra/usagestats/service/service.go +++ b/pkg/infra/usagestats/service/service.go @@ -25,7 +25,7 @@ type UsageStats struct { kvStore *kvstore.NamespacedKVStore RouteRegister routing.RouteRegister - log log.MultiLoggers + log log.Logger oauthProviders map[string]bool externalMetrics []usagestats.MetricsFunc diff --git a/pkg/login/social/generic_oauth_test.go b/pkg/login/social/generic_oauth_test.go index 9be66f1e3ac..ab9990b4f16 100644 --- a/pkg/login/social/generic_oauth_test.go +++ b/pkg/login/social/generic_oauth_test.go @@ -17,7 +17,7 @@ import ( func newLogger(name string, lev string) log.Logger { logger := log.New(name) - logger.AddLogger(logger, lev, map[string]level.Option{}) + logger.SetLogger(level.NewFilter(logger.GetLogger(), level.AllowInfo())) return logger } diff --git a/pkg/middleware/recovery.go b/pkg/middleware/recovery.go index ea8483a6911..fe5d3a22597 100644 --- a/pkg/middleware/recovery.go +++ b/pkg/middleware/recovery.go @@ -107,7 +107,7 @@ func Recovery(cfg *setting.Cfg) web.Handler { defer func() { if r := recover(); r != nil { var panicLogger log.Logger - panicLogger = log.Root + panicLogger = log.New("recovery") // try to get request logger ctx := contexthandler.FromContext(c.Req.Context()) if ctx != nil { diff --git a/pkg/plugins/manager/loader/initializer/initializer_test.go b/pkg/plugins/manager/loader/initializer/initializer_test.go index 8b6412e8372..d63a34384c3 100644 --- a/pkg/plugins/manager/loader/initializer/initializer_test.go +++ b/pkg/plugins/manager/loader/initializer/initializer_test.go @@ -212,11 +212,11 @@ func (*testLicensingService) FeatureEnabled(feature string) bool { } type fakeLogger struct { - log.MultiLoggers + *log.ConcreteLogger } -func (f fakeLogger) New(_ ...interface{}) log.MultiLoggers { - return log.MultiLoggers{} +func (f fakeLogger) New(_ ...interface{}) *log.ConcreteLogger { + return &log.ConcreteLogger{} } func (f fakeLogger) Warn(_ string, _ ...interface{}) { diff --git a/pkg/plugins/manager/loader/loader_test.go b/pkg/plugins/manager/loader/loader_test.go index 59cb67d8e86..b21a9b6f02f 100644 --- a/pkg/plugins/manager/loader/loader_test.go +++ b/pkg/plugins/manager/loader/loader_test.go @@ -1128,8 +1128,8 @@ type fakeLogger struct { log.Logger } -func (fl fakeLogger) New(_ ...interface{}) log.MultiLoggers { - return log.MultiLoggers{} +func (fl fakeLogger) New(_ ...interface{}) *log.ConcreteLogger { + return &log.ConcreteLogger{} } func (fl fakeLogger) Info(_ string, _ ...interface{}) { diff --git a/pkg/services/login/loginservice/loginservice_test.go b/pkg/services/login/loginservice/loginservice_test.go index 5cbd5acb510..14f1c146b0f 100644 --- a/pkg/services/login/loginservice/loginservice_test.go +++ b/pkg/services/login/loginservice/loginservice_test.go @@ -46,7 +46,7 @@ func Test_syncOrgRoles_doesNotBreakWhenTryingToRemoveLastOrgAdmin(t *testing.T) func Test_syncOrgRoles_whenTryingToRemoveLastOrgLogsError(t *testing.T) { buf := &bytes.Buffer{} - logger.AddLogger(log.NewLogfmtLogger(buf), "info", map[string]level.Option{}) + logger.SetLogger(level.NewFilter(log.NewLogfmtLogger(buf), level.AllowInfo())) user := createSimpleUser() externalUser := createSimpleExternalUser() diff --git a/pkg/setting/setting.go b/pkg/setting/setting.go index 713bfc31d3f..aa7375f0e33 100644 --- a/pkg/setting/setting.go +++ b/pkg/setting/setting.go @@ -772,6 +772,8 @@ func (cfg *Cfg) loadConfiguration(args CommandLineArgs) (*ini.File, error) { return nil, err } + cfg.Logger.Info(fmt.Sprintf("Starting %s", ApplicationName), "version", BuildVersion, "commit", BuildCommit, "branch", BuildBranch, "compiled", time.Unix(BuildStamp, 0)) + return parsedFile, err } From 358db0d1305364acb97c6462f2bd0cf98a0455f0 Mon Sep 17 00:00:00 2001 From: "Grot (@grafanabot)" <43478413+grafanabot@users.noreply.github.com> Date: Thu, 3 Feb 2022 16:48:13 +0100 Subject: [PATCH 015/113] Access control: FGAC for team sync endpoints (#44673) (#44856) * add actions for team group sync * extend the hook to allow specifying whether the user is external * move user struct to type package * interface for permission service to allow mocking it * reuse existing permissions * test fix * refactor * linting (cherry picked from commit 602d62ebcc322bbb5d2fa3e65814781affcc3904) Co-authored-by: Ieva --- pkg/api/team_members.go | 5 +-- pkg/services/accesscontrol/accesscontrol.go | 7 +++- .../accesscontrol/database/database_test.go | 3 +- .../database/resource_permissions.go | 8 ++--- .../resource_permissions_bench_test.go | 2 +- .../database/resource_permissions_test.go | 6 ++-- .../accesscontrol/resourcepermissions/api.go | 2 +- .../resourcepermissions/api_test.go | 2 +- .../resourcepermissions/options.go | 3 +- .../resourcepermissions/service.go | 9 ++--- .../resourcepermissions/service_mock.go | 33 +++++++++++++++++++ .../resourcepermissions/service_test.go | 4 +-- .../resourcepermissions/types/hook.go | 12 +++++-- .../resourceservices/resource_services.go | 8 ++--- 14 files changed, 76 insertions(+), 28 deletions(-) create mode 100644 pkg/services/accesscontrol/resourcepermissions/service_mock.go diff --git a/pkg/api/team_members.go b/pkg/api/team_members.go index cc0cd45daf4..3c094622086 100644 --- a/pkg/api/team_members.go +++ b/pkg/api/team_members.go @@ -10,6 +10,7 @@ import ( "github.com/grafana/grafana/pkg/api/dtos" "github.com/grafana/grafana/pkg/api/response" "github.com/grafana/grafana/pkg/models" + "github.com/grafana/grafana/pkg/services/accesscontrol" "github.com/grafana/grafana/pkg/services/accesscontrol/resourcepermissions" "github.com/grafana/grafana/pkg/services/featuremgmt" "github.com/grafana/grafana/pkg/util" @@ -152,7 +153,7 @@ func (hs *HTTPServer) RemoveTeamMember(c *models.ReqContext) response.Response { } teamIDString := strconv.FormatInt(teamId, 10) - if _, err := hs.TeamPermissionsService.SetUserPermission(c.Req.Context(), orgId, userId, teamIDString, ""); err != nil { + if _, err := hs.TeamPermissionsService.SetUserPermission(c.Req.Context(), orgId, accesscontrol.User{ID: userId}, teamIDString, ""); err != nil { if errors.Is(err, models.ErrTeamNotFound) { return response.Error(404, "Team not found", nil) } @@ -171,7 +172,7 @@ func (hs *HTTPServer) RemoveTeamMember(c *models.ReqContext) response.Response { // Stubbable by tests. var addOrUpdateTeamMember = func(ctx context.Context, resourcePermissionService *resourcepermissions.Service, userID, orgID, teamID int64, permission string) error { teamIDString := strconv.FormatInt(teamID, 10) - if _, err := resourcePermissionService.SetUserPermission(ctx, orgID, userID, teamIDString, permission); err != nil { + if _, err := resourcePermissionService.SetUserPermission(ctx, orgID, accesscontrol.User{ID: userID}, teamIDString, permission); err != nil { return fmt.Errorf("failed setting permissions for user %d in team %d: %w", userID, teamID, err) } return nil diff --git a/pkg/services/accesscontrol/accesscontrol.go b/pkg/services/accesscontrol/accesscontrol.go index 83fbeeaf4d3..3bf514d6371 100644 --- a/pkg/services/accesscontrol/accesscontrol.go +++ b/pkg/services/accesscontrol/accesscontrol.go @@ -37,13 +37,18 @@ type ResourcePermissionsService interface { // GetPermissions returns all permissions for given resourceID GetPermissions(ctx context.Context, orgID int64, resourceID string) ([]ResourcePermission, error) // SetUserPermission sets permission on resource for a user - SetUserPermission(ctx context.Context, orgID, userID int64, resourceID, permission string) (*ResourcePermission, error) + SetUserPermission(ctx context.Context, orgID int64, user User, resourceID, permission string) (*ResourcePermission, error) // SetTeamPermission sets permission on resource for a team SetTeamPermission(ctx context.Context, orgID, teamID int64, resourceID, permission string) (*ResourcePermission, error) // SetBuiltInRolePermission sets permission on resource for a built-in role (Admin, Editor, Viewer) SetBuiltInRolePermission(ctx context.Context, orgID int64, builtInRole string, resourceID string, permission string) (*ResourcePermission, error) } +type User struct { + ID int64 + IsExternal bool +} + // Metadata contains user accesses for a given resource // Ex: map[string]bool{"create":true, "delete": true} type Metadata map[string]bool diff --git a/pkg/services/accesscontrol/database/database_test.go b/pkg/services/accesscontrol/database/database_test.go index d4fa790178f..441b0deea29 100644 --- a/pkg/services/accesscontrol/database/database_test.go +++ b/pkg/services/accesscontrol/database/database_test.go @@ -5,7 +5,6 @@ import ( "testing" "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" "github.com/grafana/grafana/pkg/models" @@ -81,7 +80,7 @@ func TestAccessControlStore_GetUserPermissions(t *testing.T) { user, team := createUserAndTeam(t, sql, tt.orgID) for _, id := range tt.userPermissions { - _, err := store.SetUserResourcePermission(context.Background(), tt.orgID, user.Id, accesscontrol.SetResourcePermissionCommand{ + _, err := store.SetUserResourcePermission(context.Background(), tt.orgID, accesscontrol.User{ID: user.Id}, accesscontrol.SetResourcePermissionCommand{ Actions: []string{"dashboards:write"}, Resource: "dashboards", ResourceID: id, diff --git a/pkg/services/accesscontrol/database/resource_permissions.go b/pkg/services/accesscontrol/database/resource_permissions.go index 436d6179df5..651fad1ee12 100644 --- a/pkg/services/accesscontrol/database/resource_permissions.go +++ b/pkg/services/accesscontrol/database/resource_permissions.go @@ -34,20 +34,20 @@ func (p *flatResourcePermission) Managed() bool { } func (s *AccessControlStore) SetUserResourcePermission( - ctx context.Context, orgID, userID int64, + ctx context.Context, orgID int64, user accesscontrol.User, cmd accesscontrol.SetResourcePermissionCommand, hook types.UserResourceHookFunc, ) (*accesscontrol.ResourcePermission, error) { - if userID == 0 { + if user.ID == 0 { return nil, models.ErrUserNotFound } var err error var permission *accesscontrol.ResourcePermission err = s.sql.WithTransactionalDbSession(ctx, func(sess *sqlstore.DBSession) error { - permission, err = s.setResourcePermission(sess, orgID, managedUserRoleName(userID), s.userAdder(sess, orgID, userID), cmd) + permission, err = s.setResourcePermission(sess, orgID, managedUserRoleName(user.ID), s.userAdder(sess, orgID, user.ID), cmd) if err == nil && hook != nil { - return hook(sess, orgID, userID, cmd.ResourceID, cmd.Permission) + return hook(sess, orgID, user, cmd.ResourceID, cmd.Permission) } return err diff --git a/pkg/services/accesscontrol/database/resource_permissions_bench_test.go b/pkg/services/accesscontrol/database/resource_permissions_bench_test.go index 77a6a9fa398..27047433870 100644 --- a/pkg/services/accesscontrol/database/resource_permissions_bench_test.go +++ b/pkg/services/accesscontrol/database/resource_permissions_bench_test.go @@ -93,7 +93,7 @@ func GenerateDatasourcePermissions(b *testing.B, db *sqlstore.SQLStore, ac *Acce _, err := ac.SetUserResourcePermission( context.Background(), accesscontrol.GlobalOrgID, - userIds[i], + accesscontrol.User{ID: userIds[i]}, accesscontrol.SetResourcePermissionCommand{ Actions: []string{dsAction}, Resource: dsResource, diff --git a/pkg/services/accesscontrol/database/resource_permissions_test.go b/pkg/services/accesscontrol/database/resource_permissions_test.go index e029da2b1f5..00d6a5bfae9 100644 --- a/pkg/services/accesscontrol/database/resource_permissions_test.go +++ b/pkg/services/accesscontrol/database/resource_permissions_test.go @@ -70,11 +70,11 @@ func TestAccessControlStore_SetUserResourcePermission(t *testing.T) { store, _ := setupTestEnv(t) for _, s := range test.seeds { - _, err := store.SetUserResourcePermission(context.Background(), test.orgID, test.userID, s, nil) + _, err := store.SetUserResourcePermission(context.Background(), test.orgID, accesscontrol.User{ID: test.userID}, s, nil) require.NoError(t, err) } - added, err := store.SetUserResourcePermission(context.Background(), test.userID, test.userID, accesscontrol.SetResourcePermissionCommand{ + added, err := store.SetUserResourcePermission(context.Background(), test.userID, accesscontrol.User{ID: test.userID}, accesscontrol.SetResourcePermissionCommand{ Actions: test.actions, Resource: test.resource, ResourceID: test.resourceID, @@ -352,7 +352,7 @@ func seedResourcePermissions(t *testing.T, store *AccessControlStore, sql *sqlst }) require.NoError(t, err) - _, err = store.SetUserResourcePermission(context.Background(), 1, u.Id, accesscontrol.SetResourcePermissionCommand{ + _, err = store.SetUserResourcePermission(context.Background(), 1, accesscontrol.User{ID: u.Id}, accesscontrol.SetResourcePermissionCommand{ Actions: actions, Resource: resource, ResourceID: resourceID, diff --git a/pkg/services/accesscontrol/resourcepermissions/api.go b/pkg/services/accesscontrol/resourcepermissions/api.go index 84a93834854..987a57948fd 100644 --- a/pkg/services/accesscontrol/resourcepermissions/api.go +++ b/pkg/services/accesscontrol/resourcepermissions/api.go @@ -131,7 +131,7 @@ func (a *api) setUserPermission(c *models.ReqContext) response.Response { return response.Error(http.StatusBadRequest, "bad request data", err) } - _, err = a.service.SetUserPermission(c.Req.Context(), c.OrgId, userID, resourceID, cmd.Permission) + _, err = a.service.SetUserPermission(c.Req.Context(), c.OrgId, accesscontrol.User{ID: userID}, resourceID, cmd.Permission) if err != nil { return response.Error(http.StatusBadRequest, "failed to set user permission", err) } diff --git a/pkg/services/accesscontrol/resourcepermissions/api_test.go b/pkg/services/accesscontrol/resourcepermissions/api_test.go index 6e596875cd4..bf64c93d0d4 100644 --- a/pkg/services/accesscontrol/resourcepermissions/api_test.go +++ b/pkg/services/accesscontrol/resourcepermissions/api_test.go @@ -160,7 +160,7 @@ func TestApi_getPermissions(t *testing.T) { // seed user 1 with "View" permission on dashboard 1 u, err := sql.CreateUser(context.Background(), models.CreateUserCommand{Login: "test", OrgId: 1}) require.NoError(t, err) - _, err = service.SetUserPermission(context.Background(), u.OrgId, u.Id, tt.resourceID, "View") + _, err = service.SetUserPermission(context.Background(), u.OrgId, accesscontrol.User{ID: u.Id}, tt.resourceID, "View") require.NoError(t, err) // seed built in role Admin with "Edit" permission on dashboard 1 diff --git a/pkg/services/accesscontrol/resourcepermissions/options.go b/pkg/services/accesscontrol/resourcepermissions/options.go index 907a202ad5c..12fcb5acdb9 100644 --- a/pkg/services/accesscontrol/resourcepermissions/options.go +++ b/pkg/services/accesscontrol/resourcepermissions/options.go @@ -3,6 +3,7 @@ package resourcepermissions import ( "context" + "github.com/grafana/grafana/pkg/services/accesscontrol" "github.com/grafana/grafana/pkg/services/sqlstore" ) @@ -28,7 +29,7 @@ type Options struct { // RoleGroup is the group name for the generated fixed roles RoleGroup string // OnSetUser if configured will be called each time a permission is set for a user - OnSetUser func(session *sqlstore.DBSession, orgID, userID int64, resourceID, permission string) error + OnSetUser func(session *sqlstore.DBSession, orgID int64, user accesscontrol.User, resourceID, permission string) error // OnSetTeam if configured will be called each time a permission is set for a team OnSetTeam func(session *sqlstore.DBSession, orgID, teamID int64, resourceID, permission string) error // OnSetBuiltInRole if configured will be called each time a permission is set for a built-in role diff --git a/pkg/services/accesscontrol/resourcepermissions/service.go b/pkg/services/accesscontrol/resourcepermissions/service.go index 6ab670a313e..ca5a587baa9 100644 --- a/pkg/services/accesscontrol/resourcepermissions/service.go +++ b/pkg/services/accesscontrol/resourcepermissions/service.go @@ -16,7 +16,8 @@ import ( type Store interface { // SetUserResourcePermission sets permission for managed user role on a resource SetUserResourcePermission( - ctx context.Context, orgID, userID int64, + ctx context.Context, orgID int64, + user accesscontrol.User, cmd accesscontrol.SetResourcePermissionCommand, hook types.UserResourceHookFunc, ) (*accesscontrol.ResourcePermission, error) @@ -100,7 +101,7 @@ func (s *Service) GetPermissions(ctx context.Context, orgID int64, resourceID st }) } -func (s *Service) SetUserPermission(ctx context.Context, orgID, userID int64, resourceID, permission string) (*accesscontrol.ResourcePermission, error) { +func (s *Service) SetUserPermission(ctx context.Context, orgID int64, user accesscontrol.User, resourceID, permission string) (*accesscontrol.ResourcePermission, error) { if !s.options.Assignments.Users { return nil, ErrInvalidAssignment } @@ -114,11 +115,11 @@ func (s *Service) SetUserPermission(ctx context.Context, orgID, userID int64, re return nil, err } - if err := s.validateUser(ctx, orgID, userID); err != nil { + if err := s.validateUser(ctx, orgID, user.ID); err != nil { return nil, err } - return s.store.SetUserResourcePermission(ctx, orgID, userID, accesscontrol.SetResourcePermissionCommand{ + return s.store.SetUserResourcePermission(ctx, orgID, user, accesscontrol.SetResourcePermissionCommand{ Actions: actions, Permission: permission, ResourceID: resourceID, diff --git a/pkg/services/accesscontrol/resourcepermissions/service_mock.go b/pkg/services/accesscontrol/resourcepermissions/service_mock.go new file mode 100644 index 00000000000..a1ad509e7cd --- /dev/null +++ b/pkg/services/accesscontrol/resourcepermissions/service_mock.go @@ -0,0 +1,33 @@ +package resourcepermissions + +import ( + "context" + + "github.com/stretchr/testify/mock" + + "github.com/grafana/grafana/pkg/services/accesscontrol" +) + +type MockService struct { + mock.Mock +} + +func (m *MockService) GetPermissions(ctx context.Context, orgID int64, resourceID string) ([]accesscontrol.ResourcePermission, error) { + mockedArgs := m.Called(ctx, orgID, resourceID) + return mockedArgs.Get(0).([]accesscontrol.ResourcePermission), mockedArgs.Error(1) +} + +func (m *MockService) SetUserPermission(ctx context.Context, orgID int64, user accesscontrol.User, resourceID, permission string) (*accesscontrol.ResourcePermission, error) { + mockedArgs := m.Called(ctx, orgID, user, resourceID, permission) + return mockedArgs.Get(0).(*accesscontrol.ResourcePermission), mockedArgs.Error(1) +} + +func (m *MockService) SetTeamPermission(ctx context.Context, orgID, teamID int64, resourceID, permission string) (*accesscontrol.ResourcePermission, error) { + mockedArgs := m.Called(ctx, orgID, teamID, resourceID, permission) + return mockedArgs.Get(0).(*accesscontrol.ResourcePermission), mockedArgs.Error(1) +} + +func (m *MockService) SetBuiltInRolePermission(ctx context.Context, orgID int64, builtInRole, resourceID, permission string) (*accesscontrol.ResourcePermission, error) { + mockedArgs := m.Called(ctx, orgID, builtInRole, resourceID, permission) + return mockedArgs.Get(0).(*accesscontrol.ResourcePermission), mockedArgs.Error(1) +} diff --git a/pkg/services/accesscontrol/resourcepermissions/service_test.go b/pkg/services/accesscontrol/resourcepermissions/service_test.go index dbdcd3e4b5e..cec11542dd4 100644 --- a/pkg/services/accesscontrol/resourcepermissions/service_test.go +++ b/pkg/services/accesscontrol/resourcepermissions/service_test.go @@ -46,13 +46,13 @@ func TestService_SetUserPermission(t *testing.T) { var hookCalled bool if tt.callHook { - service.options.OnSetUser = func(session *sqlstore.DBSession, orgID, userID int64, resourceID, permission string) error { + service.options.OnSetUser = func(session *sqlstore.DBSession, orgID int64, user accesscontrol.User, resourceID, permission string) error { hookCalled = true return nil } } - _, err = service.SetUserPermission(context.Background(), user.OrgId, user.Id, "1", "") + _, err = service.SetUserPermission(context.Background(), user.OrgId, accesscontrol.User{ID: user.Id}, "1", "") require.NoError(t, err) assert.Equal(t, tt.callHook, hookCalled) }) diff --git a/pkg/services/accesscontrol/resourcepermissions/types/hook.go b/pkg/services/accesscontrol/resourcepermissions/types/hook.go index 5e86cc3893c..b389e8d101c 100644 --- a/pkg/services/accesscontrol/resourcepermissions/types/hook.go +++ b/pkg/services/accesscontrol/resourcepermissions/types/hook.go @@ -1,7 +1,15 @@ package types -import "github.com/grafana/grafana/pkg/services/sqlstore" +import ( + "github.com/grafana/grafana/pkg/services/accesscontrol" + "github.com/grafana/grafana/pkg/services/sqlstore" +) -type UserResourceHookFunc func(session *sqlstore.DBSession, orgID, userID int64, resourceID, permission string) error +type UserResourceHookFunc func(session *sqlstore.DBSession, orgID int64, user accesscontrol.User, resourceID, permission string) error type TeamResourceHookFunc func(session *sqlstore.DBSession, orgID, teamID int64, resourceID, permission string) error type BuiltinResourceHookFunc func(session *sqlstore.DBSession, orgID int64, builtInRole, resourceID, permission string) error + +type User struct { + ID int64 + IsExternal bool +} diff --git a/pkg/services/accesscontrol/resourceservices/resource_services.go b/pkg/services/accesscontrol/resourceservices/resource_services.go index 077e6cd93a5..3cf2adfb95f 100644 --- a/pkg/services/accesscontrol/resourceservices/resource_services.go +++ b/pkg/services/accesscontrol/resourceservices/resource_services.go @@ -77,20 +77,20 @@ func ProvideTeamPermissions(router routing.RouteRegister, sql *sqlstore.SQLStore ReaderRoleName: "Team permission reader", WriterRoleName: "Team permission writer", RoleGroup: "Teams", - OnSetUser: func(session *sqlstore.DBSession, orgID, userID int64, resourceID, permission string) error { + OnSetUser: func(session *sqlstore.DBSession, orgID int64, user accesscontrol.User, resourceID, permission string) error { teamId, err := strconv.ParseInt(resourceID, 10, 64) if err != nil { return err } switch permission { case "Member": - return sqlstore.AddOrUpdateTeamMemberHook(session, userID, orgID, teamId, false, 0) + return sqlstore.AddOrUpdateTeamMemberHook(session, user.ID, orgID, teamId, user.IsExternal, 0) case "Admin": - return sqlstore.AddOrUpdateTeamMemberHook(session, userID, orgID, teamId, false, models.PERMISSION_ADMIN) + return sqlstore.AddOrUpdateTeamMemberHook(session, user.ID, orgID, teamId, user.IsExternal, models.PERMISSION_ADMIN) case "": return sqlstore.RemoveTeamMemberHook(session, &models.RemoveTeamMemberCommand{ OrgId: orgID, - UserId: userID, + UserId: user.ID, TeamId: teamId, }) default: From 36edcfe208587d9468a60456e51ae515c838fc1d Mon Sep 17 00:00:00 2001 From: "Grot (@grafanabot)" <43478413+grafanabot@users.noreply.github.com> Date: Thu, 3 Feb 2022 16:49:46 +0100 Subject: [PATCH 016/113] Prometheus: Set interval on time field (#44802) (#44837) (cherry picked from commit c8bb4c733ec5fd0bf9625d96a083a00ce58ed6f2) Co-authored-by: Todd Treece <360020+toddtreece@users.noreply.github.com> --- pkg/tsdb/prometheus/framing_test.go | 2 +- pkg/tsdb/prometheus/prometeus_bench_test.go | 2 +- .../testdata/range_infinity.result.golden.txt | 2 +- .../testdata/range_missing.result.golden.txt | 10 +-- .../testdata/range_nan.result.golden.txt | 2 +- .../testdata/range_simple.result.golden.txt | 4 +- pkg/tsdb/prometheus/time_series_query.go | 70 ++----------------- pkg/tsdb/prometheus/time_series_query_test.go | 26 +++---- 8 files changed, 27 insertions(+), 91 deletions(-) diff --git a/pkg/tsdb/prometheus/framing_test.go b/pkg/tsdb/prometheus/framing_test.go index 6db7e7e9c67..c2ddadd0dac 100644 --- a/pkg/tsdb/prometheus/framing_test.go +++ b/pkg/tsdb/prometheus/framing_test.go @@ -132,5 +132,5 @@ func runQuery(response []byte, query PrometheusQuery) (*backend.QueryDataRespons } s := Service{tracer: tracer} - return s.runQueries(context.Background(), api, []*PrometheusQuery{&query}, true) + return s.runQueries(context.Background(), api, []*PrometheusQuery{&query}) } diff --git a/pkg/tsdb/prometheus/prometeus_bench_test.go b/pkg/tsdb/prometheus/prometeus_bench_test.go index 5bf01f8ae57..b7365e30c44 100644 --- a/pkg/tsdb/prometheus/prometeus_bench_test.go +++ b/pkg/tsdb/prometheus/prometeus_bench_test.go @@ -28,7 +28,7 @@ func BenchmarkJson(b *testing.B) { b.ResetTimer() for n := 0; n < b.N; n++ { - _, _ = s.runQueries(context.Background(), api, []*PrometheusQuery{&query}, true) + _, _ = s.runQueries(context.Background(), api, []*PrometheusQuery{&query}) } } diff --git a/pkg/tsdb/prometheus/testdata/range_infinity.result.golden.txt b/pkg/tsdb/prometheus/testdata/range_infinity.result.golden.txt index 2aad902f0db..68c697dd90e 100644 --- a/pkg/tsdb/prometheus/testdata/range_infinity.result.golden.txt +++ b/pkg/tsdb/prometheus/testdata/range_infinity.result.golden.txt @@ -19,4 +19,4 @@ Dimensions: 2 Fields by 3 Rows ====== TEST DATA RESPONSE (arrow base64) ====== -FRAME=QVJST1cxAAD/////KAIAABAAAAAAAAoADgAMAAsABAAKAAAAFAAAAAAAAAEDAAoADAAAAAgABAAKAAAACAAAAJgAAAADAAAAUAAAACgAAAAEAAAAbP7//wgAAAAMAAAAAAAAAAAAAAAFAAAAcmVmSWQAAACM/v//CAAAABAAAAAFAAAAMSAvIDAAAAAEAAAAbmFtZQAAAACw/v//CAAAACwAAAAiAAAAeyJjdXN0b20iOnsicmVzdWx0VHlwZSI6Im1hdHJpeCJ9fQAABAAAAG1ldGEAAAAAAgAAAOwAAAAYAAAAAAASABgAFAATABIADAAAAAgABAASAAAAFAAAAKAAAACgAAAAAAADAaAAAAADAAAAUAAAACwAAAAEAAAAOP///wgAAAAQAAAABQAAAFZhbHVlAAAABAAAAG5hbWUAAAAAXP///wgAAAAMAAAAAgAAAHt9AAAGAAAAbGFiZWxzAAB8////CAAAACgAAAAdAAAAeyJkaXNwbGF5TmFtZUZyb21EUyI6IjEgLyAwIn0AAAAGAAAAY29uZmlnAAAAAAAAiv///wAAAgAFAAAAVmFsdWUAEgAYABQAAAATAAwAAAAIAAQAEgAAABQAAABEAAAATAAAAAAAAApMAAAAAQAAAAwAAAAIAAwACAAEAAgAAAAIAAAAEAAAAAQAAABUaW1lAAAAAAQAAABuYW1lAAAAAAAAAAAAAAYACAAGAAYAAAAAAAMABAAAAFRpbWUAAAAAAAAAAP////+4AAAAFAAAAAAAAAAMABYAFAATAAwABAAMAAAAMAAAAAAAAAAUAAAAAAAAAwMACgAYAAwACAAEAAoAAAAUAAAAWAAAAAMAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAYAAAAAAAAABgAAAAAAAAAAAAAAAAAAAAYAAAAAAAAABgAAAAAAAAAAAAAAAIAAAADAAAAAAAAAAAAAAAAAAAAAwAAAAAAAAAAAAAAAAAAAABEFRTUKckWAA6wT9QpyRYA2EqL1CnJFgAAAAAAAPB/AAAAAAAA8H8AAAAAAADwfxAAAAAMABQAEgAMAAgABAAMAAAAEAAAACwAAAA4AAAAAAADAAEAAAA4AgAAAAAAAMAAAAAAAAAAMAAAAAAAAAAAAAAAAAAAAAAACgAMAAAACAAEAAoAAAAIAAAAmAAAAAMAAABQAAAAKAAAAAQAAABs/v//CAAAAAwAAAAAAAAAAAAAAAUAAAByZWZJZAAAAIz+//8IAAAAEAAAAAUAAAAxIC8gMAAAAAQAAABuYW1lAAAAALD+//8IAAAALAAAACIAAAB7ImN1c3RvbSI6eyJyZXN1bHRUeXBlIjoibWF0cml4In19AAAEAAAAbWV0YQAAAAACAAAA7AAAABgAAAAAABIAGAAUABMAEgAMAAAACAAEABIAAAAUAAAAoAAAAKAAAAAAAAMBoAAAAAMAAABQAAAALAAAAAQAAAA4////CAAAABAAAAAFAAAAVmFsdWUAAAAEAAAAbmFtZQAAAABc////CAAAAAwAAAACAAAAe30AAAYAAABsYWJlbHMAAHz///8IAAAAKAAAAB0AAAB7ImRpc3BsYXlOYW1lRnJvbURTIjoiMSAvIDAifQAAAAYAAABjb25maWcAAAAAAACK////AAACAAUAAABWYWx1ZQASABgAFAAAABMADAAAAAgABAASAAAAFAAAAEQAAABMAAAAAAAACkwAAAABAAAADAAAAAgADAAIAAQACAAAAAgAAAAQAAAABAAAAFRpbWUAAAAABAAAAG5hbWUAAAAAAAAAAAAABgAIAAYABgAAAAAAAwAEAAAAVGltZQAAAABQAgAAQVJST1cx +FRAME=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 diff --git a/pkg/tsdb/prometheus/testdata/range_missing.result.golden.txt b/pkg/tsdb/prometheus/testdata/range_missing.result.golden.txt index e63cc8828b8..fe85719448f 100644 --- a/pkg/tsdb/prometheus/testdata/range_missing.result.golden.txt +++ b/pkg/tsdb/prometheus/testdata/range_missing.result.golden.txt @@ -6,23 +6,17 @@ Frame[0] { } } Name: go_goroutines{job="prometheus"} -Dimensions: 2 Fields by 9 Rows +Dimensions: 2 Fields by 3 Rows +-------------------------------+------------------------------------------------+ | Name: Time | Name: Value | | Labels: | Labels: __name__=go_goroutines, job=prometheus | | Type: []time.Time | Type: []*float64 | +-------------------------------+------------------------------------------------+ -| 2022-01-11 08:25:30 +0000 UTC | null | -| 2022-01-11 08:25:31 +0000 UTC | null | -| 2022-01-11 08:25:32 +0000 UTC | null | | 2022-01-11 08:25:33 +0000 UTC | 21 | | 2022-01-11 08:25:34 +0000 UTC | 32 | -| 2022-01-11 08:25:35 +0000 UTC | null | -| 2022-01-11 08:25:36 +0000 UTC | null | | 2022-01-11 08:25:37 +0000 UTC | 43 | -| 2022-01-11 08:25:38 +0000 UTC | null | +-------------------------------+------------------------------------------------+ ====== TEST DATA RESPONSE (arrow base64) ====== -FRAME=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 +FRAME=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 diff --git a/pkg/tsdb/prometheus/testdata/range_nan.result.golden.txt b/pkg/tsdb/prometheus/testdata/range_nan.result.golden.txt index f4d34e2a377..78de205a99e 100644 --- a/pkg/tsdb/prometheus/testdata/range_nan.result.golden.txt +++ b/pkg/tsdb/prometheus/testdata/range_nan.result.golden.txt @@ -19,4 +19,4 @@ Dimensions: 2 Fields by 3 Rows ====== TEST DATA RESPONSE (arrow base64) ====== -FRAME=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 +FRAME=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 diff --git a/pkg/tsdb/prometheus/testdata/range_simple.result.golden.txt b/pkg/tsdb/prometheus/testdata/range_simple.result.golden.txt index 7866187f0e6..bd1e0d265da 100644 --- a/pkg/tsdb/prometheus/testdata/range_simple.result.golden.txt +++ b/pkg/tsdb/prometheus/testdata/range_simple.result.golden.txt @@ -38,5 +38,5 @@ Dimensions: 2 Fields by 3 Rows ====== TEST DATA RESPONSE (arrow base64) ====== -FRAME=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 -FRAME=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 +FRAME=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 +FRAME=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 diff --git a/pkg/tsdb/prometheus/time_series_query.go b/pkg/tsdb/prometheus/time_series_query.go index f86638771b8..ae7abf12406 100644 --- a/pkg/tsdb/prometheus/time_series_query.go +++ b/pkg/tsdb/prometheus/time_series_query.go @@ -47,7 +47,7 @@ const ( ExemplarQueryType TimeSeriesQueryType = "exemplar" ) -func (s *Service) runQueries(ctx context.Context, client apiv1.API, queries []*PrometheusQuery, fillNulls bool) (*backend.QueryDataResponse, error) { +func (s *Service) runQueries(ctx context.Context, client apiv1.API, queries []*PrometheusQuery) (*backend.QueryDataResponse, error) { result := backend.QueryDataResponse{ Responses: backend.Responses{}, } @@ -101,7 +101,7 @@ func (s *Service) runQueries(ctx context.Context, client apiv1.API, queries []*P } } - frames, err := parseTimeSeriesResponse(response, query, fillNulls) + frames, err := parseTimeSeriesResponse(response, query) if err != nil { return &result, err } @@ -128,12 +128,7 @@ func (s *Service) executeTimeSeriesQuery(ctx context.Context, req *backend.Query return &result, err } - fillNulls := true - if req.Headers["FromAlert"] == "true" { - fillNulls = false - } - - return s.runQueries(ctx, client, queries, fillNulls) + return s.runQueries(ctx, client, queries) } func formatLegend(metric model.Metric, query *PrometheusQuery) string { @@ -207,7 +202,7 @@ func (s *Service) parseTimeSeriesQuery(queryContext *backend.QueryDataRequest, d return qs, nil } -func parseTimeSeriesResponse(value map[TimeSeriesQueryType]interface{}, query *PrometheusQuery, fillNulls bool) (data.Frames, error) { +func parseTimeSeriesResponse(value map[TimeSeriesQueryType]interface{}, query *PrometheusQuery) (data.Frames, error) { var ( frames = data.Frames{} nextFrames = data.Frames{} @@ -219,11 +214,7 @@ func parseTimeSeriesResponse(value map[TimeSeriesQueryType]interface{}, query *P switch v := value.(type) { case model.Matrix: - if fillNulls { - nextFrames = matrixToDataFramesWithNullFill(v, query, nextFrames) - } else { - nextFrames = matrixToDataFrames(v, query, nextFrames) - } + nextFrames = matrixToDataFrames(v, query, nextFrames) case model.Vector: nextFrames = vectorToDataFrames(v, query, nextFrames) case *model.Scalar: @@ -317,56 +308,6 @@ func interpolateVariables(model *QueryModel, interval time.Duration, timeRange t return expr } -func matrixToDataFramesWithNullFill(matrix model.Matrix, query *PrometheusQuery, frames data.Frames) data.Frames { - for _, v := range matrix { - tags := make(map[string]string, len(v.Metric)) - for k, v := range v.Metric { - tags[string(k)] = string(v) - } - - baseTimestamp := alignTimeRange(query.Start, query.Step, query.UtcOffsetSec).UnixMilli() - endTimestamp := alignTimeRange(query.End, query.Step, query.UtcOffsetSec).UnixMilli() - // For each step we create 1 data point. This results in range / step + 1 data points. - datapointsCount := int((endTimestamp-baseTimestamp)/query.Step.Milliseconds()) + 1 - - timeField := data.NewFieldFromFieldType(data.FieldTypeTime, datapointsCount) - valueField := data.NewFieldFromFieldType(data.FieldTypeNullableFloat64, datapointsCount) - idx := 0 - - for _, pair := range v.Values { - timestamp := int64(pair.Timestamp) - value := float64(pair.Value) - - for t := baseTimestamp; t < timestamp; t += query.Step.Milliseconds() { - timeField.Set(idx, time.Unix(0, t*1000000).UTC()) - idx++ - } - - timeField.Set(idx, time.Unix(pair.Timestamp.Unix(), 0).UTC()) - if !math.IsNaN(value) { - valueField.Set(idx, &value) - } - baseTimestamp = timestamp + query.Step.Milliseconds() - idx++ - } - - for t := baseTimestamp; t <= endTimestamp; t += query.Step.Milliseconds() { - timeField.Set(idx, time.Unix(0, t*1000000).UTC()) - idx++ - } - - name := formatLegend(v.Metric, query) - timeField.Name = data.TimeSeriesTimeFieldName - valueField.Name = data.TimeSeriesValueFieldName - valueField.Config = &data.FieldConfig{DisplayNameFromDS: name} - valueField.Labels = tags - - frames = append(frames, newDataFrame(name, "matrix", timeField, valueField)) - } - - return frames -} - func matrixToDataFrames(matrix model.Matrix, query *PrometheusQuery, frames data.Frames) data.Frames { for _, v := range matrix { tags := make(map[string]string, len(v.Metric)) @@ -387,6 +328,7 @@ func matrixToDataFrames(matrix model.Matrix, query *PrometheusQuery, frames data name := formatLegend(v.Metric, query) timeField.Name = data.TimeSeriesTimeFieldName + timeField.Config = &data.FieldConfig{Interval: float64(query.Step.Milliseconds())} valueField.Name = data.TimeSeriesValueFieldName valueField.Config = &data.FieldConfig{DisplayNameFromDS: name} valueField.Labels = tags diff --git a/pkg/tsdb/prometheus/time_series_query_test.go b/pkg/tsdb/prometheus/time_series_query_test.go index 2f5ec14f160..fc50c3fb78f 100644 --- a/pkg/tsdb/prometheus/time_series_query_test.go +++ b/pkg/tsdb/prometheus/time_series_query_test.go @@ -556,7 +556,7 @@ func TestPrometheus_parseTimeSeriesResponse(t *testing.T) { query := &PrometheusQuery{ LegendFormat: "legend {{app}}", } - res, err := parseTimeSeriesResponse(value, query, true) + res, err := parseTimeSeriesResponse(value, query) require.NoError(t, err) // Test fields @@ -594,7 +594,7 @@ func TestPrometheus_parseTimeSeriesResponse(t *testing.T) { End: time.Unix(5, 0).UTC(), UtcOffsetSec: 0, } - res, err := parseTimeSeriesResponse(value, query, true) + res, err := parseTimeSeriesResponse(value, query) require.NoError(t, err) require.Len(t, res, 1) @@ -631,16 +631,16 @@ func TestPrometheus_parseTimeSeriesResponse(t *testing.T) { End: time.Unix(4, 0).UTC(), UtcOffsetSec: 0, } - res, err := parseTimeSeriesResponse(value, query, true) + res, err := parseTimeSeriesResponse(value, query) require.NoError(t, err) require.Len(t, res, 1) - require.Equal(t, res[0].Fields[0].Len(), 4) - require.Equal(t, res[0].Fields[0].At(1), time.Unix(2, 0).UTC()) - require.Equal(t, res[0].Fields[0].At(2), time.Unix(3, 0).UTC()) - require.Equal(t, res[0].Fields[1].Len(), 4) - require.Nil(t, res[0].Fields[1].At(1)) - require.Nil(t, res[0].Fields[1].At(2)) + require.Equal(t, res[0].Fields[0].Len(), 2) + require.Equal(t, time.Unix(1, 0).UTC(), res[0].Fields[0].At(0)) + require.Equal(t, time.Unix(4, 0).UTC(), res[0].Fields[0].At(1)) + require.Equal(t, res[0].Fields[1].Len(), 2) + require.Equal(t, float64(1), *res[0].Fields[1].At(0).(*float64)) + require.Equal(t, float64(4), *res[0].Fields[1].At(1).(*float64)) }) t.Run("matrix response with from alerting missed data points should be parsed correctly", func(t *testing.T) { @@ -662,7 +662,7 @@ func TestPrometheus_parseTimeSeriesResponse(t *testing.T) { End: time.Unix(4, 0).UTC(), UtcOffsetSec: 0, } - res, err := parseTimeSeriesResponse(value, query, false) + res, err := parseTimeSeriesResponse(value, query) require.NoError(t, err) require.Len(t, res, 1) @@ -693,7 +693,7 @@ func TestPrometheus_parseTimeSeriesResponse(t *testing.T) { End: time.Unix(4, 0).UTC(), UtcOffsetSec: 0, } - res, err := parseTimeSeriesResponse(value, query, true) + res, err := parseTimeSeriesResponse(value, query) require.NoError(t, err) var nilPointer *float64 @@ -713,7 +713,7 @@ func TestPrometheus_parseTimeSeriesResponse(t *testing.T) { query := &PrometheusQuery{ LegendFormat: "legend {{app}}", } - res, err := parseTimeSeriesResponse(value, query, true) + res, err := parseTimeSeriesResponse(value, query) require.NoError(t, err) require.Len(t, res, 1) @@ -740,7 +740,7 @@ func TestPrometheus_parseTimeSeriesResponse(t *testing.T) { } query := &PrometheusQuery{} - res, err := parseTimeSeriesResponse(value, query, true) + res, err := parseTimeSeriesResponse(value, query) require.NoError(t, err) require.Len(t, res, 1) From 8ff34fa36e3965ac512576c2aa1191752d0fdbd6 Mon Sep 17 00:00:00 2001 From: "Grot (@grafanabot)" <43478413+grafanabot@users.noreply.github.com> Date: Thu, 3 Feb 2022 16:52:12 +0100 Subject: [PATCH 017/113] AccessControl: introduce a different accesscontrol check (licensed or not) (#44777) (#44857) Co-authored-by: ievaVasiljeva (cherry picked from commit 86756ee3e5dba1b374edcc9801257d44967a5e7b) Co-authored-by: Gabriel MABILLE --- public/app/core/services/context_srv.ts | 16 ++++++++++------ public/app/features/admin/UserOrgs.tsx | 2 +- public/app/features/teams/TeamList.tsx | 6 +++--- public/app/features/users/UsersTable.test.tsx | 2 +- public/app/features/users/UsersTable.tsx | 4 ++-- 5 files changed, 17 insertions(+), 13 deletions(-) diff --git a/public/app/core/services/context_srv.ts b/public/app/core/services/context_srv.ts index 262ac14abd4..507ebcaf60f 100644 --- a/public/app/core/services/context_srv.ts +++ b/public/app/core/services/context_srv.ts @@ -1,8 +1,8 @@ import config from '../../core/config'; import { extend } from 'lodash'; import { rangeUtil, WithAccessControlMetadata } from '@grafana/data'; -import { featureEnabled } from '@grafana/runtime'; import { AccessControlAction, UserPermission } from 'app/types'; +import { featureEnabled } from '@grafana/runtime'; export class User { id: number; @@ -83,13 +83,17 @@ export class ContextSrv { } accessControlEnabled(): boolean { + return Boolean(config.featureToggles['accesscontrol']); + } + + licensedAccessControlEnabled(): boolean { return featureEnabled('accesscontrol') && Boolean(config.featureToggles['accesscontrol']); } // Checks whether user has required permission hasPermissionInMetadata(action: AccessControlAction | string, object: WithAccessControlMetadata): boolean { // Fallback if access control disabled - if (!config.featureToggles['accesscontrol']) { + if (!this.accessControlEnabled()) { return true; } @@ -99,7 +103,7 @@ export class ContextSrv { // Checks whether user has required permission hasPermission(action: AccessControlAction | string): boolean { // Fallback if access control disabled - if (!config.featureToggles['accesscontrol']) { + if (!this.accessControlEnabled()) { return true; } @@ -126,14 +130,14 @@ export class ContextSrv { } hasAccessToExplore() { - if (config.featureToggles['accesscontrol']) { + if (this.accessControlEnabled()) { return this.hasPermission(AccessControlAction.DataSourcesExplore); } return (this.isEditor || config.viewersCanEdit) && config.exploreEnabled; } hasAccess(action: string, fallBack: boolean) { - if (!config.featureToggles['accesscontrol']) { + if (!this.accessControlEnabled()) { return fallBack; } return this.hasPermission(action); @@ -141,7 +145,7 @@ export class ContextSrv { // evaluates access control permissions, granting access if the user has any of them; uses fallback if access control is disabled evaluatePermission(fallback: () => string[], actions: string[]) { - if (!config.featureToggles['accesscontrol']) { + if (!this.accessControlEnabled()) { return fallback(); } if (actions.some((action) => this.hasPermission(action))) { diff --git a/public/app/features/admin/UserOrgs.tsx b/public/app/features/admin/UserOrgs.tsx index 8cff0b30c8c..5af09928063 100644 --- a/public/app/features/admin/UserOrgs.tsx +++ b/public/app/features/admin/UserOrgs.tsx @@ -176,7 +176,7 @@ class UnThemedOrgRow extends PureComponent { - {contextSrv.accessControlEnabled() ? ( + {contextSrv.licensedAccessControlEnabled() ? (
diff --git a/public/app/features/teams/TeamList.tsx b/public/app/features/teams/TeamList.tsx index 27c6af5be0b..9c40f286826 100644 --- a/public/app/features/teams/TeamList.tsx +++ b/public/app/features/teams/TeamList.tsx @@ -43,7 +43,7 @@ export class TeamList extends PureComponent { componentDidMount() { this.fetchTeams(); - if (contextSrv.accessControlEnabled()) { + if (contextSrv.licensedAccessControlEnabled()) { this.fetchRoleOptions(); } } @@ -89,7 +89,7 @@ export class TeamList extends PureComponent { {team.memberCount} - {contextSrv.accessControlEnabled() && ( + {contextSrv.licensedAccessControlEnabled() && ( this.state.roleOptions} /> @@ -155,7 +155,7 @@ export class TeamList extends PureComponent { Name Email Members - {contextSrv.accessControlEnabled() && Roles} + {contextSrv.licensedAccessControlEnabled() && Roles} diff --git a/public/app/features/users/UsersTable.test.tsx b/public/app/features/users/UsersTable.test.tsx index 68bba275f15..f71e0517eb7 100644 --- a/public/app/features/users/UsersTable.test.tsx +++ b/public/app/features/users/UsersTable.test.tsx @@ -9,7 +9,7 @@ jest.mock('app/core/core', () => ({ contextSrv: { hasPermission: () => true, hasPermissionInMetadata: () => true, - accessControlEnabled: () => false, + licensedAccessControlEnabled: () => false, }, })); diff --git a/public/app/features/users/UsersTable.tsx b/public/app/features/users/UsersTable.tsx index 427a32ef53a..97ae76998d5 100644 --- a/public/app/features/users/UsersTable.tsx +++ b/public/app/features/users/UsersTable.tsx @@ -40,7 +40,7 @@ const UsersTable: FC = (props) => { console.error('Error loading options'); } } - if (contextSrv.accessControlEnabled()) { + if (contextSrv.licensedAccessControlEnabled()) { fetchOptions(); } }, [orgId]); @@ -88,7 +88,7 @@ const UsersTable: FC = (props) => { {user.lastSeenAtAge} - {contextSrv.accessControlEnabled() ? ( + {contextSrv.licensedAccessControlEnabled() ? ( Date: Thu, 3 Feb 2022 17:23:49 +0100 Subject: [PATCH 018/113] AccessControl: Show UserPicker based on `canListUsers` (#44843) (#44860) * AccessControl: Show UserPicker based on canListUser * Update public/app/core/components/AccessControl/AddPermission.tsx Co-authored-by: Ieva (cherry picked from commit 6d931226d89d5d972fff47c389ab1f8dc88322c7) Co-authored-by: Gabriel MABILLE --- .../components/AccessControl/AddPermission.tsx | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/public/app/core/components/AccessControl/AddPermission.tsx b/public/app/core/components/AccessControl/AddPermission.tsx index b2da564b1fe..aef87a20de6 100644 --- a/public/app/core/components/AccessControl/AddPermission.tsx +++ b/public/app/core/components/AccessControl/AddPermission.tsx @@ -1,7 +1,7 @@ import React, { useEffect, useMemo, useState } from 'react'; import { UserPicker } from 'app/core/components/Select/UserPicker'; import { TeamPicker } from 'app/core/components/Select/TeamPicker'; -import { Button, Form, HorizontalGroup, Select } from '@grafana/ui'; +import { Alert, Button, Form, HorizontalGroup, Input, Select } from '@grafana/ui'; import { OrgRole } from 'app/types/acl'; import { CloseButton } from 'app/core/components/CloseButton/CloseButton'; import { Assignments, PermissionTarget, SetPermission } from './types'; @@ -54,10 +54,22 @@ export const AddPermission = ({ (target === PermissionTarget.User && userId > 0) || (PermissionTarget.BuiltInRole && OrgRole.hasOwnProperty(builtInRole)); + const renderMissingListUserRights = () => { + return ( + + You are missing the permission to list users (org.users:read). Please contact your administrator to get this + resolved. + + ); + }; + return (
{title}
+ + {target === PermissionTarget.User && !canListUsers && renderMissingListUserRights()} +
setPermissionTarget(v.value!)} + disabled={targetOptions.length === 0} menuShouldPortal /> - {target === PermissionTarget.User && ( + {target === PermissionTarget.User && canListUsers && ( setUserId(u.value || 0)} className={'width-20'} /> )} + {target === PermissionTarget.User && !canListUsers && } {target === PermissionTarget.Team && ( setTeamId(t.value?.id || 0)} className={'width-20'} /> From 2b516126cf2d8b977a7451869ef8bb19224da6d0 Mon Sep 17 00:00:00 2001 From: "Grot (@grafanabot)" <43478413+grafanabot@users.noreply.github.com> Date: Thu, 3 Feb 2022 17:27:57 +0100 Subject: [PATCH 019/113] Prometheus: Fix Azure authentication support (#44407) (#44861) Re-adding back Azure authentication support to Prometheus datasource after the datasource query logic was rewritten from plugin.json routes to Go backend. Ref #35857 (cherry picked from commit 85ea1a5d6449b0e68b2683a19cd4514a98883c2e) Co-authored-by: Sergey Kostrukov --- pkg/tsdb/prometheus/promclient/provider.go | 22 +++--- .../prometheus/promclient/provider_azure.go | 32 ++++++++ .../prometheus/promclient/provider_test.go | 2 +- pkg/tsdb/prometheus/prometheus.go | 10 ++- pkg/util/maputil/maputil.go | 73 +++++++++++++++++++ 5 files changed, 127 insertions(+), 12 deletions(-) create mode 100644 pkg/tsdb/prometheus/promclient/provider_azure.go create mode 100644 pkg/util/maputil/maputil.go diff --git a/pkg/tsdb/prometheus/promclient/provider.go b/pkg/tsdb/prometheus/promclient/provider.go index c84a9651773..c33d6a14c11 100644 --- a/pkg/tsdb/prometheus/promclient/provider.go +++ b/pkg/tsdb/prometheus/promclient/provider.go @@ -4,8 +4,8 @@ import ( "strings" "github.com/grafana/grafana-plugin-sdk-go/backend" - "github.com/grafana/grafana/pkg/tsdb/prometheus/middleware" + "github.com/grafana/grafana/pkg/util/maputil" sdkhttpclient "github.com/grafana/grafana-plugin-sdk-go/backend/httpclient" "github.com/grafana/grafana/pkg/infra/httpclient" @@ -16,30 +16,28 @@ import ( type Provider struct { settings backend.DataSourceInstanceSettings - jsonData JsonData + jsonData map[string]interface{} + httpMethod string clientProvider httpclient.Provider log log.Logger } func NewProvider( settings backend.DataSourceInstanceSettings, - jsonData JsonData, + jsonData map[string]interface{}, clientProvider httpclient.Provider, log log.Logger, ) *Provider { + httpMethod, _ := maputil.GetStringOptional(jsonData, "httpMethod") return &Provider{ settings: settings, jsonData: jsonData, + httpMethod: httpMethod, clientProvider: clientProvider, log: log, } } -type JsonData struct { - Method string `json:"httpMethod"` - TimeInterval string `json:"timeInterval"` -} - func (p *Provider) GetClient(headers map[string]string) (apiv1.API, error) { opts, err := p.settings.HTTPClientOptions() if err != nil { @@ -54,6 +52,12 @@ func (p *Provider) GetClient(headers map[string]string) (apiv1.API, error) { opts.SigV4.Service = "aps" } + // Azure authentication + err = p.configureAzureAuthentication(opts) + if err != nil { + return nil, err + } + roundTripper, err := p.clientProvider.GetTransport(opts) if err != nil { return nil, err @@ -77,7 +81,7 @@ func (p *Provider) middlewares() []sdkhttpclient.Middleware { middleware.CustomQueryParameters(p.log), sdkhttpclient.CustomHeadersMiddleware(), } - if strings.ToLower(p.jsonData.Method) == "get" { + if strings.ToLower(p.httpMethod) == "get" { middlewares = append(middlewares, middleware.ForceHttpGet(p.log)) } diff --git a/pkg/tsdb/prometheus/promclient/provider_azure.go b/pkg/tsdb/prometheus/promclient/provider_azure.go new file mode 100644 index 00000000000..920f253a75d --- /dev/null +++ b/pkg/tsdb/prometheus/promclient/provider_azure.go @@ -0,0 +1,32 @@ +package promclient + +import ( + "fmt" + + sdkhttpclient "github.com/grafana/grafana-plugin-sdk-go/backend/httpclient" + "github.com/grafana/grafana/pkg/tsdb/azuremonitor/azcredentials" + "github.com/grafana/grafana/pkg/util/maputil" +) + +func (p *Provider) configureAzureAuthentication(opts sdkhttpclient.Options) error { + credentials, err := azcredentials.FromDatasourceData(p.jsonData, p.settings.DecryptedSecureJSONData) + if err != nil { + err = fmt.Errorf("invalid Azure credentials: %s", err) + return err + } + + if credentials != nil { + opts.CustomOptions["_azureCredentials"] = credentials + + resourceId, err := maputil.GetStringOptional(p.jsonData, "azureEndpointResourceId") + if err != nil { + return err + } + + if resourceId != "" { + opts.CustomOptions["azureEndpointResourceId"] = resourceId + } + } + + return nil +} diff --git a/pkg/tsdb/prometheus/promclient/provider_test.go b/pkg/tsdb/prometheus/promclient/provider_test.go index 935b3d07c5a..43acc14b362 100644 --- a/pkg/tsdb/prometheus/promclient/provider_test.go +++ b/pkg/tsdb/prometheus/promclient/provider_test.go @@ -135,7 +135,7 @@ func setup(jsonData ...string) *testContext { rawData = []byte(jsonData[0]) } - var jd promclient.JsonData + var jd map[string]interface{} _ = json.Unmarshal(rawData, &jd) settings := backend.DataSourceInstanceSettings{URL: "test-url", JSONData: rawData} diff --git a/pkg/tsdb/prometheus/prometheus.go b/pkg/tsdb/prometheus/prometheus.go index e16597c50fb..9ba855411ad 100644 --- a/pkg/tsdb/prometheus/prometheus.go +++ b/pkg/tsdb/prometheus/prometheus.go @@ -16,6 +16,7 @@ import ( "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/infra/tracing" "github.com/grafana/grafana/pkg/tsdb/intervalv2" + "github.com/grafana/grafana/pkg/util/maputil" apiv1 "github.com/prometheus/client_golang/api/prometheus/v1" ) @@ -42,7 +43,7 @@ func ProvideService(httpClientProvider httpclient.Provider, tracer tracing.Trace func newInstanceSettings(httpClientProvider httpclient.Provider) datasource.InstanceFactoryFunc { return func(settings backend.DataSourceInstanceSettings) (instancemgmt.Instance, error) { - var jsonData promclient.JsonData + var jsonData map[string]interface{} err := json.Unmarshal(settings.JSONData, &jsonData) if err != nil { return nil, fmt.Errorf("error reading settings: %w", err) @@ -54,10 +55,15 @@ func newInstanceSettings(httpClientProvider httpclient.Provider) datasource.Inst return nil, err } + timeInterval, err := maputil.GetStringOptional(jsonData, "timeInterval") + if err != nil { + return nil, err + } + mdl := DatasourceInfo{ ID: settings.ID, URL: settings.URL, - TimeInterval: jsonData.TimeInterval, + TimeInterval: timeInterval, getClient: pc.GetClient, } diff --git a/pkg/util/maputil/maputil.go b/pkg/util/maputil/maputil.go new file mode 100644 index 00000000000..becd6d81e0c --- /dev/null +++ b/pkg/util/maputil/maputil.go @@ -0,0 +1,73 @@ +package maputil + +import "fmt" + +func GetMap(obj map[string]interface{}, key string) (map[string]interface{}, error) { + if untypedValue, ok := obj[key]; ok { + if value, ok := untypedValue.(map[string]interface{}); ok { + return value, nil + } else { + err := fmt.Errorf("the field '%s' should be an object", key) + return nil, err + } + } else { + err := fmt.Errorf("the field '%s' should be set", key) + return nil, err + } +} + +func GetBool(obj map[string]interface{}, key string) (bool, error) { + if untypedValue, ok := obj[key]; ok { + if value, ok := untypedValue.(bool); ok { + return value, nil + } else { + err := fmt.Errorf("the field '%s' should be a bool", key) + return false, err + } + } else { + err := fmt.Errorf("the field '%s' should be set", key) + return false, err + } +} + +func GetBoolOptional(obj map[string]interface{}, key string) (bool, error) { + if untypedValue, ok := obj[key]; ok { + if value, ok := untypedValue.(bool); ok { + return value, nil + } else { + err := fmt.Errorf("the field '%s' should be a bool", key) + return false, err + } + } else { + // Value optional, not error + return false, nil + } +} + +func GetString(obj map[string]interface{}, key string) (string, error) { + if untypedValue, ok := obj[key]; ok { + if value, ok := untypedValue.(string); ok { + return value, nil + } else { + err := fmt.Errorf("the field '%s' should be a string", key) + return "", err + } + } else { + err := fmt.Errorf("the field '%s' should be set", key) + return "", err + } +} + +func GetStringOptional(obj map[string]interface{}, key string) (string, error) { + if untypedValue, ok := obj[key]; ok { + if value, ok := untypedValue.(string); ok { + return value, nil + } else { + err := fmt.Errorf("the field '%s' should be a string", key) + return "", err + } + } else { + // Value optional, not error + return "", nil + } +} From ee54923bdd552b80a451ebb0a5bf9a83c11e018f Mon Sep 17 00:00:00 2001 From: "Grot (@grafanabot)" <43478413+grafanabot@users.noreply.github.com> Date: Thu, 3 Feb 2022 17:37:00 +0100 Subject: [PATCH 020/113] Chore: Update latest.json (#44854) (#44859) (cherry picked from commit 53a693fd847b441956f7d06466093d235c472881) Co-authored-by: Andres Martinez Gotor --- latest.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/latest.json b/latest.json index 91df80dab6b..707ba83022e 100644 --- a/latest.json +++ b/latest.json @@ -1,4 +1,4 @@ { "stable": "8.3.4", - "testing": "8.3.4" + "testing": "8.4.0-beta1" } From 6909246a7a973a9cba90da819d2ff5f39c536523 Mon Sep 17 00:00:00 2001 From: "Grot (@grafanabot)" <43478413+grafanabot@users.noreply.github.com> Date: Thu, 3 Feb 2022 18:00:38 +0100 Subject: [PATCH 021/113] AccessControl: Change teams permissions page when accesscontrol is enabled (#43971) (#44864) * AccessControl: Change teams permissions page when frontend is hit * Implement frontend changes for group sync * Changing the org/teams/edit permissions Co-authored-by: ievaVasiljeva * Fixing routes Co-authored-by: ievaVasiljeva * Use props straight away no need to go through the state Co-authored-by: Alex Khomenko * Update public/app/features/teams/TeamPages.tsx Co-authored-by: ievaVasiljeva Co-authored-by: Alex Khomenko (cherry picked from commit 8217d6d206692a411f5055780a3a6518711ca365) Co-authored-by: Gabriel MABILLE --- pkg/api/api.go | 5 +- pkg/api/common_test.go | 7 +-- pkg/api/index.go | 4 +- pkg/api/roles.go | 19 +++++++ pkg/api/team.go | 45 ++++++++++++++++ pkg/models/team.go | 15 +++--- public/app/core/services/context_srv.ts | 7 +++ .../app/features/teams/TeamGroupSync.test.tsx | 1 + public/app/features/teams/TeamGroupSync.tsx | 17 ++++-- public/app/features/teams/TeamList.tsx | 6 ++- public/app/features/teams/TeamPages.test.tsx | 1 + public/app/features/teams/TeamPages.tsx | 52 ++++++++++++++---- public/app/features/teams/TeamPermissions.tsx | 31 +++++++++++ .../app/features/teams/TeamSettings.test.tsx | 6 +++ public/app/features/teams/TeamSettings.tsx | 8 ++- .../__snapshots__/TeamGroupSync.test.tsx.snap | 7 +++ .../__snapshots__/TeamPages.test.tsx.snap | 4 +- .../__snapshots__/TeamSettings.test.tsx.snap | 2 + public/app/features/teams/state/actions.ts | 8 ++- public/app/features/teams/state/navModel.ts | 54 +++++++++++++------ public/app/routes/routes.tsx | 19 +++++-- public/app/types/accessControl.ts | 5 ++ public/app/types/teams.ts | 3 +- 23 files changed, 270 insertions(+), 56 deletions(-) create mode 100644 public/app/features/teams/TeamPermissions.tsx diff --git a/pkg/api/api.go b/pkg/api/api.go index ea6c3859c08..32c0fbe88ce 100644 --- a/pkg/api/api.go +++ b/pkg/api/api.go @@ -58,8 +58,9 @@ func (hs *HTTPServer) registerRoutes() { r.Get("/org/users", authorize(reqOrgAdmin, ac.EvalPermission(ac.ActionOrgUsersRead)), hs.Index) r.Get("/org/users/new", reqOrgAdmin, hs.Index) r.Get("/org/users/invite", authorize(reqOrgAdmin, ac.EvalPermission(ac.ActionUsersCreate)), hs.Index) - r.Get("/org/teams", reqCanAccessTeams, hs.Index) - r.Get("/org/teams/*", reqCanAccessTeams, hs.Index) + r.Get("/org/teams", authorize(reqCanAccessTeams, ac.EvalPermission(ac.ActionTeamsRead)), hs.Index) + r.Get("/org/teams/edit/*", authorize(reqCanAccessTeams, teamsEditAccessEvaluator), hs.Index) + r.Get("/org/teams/new", authorize(reqCanAccessTeams, ac.EvalPermission(ac.ActionTeamsCreate)), hs.Index) r.Get("/org/serviceaccounts", middleware.ReqOrgAdmin, hs.Index) r.Get("/org/serviceaccounts/:serviceAccountId", middleware.ReqOrgAdmin, hs.Index) r.Get("/org/apikeys/", reqOrgAdmin, hs.Index) diff --git a/pkg/api/common_test.go b/pkg/api/common_test.go index ed2e956e096..d5956922e28 100644 --- a/pkg/api/common_test.go +++ b/pkg/api/common_test.go @@ -309,9 +309,10 @@ func setupSimpleHTTPServer(features *featuremgmt.FeatureManager) *HTTPServer { cfg.IsFeatureToggleEnabled = features.IsEnabled return &HTTPServer{ - Cfg: cfg, - Features: features, - Bus: bus.GetBus(), + Cfg: cfg, + Features: features, + Bus: bus.GetBus(), + AccessControl: accesscontrolmock.New().WithDisabled(), } } diff --git a/pkg/api/index.go b/pkg/api/index.go index 25565347c10..da0c9c45bf1 100644 --- a/pkg/api/index.go +++ b/pkg/api/index.go @@ -148,7 +148,7 @@ func enableServiceAccount(hs *HTTPServer, c *models.ReqContext) bool { hs.Features.IsEnabled(featuremgmt.FlagServiceAccounts) } -func enableTeams(hs *HTTPServer, c *models.ReqContext) bool { +func (hs *HTTPServer) ReqCanAdminTeams(c *models.ReqContext) bool { return c.OrgRole == models.ROLE_ADMIN || (hs.Cfg.EditorsCanAdmin && c.OrgRole == models.ROLE_EDITOR) } @@ -263,7 +263,7 @@ func (hs *HTTPServer) getNavTree(c *models.ReqContext, hasEditPerm bool) ([]*dto }) } - if enableTeams(hs, c) { + if hasAccess(hs.ReqCanAdminTeams, teamsAccessEvaluator) { configNodes = append(configNodes, &dtos.NavLink{ Text: "Teams", Id: "teams", diff --git a/pkg/api/roles.go b/pkg/api/roles.go index e8f3fc9fe53..0fc5d75723e 100644 --- a/pkg/api/roles.go +++ b/pkg/api/roles.go @@ -298,3 +298,22 @@ var orgsCreateAccessEvaluator = accesscontrol.EvalAll( accesscontrol.EvalPermission(ActionOrgsRead), accesscontrol.EvalPermission(ActionOrgsCreate), ) + +// teamsAccessEvaluator is used to protect the "Configuration > Teams" page access +var teamsAccessEvaluator = accesscontrol.EvalAll( + accesscontrol.EvalPermission(accesscontrol.ActionTeamsRead), + accesscontrol.EvalAny( + accesscontrol.EvalPermission(accesscontrol.ActionTeamsCreate), + accesscontrol.EvalPermission(accesscontrol.ActionTeamsWrite), + accesscontrol.EvalPermission(accesscontrol.ActionTeamsPermissionsWrite), + ), +) + +// teamsEditAccessEvaluator is used to protect the "Configuration > Teams > edit" page access +var teamsEditAccessEvaluator = accesscontrol.EvalAll( + accesscontrol.EvalPermission(accesscontrol.ActionTeamsRead), + accesscontrol.EvalAny( + accesscontrol.EvalPermission(accesscontrol.ActionTeamsWrite), + accesscontrol.EvalPermission(accesscontrol.ActionTeamsPermissionsWrite), + ), +) diff --git a/pkg/api/team.go b/pkg/api/team.go index c695db38b4c..b59db581f49 100644 --- a/pkg/api/team.go +++ b/pkg/api/team.go @@ -2,12 +2,14 @@ package api import ( "errors" + "fmt" "net/http" "strconv" "github.com/grafana/grafana/pkg/api/dtos" "github.com/grafana/grafana/pkg/api/response" "github.com/grafana/grafana/pkg/models" + "github.com/grafana/grafana/pkg/services/accesscontrol" "github.com/grafana/grafana/pkg/services/featuremgmt" "github.com/grafana/grafana/pkg/services/sqlstore" "github.com/grafana/grafana/pkg/util" @@ -105,6 +107,20 @@ func (hs *HTTPServer) DeleteTeamByID(c *models.ReqContext) response.Response { return response.Success("Team deleted") } +func (hs *HTTPServer) getTeamsAccessControlMetadata(c *models.ReqContext, teamIDs map[string]bool) (map[string]accesscontrol.Metadata, error) { + if hs.AccessControl.IsDisabled() || !c.QueryBool("accesscontrol") { + return nil, nil + } + + userPermissions, err := hs.AccessControl.GetUserPermissions(c.Req.Context(), c.SignedInUser) + if err != nil || len(userPermissions) == 0 { + hs.log.Warn("could not fetch accesscontrol metadata for teams", "error", err) + return nil, err + } + + return accesscontrol.GetResourcesMetadata(c.Req.Context(), userPermissions, "teams", teamIDs), nil +} + // GET /api/teams/search func (hs *HTTPServer) SearchTeams(c *models.ReqContext) response.Response { perPage := c.QueryInt("perpage") @@ -136,8 +152,17 @@ func (hs *HTTPServer) SearchTeams(c *models.ReqContext) response.Response { return response.Error(500, "Failed to search Teams", err) } + teamIDs := map[string]bool{} for _, team := range query.Result.Teams { team.AvatarUrl = dtos.GetGravatarUrlWithDefault(team.Email, team.Name) + teamIDs[strconv.FormatInt(team.Id, 10)] = true + } + + metadata, err := hs.getTeamsAccessControlMetadata(c, teamIDs) + if err == nil && len(metadata) != 0 { + for _, team := range query.Result.Teams { + team.AccessControl = metadata[strconv.FormatInt(team.Id, 10)] + } } query.Result.Page = page @@ -146,6 +171,23 @@ func (hs *HTTPServer) SearchTeams(c *models.ReqContext) response.Response { return response.JSON(200, query.Result) } +func (hs *HTTPServer) getTeamAccessControlMetadata(c *models.ReqContext, teamID int64) (accesscontrol.Metadata, error) { + if hs.AccessControl.IsDisabled() || !c.QueryBool("accesscontrol") { + return nil, nil + } + + userPermissions, err := hs.AccessControl.GetUserPermissions(c.Req.Context(), c.SignedInUser) + if err != nil || len(userPermissions) == 0 { + hs.log.Warn("could not fetch accesscontrol metadata", "team", teamID, "error", err) + return nil, err + } + + key := fmt.Sprintf("%d", teamID) + teamIDs := map[string]bool{key: true} + + return accesscontrol.GetResourcesMetadata(c.Req.Context(), userPermissions, "teams", teamIDs)[key], nil +} + // GET /api/teams/:teamId func (hs *HTTPServer) GetTeamByID(c *models.ReqContext) response.Response { teamId, err := strconv.ParseInt(web.Params(c.Req)[":teamId"], 10, 64) @@ -167,6 +209,9 @@ func (hs *HTTPServer) GetTeamByID(c *models.ReqContext) response.Response { return response.Error(500, "Failed to get Team", err) } + metadata, _ := hs.getTeamAccessControlMetadata(c, query.Result.Id) + query.Result.AccessControl = metadata + query.Result.AvatarUrl = dtos.GetGravatarUrlWithDefault(query.Result.Email, query.Result.Name) return response.JSON(200, &query.Result) } diff --git a/pkg/models/team.go b/pkg/models/team.go index 328e1815b90..38f434458af 100644 --- a/pkg/models/team.go +++ b/pkg/models/team.go @@ -77,13 +77,14 @@ type SearchTeamsQuery struct { } type TeamDTO struct { - Id int64 `json:"id"` - OrgId int64 `json:"orgId"` - Name string `json:"name"` - Email string `json:"email"` - AvatarUrl string `json:"avatarUrl"` - MemberCount int64 `json:"memberCount"` - Permission PermissionType `json:"permission"` + Id int64 `json:"id"` + OrgId int64 `json:"orgId"` + Name string `json:"name"` + Email string `json:"email"` + AvatarUrl string `json:"avatarUrl"` + MemberCount int64 `json:"memberCount"` + Permission PermissionType `json:"permission"` + AccessControl map[string]bool `json:"accessControl"` } type SearchTeamQueryResult struct { diff --git a/public/app/core/services/context_srv.ts b/public/app/core/services/context_srv.ts index 507ebcaf60f..bf719821487 100644 --- a/public/app/core/services/context_srv.ts +++ b/public/app/core/services/context_srv.ts @@ -143,6 +143,13 @@ export class ContextSrv { return this.hasPermission(action); } + hasAccessInMetadata(action: string, object: WithAccessControlMetadata, fallBack: boolean) { + if (!config.featureToggles['accesscontrol']) { + return fallBack; + } + return this.hasPermissionInMetadata(action, object); + } + // evaluates access control permissions, granting access if the user has any of them; uses fallback if access control is disabled evaluatePermission(fallback: () => string[], actions: string[]) { if (!this.accessControlEnabled()) { diff --git a/public/app/features/teams/TeamGroupSync.test.tsx b/public/app/features/teams/TeamGroupSync.test.tsx index f3deb62c77b..6ba6f050278 100644 --- a/public/app/features/teams/TeamGroupSync.test.tsx +++ b/public/app/features/teams/TeamGroupSync.test.tsx @@ -6,6 +6,7 @@ import { getMockTeamGroups } from './__mocks__/teamMocks'; const setup = (propOverrides?: object) => { const props: Props = { + isReadOnly: false, groups: [] as TeamGroup[], loadTeamGroups: jest.fn(), addTeamGroup: jest.fn(), diff --git a/public/app/features/teams/TeamGroupSync.tsx b/public/app/features/teams/TeamGroupSync.tsx index aeedfb12f16..a550b0cd9df 100644 --- a/public/app/features/teams/TeamGroupSync.tsx +++ b/public/app/features/teams/TeamGroupSync.tsx @@ -23,13 +23,17 @@ const mapDispatchToProps = { removeTeamGroup, }; +interface OwnProps { + isReadOnly: boolean; +} + interface State { isAdding: boolean; newGroupId: string; } const connector = connect(mapStateToProps, mapDispatchToProps); -export type Props = ConnectedProps; +export type Props = OwnProps & ConnectedProps; const headerTooltip = `Sync LDAP or OAuth groups with your Grafana teams.`; @@ -70,11 +74,12 @@ export class TeamGroupSync extends PureComponent { } renderGroup(group: TeamGroup) { + const { isReadOnly } = this.props; return ( {group.groupId} - @@ -84,7 +89,7 @@ export class TeamGroupSync extends PureComponent { render() { const { isAdding, newGroupId } = this.state; - const groups = this.props.groups; + const { groups, isReadOnly } = this.props; return (
@@ -95,7 +100,7 @@ export class TeamGroupSync extends PureComponent {
{groups.length > 0 && ( - )} @@ -113,11 +118,12 @@ export class TeamGroupSync extends PureComponent { value={newGroupId} onChange={this.onNewGroupIdChanged} placeholder="cn=ops,ou=groups,dc=grafana,dc=org" + disabled={isReadOnly} />
-
@@ -135,6 +141,7 @@ export class TeamGroupSync extends PureComponent { proTipLinkTitle="Learn more" proTipLink="http://docs.grafana.org/auth/enhanced_ldap/" proTipTarget="_blank" + buttonDisabled={isReadOnly} /> )} diff --git a/public/app/features/teams/TeamList.tsx b/public/app/features/teams/TeamList.tsx index 9c40f286826..83894dc5e46 100644 --- a/public/app/features/teams/TeamList.tsx +++ b/public/app/features/teams/TeamList.tsx @@ -69,7 +69,11 @@ export class TeamList extends PureComponent { const { editorsCanAdmin, signedInUser } = this.props; const permission = team.permission; const teamUrl = `org/teams/edit/${team.id}`; - const canDelete = isPermissionTeamAdmin({ permission, editorsCanAdmin, signedInUser }); + const canDelete = contextSrv.hasAccessInMetadata( + AccessControlAction.ActionTeamsDelete, + team, + isPermissionTeamAdmin({ permission, editorsCanAdmin, signedInUser }) + ); return ( diff --git a/public/app/features/teams/TeamPages.test.tsx b/public/app/features/teams/TeamPages.test.tsx index 8f5fa7ec666..c4f852b2bf6 100644 --- a/public/app/features/teams/TeamPages.test.tsx +++ b/public/app/features/teams/TeamPages.test.tsx @@ -13,6 +13,7 @@ jest.mock('@grafana/runtime/src/config', () => ({ licenseInfo: { enabledFeatures: { teamsync: true }, }, + featureToggles: { accesscontrol: false }, }, })); diff --git a/public/app/features/teams/TeamPages.tsx b/public/app/features/teams/TeamPages.tsx index 9817ddefe5b..65430e5e898 100644 --- a/public/app/features/teams/TeamPages.tsx +++ b/public/app/features/teams/TeamPages.tsx @@ -4,9 +4,10 @@ import { includes } from 'lodash'; import config from 'app/core/config'; import Page from 'app/core/components/Page/Page'; import TeamMembers from './TeamMembers'; +import TeamPermissions from './TeamPermissions'; import TeamSettings from './TeamSettings'; import TeamGroupSync from './TeamGroupSync'; -import { StoreState } from 'app/types'; +import { AccessControlAction, StoreState } from 'app/types'; import { loadTeam, loadTeamMembers } from './state/actions'; import { getTeam, getTeamMembers, isSignedInUserTeamAdmin } from './state/selectors'; import { getTeamLoadingNav } from './state/navModel'; @@ -37,10 +38,17 @@ enum PageTypes { function mapStateToProps(state: StoreState, props: OwnProps) { const teamId = parseInt(props.match.params.id, 10); - const pageName = props.match.params.page ?? 'members'; + const team = getTeam(state.team, teamId); + let defaultPage = 'members'; + if (contextSrv.accessControlEnabled()) { + // With FGAC the settings page will always be available + if (!team || !contextSrv.hasPermissionInMetadata(AccessControlAction.ActionTeamsPermissionsRead, team)) { + defaultPage = 'settings'; + } + } + const pageName = props.match.params.page ?? defaultPage; const teamLoadingNav = getTeamLoadingNav(pageName as string); const navModel = getNavModel(state.navIndex, `team-${pageName}-${teamId}`, teamLoadingNav); - const team = getTeam(state.team, teamId); const members = getTeamMembers(state.team); return { @@ -81,7 +89,10 @@ export class TeamPages extends PureComponent { const { loadTeam, teamId } = this.props; this.setState({ isLoading: true }); const team = await loadTeam(teamId); - await this.props.loadTeamMembers(); + // With accesscontrol, the TeamPermissions will fetch team members + if (!contextSrv.accessControlEnabled()) { + await this.props.loadTeamMembers(); + } this.setState({ isLoading: false }); return team; } @@ -105,6 +116,10 @@ export class TeamPages extends PureComponent { }; hideTabsFromNonTeamAdmin = (navModel: NavModel, isSignedInUserTeamAdmin: boolean) => { + if (contextSrv.accessControlEnabled()) { + return navModel; + } + if (!isSignedInUserTeamAdmin && navModel.main && navModel.main.children) { navModel.main.children .filter((navItem) => !this.textsAreEqual(navItem.text, PageTypes.Members)) @@ -121,15 +136,34 @@ export class TeamPages extends PureComponent { const { members, team } = this.props; const currentPage = this.getCurrentPage(); + const canReadTeam = contextSrv.hasAccessInMetadata( + AccessControlAction.ActionTeamsRead, + team!, + isSignedInUserTeamAdmin + ); + const canReadTeamPermissions = contextSrv.hasAccessInMetadata( + AccessControlAction.ActionTeamsPermissionsRead, + team!, + isSignedInUserTeamAdmin + ); + const canWriteTeamPermissions = contextSrv.hasAccessInMetadata( + AccessControlAction.ActionTeamsPermissionsWrite, + team!, + isSignedInUserTeamAdmin + ); + switch (currentPage) { case PageTypes.Members: - return ; - + if (contextSrv.accessControlEnabled()) { + return ; + } else { + return ; + } case PageTypes.Settings: - return isSignedInUserTeamAdmin && ; + return canReadTeam && ; case PageTypes.GroupSync: - if (isSignedInUserTeamAdmin && isSyncEnabled) { - return ; + if (canReadTeamPermissions && isSyncEnabled) { + return ; } else if (config.featureToggles.featureHighlights) { return ( { + const canListUsers = contextSrv.hasPermission(AccessControlAction.OrgUsersRead); + const canSetPermissions = contextSrv.hasPermissionInMetadata( + AccessControlAction.ActionTeamsPermissionsWrite, + props.team + ); + + return ( + + ); +}; + +export default TeamPermissions; diff --git a/public/app/features/teams/TeamSettings.test.tsx b/public/app/features/teams/TeamSettings.test.tsx index b261116718e..4d4cfc0e83c 100644 --- a/public/app/features/teams/TeamSettings.test.tsx +++ b/public/app/features/teams/TeamSettings.test.tsx @@ -3,6 +3,12 @@ import { shallow } from 'enzyme'; import { Props, TeamSettings } from './TeamSettings'; import { getMockTeam } from './__mocks__/teamMocks'; +jest.mock('app/core/core', () => ({ + contextSrv: { + hasPermissionInMetadata: () => true, + }, +})); + const setup = (propOverrides?: object) => { const props: Props = { team: getMockTeam(), diff --git a/public/app/features/teams/TeamSettings.tsx b/public/app/features/teams/TeamSettings.tsx index 8ef7dd288f5..1d34aee41e9 100644 --- a/public/app/features/teams/TeamSettings.tsx +++ b/public/app/features/teams/TeamSettings.tsx @@ -4,7 +4,8 @@ import { Input, Field, Form, Button, FieldSet, VerticalGroup } from '@grafana/ui import { SharedPreferences } from 'app/core/components/SharedPreferences/SharedPreferences'; import { updateTeam } from './state/actions'; -import { Team } from 'app/types'; +import { AccessControlAction, Team } from 'app/types'; +import { contextSrv } from 'app/core/core'; const mapDispatchToProps = { updateTeam, @@ -18,6 +19,8 @@ interface OwnProps { export type Props = ConnectedProps & OwnProps; export const TeamSettings: FC = ({ team, updateTeam }) => { + const canWriteTeamSettings = contextSrv.hasPermissionInMetadata(AccessControlAction.ActionTeamsWrite, team); + return (
@@ -26,6 +29,7 @@ export const TeamSettings: FC = ({ team, updateTeam }) => { onSubmit={(formTeam: Team) => { updateTeam(formTeam.name, formTeam.email); }} + disabled={!canWriteTeamSettings} > {({ register }) => ( <> @@ -44,7 +48,7 @@ export const TeamSettings: FC = ({ team, updateTeam }) => { )}
- +
); }; diff --git a/public/app/features/teams/__snapshots__/TeamGroupSync.test.tsx.snap b/public/app/features/teams/__snapshots__/TeamGroupSync.test.tsx.snap index 35719d0908d..3abd26f7778 100644 --- a/public/app/features/teams/__snapshots__/TeamGroupSync.test.tsx.snap +++ b/public/app/features/teams/__snapshots__/TeamGroupSync.test.tsx.snap @@ -44,6 +44,7 @@ exports[`Render should render component 1`] = ` >
+ ); })} @@ -118,7 +118,11 @@ class UnThemedColorPickerPopover extends Reac const styles = getStyles(theme); return ( -
+ {/* + tabIndex=-1 is needed here to support highlighting text within the picker when using FocusScope + see https://github.com/adobe/react-spectrum/issues/1604#issuecomment-781574668 + */} +