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:
parent
d0e7d53c69
commit
ae0f8c77d1
@ -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
|
||||
|
@ -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
|
||||
|
@ -174,6 +174,10 @@
|
||||
;header_property = username
|
||||
;auto_sign_up = true
|
||||
|
||||
#################################### Basic Auth ##########################
|
||||
[auth.basic]
|
||||
;enabled = true
|
||||
|
||||
#################################### SMTP / Emailing ##########################
|
||||
[smtp]
|
||||
;enabled = false
|
||||
|
@ -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)
|
||||
|
@ -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" {
|
||||
|
@ -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")
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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")
|
||||
})
|
||||
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user