diff --git a/CHANGELOG.md b/CHANGELOG.md index 22e4f5482b3..a8f21eae095 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,7 +10,7 @@ **Backend** - [Issue #1905](https://github.com/grafana/grafana/issues/1905). Github OAuth: You can now configure a Github team membership requirement, thx @dewski - [Issue #1891](https://github.com/grafana/grafana/issues/1891). Security: New config option to disable the use of gravatar for profile images - +- [Issue #1921](https://github.com/grafana/grafana/issues/1921). Auth: Support for user authentication via reverse proxy header (like X-Authenticated-User, or X-WEBAUTH-USER) # 2.0.3 (unreleased - 2.0.x branch) diff --git a/pkg/middleware/auth_proxy.go b/pkg/middleware/auth_proxy.go new file mode 100644 index 00000000000..d735779f694 --- /dev/null +++ b/pkg/middleware/auth_proxy.go @@ -0,0 +1,67 @@ +package middleware + +import ( + "github.com/grafana/grafana/pkg/bus" + m "github.com/grafana/grafana/pkg/models" + "github.com/grafana/grafana/pkg/setting" +) + +func initContextWithAuthProxy(ctx *Context) bool { + if !setting.AuthProxyEnabled { + return false + } + + proxyHeaderValue := ctx.Req.Header.Get(setting.AuthProxyHeaderName) + if len(proxyHeaderValue) <= 0 { + return false + } + + query := getSignedInUserQueryForProxyAuth(proxyHeaderValue) + if err := bus.Dispatch(query); err != nil { + if err != m.ErrUserNotFound { + ctx.Handle(500, "Failed find user specifed in auth proxy header", err) + return true + } + + if setting.AuthProxyAutoSignUp { + cmd := getCreateUserCommandForProxyAuth(proxyHeaderValue) + if err := bus.Dispatch(cmd); err != nil { + ctx.Handle(500, "Failed to create user specified in auth proxy header", err) + return true + } + query = &m.GetSignedInUserQuery{UserId: cmd.Result.Id} + if err := bus.Dispatch(query); err != nil { + ctx.Handle(500, "Failed find user after creation", err) + return true + } + } + } + + ctx.SignedInUser = query.Result + ctx.IsSignedIn = true + return true +} + +func getSignedInUserQueryForProxyAuth(headerVal string) *m.GetSignedInUserQuery { + query := m.GetSignedInUserQuery{} + if setting.AuthProxyHeaderProperty == "username" { + query.Login = headerVal + } else if setting.AuthProxyHeaderProperty == "email" { + query.Email = headerVal + } else { + panic("Auth proxy header property invalid") + } + return &query +} + +func getCreateUserCommandForProxyAuth(headerVal string) *m.CreateUserCommand { + cmd := m.CreateUserCommand{} + if setting.AuthProxyHeaderProperty == "username" { + cmd.Login = headerVal + } else if setting.AuthProxyHeaderProperty == "email" { + cmd.Email = headerVal + } else { + panic("Auth proxy header property invalid") + } + return &cmd +} diff --git a/pkg/middleware/auth_test.go b/pkg/middleware/auth_test.go new file mode 100644 index 00000000000..81b0f525e98 --- /dev/null +++ b/pkg/middleware/auth_test.go @@ -0,0 +1,35 @@ +package middleware + +import ( + "testing" + + . "github.com/smartystreets/goconvey/convey" +) + +func TestMiddlewareAuth(t *testing.T) { + + Convey("Given the grafana middleware", t, func() { + reqSignIn := Auth(&AuthOptions{ReqSignedIn: true}) + + middlewareScenario("ReqSignIn true and unauthenticated request", func(sc *scenarioContext) { + sc.m.Get("/secure", reqSignIn, sc.defaultHandler) + + sc.fakeReq("GET", "/secure").exec() + + Convey("Should redirect to login", func() { + So(sc.resp.Code, ShouldEqual, 302) + }) + }) + + middlewareScenario("ReqSignIn true and unauthenticated API request", func(sc *scenarioContext) { + sc.m.Get("/api/secure", reqSignIn, sc.defaultHandler) + + sc.fakeReq("GET", "/api/secure").exec() + + Convey("Should return 401", func() { + So(sc.resp.Code, ShouldEqual, 401) + }) + }) + + }) +} diff --git a/pkg/middleware/middleware.go b/pkg/middleware/middleware.go index efeee0ac3e7..2a6873076c6 100644 --- a/pkg/middleware/middleware.go +++ b/pkg/middleware/middleware.go @@ -1,7 +1,6 @@ package middleware import ( - "fmt" "strconv" "strings" @@ -41,6 +40,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) || + initContextWithAuthProxy(ctx) || initContextWithUserSessionCookie(ctx) || initContextWithApiKeyFromSession(ctx) || initContextWithAnonymousUser(ctx) { @@ -79,7 +79,6 @@ func initContextWithUserSessionCookie(ctx *Context) bool { var userId int64 if userId = getRequestUserId(ctx); userId == 0 { - fmt.Printf("Not userId") return false } diff --git a/pkg/middleware/middleware_test.go b/pkg/middleware/middleware_test.go index ebbf0a1f038..212e250cc1c 100644 --- a/pkg/middleware/middleware_test.go +++ b/pkg/middleware/middleware_test.go @@ -128,6 +128,62 @@ func TestMiddlewareContext(t *testing.T) { So(sc.context.IsSignedIn, ShouldBeFalse) }) }) + + middlewareScenario("When auth_proxy is enabled enabled and user exists", func(sc *scenarioContext) { + setting.AuthProxyEnabled = true + setting.AuthProxyHeaderName = "X-WEBAUTH-USER" + setting.AuthProxyHeaderProperty = "username" + + bus.AddHandler("test", func(query *m.GetSignedInUserQuery) error { + query.Result = &m.SignedInUser{OrgId: 2, UserId: 12} + return nil + }) + + sc.fakeReq("GET", "/") + sc.req.Header.Add("X-WEBAUTH-USER", "torkelo") + sc.exec() + + Convey("should init context with user info", func() { + So(sc.context.IsSignedIn, ShouldBeTrue) + So(sc.context.UserId, ShouldEqual, 12) + So(sc.context.OrgId, ShouldEqual, 2) + }) + }) + + middlewareScenario("When auth_proxy is enabled enabled and user does not exists", func(sc *scenarioContext) { + setting.AuthProxyEnabled = true + setting.AuthProxyHeaderName = "X-WEBAUTH-USER" + setting.AuthProxyHeaderProperty = "username" + setting.AuthProxyAutoSignUp = true + + bus.AddHandler("test", func(query *m.GetSignedInUserQuery) error { + if query.UserId > 0 { + query.Result = &m.SignedInUser{OrgId: 4, UserId: 33} + return nil + } else { + return m.ErrUserNotFound + } + }) + + var createUserCmd *m.CreateUserCommand + bus.AddHandler("test", func(cmd *m.CreateUserCommand) error { + createUserCmd = cmd + cmd.Result = m.User{Id: 33} + return nil + }) + + sc.fakeReq("GET", "/") + sc.req.Header.Add("X-WEBAUTH-USER", "torkelo") + sc.exec() + + Convey("Should create user if auto sign up is enabled", func() { + So(sc.context.IsSignedIn, ShouldBeTrue) + So(sc.context.UserId, ShouldEqual, 33) + So(sc.context.OrgId, ShouldEqual, 4) + + }) + }) + }) } @@ -149,24 +205,27 @@ func middlewareScenario(desc string, fn scenarioFunc) { startSessionGC = func() {} sc.m.Use(Sessioner(&session.Options{})) - sc.m.Get("/", func(c *Context) { + sc.defaultHandler = func(c *Context) { sc.context = c if sc.handlerFunc != nil { sc.handlerFunc(sc.context) } - }) + } + + sc.m.Get("/", sc.defaultHandler) fn(sc) }) } type scenarioContext struct { - m *macaron.Macaron - context *Context - resp *httptest.ResponseRecorder - apiKey string - respJson map[string]interface{} - handlerFunc handlerFunc + m *macaron.Macaron + context *Context + resp *httptest.ResponseRecorder + apiKey string + respJson map[string]interface{} + handlerFunc handlerFunc + defaultHandler macaron.Handler req *http.Request } diff --git a/pkg/models/user.go b/pkg/models/user.go index cdc688d4c1f..68e0001b99c 100644 --- a/pkg/models/user.go +++ b/pkg/models/user.go @@ -89,6 +89,8 @@ type GetUserByIdQuery struct { type GetSignedInUserQuery struct { UserId int64 + Login string + Email string Result *SignedInUser } diff --git a/pkg/services/sqlstore/user.go b/pkg/services/sqlstore/user.go index 61798d724ef..c2092e963f8 100644 --- a/pkg/services/sqlstore/user.go +++ b/pkg/services/sqlstore/user.go @@ -263,8 +263,15 @@ func GetSignedInUser(query *m.GetSignedInUserQuery) error { org.id as org_id FROM ` + dialect.Quote("user") + ` as u LEFT OUTER JOIN org_user on org_user.org_id = u.org_id and org_user.user_id = u.id - LEFT OUTER JOIN org on org.id = u.org_id - WHERE u.id=?` + LEFT OUTER JOIN org on org.id = u.org_id ` + + if query.UserId > 0 { + rawSql += "WHERE u.id=?" + } else if query.Login != "" { + rawSql += "WHERE u.login=?" + } else if query.Email != "" { + rawSql += "WHERE u.email=?" + } var user m.SignedInUser sess := x.Table("user")