Skip to content

Commit

Permalink
Auth: You can now authenicate against api with username / password us…
Browse files Browse the repository at this point in the history
…ing basic auth, Closes #2218
  • Loading branch information
torkelo committed Jun 30, 2015
1 parent d0e7d53 commit ae0f8c7
Show file tree
Hide file tree
Showing 8 changed files with 126 additions and 0 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 4 additions & 0 deletions conf/defaults.ini
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 4 additions & 0 deletions conf/sample.ini
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,10 @@
;header_property = username
;auto_sign_up = true

#################################### Basic Auth ##########################
[auth.basic]
;enabled = true

#################################### SMTP / Emailing ##########################
[smtp]
;enabled = false
Expand Down
43 changes: 43 additions & 0 deletions pkg/middleware/middleware.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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) ||
Expand Down Expand Up @@ -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)
Expand Down
36 changes: 36 additions & 0 deletions pkg/middleware/middleware_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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")

Expand Down Expand Up @@ -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
Expand All @@ -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)
Expand All @@ -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" {
Expand Down
6 changes: 6 additions & 0 deletions pkg/setting/setting.go
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,9 @@ var (
AuthProxyHeaderProperty string
AuthProxyAutoSignUp bool

// Basic Auth
BasicAuthEnabled bool

// Session settings.
SessionOptions session.Options

Expand Down Expand Up @@ -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")
Expand Down
22 changes: 22 additions & 0 deletions pkg/util/encoding.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
}
10 changes: 10 additions & 0 deletions pkg/util/encoding_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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")
})

}

0 comments on commit ae0f8c7

Please sign in to comment.