Compare commits

...

4 Commits

Author SHA1 Message Date
Marcus Efraimsson
8e44bbc5f5 Release v6.7.4 2020-05-26 19:35:38 +02:00
Marcus Efraimsson
7031b922c6 Only allow 32 hexadecimal digits for the avatar hash 2020-05-26 19:35:38 +02:00
Arve Knudsen
a04ef6cefc 6.7.3 cherry-picks (#23808)
* AuthProxy: Fixes bug where long username could not be cached (#22926)

(cherry picked from commit 6c9d833602)

* Server: Exit with 0 if no error (#23312)

Make grafana-server exit with 0 if no error occurred.

(cherry picked from commit 5645d74cbc)

* Dashboard: Save json should preserve folderId (#23314)

(cherry picked from commit 7e3b43eabb)

* TimeSrv: Try to parse 8 and 15 digit numbers as timestamps if parsing as date fails (#21694)

* Try to parse 8 and 15 digit numbers as timestamps if parsing as date fails

Fixes #19738

* Add tests

(cherry picked from commit c89ad9b038)

* BackendSrv: include credentials when withCredentials option is set (#23380)

The fetch() API won't send cookies or other type of credentials unless
you set the credentials init option. Some datasources like Prometheus
and Elasticsearch have `withCredentials` option in Browser access mode,
but this option is not currently getting passed in the fetch() API.

Fixes #23338.

(cherry picked from commit afd8ffde69)

* Dashlist: Fixed dashlist broken in edit mode (#23426)

(cherry picked from commit 363bf7506d)

* Admin: Fix Synced via LDAP message for non-LDAP external users (#23477)

* UserAdmin: remove Synced via LDAP message for non-LDAP users

* UserAdmin: show "Synced via <provider>" message for external users

(cherry picked from commit 4d81cec34f)

* Graphite: Fixed cannot read finally of undefiend (#23512)

(cherry picked from commit 61460ea3a2)

* Hangouts: fixes notifications for alerts with empty message (#23559)

* Hangouts: fixes notifications for alerts with empty message

* Update pkg/services/alerting/notifiers/googlechat.go

Co-Authored-By: Marcus Efraimsson <marcus.efraimsson@gmail.com>

Co-authored-by: Marcus Efraimsson <marcus.efraimsson@gmail.com>
(cherry picked from commit 2661054fe8)

* Variables: fixes error when setting adhoc variables values (#23580)

(cherry picked from commit 0091885b13)

* Release 6.7.3

Signed-off-by: Arve Knudsen <arve.knudsen@gmail.com>

* ci-metrics-publisher.sh: Fix linting issue

Signed-off-by: Arve Knudsen <arve.knudsen@gmail.com>

* TablePanel: Fix XSS issue in header column rename (backport) (#23814)

* escaping html when rendering table header alias.

* fixed tooltip.

Co-authored-by: Marcus Andersson <marcus.andersson@grafana.com>

* Security: Fix annotation popup XSS vulnerability (#23813)

Co-authored-by: Marcus Efraimsson <marcus.efraimsson@gmail.com>
(cherry picked from commit 3955e8cbad)

Co-authored-by: Jon McKenzie <jcmcken@gmail.com>
Co-authored-by: Peter Holmberg <peterholmberg@users.noreply.github.com>
Co-authored-by: Jesse Tan <jessetan@users.noreply.github.com>
Co-authored-by: Tuan Anh Hoang-Vu <hvtuananh@gmail.com>
Co-authored-by: Torkel Ödegaard <torkel@grafana.com>
Co-authored-by: Alexander Zobnin <alexanderzobnin@gmail.com>
Co-authored-by: Hugo Häggmark <hugo.haggmark@grafana.com>
Co-authored-by: Marcus Andersson <marcus.andersson@grafana.com>
2020-04-23 12:12:53 +02:00
Sofia Papagiannaki
c5ea64c2c2 Fix CI for pushing a multi-architecture manifest (#23327) 2020-04-03 17:20:45 +03:00
26 changed files with 126 additions and 59 deletions

View File

@@ -48,7 +48,7 @@ jobs:
- run:
name: Install Grafana Build Pipeline
command: |
curl -fLO https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v0.1.0/grabpl
curl -fLO https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v0.1.1/grabpl
chmod +x grabpl
mkdir bin
mv grabpl bin/

View File

@@ -2,5 +2,5 @@
"npmClient": "yarn",
"useWorkspaces": true,
"packages": ["packages/*"],
"version": "6.7.2"
"version": "6.7.4"
}

View File

@@ -3,7 +3,7 @@
"license": "Apache-2.0",
"private": true,
"name": "grafana",
"version": "6.7.2",
"version": "6.7.4",
"repository": "github:grafana/grafana",
"devDependencies": {
"@babel/core": "7.8.4",

View File

@@ -2,7 +2,7 @@
"author": "Grafana Labs",
"license": "Apache-2.0",
"name": "@grafana/data",
"version": "6.7.2",
"version": "6.7.4",
"description": "Grafana Data Library",
"keywords": [
"typescript"

View File

@@ -2,7 +2,7 @@
"author": "Grafana Labs",
"license": "Apache-2.0",
"name": "@grafana/e2e",
"version": "6.7.2",
"version": "6.7.4",
"description": "Grafana End-to-End Test Library",
"keywords": [
"cli",

View File

@@ -2,7 +2,7 @@
"author": "Grafana Labs",
"license": "Apache-2.0",
"name": "@grafana/runtime",
"version": "6.7.2",
"version": "6.7.4",
"description": "Grafana Runtime Library",
"keywords": [
"grafana",
@@ -23,8 +23,8 @@
"typecheck": "tsc --noEmit"
},
"dependencies": {
"@grafana/data": "6.7.2",
"@grafana/ui": "6.7.2",
"@grafana/data": "6.7.4",
"@grafana/ui": "6.7.4",
"systemjs": "0.20.19",
"systemjs-plugin-css": "0.1.37"
},

View File

@@ -2,7 +2,7 @@
"author": "Grafana Labs",
"license": "Apache-2.0",
"name": "@grafana/toolkit",
"version": "6.7.2",
"version": "6.7.4",
"description": "Grafana Toolkit",
"keywords": [
"grafana",
@@ -29,10 +29,10 @@
"dependencies": {
"@babel/core": "7.9.0",
"@babel/preset-env": "7.9.0",
"@grafana/data": "6.7.2",
"@grafana/data": "6.7.4",
"@grafana/eslint-config": "^1.0.0-rc1",
"@grafana/tsconfig": "^1.0.0-rc1",
"@grafana/ui": "6.7.2",
"@grafana/ui": "6.7.4",
"@types/command-exists": "^1.2.0",
"@types/execa": "^0.9.0",
"@types/expect-puppeteer": "3.3.1",

View File

@@ -2,7 +2,7 @@
"author": "Grafana Labs",
"license": "Apache-2.0",
"name": "@grafana/ui",
"version": "6.7.2",
"version": "6.7.4",
"description": "Grafana Components Library",
"keywords": [
"grafana",
@@ -28,7 +28,7 @@
},
"dependencies": {
"@emotion/core": "^10.0.27",
"@grafana/data": "6.7.2",
"@grafana/data": "6.7.4",
"@grafana/slate-react": "0.22.9-grafana",
"@grafana/tsconfig": "^1.0.0-rc1",
"@torkelo/react-select": "3.0.8",

View File

@@ -15,14 +15,14 @@ import (
"net/http"
"net/url"
"path/filepath"
"regexp"
"strconv"
"strings"
"sync"
"time"
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/setting"
"gopkg.in/macaron.v1"
gocache "github.com/patrickmn/go-cache"
)
@@ -73,9 +73,15 @@ type CacheServer struct {
cache *gocache.Cache
}
func (this *CacheServer) Handler(ctx *macaron.Context) {
urlPath := ctx.Req.URL.Path
hash := urlPath[strings.LastIndex(urlPath, "/")+1:]
var validMD5 = regexp.MustCompile("^[a-fA-F0-9]{32}$")
func (this *CacheServer) Handler(ctx *models.ReqContext) {
hash := ctx.Params("hash")
if len(hash) != 32 || !validMD5.MatchString(hash) {
ctx.JsonApiErr(404, "Avatar not found", nil)
return
}
var avatar *Avatar
obj, exists := this.cache.Get(hash)

View File

@@ -115,8 +115,10 @@ func main() {
go listenToSystemSignals(server)
err := server.Run()
code := server.ExitCode(err)
code := 0
if err != nil {
code = server.ExitCode(err)
}
trace.Stop()
log.Close()

View File

@@ -1,8 +1,9 @@
package authproxy
import (
"encoding/base32"
"encoding/hex"
"fmt"
"hash/fnv"
"net"
"net/mail"
"reflect"
@@ -146,6 +147,13 @@ func (auth *AuthProxy) IsAllowedIP() (bool, *Error) {
return false, newError("Proxy authentication required", err)
}
func HashCacheKey(key string) string {
hasher := fnv.New128a()
// according to the documentation, Hash.Write cannot error, but linter is complaining
hasher.Write([]byte(key)) // nolint: errcheck
return hex.EncodeToString(hasher.Sum(nil))
}
// getKey forms a key for the cache based on the headers received as part of the authentication flow.
// Our configuration supports multiple headers. The main header contains the email or username.
// And the additional ones that allow us to specify extra attributes: Name, Email or Groups.
@@ -156,7 +164,7 @@ func (auth *AuthProxy) getKey() string {
key = strings.Join([]string{key, header}, "-") // compose the key with any additional headers
})
hashedKey := base32.StdEncoding.EncodeToString([]byte(key))
hashedKey := HashCacheKey(key)
return fmt.Sprintf(CachePrefix, hashedKey)
}

View File

@@ -1,7 +1,6 @@
package authproxy
import (
"encoding/base32"
"errors"
"fmt"
"net/http"
@@ -79,7 +78,7 @@ func TestMiddlewareContext(t *testing.T) {
Convey("with a simple cache key", func() {
// Set cache key
key := fmt.Sprintf(CachePrefix, base32.StdEncoding.EncodeToString([]byte(name)))
key := fmt.Sprintf(CachePrefix, HashCacheKey(name))
err := store.Set(key, int64(33), 0)
So(err, ShouldBeNil)
@@ -88,7 +87,7 @@ func TestMiddlewareContext(t *testing.T) {
id, err := auth.Login()
So(err, ShouldBeNil)
So(auth.getKey(), ShouldEqual, "auth-proxy-sync-ttl:NVQXE23FNRXWO===")
So(auth.getKey(), ShouldEqual, "auth-proxy-sync-ttl:0a7f3374e9659b10980fd66247b0cf2f")
So(id, ShouldEqual, 33)
})
@@ -97,7 +96,7 @@ func TestMiddlewareContext(t *testing.T) {
group := "grafana-core-team"
req.Header.Add("X-WEBAUTH-GROUPS", group)
key := fmt.Sprintf(CachePrefix, base32.StdEncoding.EncodeToString([]byte(name+"-"+group)))
key := fmt.Sprintf(CachePrefix, HashCacheKey(name+"-"+group))
err := store.Set(key, int64(33), 0)
So(err, ShouldBeNil)
@@ -105,7 +104,7 @@ func TestMiddlewareContext(t *testing.T) {
id, err := auth.Login()
So(err, ShouldBeNil)
So(auth.getKey(), ShouldEqual, "auth-proxy-sync-ttl:NVQXE23FNRXWOLLHOJQWMYLOMEWWG33SMUWXIZLBNU======")
So(auth.getKey(), ShouldEqual, "auth-proxy-sync-ttl:14f69b7023baa0ac98c96b31cec07bc0")
So(id, ShouldEqual, 33)
})

View File

@@ -2,7 +2,6 @@ package middleware
import (
"context"
"encoding/base32"
"errors"
"fmt"
"net/http"
@@ -364,7 +363,7 @@ func TestMiddlewareContext(t *testing.T) {
return nil
})
key := fmt.Sprintf(authproxy.CachePrefix, base32.StdEncoding.EncodeToString([]byte(name+"-"+group)))
key := fmt.Sprintf(authproxy.CachePrefix, authproxy.HashCacheKey(name+"-"+group))
err := sc.remoteCacheService.Set(key, int64(33), 0)
So(err, ShouldBeNil)
sc.fakeReq("GET", "/")

View File

@@ -126,13 +126,15 @@ func (gcn *GoogleChatNotifier) Notify(evalContext *alerting.EvalContext) error {
gcn.log.Error("evalContext returned an invalid rule URL")
}
// add a text paragraph widget for the message
widgets := []widget{
textParagraphWidget{
widgets := []widget{}
if len(evalContext.Rule.Message) > 0 {
// add a text paragraph widget for the message if there is a message
// Google Chat API doesn't accept an empty text property
widgets = append(widgets, textParagraphWidget{
Text: text{
Text: evalContext.Rule.Message,
},
},
})
}
// add a text paragraph widget for the fields

View File

@@ -109,7 +109,7 @@ export class FolderPicker extends PureComponent<Props, State> {
let folder: SelectableValue<number> = { value: -1 };
if (initialFolderId !== undefined && initialFolderId > -1) {
if (initialFolderId !== undefined && initialFolderId !== null && initialFolderId > -1) {
folder = options.find(option => option.value === initialFolderId) || { value: -1 };
} else if (enableReset && initialTitle) {
folder = resetFolder;

View File

@@ -27,16 +27,17 @@ describe('parseUrlFromOptions', () => {
describe('parseInitFromOptions', () => {
it.each`
method | data | expected
${undefined} | ${undefined} | ${{ method: undefined, headers: { map: { accept: 'application/json, text/plain, */*' } }, body: undefined }}
${'GET'} | ${undefined} | ${{ method: 'GET', headers: { map: { accept: 'application/json, text/plain, */*' } }, body: undefined }}
${'POST'} | ${{ id: '0' }} | ${{ method: 'POST', headers: { map: { 'content-type': 'application/json', accept: 'application/json, text/plain, */*' } }, body: '{"id":"0"}' }}
${'PUT'} | ${{ id: '0' }} | ${{ method: 'PUT', headers: { map: { 'content-type': 'application/json', accept: 'application/json, text/plain, */*' } }, body: '{"id":"0"}' }}
${'monkey'} | ${undefined} | ${{ method: 'monkey', headers: { map: { accept: 'application/json, text/plain, */*' } }, body: undefined }}
method | data | withCredentials | expected
${undefined} | ${undefined} | ${undefined} | ${{ method: undefined, headers: { map: { accept: 'application/json, text/plain, */*' } }, body: undefined }}
${'GET'} | ${undefined} | ${undefined} | ${{ method: 'GET', headers: { map: { accept: 'application/json, text/plain, */*' } }, body: undefined }}
${'POST'} | ${{ id: '0' }} | ${undefined} | ${{ method: 'POST', headers: { map: { 'content-type': 'application/json', accept: 'application/json, text/plain, */*' } }, body: '{"id":"0"}' }}
${'PUT'} | ${{ id: '0' }} | ${undefined} | ${{ method: 'PUT', headers: { map: { 'content-type': 'application/json', accept: 'application/json, text/plain, */*' } }, body: '{"id":"0"}' }}
${'monkey'} | ${undefined} | ${undefined} | ${{ method: 'monkey', headers: { map: { accept: 'application/json, text/plain, */*' } }, body: undefined }}
${'GET'} | ${undefined} | ${true} | ${{ method: 'GET', headers: { map: { accept: 'application/json, text/plain, */*' } }, body: undefined, credentials: 'include' }}
`(
"when called with method: '$method' and data: '$data' then result should be '$expected'",
({ method, data, expected }) => {
expect(parseInitFromOptions({ method, data, url: '' })).toEqual(expected);
"when called with method: '$method', data: '$data' and withCredentials: '$withCredentials' then result should be '$expected'",
({ method, data, withCredentials, expected }) => {
expect(parseInitFromOptions({ method, data, withCredentials, url: '' })).toEqual(expected);
}
);
});

View File

@@ -7,6 +7,15 @@ export const parseInitFromOptions = (options: BackendSrvRequest): RequestInit =>
const isAppJson = isContentTypeApplicationJson(headers);
const body = parseBody(options, isAppJson);
if (options?.withCredentials) {
return {
method,
headers,
body,
credentials: 'include',
};
}
return {
method,
headers,

View File

@@ -82,7 +82,8 @@ export class UserProfile extends PureComponent<Props, State> {
render() {
const { user } = this.props;
const { showDeleteModal, showDisableModal } = this.state;
const lockMessage = 'Synced via LDAP';
const authSource = user.authLabels?.length && user.authLabels[0];
const lockMessage = authSource ? `Synced via ${authSource}` : '';
const styles = getStyles(config.theme);
return (

View File

@@ -72,7 +72,7 @@ export function annotationTooltipDirective(
tooltip += '<div class="graph-annotation__body">';
if (text) {
tooltip += '<div>' + sanitizeString(text.replace(/\n/g, '<br>')) + '</div>';
tooltip += '<div ng-non-bindable>' + sanitizeString(text.replace(/\n/g, '<br>')) + '</div>';
}
const tags = event.tags;

View File

@@ -79,7 +79,10 @@ export class DashboardSrv {
};
saveJSONDashboard(json: string) {
return getBackendSrv().saveDashboard(JSON.parse(json), {});
const parsedJson = JSON.parse(json);
return getBackendSrv().saveDashboard(parsedJson, {
folderId: this.dashboard.meta.folderId || parsedJson.folderId,
});
}
starDashboard(dashboardId: string, isStarred: any) {

View File

@@ -135,6 +135,38 @@ describe('timeSrv', () => {
expect(time.to.valueOf()).toEqual(1410337665699);
});
it('should handle epochs that look like formatted date without time', () => {
location = {
search: jest.fn(() => ({
from: '20149999',
to: '20159999',
})),
};
timeSrv = new TimeSrv(rootScope as any, jest.fn() as any, location as any, timer, new ContextSrvStub() as any);
timeSrv.init(_dashboard);
const time = timeSrv.timeRange();
expect(time.from.valueOf()).toEqual(20149999);
expect(time.to.valueOf()).toEqual(20159999);
});
it('should handle epochs that look like formatted date', () => {
location = {
search: jest.fn(() => ({
from: '201499991234567',
to: '201599991234567',
})),
};
timeSrv = new TimeSrv(rootScope as any, jest.fn() as any, location as any, timer, new ContextSrvStub() as any);
timeSrv.init(_dashboard);
const time = timeSrv.timeRange();
expect(time.from.valueOf()).toEqual(201499991234567);
expect(time.to.valueOf()).toEqual(201599991234567);
});
it('should handle bad dates', () => {
location = {
search: jest.fn(() => ({

View File

@@ -102,10 +102,15 @@ export class TimeSrv {
return value;
}
if (value.length === 8) {
return toUtc(value, 'YYYYMMDD');
}
if (value.length === 15) {
return toUtc(value, 'YYYYMMDDTHHmmss');
const utcValue = toUtc(value, 'YYYYMMDD');
if (utcValue.isValid()) {
return utcValue;
}
} else if (value.length === 15) {
const utcValue = toUtc(value, 'YYYYMMDDTHHmmss');
if (utcValue.isValid()) {
return utcValue;
}
}
if (!isNaN(value)) {

View File

@@ -3,7 +3,7 @@ import angular, { auto, ILocationService, IPromise, IQService } from 'angular';
import _ from 'lodash';
// Utils & Services
import coreModule from 'app/core/core_module';
import { variableTypes } from './variable';
import { VariableActions, variableTypes } from './variable';
import { Graph } from 'app/core/utils/dag';
import { TemplateSrv } from 'app/features/templating/template_srv';
import { TimeSrv } from 'app/features/dashboard/services/TimeSrv';
@@ -344,9 +344,9 @@ export class VariableSrv {
}
}
isVariableUrlValueDifferentFromCurrent(variable: any, urlValue: any) {
isVariableUrlValueDifferentFromCurrent(variable: VariableActions, urlValue: any) {
// lodash _.isEqual handles array of value equality checks as well
return !_.isEqual(variable.current.value, urlValue);
return !_.isEqual(variable.getValueForUrl(), urlValue);
}
updateUrlParamsWithCurrentVariables() {

View File

@@ -75,12 +75,12 @@ export class GraphiteQueryCtrl extends QueryCtrl {
checkOtherSegments(fromIndex: number, modifyLastSegment = true) {
if (this.queryModel.segments.length === 1 && this.queryModel.segments[0].type === 'series-ref') {
return;
return Promise.resolve();
}
if (fromIndex === 0) {
this.addSelectMetricSegment();
return;
return Promise.resolve();
}
const path = this.queryModel.getSegmentPathUpTo(fromIndex + 1);

View File

@@ -14,7 +14,7 @@ import {
import { TemplateSrv } from 'app/features/templating/template_srv';
import { ColumnRender, TableRenderModel, ColumnStyle } from './types';
import { ColumnOptionsCtrl } from './column_options';
import { sanitizeUrl } from 'app/core/utils/text';
import { sanitizeUrl, escapeHtml } from 'app/core/utils/text';
export class TableRenderer {
formatters: any[];
@@ -56,7 +56,7 @@ export class TableRenderer {
column.style = style;
if (style.alias) {
column.title = column.text.replace(regex, style.alias);
column.title = escapeHtml(column.text.replace(regex, style.alias));
}
break;
@@ -300,7 +300,7 @@ export class TableRenderer {
const cellLink = this.templateSrv.replace(column.style.linkUrl, scopedVars, encodeURIComponent);
const sanitizedCellLink = sanitizeUrl(cellLink);
const cellLinkTooltip = this.templateSrv.replace(column.style.linkTooltip, scopedVars);
const cellLinkTooltip = escapeHtml(this.templateSrv.replace(column.style.linkTooltip, scopedVars));
const cellTarget = column.style.linkTargetBlank ? '_blank' : '';
cellClasses.push('table-panel-cell-link');

View File

@@ -8,7 +8,7 @@ for ((i = 1; i <= $#; i++ )); do
remainder="${!i}"
# Find everything until last = character (= is included in the result)
# This allows to add tags to metric names
metricName=$(grep -o "\(.*\)=" <<< $remainder)
metricName=$(grep -o "\(.*\)=" <<< "$remainder")
# Get the metric value
value=${remainder#"$metricName"}
# Remove remaining = character from metric name