mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Auth: You can now authenicate against api with username / password using basic auth, Closes #2218
This commit is contained in:
@@ -19,6 +19,7 @@
|
|||||||
- [Issue #2088](https://github.com/grafana/grafana/issues/2088). Roles: New user role `Read Only Editor` that replaces the old `Viewer` role behavior
|
- [Issue #2088](https://github.com/grafana/grafana/issues/2088). Roles: New user role `Read Only Editor` that replaces the old `Viewer` role behavior
|
||||||
|
|
||||||
**Backend**
|
**Backend**
|
||||||
|
- [Issue #2218](https://github.com/grafana/grafana/issues/2218). Auth: You can now authenicate against api with username / password using basic auth
|
||||||
- [Issue #2095](https://github.com/grafana/grafana/issues/2095). Search: Search now supports filtering by multiple dashboard tags
|
- [Issue #2095](https://github.com/grafana/grafana/issues/2095). Search: Search now supports filtering by multiple dashboard tags
|
||||||
- [Issue #1905](https://github.com/grafana/grafana/issues/1905). Github OAuth: You can now configure a Github team membership requirement, thx @dewski
|
- [Issue #1905](https://github.com/grafana/grafana/issues/1905). Github OAuth: You can now configure a Github team membership requirement, thx @dewski
|
||||||
- [Issue #2052](https://github.com/grafana/grafana/issues/2052). Github OAuth: You can now configure a Github organization requirement, thx @indrekj
|
- [Issue #2052](https://github.com/grafana/grafana/issues/2052). Github OAuth: You can now configure a Github organization requirement, thx @indrekj
|
||||||
|
|||||||
@@ -168,6 +168,10 @@ token_url = https://accounts.google.com/o/oauth2/token
|
|||||||
api_url = https://www.googleapis.com/oauth2/v1/userinfo
|
api_url = https://www.googleapis.com/oauth2/v1/userinfo
|
||||||
allowed_domains =
|
allowed_domains =
|
||||||
|
|
||||||
|
#################################### Basic Auth ##########################
|
||||||
|
[auth.basic]
|
||||||
|
enabled = true
|
||||||
|
|
||||||
#################################### Auth Proxy ##########################
|
#################################### Auth Proxy ##########################
|
||||||
[auth.proxy]
|
[auth.proxy]
|
||||||
enabled = false
|
enabled = false
|
||||||
|
|||||||
@@ -174,6 +174,10 @@
|
|||||||
;header_property = username
|
;header_property = username
|
||||||
;auto_sign_up = true
|
;auto_sign_up = true
|
||||||
|
|
||||||
|
#################################### Basic Auth ##########################
|
||||||
|
[auth.basic]
|
||||||
|
;enabled = true
|
||||||
|
|
||||||
#################################### SMTP / Emailing ##########################
|
#################################### SMTP / Emailing ##########################
|
||||||
[smtp]
|
[smtp]
|
||||||
;enabled = false
|
;enabled = false
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import (
|
|||||||
"github.com/grafana/grafana/pkg/metrics"
|
"github.com/grafana/grafana/pkg/metrics"
|
||||||
m "github.com/grafana/grafana/pkg/models"
|
m "github.com/grafana/grafana/pkg/models"
|
||||||
"github.com/grafana/grafana/pkg/setting"
|
"github.com/grafana/grafana/pkg/setting"
|
||||||
|
"github.com/grafana/grafana/pkg/util"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Context struct {
|
type Context struct {
|
||||||
@@ -40,6 +41,7 @@ func GetContextHandler() macaron.Handler {
|
|||||||
// then look for api key in session (special case for render calls via api)
|
// then look for api key in session (special case for render calls via api)
|
||||||
// then test if anonymous access is enabled
|
// then test if anonymous access is enabled
|
||||||
if initContextWithApiKey(ctx) ||
|
if initContextWithApiKey(ctx) ||
|
||||||
|
initContextWithBasicAuth(ctx) ||
|
||||||
initContextWithAuthProxy(ctx) ||
|
initContextWithAuthProxy(ctx) ||
|
||||||
initContextWithUserSessionCookie(ctx) ||
|
initContextWithUserSessionCookie(ctx) ||
|
||||||
initContextWithApiKeyFromSession(ctx) ||
|
initContextWithApiKeyFromSession(ctx) ||
|
||||||
@@ -128,6 +130,47 @@ func initContextWithApiKey(ctx *Context) bool {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func initContextWithBasicAuth(ctx *Context) bool {
|
||||||
|
if !setting.BasicAuthEnabled {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
header := ctx.Req.Header.Get("Authorization")
|
||||||
|
if header == "" {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
username, password, err := util.DecodeBasicAuthHeader(header)
|
||||||
|
if err != nil {
|
||||||
|
ctx.JsonApiErr(401, "Invalid Basic Auth Header", err)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
loginQuery := m.GetUserByLoginQuery{LoginOrEmail: username}
|
||||||
|
if err := bus.Dispatch(&loginQuery); err != nil {
|
||||||
|
ctx.JsonApiErr(401, "Basic auth failed", err)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
user := loginQuery.Result
|
||||||
|
|
||||||
|
// validate password
|
||||||
|
if util.EncodePassword(password, user.Salt) != user.Password {
|
||||||
|
ctx.JsonApiErr(401, "Invalid username or password", nil)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
query := m.GetSignedInUserQuery{UserId: user.Id}
|
||||||
|
if err := bus.Dispatch(&query); err != nil {
|
||||||
|
ctx.JsonApiErr(401, "Authentication error", err)
|
||||||
|
return true
|
||||||
|
} else {
|
||||||
|
ctx.SignedInUser = query.Result
|
||||||
|
ctx.IsSignedIn = true
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// special case for panel render calls with api key
|
// special case for panel render calls with api key
|
||||||
func initContextWithApiKeyFromSession(ctx *Context) bool {
|
func initContextWithApiKeyFromSession(ctx *Context) bool {
|
||||||
keyId := ctx.Session.Get(SESS_KEY_APIKEY)
|
keyId := ctx.Session.Get(SESS_KEY_APIKEY)
|
||||||
|
|||||||
@@ -48,6 +48,32 @@ func TestMiddlewareContext(t *testing.T) {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
middlewareScenario("Using basic auth", func(sc *scenarioContext) {
|
||||||
|
|
||||||
|
bus.AddHandler("test", func(query *m.GetUserByLoginQuery) error {
|
||||||
|
query.Result = &m.User{
|
||||||
|
Password: util.EncodePassword("myPass", "salt"),
|
||||||
|
Salt: "salt",
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
|
||||||
|
bus.AddHandler("test", func(query *m.GetSignedInUserQuery) error {
|
||||||
|
query.Result = &m.SignedInUser{OrgId: 2, UserId: 12}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
|
||||||
|
setting.BasicAuthEnabled = true
|
||||||
|
authHeader := util.GetBasicAuthHeader("myUser", "myPass")
|
||||||
|
sc.fakeReq("GET", "/").withAuthoriziationHeader(authHeader).exec()
|
||||||
|
|
||||||
|
Convey("Should init middleware context with user", func() {
|
||||||
|
So(sc.context.IsSignedIn, ShouldEqual, true)
|
||||||
|
So(sc.context.OrgId, ShouldEqual, 2)
|
||||||
|
So(sc.context.UserId, ShouldEqual, 12)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
middlewareScenario("Valid api key", func(sc *scenarioContext) {
|
middlewareScenario("Valid api key", func(sc *scenarioContext) {
|
||||||
keyhash := util.EncodePassword("v5nAwpMafFP6znaS4urhdWDLS5511M42", "asd")
|
keyhash := util.EncodePassword("v5nAwpMafFP6znaS4urhdWDLS5511M42", "asd")
|
||||||
|
|
||||||
@@ -223,6 +249,7 @@ type scenarioContext struct {
|
|||||||
context *Context
|
context *Context
|
||||||
resp *httptest.ResponseRecorder
|
resp *httptest.ResponseRecorder
|
||||||
apiKey string
|
apiKey string
|
||||||
|
authHeader string
|
||||||
respJson map[string]interface{}
|
respJson map[string]interface{}
|
||||||
handlerFunc handlerFunc
|
handlerFunc handlerFunc
|
||||||
defaultHandler macaron.Handler
|
defaultHandler macaron.Handler
|
||||||
@@ -240,6 +267,11 @@ func (sc *scenarioContext) withInvalidApiKey() *scenarioContext {
|
|||||||
return sc
|
return sc
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (sc *scenarioContext) withAuthoriziationHeader(authHeader string) *scenarioContext {
|
||||||
|
sc.authHeader = authHeader
|
||||||
|
return sc
|
||||||
|
}
|
||||||
|
|
||||||
func (sc *scenarioContext) fakeReq(method, url string) *scenarioContext {
|
func (sc *scenarioContext) fakeReq(method, url string) *scenarioContext {
|
||||||
sc.resp = httptest.NewRecorder()
|
sc.resp = httptest.NewRecorder()
|
||||||
req, err := http.NewRequest(method, url, nil)
|
req, err := http.NewRequest(method, url, nil)
|
||||||
@@ -266,6 +298,10 @@ func (sc *scenarioContext) exec() {
|
|||||||
sc.req.Header.Add("Authorization", "Bearer "+sc.apiKey)
|
sc.req.Header.Add("Authorization", "Bearer "+sc.apiKey)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if sc.authHeader != "" {
|
||||||
|
sc.req.Header.Add("Authorization", sc.authHeader)
|
||||||
|
}
|
||||||
|
|
||||||
sc.m.ServeHTTP(sc.resp, sc.req)
|
sc.m.ServeHTTP(sc.resp, sc.req)
|
||||||
|
|
||||||
if sc.resp.Header().Get("Content-Type") == "application/json; charset=UTF-8" {
|
if sc.resp.Header().Get("Content-Type") == "application/json; charset=UTF-8" {
|
||||||
|
|||||||
@@ -94,6 +94,9 @@ var (
|
|||||||
AuthProxyHeaderProperty string
|
AuthProxyHeaderProperty string
|
||||||
AuthProxyAutoSignUp bool
|
AuthProxyAutoSignUp bool
|
||||||
|
|
||||||
|
// Basic Auth
|
||||||
|
BasicAuthEnabled bool
|
||||||
|
|
||||||
// Session settings.
|
// Session settings.
|
||||||
SessionOptions session.Options
|
SessionOptions session.Options
|
||||||
|
|
||||||
@@ -398,6 +401,9 @@ func NewConfigContext(args *CommandLineArgs) {
|
|||||||
AuthProxyHeaderProperty = authProxy.Key("header_property").String()
|
AuthProxyHeaderProperty = authProxy.Key("header_property").String()
|
||||||
AuthProxyAutoSignUp = authProxy.Key("auto_sign_up").MustBool(true)
|
AuthProxyAutoSignUp = authProxy.Key("auto_sign_up").MustBool(true)
|
||||||
|
|
||||||
|
authBasic := Cfg.Section("auth.basic")
|
||||||
|
AuthProxyEnabled = authBasic.Key("enabled").MustBool(true)
|
||||||
|
|
||||||
// PhantomJS rendering
|
// PhantomJS rendering
|
||||||
ImagesDir = filepath.Join(DataPath, "png")
|
ImagesDir = filepath.Join(DataPath, "png")
|
||||||
PhantomDir = filepath.Join(HomePath, "vendor/phantomjs")
|
PhantomDir = filepath.Join(HomePath, "vendor/phantomjs")
|
||||||
|
|||||||
@@ -7,8 +7,10 @@ import (
|
|||||||
"crypto/sha256"
|
"crypto/sha256"
|
||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
"encoding/hex"
|
"encoding/hex"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"hash"
|
"hash"
|
||||||
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
// source: https://github.com/gogits/gogs/blob/9ee80e3e5426821f03a4e99fad34418f5c736413/modules/base/tool.go#L58
|
// source: https://github.com/gogits/gogs/blob/9ee80e3e5426821f03a4e99fad34418f5c736413/modules/base/tool.go#L58
|
||||||
@@ -80,3 +82,23 @@ func GetBasicAuthHeader(user string, password string) string {
|
|||||||
var userAndPass = user + ":" + password
|
var userAndPass = user + ":" + password
|
||||||
return "Basic " + base64.StdEncoding.EncodeToString([]byte(userAndPass))
|
return "Basic " + base64.StdEncoding.EncodeToString([]byte(userAndPass))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func DecodeBasicAuthHeader(header string) (string, string, error) {
|
||||||
|
var code string
|
||||||
|
parts := strings.SplitN(header, " ", 2)
|
||||||
|
if len(parts) == 2 && parts[0] == "Basic" {
|
||||||
|
code = parts[1]
|
||||||
|
}
|
||||||
|
|
||||||
|
decoded, err := base64.StdEncoding.DecodeString(code)
|
||||||
|
if err != nil {
|
||||||
|
return "", "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
userAndPass := strings.SplitN(string(decoded), ":", 2)
|
||||||
|
if len(userAndPass) != 2 {
|
||||||
|
return "", "", errors.New("Invalid basic auth header")
|
||||||
|
}
|
||||||
|
|
||||||
|
return userAndPass[0], userAndPass[1], nil
|
||||||
|
}
|
||||||
|
|||||||
@@ -13,4 +13,14 @@ func TestEncoding(t *testing.T) {
|
|||||||
|
|
||||||
So(result, ShouldEqual, "Basic Z3JhZmFuYToxMjM0")
|
So(result, ShouldEqual, "Basic Z3JhZmFuYToxMjM0")
|
||||||
})
|
})
|
||||||
|
|
||||||
|
Convey("When decoding basic auth header", t, func() {
|
||||||
|
header := GetBasicAuthHeader("grafana", "1234")
|
||||||
|
username, password, err := DecodeBasicAuthHeader(header)
|
||||||
|
So(err, ShouldBeNil)
|
||||||
|
|
||||||
|
So(username, ShouldEqual, "grafana")
|
||||||
|
So(password, ShouldEqual, "1234")
|
||||||
|
})
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user