From ae0f8c77d16840bf4526cab68da54460b043db9b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Tue, 30 Jun 2015 09:37:52 +0200 Subject: [PATCH] Auth: You can now authenicate against api with username / password using basic auth, Closes #2218 --- CHANGELOG.md | 1 + conf/defaults.ini | 4 +++ conf/sample.ini | 4 +++ pkg/middleware/middleware.go | 43 +++++++++++++++++++++++++++++++ pkg/middleware/middleware_test.go | 36 ++++++++++++++++++++++++++ pkg/setting/setting.go | 6 +++++ pkg/util/encoding.go | 22 ++++++++++++++++ pkg/util/encoding_test.go | 10 +++++++ 8 files changed, 126 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index fdaa12d83c9..e4902751245 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 **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 #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 diff --git a/conf/defaults.ini b/conf/defaults.ini index e9d29bccac9..d6d81c8028c 100644 --- a/conf/defaults.ini +++ b/conf/defaults.ini @@ -168,6 +168,10 @@ token_url = https://accounts.google.com/o/oauth2/token api_url = https://www.googleapis.com/oauth2/v1/userinfo allowed_domains = +#################################### Basic Auth ########################## +[auth.basic] +enabled = true + #################################### Auth Proxy ########################## [auth.proxy] enabled = false diff --git a/conf/sample.ini b/conf/sample.ini index d3e122fbfd5..ef082ccff98 100644 --- a/conf/sample.ini +++ b/conf/sample.ini @@ -174,6 +174,10 @@ ;header_property = username ;auto_sign_up = true +#################################### Basic Auth ########################## +[auth.basic] +;enabled = true + #################################### SMTP / Emailing ########################## [smtp] ;enabled = false diff --git a/pkg/middleware/middleware.go b/pkg/middleware/middleware.go index e6c2fdbea38..8704ec5a787 100644 --- a/pkg/middleware/middleware.go +++ b/pkg/middleware/middleware.go @@ -12,6 +12,7 @@ import ( "github.com/grafana/grafana/pkg/metrics" m "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/setting" + "github.com/grafana/grafana/pkg/util" ) 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 test if anonymous access is enabled if initContextWithApiKey(ctx) || + initContextWithBasicAuth(ctx) || initContextWithAuthProxy(ctx) || initContextWithUserSessionCookie(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 func initContextWithApiKeyFromSession(ctx *Context) bool { keyId := ctx.Session.Get(SESS_KEY_APIKEY) diff --git a/pkg/middleware/middleware_test.go b/pkg/middleware/middleware_test.go index 212e250cc1c..97d369d00cf 100644 --- a/pkg/middleware/middleware_test.go +++ b/pkg/middleware/middleware_test.go @@ -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) { keyhash := util.EncodePassword("v5nAwpMafFP6znaS4urhdWDLS5511M42", "asd") @@ -223,6 +249,7 @@ type scenarioContext struct { context *Context resp *httptest.ResponseRecorder apiKey string + authHeader string respJson map[string]interface{} handlerFunc handlerFunc defaultHandler macaron.Handler @@ -240,6 +267,11 @@ func (sc *scenarioContext) withInvalidApiKey() *scenarioContext { return sc } +func (sc *scenarioContext) withAuthoriziationHeader(authHeader string) *scenarioContext { + sc.authHeader = authHeader + return sc +} + func (sc *scenarioContext) fakeReq(method, url string) *scenarioContext { sc.resp = httptest.NewRecorder() req, err := http.NewRequest(method, url, nil) @@ -266,6 +298,10 @@ func (sc *scenarioContext) exec() { 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) if sc.resp.Header().Get("Content-Type") == "application/json; charset=UTF-8" { diff --git a/pkg/setting/setting.go b/pkg/setting/setting.go index b015062fa9b..ddca37c41ff 100644 --- a/pkg/setting/setting.go +++ b/pkg/setting/setting.go @@ -94,6 +94,9 @@ var ( AuthProxyHeaderProperty string AuthProxyAutoSignUp bool + // Basic Auth + BasicAuthEnabled bool + // Session settings. SessionOptions session.Options @@ -398,6 +401,9 @@ func NewConfigContext(args *CommandLineArgs) { AuthProxyHeaderProperty = authProxy.Key("header_property").String() AuthProxyAutoSignUp = authProxy.Key("auto_sign_up").MustBool(true) + authBasic := Cfg.Section("auth.basic") + AuthProxyEnabled = authBasic.Key("enabled").MustBool(true) + // PhantomJS rendering ImagesDir = filepath.Join(DataPath, "png") PhantomDir = filepath.Join(HomePath, "vendor/phantomjs") diff --git a/pkg/util/encoding.go b/pkg/util/encoding.go index 27169133a42..e87da9d3d55 100644 --- a/pkg/util/encoding.go +++ b/pkg/util/encoding.go @@ -7,8 +7,10 @@ import ( "crypto/sha256" "encoding/base64" "encoding/hex" + "errors" "fmt" "hash" + "strings" ) // 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 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 +} diff --git a/pkg/util/encoding_test.go b/pkg/util/encoding_test.go index afe299f9f92..abcf5425826 100644 --- a/pkg/util/encoding_test.go +++ b/pkg/util/encoding_test.go @@ -13,4 +13,14 @@ func TestEncoding(t *testing.T) { 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") + }) + }