The open and composable observability and data visualization platform. Visualize metrics, logs, and traces from multiple sources like Prometheus, Loki, Elasticsearch, InfluxDB, Postgres and many more.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
grafana/pkg/services/ldap/ldap_test.go

497 lines
14 KiB

package ldap
import (
"context"
"testing"
. "github.com/smartystreets/goconvey/convey"
"gopkg.in/ldap.v3"
"github.com/grafana/grafana/pkg/bus"
"github.com/grafana/grafana/pkg/log"
m "github.com/grafana/grafana/pkg/models"
)
func TestAuth(t *testing.T) {
Convey("initialBind", t, func() {
Convey("Given bind dn and password configured", func() {
conn := &mockLdapConn{}
var actualUsername, actualPassword string
conn.bindProvider = func(username, password string) error {
actualUsername = username
actualPassword = password
return nil
}
Auth := &Auth{
conn: conn,
server: &ServerConfig{
BindDN: "cn=%s,o=users,dc=grafana,dc=org",
BindPassword: "bindpwd",
},
}
err := Auth.initialBind("user", "pwd")
So(err, ShouldBeNil)
So(Auth.requireSecondBind, ShouldBeTrue)
So(actualUsername, ShouldEqual, "cn=user,o=users,dc=grafana,dc=org")
So(actualPassword, ShouldEqual, "bindpwd")
})
Convey("Given bind dn configured", func() {
conn := &mockLdapConn{}
var actualUsername, actualPassword string
conn.bindProvider = func(username, password string) error {
actualUsername = username
actualPassword = password
return nil
}
Auth := &Auth{
conn: conn,
server: &ServerConfig{
BindDN: "cn=%s,o=users,dc=grafana,dc=org",
},
}
err := Auth.initialBind("user", "pwd")
So(err, ShouldBeNil)
So(Auth.requireSecondBind, ShouldBeFalse)
So(actualUsername, ShouldEqual, "cn=user,o=users,dc=grafana,dc=org")
So(actualPassword, ShouldEqual, "pwd")
})
Convey("Given empty bind dn and password", func() {
conn := &mockLdapConn{}
unauthenticatedBindWasCalled := false
var actualUsername string
conn.unauthenticatedBindProvider = func(username string) error {
unauthenticatedBindWasCalled = true
actualUsername = username
return nil
}
Auth := &Auth{
conn: conn,
server: &ServerConfig{},
}
err := Auth.initialBind("user", "pwd")
So(err, ShouldBeNil)
So(Auth.requireSecondBind, ShouldBeTrue)
So(unauthenticatedBindWasCalled, ShouldBeTrue)
So(actualUsername, ShouldBeEmpty)
})
})
Convey("serverBind", t, func() {
Convey("Given bind dn and password configured", func() {
conn := &mockLdapConn{}
var actualUsername, actualPassword string
conn.bindProvider = func(username, password string) error {
actualUsername = username
actualPassword = password
return nil
}
Auth := &Auth{
conn: conn,
server: &ServerConfig{
BindDN: "o=users,dc=grafana,dc=org",
BindPassword: "bindpwd",
},
}
err := Auth.serverBind()
So(err, ShouldBeNil)
So(actualUsername, ShouldEqual, "o=users,dc=grafana,dc=org")
So(actualPassword, ShouldEqual, "bindpwd")
})
Convey("Given bind dn configured", func() {
conn := &mockLdapConn{}
unauthenticatedBindWasCalled := false
var actualUsername string
conn.unauthenticatedBindProvider = func(username string) error {
unauthenticatedBindWasCalled = true
actualUsername = username
return nil
}
Auth := &Auth{
conn: conn,
server: &ServerConfig{
BindDN: "o=users,dc=grafana,dc=org",
},
}
err := Auth.serverBind()
So(err, ShouldBeNil)
So(unauthenticatedBindWasCalled, ShouldBeTrue)
So(actualUsername, ShouldEqual, "o=users,dc=grafana,dc=org")
})
Convey("Given empty bind dn and password", func() {
conn := &mockLdapConn{}
unauthenticatedBindWasCalled := false
var actualUsername string
conn.unauthenticatedBindProvider = func(username string) error {
unauthenticatedBindWasCalled = true
actualUsername = username
return nil
}
Auth := &Auth{
conn: conn,
server: &ServerConfig{},
}
err := Auth.serverBind()
So(err, ShouldBeNil)
So(unauthenticatedBindWasCalled, ShouldBeTrue)
So(actualUsername, ShouldBeEmpty)
})
})
Convey("When translating ldap user to grafana user", t, func() {
var user1 = &m.User{}
bus.AddHandlerCtx("test", func(ctx context.Context, cmd *m.UpsertUserCommand) error {
cmd.Result = user1
cmd.Result.Login = "torkelo"
return nil
})
Convey("Given no ldap group map match", func() {
Auth := New(&ServerConfig{
Groups: []*GroupToOrgRole{{}},
})
_, err := Auth.GetGrafanaUserFor(nil, &UserInfo{})
So(err, ShouldEqual, ErrInvalidCredentials)
})
AuthScenario("Given wildcard group match", func(sc *scenarioContext) {
Auth := New(&ServerConfig{
Groups: []*GroupToOrgRole{
{GroupDN: "*", OrgRole: "Admin"},
},
})
sc.userQueryReturns(user1)
result, err := Auth.GetGrafanaUserFor(nil, &UserInfo{})
So(err, ShouldBeNil)
So(result, ShouldEqual, user1)
})
AuthScenario("Given exact group match", func(sc *scenarioContext) {
Auth := New(&ServerConfig{
Groups: []*GroupToOrgRole{
{GroupDN: "cn=users", OrgRole: "Admin"},
},
})
sc.userQueryReturns(user1)
result, err := Auth.GetGrafanaUserFor(nil, &UserInfo{MemberOf: []string{"cn=users"}})
So(err, ShouldBeNil)
So(result, ShouldEqual, user1)
})
AuthScenario("Given group match with different case", func(sc *scenarioContext) {
Auth := New(&ServerConfig{
Groups: []*GroupToOrgRole{
{GroupDN: "cn=users", OrgRole: "Admin"},
},
})
sc.userQueryReturns(user1)
result, err := Auth.GetGrafanaUserFor(nil, &UserInfo{MemberOf: []string{"CN=users"}})
So(err, ShouldBeNil)
So(result, ShouldEqual, user1)
})
AuthScenario("Given no existing grafana user", func(sc *scenarioContext) {
Auth := New(&ServerConfig{
Groups: []*GroupToOrgRole{
{GroupDN: "cn=admin", OrgRole: "Admin"},
{GroupDN: "cn=editor", OrgRole: "Editor"},
{GroupDN: "*", OrgRole: "Viewer"},
},
})
sc.userQueryReturns(nil)
result, err := Auth.GetGrafanaUserFor(nil, &UserInfo{
DN: "torkelo",
Username: "torkelo",
Email: "my@email.com",
MemberOf: []string{"cn=editor"},
})
So(err, ShouldBeNil)
Convey("Should return new user", func() {
So(result.Login, ShouldEqual, "torkelo")
})
Convey("Should set isGrafanaAdmin to false by default", func() {
So(result.IsAdmin, ShouldBeFalse)
})
})
})
Convey("When syncing ldap groups to grafana org roles", t, func() {
AuthScenario("given no current user orgs", func(sc *scenarioContext) {
Auth := New(&ServerConfig{
Groups: []*GroupToOrgRole{
{GroupDN: "cn=users", OrgRole: "Admin"},
},
})
sc.userOrgsQueryReturns([]*m.UserOrgDTO{})
_, err := Auth.GetGrafanaUserFor(nil, &UserInfo{
MemberOf: []string{"cn=users"},
})
Convey("Should create new org user", func() {
So(err, ShouldBeNil)
So(sc.addOrgUserCmd, ShouldNotBeNil)
So(sc.addOrgUserCmd.Role, ShouldEqual, m.ROLE_ADMIN)
})
})
AuthScenario("given different current org role", func(sc *scenarioContext) {
Auth := New(&ServerConfig{
Groups: []*GroupToOrgRole{
{GroupDN: "cn=users", OrgId: 1, OrgRole: "Admin"},
},
})
sc.userOrgsQueryReturns([]*m.UserOrgDTO{{OrgId: 1, Role: m.ROLE_EDITOR}})
_, err := Auth.GetGrafanaUserFor(nil, &UserInfo{
MemberOf: []string{"cn=users"},
})
Convey("Should update org role", func() {
So(err, ShouldBeNil)
So(sc.updateOrgUserCmd, ShouldNotBeNil)
So(sc.updateOrgUserCmd.Role, ShouldEqual, m.ROLE_ADMIN)
So(sc.setUsingOrgCmd.OrgId, ShouldEqual, 1)
})
})
AuthScenario("given current org role is removed in ldap", func(sc *scenarioContext) {
Auth := New(&ServerConfig{
Groups: []*GroupToOrgRole{
{GroupDN: "cn=users", OrgId: 2, OrgRole: "Admin"},
},
})
sc.userOrgsQueryReturns([]*m.UserOrgDTO{
{OrgId: 1, Role: m.ROLE_EDITOR},
{OrgId: 2, Role: m.ROLE_EDITOR},
})
_, err := Auth.GetGrafanaUserFor(nil, &UserInfo{
MemberOf: []string{"cn=users"},
})
Convey("Should remove org role", func() {
So(err, ShouldBeNil)
So(sc.removeOrgUserCmd, ShouldNotBeNil)
So(sc.setUsingOrgCmd.OrgId, ShouldEqual, 2)
})
})
AuthScenario("given org role is updated in config", func(sc *scenarioContext) {
Auth := New(&ServerConfig{
Groups: []*GroupToOrgRole{
{GroupDN: "cn=admin", OrgId: 1, OrgRole: "Admin"},
{GroupDN: "cn=users", OrgId: 1, OrgRole: "Viewer"},
},
})
sc.userOrgsQueryReturns([]*m.UserOrgDTO{{OrgId: 1, Role: m.ROLE_EDITOR}})
_, err := Auth.GetGrafanaUserFor(nil, &UserInfo{
MemberOf: []string{"cn=users"},
})
Convey("Should update org role", func() {
So(err, ShouldBeNil)
So(sc.removeOrgUserCmd, ShouldBeNil)
So(sc.updateOrgUserCmd, ShouldNotBeNil)
So(sc.setUsingOrgCmd.OrgId, ShouldEqual, 1)
})
})
AuthScenario("given multiple matching ldap groups", func(sc *scenarioContext) {
Auth := New(&ServerConfig{
Groups: []*GroupToOrgRole{
{GroupDN: "cn=admins", OrgId: 1, OrgRole: "Admin"},
{GroupDN: "*", OrgId: 1, OrgRole: "Viewer"},
},
})
sc.userOrgsQueryReturns([]*m.UserOrgDTO{{OrgId: 1, Role: m.ROLE_ADMIN}})
_, err := Auth.GetGrafanaUserFor(nil, &UserInfo{
MemberOf: []string{"cn=admins"},
})
Convey("Should take first match, and ignore subsequent matches", func() {
So(err, ShouldBeNil)
So(sc.updateOrgUserCmd, ShouldBeNil)
So(sc.setUsingOrgCmd.OrgId, ShouldEqual, 1)
})
})
AuthScenario("given multiple matching ldap groups and no existing groups", func(sc *scenarioContext) {
Auth := New(&ServerConfig{
Groups: []*GroupToOrgRole{
{GroupDN: "cn=admins", OrgId: 1, OrgRole: "Admin"},
{GroupDN: "*", OrgId: 1, OrgRole: "Viewer"},
},
})
sc.userOrgsQueryReturns([]*m.UserOrgDTO{})
_, err := Auth.GetGrafanaUserFor(nil, &UserInfo{
MemberOf: []string{"cn=admins"},
})
Convey("Should take first match, and ignore subsequent matches", func() {
So(err, ShouldBeNil)
So(sc.addOrgUserCmd.Role, ShouldEqual, m.ROLE_ADMIN)
So(sc.setUsingOrgCmd.OrgId, ShouldEqual, 1)
})
Convey("Should not update permissions unless specified", func() {
So(err, ShouldBeNil)
So(sc.updateUserPermissionsCmd, ShouldBeNil)
})
})
AuthScenario("given ldap groups with grafana_admin=true", func(sc *scenarioContext) {
trueVal := true
Auth := New(&ServerConfig{
Groups: []*GroupToOrgRole{
{GroupDN: "cn=admins", OrgId: 1, OrgRole: "Admin", IsGrafanaAdmin: &trueVal},
},
})
sc.userOrgsQueryReturns([]*m.UserOrgDTO{})
_, err := Auth.GetGrafanaUserFor(nil, &UserInfo{
MemberOf: []string{"cn=admins"},
})
Convey("Should create user with admin set to true", func() {
So(err, ShouldBeNil)
So(sc.updateUserPermissionsCmd.IsGrafanaAdmin, ShouldBeTrue)
})
})
})
Convey("When calling SyncUser", t, func() {
mockLdapConnection := &mockLdapConn{}
auth := &Auth{
server: &ServerConfig{
Host: "",
RootCACert: "",
Groups: []*GroupToOrgRole{
{GroupDN: "*", OrgRole: "Admin"},
},
Attr: AttributeMap{
Username: "username",
Surname: "surname",
Email: "email",
Name: "name",
MemberOf: "memberof",
},
SearchBaseDNs: []string{"BaseDNHere"},
},
conn: mockLdapConnection,
log: log.New("test-logger"),
}
dialCalled := false
dial = func(network, addr string) (IConnection, error) {
dialCalled = true
return mockLdapConnection, nil
}
entry := ldap.Entry{
DN: "dn", Attributes: []*ldap.EntryAttribute{
{Name: "username", Values: []string{"roelgerrits"}},
{Name: "surname", Values: []string{"Gerrits"}},
{Name: "email", Values: []string{"roel@test.com"}},
{Name: "name", Values: []string{"Roel"}},
{Name: "memberof", Values: []string{"admins"}},
}}
result := ldap.SearchResult{Entries: []*ldap.Entry{&entry}}
mockLdapConnection.setSearchResult(&result)
AuthScenario("When ldapUser found call syncInfo and orgRoles", func(sc *scenarioContext) {
// arrange
query := &m.LoginUserQuery{
Username: "roelgerrits",
}
hookDial = nil
7 years ago
sc.userQueryReturns(&m.User{
Id: 1,
Email: "roel@test.net",
Name: "Roel Gerrits",
Login: "roelgerrits",
})
sc.userOrgsQueryReturns([]*m.UserOrgDTO{})
// act
syncErrResult := auth.SyncUser(query)
// assert
So(dialCalled, ShouldBeTrue)
So(syncErrResult, ShouldBeNil)
// User should be searched in ldap
So(mockLdapConnection.searchCalled, ShouldBeTrue)
// Info should be updated (email differs)
So(sc.updateUserCmd.Email, ShouldEqual, "roel@test.com")
// User should have admin privileges
So(sc.addOrgUserCmd.UserId, ShouldEqual, 1)
So(sc.addOrgUserCmd.Role, ShouldEqual, "Admin")
})
})
Convey("When searching for a user and not all five attributes are mapped", t, func() {
mockLdapConnection := &mockLdapConn{}
entry := ldap.Entry{
DN: "dn", Attributes: []*ldap.EntryAttribute{
{Name: "username", Values: []string{"roelgerrits"}},
{Name: "surname", Values: []string{"Gerrits"}},
{Name: "email", Values: []string{"roel@test.com"}},
{Name: "name", Values: []string{"Roel"}},
{Name: "memberof", Values: []string{"admins"}},
}}
result := ldap.SearchResult{Entries: []*ldap.Entry{&entry}}
mockLdapConnection.setSearchResult(&result)
// Set up attribute map without surname and email
Auth := &Auth{
server: &ServerConfig{
Attr: AttributeMap{
Username: "username",
Name: "name",
MemberOf: "memberof",
},
SearchBaseDNs: []string{"BaseDNHere"},
},
conn: mockLdapConnection,
log: log.New("test-logger"),
}
searchResult, err := Auth.searchForUser("roelgerrits")
So(err, ShouldBeNil)
So(searchResult, ShouldNotBeNil)
// User should be searched in ldap
So(mockLdapConnection.searchCalled, ShouldBeTrue)
// No empty attributes should be added to the search request
So(len(mockLdapConnection.searchAttributes), ShouldEqual, 3)
})
}