Browse Source

Auth: You can now authenicate against api with username / password using basic auth, Closes #2218

Torkel Ödegaard 10 years ago
parent
commit
ae0f8c77d1

+ 1 - 0
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
 - [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

+ 4 - 0
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
 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

+ 4 - 0
conf/sample.ini

@@ -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

+ 43 - 0
pkg/middleware/middleware.go

@@ -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)

+ 36 - 0
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) {
 		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" {

+ 6 - 0
pkg/setting/setting.go

@@ -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")

+ 22 - 0
pkg/util/encoding.go

@@ -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
+}

+ 10 - 0
pkg/util/encoding_test.go

@@ -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")
+	})
+
 }
 }