Compare commits

..

6 Commits

Author SHA1 Message Date
Leonard Gram 57a57932af release 6.3.0-beta4 2019-08-02 14:24:40 +02:00
Torkel Ödegaard 62a226b1c3 Navigation: Fixed double settings menus (#18349)
(cherry picked from commit f3fb178efa)
2019-08-02 14:24:40 +02:00
Oleg Gaidarenko a38dcc3ac7 Build: allow bash to expand the wildcard (#18354)
If wildcards are in the quotes they not going to be expanded

(cherry picked from commit 89a4f30eab)
2019-08-02 14:24:40 +02:00
Torkel Ödegaard c39b0e246b Gauges: Fixes error when mappings array was undefined (#18353)
(cherry picked from commit 142c7eb0e6)
2019-08-02 14:24:40 +02:00
gotjosh f8f1f506ed Auth Proxy: Include additional headers as part of the cache key (#18298)
* Auth Proxy: Include additional headers as part of the cache key

Auth proxy has support to send additional user attributes as part of the
authentication flow. These attributes (e.g. Groups) need to be monitored
as part of the process in case of change.

This commit changes the way we compute the cache key to include all of the
attributes sent as part of the authentication request. That way, if we
change any user attributes we'll upsert the user information.

(cherry picked from commit ed8aeb2999)
2019-08-02 14:24:40 +02:00
Ryan McKinley 9d57a1f192 Plugins: avoid app importDashboards() NPE (#18128)
* tables display

* add missing file

* adding toolbar option

* adding toolbar option

* add items to index

* use root import path

* merge master

* show tables info

* add importDashboards code

* remove table changes

* remove table changes

* use deprecation warning

(cherry picked from commit 06b43f6d4b)
2019-08-02 14:24:40 +02:00
12 changed files with 186 additions and 179 deletions
+4 -8
View File
@@ -37,15 +37,11 @@ export class ConfigCtrl {
postUpdate() {
if (!this.appModel.enabled) {
return this.$q.resolve();
return;
}
return this.appEditCtrl.importDashboards().then(() => {
this.enabled = true;
return {
url: "plugins/raintank-kubernetes-app/page/clusters",
message: "Kubernetes App enabled!"
};
});
// TODO, whatever you want
console.log('Post Update:', this);
}
}
ConfigCtrl.templateUrl = 'components/config/config.html';
+1 -1
View File
@@ -5,7 +5,7 @@
"company": "Grafana Labs"
},
"name": "grafana",
"version": "6.3.0-beta3",
"version": "6.3.0-beta4",
"repository": {
"type": "git",
"url": "http://github.com/grafana/grafana.git"
@@ -1,4 +1,7 @@
export const deprecationWarning = (file: string, oldName: string, newName: string) => {
const message = `[Deprecation warning] ${file}: ${oldName} is deprecated. Use ${newName} instead`;
export const deprecationWarning = (file: string, oldName: string, newName?: string) => {
let message = `[Deprecation warning] ${file}: ${oldName} is deprecated`;
if (newName) {
message += `. Use ${newName} instead`;
}
console.warn(message);
};
+60 -105
View File
@@ -242,93 +242,69 @@ func (hs *HTTPServer) setIndexViewData(c *m.ReqContext) (*dtos.IndexViewData, er
}
}
if c.IsGrafanaAdmin || c.OrgRole == m.ROLE_ADMIN {
cfgNode := &dtos.NavLink{
Id: "cfg",
Text: "Configuration",
SubTitle: "Organization: " + c.OrgName,
Icon: "gicon gicon-cog",
Url: setting.AppSubUrl + "/datasources",
Children: []*dtos.NavLink{
{
Text: "Data Sources",
Icon: "gicon gicon-datasources",
Description: "Add and configure data sources",
Id: "datasources",
Url: setting.AppSubUrl + "/datasources",
},
{
Text: "Users",
Id: "users",
Description: "Manage org members",
Icon: "gicon gicon-user",
Url: setting.AppSubUrl + "/org/users",
},
{
Text: "Teams",
Id: "teams",
Description: "Manage org groups",
Icon: "gicon gicon-team",
Url: setting.AppSubUrl + "/org/teams",
},
{
Text: "Plugins",
Id: "plugins",
Description: "View and configure plugins",
Icon: "gicon gicon-plugins",
Url: setting.AppSubUrl + "/plugins",
},
{
Text: "Preferences",
Id: "org-settings",
Description: "Organization preferences",
Icon: "gicon gicon-preferences",
Url: setting.AppSubUrl + "/org",
},
configNodes := []*dtos.NavLink{}
{
Text: "API Keys",
Id: "apikeys",
Description: "Create & manage API keys",
Icon: "gicon gicon-apikeys",
Url: setting.AppSubUrl + "/org/apikeys",
},
},
}
if c.OrgRole != m.ROLE_ADMIN {
cfgNode = &dtos.NavLink{
Id: "cfg",
Text: "Configuration",
SubTitle: "Organization: " + c.OrgName,
Icon: "gicon gicon-cog",
Url: setting.AppSubUrl + "/admin/users",
Children: make([]*dtos.NavLink, 0),
}
}
data.NavTree = append(data.NavTree, cfgNode)
} else {
cfgNode := &dtos.NavLink{
Id: "cfg",
Text: "Configuration",
SubTitle: "Organization: " + c.OrgName,
Icon: "gicon gicon-cog",
Url: setting.AppSubUrl + "/plugins",
Children: []*dtos.NavLink{
{
Text: "Plugins",
Id: "plugins",
Description: "View and configure plugins",
Icon: "gicon gicon-plugins",
Url: setting.AppSubUrl + "/plugins",
},
},
}
data.NavTree = append(data.NavTree, cfgNode)
if c.OrgRole == m.ROLE_ADMIN {
configNodes = append(configNodes, &dtos.NavLink{
Text: "Data Sources",
Icon: "gicon gicon-datasources",
Description: "Add and configure data sources",
Id: "datasources",
Url: setting.AppSubUrl + "/datasources",
})
configNodes = append(configNodes, &dtos.NavLink{
Text: "Users",
Id: "users",
Description: "Manage org members",
Icon: "gicon gicon-user",
Url: setting.AppSubUrl + "/org/users",
})
}
if c.OrgRole == m.ROLE_ADMIN || hs.Cfg.EditorsCanAdmin {
configNodes = append(configNodes, &dtos.NavLink{
Text: "Teams",
Id: "teams",
Description: "Manage org groups",
Icon: "gicon gicon-team",
Url: setting.AppSubUrl + "/org/teams",
})
}
configNodes = append(configNodes, &dtos.NavLink{
Text: "Plugins",
Id: "plugins",
Description: "View and configure plugins",
Icon: "gicon gicon-plugins",
Url: setting.AppSubUrl + "/plugins",
})
if c.OrgRole == m.ROLE_ADMIN {
configNodes = append(configNodes, &dtos.NavLink{
Text: "Preferences",
Id: "org-settings",
Description: "Organization preferences",
Icon: "gicon gicon-preferences",
Url: setting.AppSubUrl + "/org",
})
configNodes = append(configNodes, &dtos.NavLink{
Text: "API Keys",
Id: "apikeys",
Description: "Create & manage API keys",
Icon: "gicon gicon-apikeys",
Url: setting.AppSubUrl + "/org/apikeys",
})
}
data.NavTree = append(data.NavTree, &dtos.NavLink{
Id: "cfg",
Text: "Configuration",
SubTitle: "Organization: " + c.OrgName,
Icon: "gicon gicon-cog",
Url: configNodes[0].Url,
Children: configNodes,
})
if c.IsGrafanaAdmin {
data.NavTree = append(data.NavTree, &dtos.NavLink{
Text: "Server Admin",
@@ -346,27 +322,6 @@ func (hs *HTTPServer) setIndexViewData(c *m.ReqContext) (*dtos.IndexViewData, er
})
}
if (c.OrgRole == m.ROLE_EDITOR || c.OrgRole == m.ROLE_VIEWER) && hs.Cfg.EditorsCanAdmin {
cfgNode := &dtos.NavLink{
Id: "cfg",
Text: "Configuration",
SubTitle: "Organization: " + c.OrgName,
Icon: "gicon gicon-cog",
Url: setting.AppSubUrl + "/org/teams",
Children: []*dtos.NavLink{
{
Text: "Teams",
Id: "teams",
Description: "Manage org groups",
Icon: "gicon gicon-team",
Url: setting.AppSubUrl + "/org/teams",
},
},
}
data.NavTree = append(data.NavTree, cfgNode)
}
data.NavTree = append(data.NavTree, &dtos.NavLink{
Text: "Help",
SubTitle: fmt.Sprintf(`%s v%s (%s)`, setting.ApplicationName, setting.BuildVersion, setting.BuildCommit),
+43 -19
View File
@@ -1,6 +1,7 @@
package authproxy
import (
"encoding/base32"
"fmt"
"net"
"net/mail"
@@ -32,6 +33,9 @@ var isLDAPEnabled = ldap.IsEnabled
// newLDAP creates multiple LDAP instance
var newLDAP = multildap.New
// supportedHeaders states the supported headers configuration fields
var supportedHeaderFields = []string{"Name", "Email", "Login", "Groups"}
// AuthProxy struct
type AuthProxy struct {
store *remotecache.RemoteCache
@@ -142,9 +146,18 @@ func (auth *AuthProxy) IsAllowedIP() (bool, *Error) {
return false, newError("Proxy authentication required", err)
}
// getKey forms a key for the cache
// 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.
func (auth *AuthProxy) getKey() string {
return fmt.Sprintf(CachePrefix, auth.header)
key := strings.TrimSpace(auth.header) // start the key with the main header
auth.headersIterator(func(_, header string) {
key = strings.Join([]string{key, header}, "-") // compose the key with any additional headers
})
hashedKey := base32.StdEncoding.EncodeToString([]byte(key))
return fmt.Sprintf(CachePrefix, hashedKey)
}
// Login logs in user id with whatever means possible
@@ -232,40 +245,36 @@ func (auth *AuthProxy) LoginViaHeader() (int64, error) {
AuthId: auth.header,
}
if auth.headerType == "username" {
switch auth.headerType {
case "username":
extUser.Login = auth.header
// only set Email if it can be parsed as an email address
emailAddr, emailErr := mail.ParseAddress(auth.header)
emailAddr, emailErr := mail.ParseAddress(auth.header) // only set Email if it can be parsed as an email address
if emailErr == nil {
extUser.Email = emailAddr.Address
}
} else if auth.headerType == "email" {
case "email":
extUser.Email = auth.header
extUser.Login = auth.header
} else {
default:
return 0, newError("Auth proxy header property invalid", nil)
}
for _, field := range []string{"Name", "Email", "Login", "Groups"} {
if auth.headers[field] == "" {
continue
auth.headersIterator(func(field string, header string) {
if field == "Groups" {
extUser.Groups = util.SplitString(header)
} else {
reflect.ValueOf(extUser).Elem().FieldByName(field).SetString(header)
}
if val := auth.ctx.Req.Header.Get(auth.headers[field]); val != "" {
if field == "Groups" {
extUser.Groups = util.SplitString(val)
} else {
reflect.ValueOf(extUser).Elem().FieldByName(field).SetString(val)
}
}
}
})
upsert := &models.UpsertUserCommand{
ReqContext: auth.ctx,
SignupAllowed: setting.AuthProxyAutoSignUp,
ExternalUser: extUser,
}
err := bus.Dispatch(upsert)
if err != nil {
return 0, err
@@ -274,6 +283,21 @@ func (auth *AuthProxy) LoginViaHeader() (int64, error) {
return upsert.Result.Id, nil
}
// headersIterator iterates over all non-empty supported additional headers
func (auth *AuthProxy) headersIterator(fn func(field string, header string)) {
for _, field := range supportedHeaderFields {
h := auth.headers[field]
if h == "" {
continue
}
if value := auth.ctx.Req.Header.Get(h); value != "" {
fn(field, strings.TrimSpace(value))
}
}
}
// GetSignedUser get full signed user info
func (auth *AuthProxy) GetSignedUser(userID int64) (*models.SignedInUser, *Error) {
query := &models.GetSignedInUserQuery{
+58 -34
View File
@@ -1,20 +1,20 @@
package authproxy
import (
"encoding/base32"
"errors"
"fmt"
"net/http"
"testing"
. "github.com/smartystreets/goconvey/convey"
"gopkg.in/macaron.v1"
"github.com/grafana/grafana/pkg/bus"
"github.com/grafana/grafana/pkg/infra/remotecache"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/ldap"
"github.com/grafana/grafana/pkg/services/multildap"
"github.com/grafana/grafana/pkg/setting"
. "github.com/smartystreets/goconvey/convey"
"gopkg.in/macaron.v1"
)
type TestMultiLDAP struct {
@@ -45,37 +45,70 @@ func (stub *TestMultiLDAP) User(login string) (
return result, nil
}
func prepareMiddleware(t *testing.T, req *http.Request, store *remotecache.RemoteCache) *AuthProxy {
t.Helper()
ctx := &models.ReqContext{
Context: &macaron.Context{
Req: macaron.Request{
Request: req,
},
},
}
auth := New(&Options{
Store: store,
Ctx: ctx,
OrgID: 4,
})
return auth
}
func TestMiddlewareContext(t *testing.T) {
Convey("auth_proxy helper", t, func() {
req, _ := http.NewRequest("POST", "http://example.com", nil)
setting.AuthProxyHeaderName = "X-Killa"
name := "markelog"
store := remotecache.NewFakeStore(t)
name := "markelog"
req.Header.Add(setting.AuthProxyHeaderName, name)
ctx := &models.ReqContext{
Context: &macaron.Context{
Req: macaron.Request{
Request: req,
},
},
}
Convey("when the cache only contains the main header", func() {
Convey("logs in user from the cache", func() {
store := remotecache.NewFakeStore(t)
key := fmt.Sprintf(CachePrefix, name)
store.Set(key, int64(33), 0)
Convey("with a simple cache key", func() {
// Set cache key
key := fmt.Sprintf(CachePrefix, base32.StdEncoding.EncodeToString([]byte(name)))
store.Set(key, int64(33), 0)
auth := New(&Options{
Store: store,
Ctx: ctx,
OrgID: 4,
// Set up the middleware
auth := prepareMiddleware(t, req, store)
id, err := auth.Login()
So(auth.getKey(), ShouldEqual, "auth-proxy-sync-ttl:NVQXE23FNRXWO===")
So(err, ShouldBeNil)
So(id, ShouldEqual, 33)
})
id, err := auth.Login()
Convey("when the cache key contains additional headers", func() {
setting.AuthProxyHeaders = map[string]string{"Groups": "X-WEBAUTH-GROUPS"}
group := "grafana-core-team"
req.Header.Add("X-WEBAUTH-GROUPS", group)
So(err, ShouldBeNil)
So(id, ShouldEqual, 33)
key := fmt.Sprintf(CachePrefix, base32.StdEncoding.EncodeToString([]byte(name+"-"+group)))
store.Set(key, int64(33), 0)
auth := prepareMiddleware(t, req, store)
id, err := auth.Login()
So(auth.getKey(), ShouldEqual, "auth-proxy-sync-ttl:NVQXE23FNRXWOLLHOJQWMYLOMEWWG33SMUWXIZLBNU======")
So(err, ShouldBeNil)
So(id, ShouldEqual, 33)
})
Convey("when the does not exist", func() {
})
})
Convey("LDAP", func() {
@@ -119,13 +152,9 @@ func TestMiddlewareContext(t *testing.T) {
store := remotecache.NewFakeStore(t)
server := New(&Options{
Store: store,
Ctx: ctx,
OrgID: 4,
})
auth := prepareMiddleware(t, req, store)
id, err := server.Login()
id, err := auth.Login()
So(err, ShouldBeNil)
So(id, ShouldEqual, 42)
@@ -149,11 +178,7 @@ func TestMiddlewareContext(t *testing.T) {
store := remotecache.NewFakeStore(t)
auth := New(&Options{
Store: store,
Ctx: ctx,
OrgID: 4,
})
auth := prepareMiddleware(t, req, store)
stub := &TestMultiLDAP{
ID: 42,
@@ -170,7 +195,6 @@ func TestMiddlewareContext(t *testing.T) {
So(id, ShouldNotEqual, 42)
So(stub.loginCalled, ShouldEqual, false)
})
})
})
}
+5 -1
View File
@@ -2,6 +2,7 @@ package middleware
import (
"context"
"encoding/base32"
"encoding/json"
"fmt"
"net/http"
@@ -377,7 +378,9 @@ func TestMiddlewareContext(t *testing.T) {
setting.LDAPEnabled = true
setting.AuthProxyHeaderName = "X-WEBAUTH-USER"
setting.AuthProxyHeaderProperty = "username"
setting.AuthProxyHeaders = map[string]string{"Groups": "X-WEBAUTH-GROUPS"}
name := "markelog"
group := "grafana-core-team"
middlewareScenario(t, "should not sync the user if it's in the cache", func(sc *scenarioContext) {
bus.AddHandler("test", func(query *models.GetSignedInUserQuery) error {
@@ -385,11 +388,12 @@ func TestMiddlewareContext(t *testing.T) {
return nil
})
key := fmt.Sprintf(cachePrefix, name)
key := fmt.Sprintf(cachePrefix, base32.StdEncoding.EncodeToString([]byte(name+"-"+group)))
sc.remoteCacheService.Set(key, int64(33), 0)
sc.fakeReq("GET", "/")
sc.req.Header.Add(setting.AuthProxyHeaderName, name)
sc.req.Header.Add("X-WEBAUTH-GROUPS", group)
sc.exec()
Convey("Should init user via cache", func() {
@@ -3,7 +3,7 @@ import React, { PureComponent } from 'react';
import cloneDeep from 'lodash/cloneDeep';
import extend from 'lodash/extend';
import { PluginMeta, AppPlugin, Button } from '@grafana/ui';
import { PluginMeta, AppPlugin, Button, deprecationWarning } from '@grafana/ui';
import { AngularComponent, getAngularLoader } from '@grafana/runtime';
import { getBackendSrv } from 'app/core/services/backend_srv';
@@ -124,6 +124,12 @@ export class AppConfigCtrlWrapper extends PureComponent<Props, State> {
this.postUpdateHook = callback;
};
// Stub to avoid unknown function in legacy code
importDashboards = (): Promise<void> => {
deprecationWarning('AppConfig', 'importDashboards()');
return Promise.resolve();
};
enable = () => {
this.model.enabled = true;
this.model.pinned = true;
-4
View File
@@ -9,10 +9,6 @@ export interface GaugeOptions extends SingleStatBaseOptions {
export const standardGaugeFieldOptions: FieldDisplayOptions = {
...standardFieldDisplayOptions,
defaults: {
min: 0,
max: 100,
},
};
export const defaults: GaugeOptions = {
@@ -13,12 +13,10 @@ export interface SingleStatOptions extends SingleStatBaseOptions {
prefixFontSize?: string;
valueFontSize?: string;
postfixFontSize?: string;
colorBackground?: boolean;
colorValue?: boolean;
colorPrefix?: boolean;
colorPostfix?: boolean;
sparkline: SparklineOptions;
}
@@ -32,6 +30,7 @@ export const standardFieldDisplayOptions: FieldDisplayOptions = {
{ value: -Infinity, color: 'green' },
{ value: 80, color: 'red' }, // 80%
],
mappings: [],
},
override: {},
};
+1 -1
View File
@@ -36,7 +36,7 @@ mkdir -p /deb-repo/db \
gsutil -m rsync -r -d "gs://$GCP_DB_BUCKET/$RELEASE_TYPE" /deb-repo/db
# Add the new release to the repo
cp "$DIST_PATH/*.deb" /deb-repo/tmp
cp "$DIST_PATH"/*.deb /deb-repo/tmp
rm /deb-repo/tmp/grafana_latest*.deb || true
aptly repo add "$REPO" /deb-repo/tmp #adds too many packages in enterprise
+1 -1
View File
@@ -33,7 +33,7 @@ mkdir -p /rpm-repo
gsutil -m rsync -r "$BUCKET" /rpm-repo
# Add the new release to the repo
cp "$DIST_PATH/*.rpm" /rpm-repo # adds to many files for enterprise
cp "$DIST_PATH"/*.rpm /rpm-repo # adds to many files for enterprise
rm /rpm-repo/grafana-latest-1*.rpm || true
createrepo /rpm-repo