mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
LDAP Refactoring to support syncronizing more than one user at a time. (#16705)
* Feature: add cron setting for the ldap settings * Move ldap configuration read to special function * Introduce cron setting (no docs for it yet, pending approval) * Chore: duplicate ldap module as a service * Feature: implement active sync This is very early preliminary implementation of active sync. There is only one thing that's going right for this code - it works. Aside from that, there is no tests, error handling, docs, transactions, it's very much duplicative and etc. But this is the overall direction with architecture I'm going for * Chore: introduce login service * Chore: gradually switch to ldap service * Chore: use new approach for auth_proxy * Chore: use new approach along with refactoring * Chore: use new ldap interface for auth_proxy * Chore: improve auth_proxy and subsequently ldap * Chore: more of the refactoring bits * Chore: address comments from code review * Chore: more refactoring stuff * Chore: make linter happy * Chore: add cron dep for grafana enterprise * Chore: initialize config package var * Chore: disable gosec for now * Chore: update dependencies * Chore: remove unused module * Chore: address review comments * Chore: make linter happy
This commit is contained in:
parent
a8326e3e93
commit
62b85a886e
@ -362,6 +362,7 @@ headers =
|
||||
enabled = false
|
||||
config_file = /etc/grafana/ldap.toml
|
||||
allow_sign_up = true
|
||||
sync_cron = @hourly
|
||||
|
||||
# LDAP backround sync (Enterprise only)
|
||||
sync_cron = @hourly
|
||||
|
1
go.mod
1
go.mod
@ -53,6 +53,7 @@ require (
|
||||
github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90
|
||||
github.com/prometheus/common v0.2.0
|
||||
github.com/rainycape/unidecode v0.0.0-20150907023854-cb7f23ec59be // indirect
|
||||
github.com/robfig/cron v0.0.0-20180505203441-b41be1df6967
|
||||
github.com/sergi/go-diff v1.0.0 // indirect
|
||||
github.com/smartystreets/assertions v0.0.0-20190401211740-f487f9de1cd3 // indirect
|
||||
github.com/smartystreets/goconvey v0.0.0-20190330032615-68dc04aab96a
|
||||
|
2
go.sum
2
go.sum
@ -167,6 +167,8 @@ github.com/prometheus/procfs v0.0.0-20181204211112-1dc9a6cbc91a h1:9a8MnZMP0X2nL
|
||||
github.com/prometheus/procfs v0.0.0-20181204211112-1dc9a6cbc91a/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
|
||||
github.com/rainycape/unidecode v0.0.0-20150907023854-cb7f23ec59be h1:ta7tUOvsPHVHGom5hKW5VXNc2xZIkfCKP8iaqOyYtUQ=
|
||||
github.com/rainycape/unidecode v0.0.0-20150907023854-cb7f23ec59be/go.mod h1:MIDFMn7db1kT65GmV94GzpX9Qdi7N/pQlwb+AN8wh+Q=
|
||||
github.com/robfig/cron v0.0.0-20180505203441-b41be1df6967 h1:x7xEyJDP7Hv3LVgvWhzioQqbC/KtuUhTigKlH/8ehhE=
|
||||
github.com/robfig/cron v0.0.0-20180505203441-b41be1df6967/go.mod h1:JGuDeoQd7Z6yL4zQhZ3OPEVHB7fL6Ka6skscFHfmt2k=
|
||||
github.com/satori/go.uuid v1.2.0 h1:0uYX9dsZ2yD7q2RtLRtPSdGDWzjeM3TbMJP9utgA0ww=
|
||||
github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0=
|
||||
github.com/sergi/go-diff v1.0.0 h1:Kpca3qRNrduNnOQeazBd0ysaKrUJiIuISHxogkT9RPQ=
|
||||
|
@ -2,6 +2,7 @@ package extensions
|
||||
|
||||
import (
|
||||
_ "github.com/gobwas/glob"
|
||||
_ "github.com/robfig/cron"
|
||||
_ "gopkg.in/square/go-jose.v2"
|
||||
)
|
||||
|
||||
|
@ -5,10 +5,12 @@ import (
|
||||
|
||||
"github.com/grafana/grafana/pkg/bus"
|
||||
m "github.com/grafana/grafana/pkg/models"
|
||||
LDAP "github.com/grafana/grafana/pkg/services/ldap"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrEmailNotAllowed = errors.New("Required email domain not fulfilled")
|
||||
ErrNoLDAPServers = errors.New("No LDAP servers are configured")
|
||||
ErrInvalidCredentials = errors.New("Invalid Username or Password")
|
||||
ErrNoEmail = errors.New("Login provider didn't return an email address")
|
||||
ErrProviderDeniedRequest = errors.New("Login provider denied login request")
|
||||
@ -21,7 +23,6 @@ var (
|
||||
|
||||
func Init() {
|
||||
bus.AddHandler("auth", AuthenticateUser)
|
||||
loadLdapConfig()
|
||||
}
|
||||
|
||||
func AuthenticateUser(query *m.LoginUserQuery) error {
|
||||
@ -40,14 +41,14 @@ func AuthenticateUser(query *m.LoginUserQuery) error {
|
||||
|
||||
ldapEnabled, ldapErr := loginUsingLdap(query)
|
||||
if ldapEnabled {
|
||||
if ldapErr == nil || ldapErr != ErrInvalidCredentials {
|
||||
if ldapErr == nil || ldapErr != LDAP.ErrInvalidCredentials {
|
||||
return ldapErr
|
||||
}
|
||||
|
||||
err = ldapErr
|
||||
}
|
||||
|
||||
if err == ErrInvalidCredentials {
|
||||
if err == ErrInvalidCredentials || err == LDAP.ErrInvalidCredentials {
|
||||
saveInvalidLoginAttempt(query)
|
||||
}
|
||||
|
||||
|
@ -4,8 +4,10 @@ import (
|
||||
"errors"
|
||||
"testing"
|
||||
|
||||
m "github.com/grafana/grafana/pkg/models"
|
||||
. "github.com/smartystreets/goconvey/convey"
|
||||
|
||||
m "github.com/grafana/grafana/pkg/models"
|
||||
LDAP "github.com/grafana/grafana/pkg/services/ldap"
|
||||
)
|
||||
|
||||
func TestAuthenticateUser(t *testing.T) {
|
||||
@ -100,13 +102,13 @@ func TestAuthenticateUser(t *testing.T) {
|
||||
authScenario("When a non-existing grafana user authenticate and invalid ldap credentials", func(sc *authScenarioContext) {
|
||||
mockLoginAttemptValidation(nil, sc)
|
||||
mockLoginUsingGrafanaDB(m.ErrUserNotFound, sc)
|
||||
mockLoginUsingLdap(true, ErrInvalidCredentials, sc)
|
||||
mockLoginUsingLdap(true, LDAP.ErrInvalidCredentials, sc)
|
||||
mockSaveInvalidLoginAttempt(sc)
|
||||
|
||||
err := AuthenticateUser(sc.loginUserQuery)
|
||||
|
||||
Convey("it should result in", func() {
|
||||
So(err, ShouldEqual, ErrInvalidCredentials)
|
||||
So(err, ShouldEqual, LDAP.ErrInvalidCredentials)
|
||||
So(sc.loginAttemptValidationWasCalled, ShouldBeTrue)
|
||||
So(sc.grafanaLoginWasCalled, ShouldBeTrue)
|
||||
So(sc.ldapLoginWasCalled, ShouldBeTrue)
|
||||
@ -152,13 +154,13 @@ func TestAuthenticateUser(t *testing.T) {
|
||||
authScenario("When grafana user authenticate with invalid credentials and invalid ldap credentials", func(sc *authScenarioContext) {
|
||||
mockLoginAttemptValidation(nil, sc)
|
||||
mockLoginUsingGrafanaDB(ErrInvalidCredentials, sc)
|
||||
mockLoginUsingLdap(true, ErrInvalidCredentials, sc)
|
||||
mockLoginUsingLdap(true, LDAP.ErrInvalidCredentials, sc)
|
||||
mockSaveInvalidLoginAttempt(sc)
|
||||
|
||||
err := AuthenticateUser(sc.loginUserQuery)
|
||||
|
||||
Convey("it should result in", func() {
|
||||
So(err, ShouldEqual, ErrInvalidCredentials)
|
||||
So(err, ShouldEqual, LDAP.ErrInvalidCredentials)
|
||||
So(sc.loginAttemptValidationWasCalled, ShouldBeTrue)
|
||||
So(sc.grafanaLoginWasCalled, ShouldBeTrue)
|
||||
So(sc.ldapLoginWasCalled, ShouldBeTrue)
|
||||
|
@ -3,9 +3,10 @@ package login
|
||||
import (
|
||||
"testing"
|
||||
|
||||
. "github.com/smartystreets/goconvey/convey"
|
||||
|
||||
"github.com/grafana/grafana/pkg/bus"
|
||||
m "github.com/grafana/grafana/pkg/models"
|
||||
. "github.com/smartystreets/goconvey/convey"
|
||||
)
|
||||
|
||||
func TestGrafanaLogin(t *testing.T) {
|
||||
|
@ -1,430 +0,0 @@
|
||||
package login
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"strings"
|
||||
|
||||
"github.com/davecgh/go-spew/spew"
|
||||
"github.com/grafana/grafana/pkg/bus"
|
||||
"github.com/grafana/grafana/pkg/log"
|
||||
m "github.com/grafana/grafana/pkg/models"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
"gopkg.in/ldap.v3"
|
||||
)
|
||||
|
||||
type ILdapConn interface {
|
||||
Bind(username, password string) error
|
||||
UnauthenticatedBind(username string) error
|
||||
Search(*ldap.SearchRequest) (*ldap.SearchResult, error)
|
||||
StartTLS(*tls.Config) error
|
||||
Close()
|
||||
}
|
||||
|
||||
type ILdapAuther interface {
|
||||
Login(query *m.LoginUserQuery) error
|
||||
SyncUser(query *m.LoginUserQuery) error
|
||||
GetGrafanaUserFor(ctx *m.ReqContext, ldapUser *LdapUserInfo) (*m.User, error)
|
||||
}
|
||||
|
||||
type ldapAuther struct {
|
||||
server *LdapServerConf
|
||||
conn ILdapConn
|
||||
requireSecondBind bool
|
||||
log log.Logger
|
||||
}
|
||||
|
||||
var NewLdapAuthenticator = func(server *LdapServerConf) ILdapAuther {
|
||||
return &ldapAuther{server: server, log: log.New("ldap")}
|
||||
}
|
||||
|
||||
var ldapDial = func(network, addr string) (ILdapConn, error) {
|
||||
return ldap.Dial(network, addr)
|
||||
}
|
||||
|
||||
func (a *ldapAuther) Dial() error {
|
||||
var err error
|
||||
var certPool *x509.CertPool
|
||||
if a.server.RootCACert != "" {
|
||||
certPool = x509.NewCertPool()
|
||||
for _, caCertFile := range strings.Split(a.server.RootCACert, " ") {
|
||||
pem, err := ioutil.ReadFile(caCertFile)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !certPool.AppendCertsFromPEM(pem) {
|
||||
return errors.New("Failed to append CA certificate " + caCertFile)
|
||||
}
|
||||
}
|
||||
}
|
||||
var clientCert tls.Certificate
|
||||
if a.server.ClientCert != "" && a.server.ClientKey != "" {
|
||||
clientCert, err = tls.LoadX509KeyPair(a.server.ClientCert, a.server.ClientKey)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
for _, host := range strings.Split(a.server.Host, " ") {
|
||||
address := fmt.Sprintf("%s:%d", host, a.server.Port)
|
||||
if a.server.UseSSL {
|
||||
tlsCfg := &tls.Config{
|
||||
InsecureSkipVerify: a.server.SkipVerifySSL,
|
||||
ServerName: host,
|
||||
RootCAs: certPool,
|
||||
}
|
||||
if len(clientCert.Certificate) > 0 {
|
||||
tlsCfg.Certificates = append(tlsCfg.Certificates, clientCert)
|
||||
}
|
||||
if a.server.StartTLS {
|
||||
a.conn, err = ldap.Dial("tcp", address)
|
||||
if err == nil {
|
||||
if err = a.conn.StartTLS(tlsCfg); err == nil {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
} else {
|
||||
a.conn, err = ldap.DialTLS("tcp", address, tlsCfg)
|
||||
}
|
||||
} else {
|
||||
a.conn, err = ldapDial("tcp", address)
|
||||
}
|
||||
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func (a *ldapAuther) Login(query *m.LoginUserQuery) error {
|
||||
// connect to ldap server
|
||||
if err := a.Dial(); err != nil {
|
||||
return err
|
||||
}
|
||||
defer a.conn.Close()
|
||||
|
||||
// perform initial authentication
|
||||
if err := a.initialBind(query.Username, query.Password); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// find user entry & attributes
|
||||
ldapUser, err := a.searchForUser(query.Username)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
a.log.Debug("Ldap User found", "info", spew.Sdump(ldapUser))
|
||||
|
||||
// check if a second user bind is needed
|
||||
if a.requireSecondBind {
|
||||
err = a.secondBind(ldapUser, query.Password)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
grafanaUser, err := a.GetGrafanaUserFor(query.ReqContext, ldapUser)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
query.User = grafanaUser
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *ldapAuther) SyncUser(query *m.LoginUserQuery) error {
|
||||
// connect to ldap server
|
||||
err := a.Dial()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer a.conn.Close()
|
||||
|
||||
err = a.serverBind()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// find user entry & attributes
|
||||
ldapUser, err := a.searchForUser(query.Username)
|
||||
if err != nil {
|
||||
a.log.Error("Failed searching for user in ldap", "error", err)
|
||||
return err
|
||||
}
|
||||
|
||||
a.log.Debug("Ldap User found", "info", spew.Sdump(ldapUser))
|
||||
|
||||
grafanaUser, err := a.GetGrafanaUserFor(query.ReqContext, ldapUser)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
query.User = grafanaUser
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *ldapAuther) GetGrafanaUserFor(ctx *m.ReqContext, ldapUser *LdapUserInfo) (*m.User, error) {
|
||||
extUser := &m.ExternalUserInfo{
|
||||
AuthModule: "ldap",
|
||||
AuthId: ldapUser.DN,
|
||||
Name: fmt.Sprintf("%s %s", ldapUser.FirstName, ldapUser.LastName),
|
||||
Login: ldapUser.Username,
|
||||
Email: ldapUser.Email,
|
||||
Groups: ldapUser.MemberOf,
|
||||
OrgRoles: map[int64]m.RoleType{},
|
||||
}
|
||||
|
||||
for _, group := range a.server.LdapGroups {
|
||||
// only use the first match for each org
|
||||
if extUser.OrgRoles[group.OrgId] != "" {
|
||||
continue
|
||||
}
|
||||
|
||||
if ldapUser.isMemberOf(group.GroupDN) {
|
||||
extUser.OrgRoles[group.OrgId] = group.OrgRole
|
||||
if extUser.IsGrafanaAdmin == nil || !*extUser.IsGrafanaAdmin {
|
||||
extUser.IsGrafanaAdmin = group.IsGrafanaAdmin
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// validate that the user has access
|
||||
// if there are no ldap group mappings access is true
|
||||
// otherwise a single group must match
|
||||
if len(a.server.LdapGroups) > 0 && len(extUser.OrgRoles) < 1 {
|
||||
a.log.Info(
|
||||
"Ldap Auth: user does not belong in any of the specified ldap groups",
|
||||
"username", ldapUser.Username,
|
||||
"groups", ldapUser.MemberOf)
|
||||
return nil, ErrInvalidCredentials
|
||||
}
|
||||
|
||||
// add/update user in grafana
|
||||
upsertUserCmd := &m.UpsertUserCommand{
|
||||
ReqContext: ctx,
|
||||
ExternalUser: extUser,
|
||||
SignupAllowed: setting.LdapAllowSignup,
|
||||
}
|
||||
|
||||
err := bus.Dispatch(upsertUserCmd)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return upsertUserCmd.Result, nil
|
||||
}
|
||||
|
||||
func (a *ldapAuther) serverBind() error {
|
||||
bindFn := func() error {
|
||||
return a.conn.Bind(a.server.BindDN, a.server.BindPassword)
|
||||
}
|
||||
|
||||
if a.server.BindPassword == "" {
|
||||
bindFn = func() error {
|
||||
return a.conn.UnauthenticatedBind(a.server.BindDN)
|
||||
}
|
||||
}
|
||||
|
||||
// bind_dn and bind_password to bind
|
||||
if err := bindFn(); err != nil {
|
||||
a.log.Info("LDAP initial bind failed, %v", err)
|
||||
|
||||
if ldapErr, ok := err.(*ldap.Error); ok {
|
||||
if ldapErr.ResultCode == 49 {
|
||||
return ErrInvalidCredentials
|
||||
}
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *ldapAuther) secondBind(ldapUser *LdapUserInfo, userPassword string) error {
|
||||
if err := a.conn.Bind(ldapUser.DN, userPassword); err != nil {
|
||||
a.log.Info("Second bind failed", "error", err)
|
||||
|
||||
if ldapErr, ok := err.(*ldap.Error); ok {
|
||||
if ldapErr.ResultCode == 49 {
|
||||
return ErrInvalidCredentials
|
||||
}
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *ldapAuther) initialBind(username, userPassword string) error {
|
||||
if a.server.BindPassword != "" || a.server.BindDN == "" {
|
||||
userPassword = a.server.BindPassword
|
||||
a.requireSecondBind = true
|
||||
}
|
||||
|
||||
bindPath := a.server.BindDN
|
||||
if strings.Contains(bindPath, "%s") {
|
||||
bindPath = fmt.Sprintf(a.server.BindDN, username)
|
||||
}
|
||||
|
||||
bindFn := func() error {
|
||||
return a.conn.Bind(bindPath, userPassword)
|
||||
}
|
||||
|
||||
if userPassword == "" {
|
||||
bindFn = func() error {
|
||||
return a.conn.UnauthenticatedBind(bindPath)
|
||||
}
|
||||
}
|
||||
|
||||
if err := bindFn(); err != nil {
|
||||
a.log.Info("Initial bind failed", "error", err)
|
||||
|
||||
if ldapErr, ok := err.(*ldap.Error); ok {
|
||||
if ldapErr.ResultCode == 49 {
|
||||
return ErrInvalidCredentials
|
||||
}
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func appendIfNotEmpty(slice []string, values ...string) []string {
|
||||
for _, v := range values {
|
||||
if v != "" {
|
||||
slice = append(slice, v)
|
||||
}
|
||||
}
|
||||
return slice
|
||||
}
|
||||
|
||||
func (a *ldapAuther) searchForUser(username string) (*LdapUserInfo, error) {
|
||||
var searchResult *ldap.SearchResult
|
||||
var err error
|
||||
|
||||
for _, searchBase := range a.server.SearchBaseDNs {
|
||||
attributes := make([]string, 0)
|
||||
inputs := a.server.Attr
|
||||
attributes = appendIfNotEmpty(attributes,
|
||||
inputs.Username,
|
||||
inputs.Surname,
|
||||
inputs.Email,
|
||||
inputs.Name,
|
||||
inputs.MemberOf)
|
||||
|
||||
searchReq := ldap.SearchRequest{
|
||||
BaseDN: searchBase,
|
||||
Scope: ldap.ScopeWholeSubtree,
|
||||
DerefAliases: ldap.NeverDerefAliases,
|
||||
Attributes: attributes,
|
||||
Filter: strings.Replace(a.server.SearchFilter, "%s", ldap.EscapeFilter(username), -1),
|
||||
}
|
||||
|
||||
a.log.Debug("Ldap Search For User Request", "info", spew.Sdump(searchReq))
|
||||
|
||||
searchResult, err = a.conn.Search(&searchReq)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if len(searchResult.Entries) > 0 {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if len(searchResult.Entries) == 0 {
|
||||
return nil, ErrInvalidCredentials
|
||||
}
|
||||
|
||||
if len(searchResult.Entries) > 1 {
|
||||
return nil, errors.New("Ldap search matched more than one entry, please review your filter setting")
|
||||
}
|
||||
|
||||
var memberOf []string
|
||||
if a.server.GroupSearchFilter == "" {
|
||||
memberOf = getLdapAttrArray(a.server.Attr.MemberOf, searchResult)
|
||||
} else {
|
||||
// If we are using a POSIX LDAP schema it won't support memberOf, so we manually search the groups
|
||||
var groupSearchResult *ldap.SearchResult
|
||||
for _, groupSearchBase := range a.server.GroupSearchBaseDNs {
|
||||
var filter_replace string
|
||||
if a.server.GroupSearchFilterUserAttribute == "" {
|
||||
filter_replace = getLdapAttr(a.server.Attr.Username, searchResult)
|
||||
} else {
|
||||
filter_replace = getLdapAttr(a.server.GroupSearchFilterUserAttribute, searchResult)
|
||||
}
|
||||
|
||||
filter := strings.Replace(a.server.GroupSearchFilter, "%s", ldap.EscapeFilter(filter_replace), -1)
|
||||
|
||||
a.log.Info("Searching for user's groups", "filter", filter)
|
||||
|
||||
// support old way of reading settings
|
||||
groupIdAttribute := a.server.Attr.MemberOf
|
||||
// but prefer dn attribute if default settings are used
|
||||
if groupIdAttribute == "" || groupIdAttribute == "memberOf" {
|
||||
groupIdAttribute = "dn"
|
||||
}
|
||||
|
||||
groupSearchReq := ldap.SearchRequest{
|
||||
BaseDN: groupSearchBase,
|
||||
Scope: ldap.ScopeWholeSubtree,
|
||||
DerefAliases: ldap.NeverDerefAliases,
|
||||
Attributes: []string{groupIdAttribute},
|
||||
Filter: filter,
|
||||
}
|
||||
|
||||
groupSearchResult, err = a.conn.Search(&groupSearchReq)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if len(groupSearchResult.Entries) > 0 {
|
||||
for i := range groupSearchResult.Entries {
|
||||
memberOf = append(memberOf, getLdapAttrN(groupIdAttribute, groupSearchResult, i))
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return &LdapUserInfo{
|
||||
DN: searchResult.Entries[0].DN,
|
||||
LastName: getLdapAttr(a.server.Attr.Surname, searchResult),
|
||||
FirstName: getLdapAttr(a.server.Attr.Name, searchResult),
|
||||
Username: getLdapAttr(a.server.Attr.Username, searchResult),
|
||||
Email: getLdapAttr(a.server.Attr.Email, searchResult),
|
||||
MemberOf: memberOf,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func getLdapAttrN(name string, result *ldap.SearchResult, n int) string {
|
||||
if strings.ToLower(name) == "dn" {
|
||||
return result.Entries[n].DN
|
||||
}
|
||||
for _, attr := range result.Entries[n].Attributes {
|
||||
if attr.Name == name {
|
||||
if len(attr.Values) > 0 {
|
||||
return attr.Values[0]
|
||||
}
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func getLdapAttr(name string, result *ldap.SearchResult) string {
|
||||
return getLdapAttrN(name, result, 0)
|
||||
}
|
||||
|
||||
func getLdapAttrArray(name string, result *ldap.SearchResult) []string {
|
||||
for _, attr := range result.Entries[0].Attributes {
|
||||
if attr.Name == name {
|
||||
return attr.Values
|
||||
}
|
||||
}
|
||||
return []string{}
|
||||
}
|
@ -1,22 +1,34 @@
|
||||
package login
|
||||
|
||||
import (
|
||||
m "github.com/grafana/grafana/pkg/models"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
"github.com/grafana/grafana/pkg/models"
|
||||
LDAP "github.com/grafana/grafana/pkg/services/ldap"
|
||||
)
|
||||
|
||||
var loginUsingLdap = func(query *m.LoginUserQuery) (bool, error) {
|
||||
if !setting.LdapEnabled {
|
||||
var newLDAP = LDAP.New
|
||||
var readLDAPConfig = LDAP.ReadConfig
|
||||
var isLDAPEnabled = LDAP.IsEnabled
|
||||
|
||||
var loginUsingLdap = func(query *models.LoginUserQuery) (bool, error) {
|
||||
enabled := isLDAPEnabled()
|
||||
|
||||
if !enabled {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
for _, server := range LdapCfg.Servers {
|
||||
author := NewLdapAuthenticator(server)
|
||||
err := author.Login(query)
|
||||
if err == nil || err != ErrInvalidCredentials {
|
||||
config := readLDAPConfig()
|
||||
if len(config.Servers) == 0 {
|
||||
return true, ErrNoLDAPServers
|
||||
}
|
||||
|
||||
for _, server := range config.Servers {
|
||||
auth := newLDAP(server)
|
||||
|
||||
err := auth.Login(query)
|
||||
if err == nil || err != LDAP.ErrInvalidCredentials {
|
||||
return true, err
|
||||
}
|
||||
}
|
||||
|
||||
return true, ErrInvalidCredentials
|
||||
return true, LDAP.ErrInvalidCredentials
|
||||
}
|
||||
|
@ -1,71 +1,41 @@
|
||||
package login
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"testing"
|
||||
|
||||
m "github.com/grafana/grafana/pkg/models"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
. "github.com/smartystreets/goconvey/convey"
|
||||
|
||||
m "github.com/grafana/grafana/pkg/models"
|
||||
LDAP "github.com/grafana/grafana/pkg/services/ldap"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
)
|
||||
|
||||
var errTest = errors.New("Test error")
|
||||
|
||||
func TestLdapLogin(t *testing.T) {
|
||||
Convey("Login using ldap", t, func() {
|
||||
Convey("Given ldap enabled and a server configured", func() {
|
||||
setting.LdapEnabled = true
|
||||
LdapCfg.Servers = append(LdapCfg.Servers,
|
||||
&LdapServerConf{
|
||||
Host: "",
|
||||
})
|
||||
|
||||
ldapLoginScenario("When login with invalid credentials", func(sc *ldapLoginScenarioContext) {
|
||||
sc.withLoginResult(false)
|
||||
enabled, err := loginUsingLdap(sc.loginUserQuery)
|
||||
|
||||
Convey("it should return true", func() {
|
||||
So(enabled, ShouldBeTrue)
|
||||
})
|
||||
|
||||
Convey("it should return invalid credentials error", func() {
|
||||
So(err, ShouldEqual, ErrInvalidCredentials)
|
||||
})
|
||||
|
||||
Convey("it should call ldap login", func() {
|
||||
So(sc.ldapAuthenticatorMock.loginCalled, ShouldBeTrue)
|
||||
})
|
||||
})
|
||||
|
||||
ldapLoginScenario("When login with valid credentials", func(sc *ldapLoginScenarioContext) {
|
||||
sc.withLoginResult(true)
|
||||
enabled, err := loginUsingLdap(sc.loginUserQuery)
|
||||
|
||||
Convey("it should return true", func() {
|
||||
So(enabled, ShouldBeTrue)
|
||||
})
|
||||
|
||||
Convey("it should not return error", func() {
|
||||
So(err, ShouldBeNil)
|
||||
})
|
||||
|
||||
Convey("it should call ldap login", func() {
|
||||
So(sc.ldapAuthenticatorMock.loginCalled, ShouldBeTrue)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
Convey("Given ldap enabled and no server configured", func() {
|
||||
setting.LdapEnabled = true
|
||||
LdapCfg.Servers = make([]*LdapServerConf, 0)
|
||||
|
||||
ldapLoginScenario("When login", func(sc *ldapLoginScenarioContext) {
|
||||
sc.withLoginResult(true)
|
||||
sc.withLoginResult(false)
|
||||
readLDAPConfig = func() *LDAP.Config {
|
||||
config := &LDAP.Config{
|
||||
Servers: []*LDAP.ServerConfig{},
|
||||
}
|
||||
|
||||
return config
|
||||
}
|
||||
|
||||
enabled, err := loginUsingLdap(sc.loginUserQuery)
|
||||
|
||||
Convey("it should return true", func() {
|
||||
So(enabled, ShouldBeTrue)
|
||||
})
|
||||
|
||||
Convey("it should return invalid credentials error", func() {
|
||||
So(err, ShouldEqual, ErrInvalidCredentials)
|
||||
Convey("it should return no LDAP servers error", func() {
|
||||
So(err, ShouldEqual, ErrNoLDAPServers)
|
||||
})
|
||||
|
||||
Convey("it should not call ldap login", func() {
|
||||
@ -100,51 +70,55 @@ func TestLdapLogin(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
func mockLdapAuthenticator(valid bool) *mockLdapAuther {
|
||||
mock := &mockLdapAuther{
|
||||
func mockLdapAuthenticator(valid bool) *mockAuth {
|
||||
mock := &mockAuth{
|
||||
validLogin: valid,
|
||||
}
|
||||
|
||||
NewLdapAuthenticator = func(server *LdapServerConf) ILdapAuther {
|
||||
newLDAP = func(server *LDAP.ServerConfig) LDAP.IAuth {
|
||||
return mock
|
||||
}
|
||||
|
||||
return mock
|
||||
}
|
||||
|
||||
type mockLdapAuther struct {
|
||||
type mockAuth struct {
|
||||
validLogin bool
|
||||
loginCalled bool
|
||||
}
|
||||
|
||||
func (a *mockLdapAuther) Login(query *m.LoginUserQuery) error {
|
||||
a.loginCalled = true
|
||||
func (auth *mockAuth) Login(query *m.LoginUserQuery) error {
|
||||
auth.loginCalled = true
|
||||
|
||||
if !a.validLogin {
|
||||
return ErrInvalidCredentials
|
||||
if !auth.validLogin {
|
||||
return errTest
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *mockLdapAuther) SyncUser(query *m.LoginUserQuery) error {
|
||||
func (auth *mockAuth) Users() ([]*LDAP.UserInfo, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (auth *mockAuth) SyncUser(query *m.LoginUserQuery) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *mockLdapAuther) GetGrafanaUserFor(ctx *m.ReqContext, ldapUser *LdapUserInfo) (*m.User, error) {
|
||||
func (auth *mockAuth) GetGrafanaUserFor(ctx *m.ReqContext, ldapUser *LDAP.UserInfo) (*m.User, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
type ldapLoginScenarioContext struct {
|
||||
loginUserQuery *m.LoginUserQuery
|
||||
ldapAuthenticatorMock *mockLdapAuther
|
||||
ldapAuthenticatorMock *mockAuth
|
||||
}
|
||||
|
||||
type ldapLoginScenarioFunc func(c *ldapLoginScenarioContext)
|
||||
|
||||
func ldapLoginScenario(desc string, fn ldapLoginScenarioFunc) {
|
||||
Convey(desc, func() {
|
||||
origNewLdapAuthenticator := NewLdapAuthenticator
|
||||
mock := &mockAuth{}
|
||||
|
||||
sc := &ldapLoginScenarioContext{
|
||||
loginUserQuery: &m.LoginUserQuery{
|
||||
@ -152,11 +126,28 @@ func ldapLoginScenario(desc string, fn ldapLoginScenarioFunc) {
|
||||
Password: "pwd",
|
||||
IpAddress: "192.168.1.1:56433",
|
||||
},
|
||||
ldapAuthenticatorMock: &mockLdapAuther{},
|
||||
ldapAuthenticatorMock: mock,
|
||||
}
|
||||
|
||||
readLDAPConfig = func() *LDAP.Config {
|
||||
config := &LDAP.Config{
|
||||
Servers: []*LDAP.ServerConfig{
|
||||
{
|
||||
Host: "",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
return config
|
||||
}
|
||||
|
||||
newLDAP = func(server *LDAP.ServerConfig) LDAP.IAuth {
|
||||
return mock
|
||||
}
|
||||
|
||||
defer func() {
|
||||
NewLdapAuthenticator = origNewLdapAuthenticator
|
||||
newLDAP = LDAP.New
|
||||
readLDAPConfig = LDAP.ReadConfig
|
||||
}()
|
||||
|
||||
fn(sc)
|
||||
|
@ -1,104 +0,0 @@
|
||||
package login
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/BurntSushi/toml"
|
||||
"github.com/grafana/grafana/pkg/log"
|
||||
m "github.com/grafana/grafana/pkg/models"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
)
|
||||
|
||||
type LdapConfig struct {
|
||||
Servers []*LdapServerConf `toml:"servers"`
|
||||
}
|
||||
|
||||
type LdapServerConf struct {
|
||||
Host string `toml:"host"`
|
||||
Port int `toml:"port"`
|
||||
UseSSL bool `toml:"use_ssl"`
|
||||
StartTLS bool `toml:"start_tls"`
|
||||
SkipVerifySSL bool `toml:"ssl_skip_verify"`
|
||||
RootCACert string `toml:"root_ca_cert"`
|
||||
ClientCert string `toml:"client_cert"`
|
||||
ClientKey string `toml:"client_key"`
|
||||
BindDN string `toml:"bind_dn"`
|
||||
BindPassword string `toml:"bind_password"`
|
||||
Attr LdapAttributeMap `toml:"attributes"`
|
||||
|
||||
SearchFilter string `toml:"search_filter"`
|
||||
SearchBaseDNs []string `toml:"search_base_dns"`
|
||||
|
||||
GroupSearchFilter string `toml:"group_search_filter"`
|
||||
GroupSearchFilterUserAttribute string `toml:"group_search_filter_user_attribute"`
|
||||
GroupSearchBaseDNs []string `toml:"group_search_base_dns"`
|
||||
|
||||
LdapGroups []*LdapGroupToOrgRole `toml:"group_mappings"`
|
||||
}
|
||||
|
||||
type LdapAttributeMap struct {
|
||||
Username string `toml:"username"`
|
||||
Name string `toml:"name"`
|
||||
Surname string `toml:"surname"`
|
||||
Email string `toml:"email"`
|
||||
MemberOf string `toml:"member_of"`
|
||||
}
|
||||
|
||||
type LdapGroupToOrgRole struct {
|
||||
GroupDN string `toml:"group_dn"`
|
||||
OrgId int64 `toml:"org_id"`
|
||||
IsGrafanaAdmin *bool `toml:"grafana_admin"` // This is a pointer to know if it was set or not (for backwards compatibility)
|
||||
OrgRole m.RoleType `toml:"org_role"`
|
||||
}
|
||||
|
||||
var LdapCfg LdapConfig
|
||||
var ldapLogger log.Logger = log.New("ldap")
|
||||
|
||||
func loadLdapConfig() {
|
||||
if !setting.LdapEnabled {
|
||||
return
|
||||
}
|
||||
|
||||
ldapLogger.Info("Ldap enabled, reading config file", "file", setting.LdapConfigFile)
|
||||
|
||||
_, err := toml.DecodeFile(setting.LdapConfigFile, &LdapCfg)
|
||||
if err != nil {
|
||||
ldapLogger.Crit("Failed to load ldap config file", "error", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
if len(LdapCfg.Servers) == 0 {
|
||||
ldapLogger.Crit("ldap enabled but no ldap servers defined in config file")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// set default org id
|
||||
for _, server := range LdapCfg.Servers {
|
||||
assertNotEmptyCfg(server.SearchFilter, "search_filter")
|
||||
assertNotEmptyCfg(server.SearchBaseDNs, "search_base_dns")
|
||||
|
||||
for _, groupMap := range server.LdapGroups {
|
||||
if groupMap.OrgId == 0 {
|
||||
groupMap.OrgId = 1
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func assertNotEmptyCfg(val interface{}, propName string) {
|
||||
switch v := val.(type) {
|
||||
case string:
|
||||
if v == "" {
|
||||
ldapLogger.Crit("LDAP config file is missing option", "option", propName)
|
||||
os.Exit(1)
|
||||
}
|
||||
case []string:
|
||||
if len(v) == 0 {
|
||||
ldapLogger.Crit("LDAP config file is missing option", "option", propName)
|
||||
os.Exit(1)
|
||||
}
|
||||
default:
|
||||
fmt.Println("unknown")
|
||||
}
|
||||
}
|
@ -10,8 +10,8 @@ import (
|
||||
|
||||
"github.com/grafana/grafana/pkg/bus"
|
||||
"github.com/grafana/grafana/pkg/infra/remotecache"
|
||||
"github.com/grafana/grafana/pkg/login"
|
||||
models "github.com/grafana/grafana/pkg/models"
|
||||
"github.com/grafana/grafana/pkg/models"
|
||||
"github.com/grafana/grafana/pkg/services/ldap"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
)
|
||||
|
||||
@ -21,6 +21,11 @@ const (
|
||||
CachePrefix = "auth-proxy-sync-ttl:%s"
|
||||
)
|
||||
|
||||
var (
|
||||
readLDAPConfig = ldap.ReadConfig
|
||||
isLDAPEnabled = ldap.IsEnabled
|
||||
)
|
||||
|
||||
// AuthProxy struct
|
||||
type AuthProxy struct {
|
||||
store *remotecache.RemoteCache
|
||||
@ -28,14 +33,13 @@ type AuthProxy struct {
|
||||
orgID int64
|
||||
header string
|
||||
|
||||
LDAP func(server *login.LdapServerConf) login.ILdapAuther
|
||||
LDAP func(server *ldap.ServerConfig) ldap.IAuth
|
||||
|
||||
enabled bool
|
||||
whitelistIP string
|
||||
headerType string
|
||||
headers map[string]string
|
||||
cacheTTL int
|
||||
ldapEnabled bool
|
||||
}
|
||||
|
||||
// Error auth proxy specific error
|
||||
@ -74,14 +78,13 @@ func New(options *Options) *AuthProxy {
|
||||
orgID: options.OrgID,
|
||||
header: header,
|
||||
|
||||
LDAP: login.NewLdapAuthenticator,
|
||||
LDAP: ldap.New,
|
||||
|
||||
enabled: setting.AuthProxyEnabled,
|
||||
headerType: setting.AuthProxyHeaderProperty,
|
||||
headers: setting.AuthProxyHeaders,
|
||||
whitelistIP: setting.AuthProxyWhitelist,
|
||||
cacheTTL: setting.AuthProxyLdapSyncTtl,
|
||||
ldapEnabled: setting.LdapEnabled,
|
||||
}
|
||||
}
|
||||
|
||||
@ -167,11 +170,14 @@ func (auth *AuthProxy) GetUserID() (int64, *Error) {
|
||||
return id, nil
|
||||
}
|
||||
|
||||
if auth.ldapEnabled {
|
||||
if isLDAPEnabled() {
|
||||
id, err := auth.GetUserIDViaLDAP()
|
||||
|
||||
if err == login.ErrInvalidCredentials {
|
||||
return 0, newError("Proxy authentication required", login.ErrInvalidCredentials)
|
||||
if err == ldap.ErrInvalidCredentials {
|
||||
return 0, newError(
|
||||
"Proxy authentication required",
|
||||
ldap.ErrInvalidCredentials,
|
||||
)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
@ -183,7 +189,10 @@ func (auth *AuthProxy) GetUserID() (int64, *Error) {
|
||||
|
||||
id, err := auth.GetUserIDViaHeader()
|
||||
if err != nil {
|
||||
return 0, newError("Failed to login as user specified in auth proxy header", err)
|
||||
return 0, newError(
|
||||
"Failed to login as user specified in auth proxy header",
|
||||
err,
|
||||
)
|
||||
}
|
||||
|
||||
return id, nil
|
||||
@ -210,12 +219,12 @@ func (auth *AuthProxy) GetUserIDViaLDAP() (int64, *Error) {
|
||||
Username: auth.header,
|
||||
}
|
||||
|
||||
ldapCfg := login.LdapCfg
|
||||
if len(ldapCfg.Servers) < 1 {
|
||||
config := readLDAPConfig()
|
||||
if len(config.Servers) == 0 {
|
||||
return 0, newError("No LDAP servers available", nil)
|
||||
}
|
||||
|
||||
for _, server := range ldapCfg.Servers {
|
||||
for _, server := range config.Servers {
|
||||
author := auth.LDAP(server)
|
||||
if err := author.SyncUser(query); err != nil {
|
||||
return 0, newError(err.Error(), nil)
|
||||
|
@ -5,16 +5,17 @@ import (
|
||||
"net/http"
|
||||
"testing"
|
||||
|
||||
"github.com/grafana/grafana/pkg/infra/remotecache"
|
||||
"github.com/grafana/grafana/pkg/login"
|
||||
models "github.com/grafana/grafana/pkg/models"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
. "github.com/smartystreets/goconvey/convey"
|
||||
"gopkg.in/macaron.v1"
|
||||
|
||||
"github.com/grafana/grafana/pkg/infra/remotecache"
|
||||
"github.com/grafana/grafana/pkg/models"
|
||||
"github.com/grafana/grafana/pkg/services/ldap"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
)
|
||||
|
||||
type TestLDAP struct {
|
||||
login.ILdapAuther
|
||||
ldap.Auth
|
||||
ID int64
|
||||
syncCalled bool
|
||||
}
|
||||
@ -62,13 +63,23 @@ func TestMiddlewareContext(t *testing.T) {
|
||||
|
||||
Convey("LDAP", func() {
|
||||
Convey("gets data from the LDAP", func() {
|
||||
login.LdapCfg = login.LdapConfig{
|
||||
Servers: []*login.LdapServerConf{
|
||||
{},
|
||||
},
|
||||
isLDAPEnabled = func() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
setting.LdapEnabled = true
|
||||
readLDAPConfig = func() *ldap.Config {
|
||||
config := &ldap.Config{
|
||||
Servers: []*ldap.ServerConfig{
|
||||
{},
|
||||
},
|
||||
}
|
||||
return config
|
||||
}
|
||||
|
||||
defer func() {
|
||||
isLDAPEnabled = ldap.IsEnabled
|
||||
readLDAPConfig = ldap.ReadConfig
|
||||
}()
|
||||
|
||||
store := remotecache.NewFakeStore(t)
|
||||
|
||||
@ -82,7 +93,7 @@ func TestMiddlewareContext(t *testing.T) {
|
||||
ID: 42,
|
||||
}
|
||||
|
||||
auth.LDAP = func(server *login.LdapServerConf) login.ILdapAuther {
|
||||
auth.LDAP = func(server *ldap.ServerConfig) ldap.IAuth {
|
||||
return stub
|
||||
}
|
||||
|
||||
@ -94,7 +105,21 @@ func TestMiddlewareContext(t *testing.T) {
|
||||
})
|
||||
|
||||
Convey("gets nice error if ldap is enabled but not configured", func() {
|
||||
setting.LdapEnabled = false
|
||||
isLDAPEnabled = func() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
readLDAPConfig = func() *ldap.Config {
|
||||
config := &ldap.Config{
|
||||
Servers: []*ldap.ServerConfig{},
|
||||
}
|
||||
return config
|
||||
}
|
||||
|
||||
defer func() {
|
||||
isLDAPEnabled = ldap.IsEnabled
|
||||
readLDAPConfig = ldap.ReadConfig
|
||||
}()
|
||||
|
||||
store := remotecache.NewFakeStore(t)
|
||||
|
||||
@ -108,13 +133,14 @@ func TestMiddlewareContext(t *testing.T) {
|
||||
ID: 42,
|
||||
}
|
||||
|
||||
auth.LDAP = func(server *login.LdapServerConf) login.ILdapAuther {
|
||||
auth.LDAP = func(server *ldap.ServerConfig) ldap.IAuth {
|
||||
return stub
|
||||
}
|
||||
|
||||
id, err := auth.GetUserID()
|
||||
|
||||
So(err, ShouldNotBeNil)
|
||||
So(err.Error(), ShouldContainSubstring, "Failed to sync user")
|
||||
So(id, ShouldNotEqual, 42)
|
||||
So(stub.syncCalled, ShouldEqual, false)
|
||||
})
|
||||
|
5
pkg/services/ldap/hooks.go
Normal file
5
pkg/services/ldap/hooks.go
Normal file
@ -0,0 +1,5 @@
|
||||
package ldap
|
||||
|
||||
var (
|
||||
hookDial func(*Auth) error
|
||||
)
|
559
pkg/services/ldap/ldap.go
Normal file
559
pkg/services/ldap/ldap.go
Normal file
@ -0,0 +1,559 @@
|
||||
package ldap
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"strings"
|
||||
|
||||
"github.com/davecgh/go-spew/spew"
|
||||
LDAP "gopkg.in/ldap.v3"
|
||||
|
||||
"github.com/grafana/grafana/pkg/bus"
|
||||
"github.com/grafana/grafana/pkg/log"
|
||||
models "github.com/grafana/grafana/pkg/models"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
)
|
||||
|
||||
// IConnection is interface for LDAP connection manipulation
|
||||
type IConnection interface {
|
||||
Bind(username, password string) error
|
||||
UnauthenticatedBind(username string) error
|
||||
Search(*LDAP.SearchRequest) (*LDAP.SearchResult, error)
|
||||
StartTLS(*tls.Config) error
|
||||
Close()
|
||||
}
|
||||
|
||||
// IAuth is interface for LDAP authorization
|
||||
type IAuth interface {
|
||||
Login(query *models.LoginUserQuery) error
|
||||
SyncUser(query *models.LoginUserQuery) error
|
||||
GetGrafanaUserFor(
|
||||
ctx *models.ReqContext,
|
||||
user *UserInfo,
|
||||
) (*models.User, error)
|
||||
Users() ([]*UserInfo, error)
|
||||
}
|
||||
|
||||
// Auth is basic struct of LDAP authorization
|
||||
type Auth struct {
|
||||
server *ServerConfig
|
||||
conn IConnection
|
||||
requireSecondBind bool
|
||||
log log.Logger
|
||||
}
|
||||
|
||||
var (
|
||||
|
||||
// ErrInvalidCredentials is returned if username and password do not match
|
||||
ErrInvalidCredentials = errors.New("Invalid Username or Password")
|
||||
)
|
||||
|
||||
var dial = func(network, addr string) (IConnection, error) {
|
||||
return LDAP.Dial(network, addr)
|
||||
}
|
||||
|
||||
// New creates the new LDAP auth
|
||||
func New(server *ServerConfig) IAuth {
|
||||
return &Auth{
|
||||
server: server,
|
||||
log: log.New("ldap"),
|
||||
}
|
||||
}
|
||||
|
||||
// Dial dials in the LDAP
|
||||
func (auth *Auth) Dial() error {
|
||||
if hookDial != nil {
|
||||
return hookDial(auth)
|
||||
}
|
||||
|
||||
var err error
|
||||
var certPool *x509.CertPool
|
||||
if auth.server.RootCACert != "" {
|
||||
certPool = x509.NewCertPool()
|
||||
for _, caCertFile := range strings.Split(auth.server.RootCACert, " ") {
|
||||
pem, err := ioutil.ReadFile(caCertFile)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !certPool.AppendCertsFromPEM(pem) {
|
||||
return errors.New("Failed to append CA certificate " + caCertFile)
|
||||
}
|
||||
}
|
||||
}
|
||||
var clientCert tls.Certificate
|
||||
if auth.server.ClientCert != "" && auth.server.ClientKey != "" {
|
||||
clientCert, err = tls.LoadX509KeyPair(auth.server.ClientCert, auth.server.ClientKey)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
for _, host := range strings.Split(auth.server.Host, " ") {
|
||||
address := fmt.Sprintf("%s:%d", host, auth.server.Port)
|
||||
if auth.server.UseSSL {
|
||||
tlsCfg := &tls.Config{
|
||||
InsecureSkipVerify: auth.server.SkipVerifySSL,
|
||||
ServerName: host,
|
||||
RootCAs: certPool,
|
||||
}
|
||||
if len(clientCert.Certificate) > 0 {
|
||||
tlsCfg.Certificates = append(tlsCfg.Certificates, clientCert)
|
||||
}
|
||||
if auth.server.StartTLS {
|
||||
auth.conn, err = dial("tcp", address)
|
||||
if err == nil {
|
||||
if err = auth.conn.StartTLS(tlsCfg); err == nil {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
} else {
|
||||
auth.conn, err = LDAP.DialTLS("tcp", address, tlsCfg)
|
||||
}
|
||||
} else {
|
||||
auth.conn, err = dial("tcp", address)
|
||||
}
|
||||
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// Login logs in the user
|
||||
func (auth *Auth) Login(query *models.LoginUserQuery) error {
|
||||
// connect to ldap server
|
||||
if err := auth.Dial(); err != nil {
|
||||
return err
|
||||
}
|
||||
defer auth.conn.Close()
|
||||
|
||||
// perform initial authentication
|
||||
if err := auth.initialBind(query.Username, query.Password); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// find user entry & attributes
|
||||
user, err := auth.searchForUser(query.Username)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
auth.log.Debug("Ldap User found", "info", spew.Sdump(user))
|
||||
|
||||
// check if a second user bind is needed
|
||||
if auth.requireSecondBind {
|
||||
err = auth.secondBind(user, query.Password)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
grafanaUser, err := auth.GetGrafanaUserFor(query.ReqContext, user)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
query.User = grafanaUser
|
||||
return nil
|
||||
}
|
||||
|
||||
// SyncUser syncs user with Grafana
|
||||
func (auth *Auth) SyncUser(query *models.LoginUserQuery) error {
|
||||
// connect to ldap server
|
||||
err := auth.Dial()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer auth.conn.Close()
|
||||
|
||||
err = auth.serverBind()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// find user entry & attributes
|
||||
user, err := auth.searchForUser(query.Username)
|
||||
if err != nil {
|
||||
auth.log.Error("Failed searching for user in ldap", "error", err)
|
||||
return err
|
||||
}
|
||||
|
||||
auth.log.Debug("Ldap User found", "info", spew.Sdump(user))
|
||||
|
||||
grafanaUser, err := auth.GetGrafanaUserFor(query.ReqContext, user)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
query.User = grafanaUser
|
||||
return nil
|
||||
}
|
||||
|
||||
func (auth *Auth) GetGrafanaUserFor(
|
||||
ctx *models.ReqContext,
|
||||
user *UserInfo,
|
||||
) (*models.User, error) {
|
||||
extUser := &models.ExternalUserInfo{
|
||||
AuthModule: "ldap",
|
||||
AuthId: user.DN,
|
||||
Name: fmt.Sprintf("%s %s", user.FirstName, user.LastName),
|
||||
Login: user.Username,
|
||||
Email: user.Email,
|
||||
Groups: user.MemberOf,
|
||||
OrgRoles: map[int64]models.RoleType{},
|
||||
}
|
||||
|
||||
for _, group := range auth.server.Groups {
|
||||
// only use the first match for each org
|
||||
if extUser.OrgRoles[group.OrgId] != "" {
|
||||
continue
|
||||
}
|
||||
|
||||
if user.isMemberOf(group.GroupDN) {
|
||||
extUser.OrgRoles[group.OrgId] = group.OrgRole
|
||||
if extUser.IsGrafanaAdmin == nil || !*extUser.IsGrafanaAdmin {
|
||||
extUser.IsGrafanaAdmin = group.IsGrafanaAdmin
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// validate that the user has access
|
||||
// if there are no ldap group mappings access is true
|
||||
// otherwise a single group must match
|
||||
if len(auth.server.Groups) > 0 && len(extUser.OrgRoles) < 1 {
|
||||
auth.log.Info(
|
||||
"Ldap Auth: user does not belong in any of the specified ldap groups",
|
||||
"username", user.Username,
|
||||
"groups", user.MemberOf,
|
||||
)
|
||||
return nil, ErrInvalidCredentials
|
||||
}
|
||||
|
||||
// add/update user in grafana
|
||||
upsertUserCmd := &models.UpsertUserCommand{
|
||||
ReqContext: ctx,
|
||||
ExternalUser: extUser,
|
||||
SignupAllowed: setting.LdapAllowSignup,
|
||||
}
|
||||
|
||||
err := bus.Dispatch(upsertUserCmd)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return upsertUserCmd.Result, nil
|
||||
}
|
||||
|
||||
func (auth *Auth) serverBind() error {
|
||||
bindFn := func() error {
|
||||
return auth.conn.Bind(auth.server.BindDN, auth.server.BindPassword)
|
||||
}
|
||||
|
||||
if auth.server.BindPassword == "" {
|
||||
bindFn = func() error {
|
||||
return auth.conn.UnauthenticatedBind(auth.server.BindDN)
|
||||
}
|
||||
}
|
||||
|
||||
// bind_dn and bind_password to bind
|
||||
if err := bindFn(); err != nil {
|
||||
auth.log.Info("LDAP initial bind failed, %v", err)
|
||||
|
||||
if ldapErr, ok := err.(*LDAP.Error); ok {
|
||||
if ldapErr.ResultCode == 49 {
|
||||
return ErrInvalidCredentials
|
||||
}
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (auth *Auth) secondBind(user *UserInfo, userPassword string) error {
|
||||
if err := auth.conn.Bind(user.DN, userPassword); err != nil {
|
||||
auth.log.Info("Second bind failed", "error", err)
|
||||
|
||||
if ldapErr, ok := err.(*LDAP.Error); ok {
|
||||
if ldapErr.ResultCode == 49 {
|
||||
return ErrInvalidCredentials
|
||||
}
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (auth *Auth) initialBind(username, userPassword string) error {
|
||||
if auth.server.BindPassword != "" || auth.server.BindDN == "" {
|
||||
userPassword = auth.server.BindPassword
|
||||
auth.requireSecondBind = true
|
||||
}
|
||||
|
||||
bindPath := auth.server.BindDN
|
||||
if strings.Contains(bindPath, "%s") {
|
||||
bindPath = fmt.Sprintf(auth.server.BindDN, username)
|
||||
}
|
||||
|
||||
bindFn := func() error {
|
||||
return auth.conn.Bind(bindPath, userPassword)
|
||||
}
|
||||
|
||||
if userPassword == "" {
|
||||
bindFn = func() error {
|
||||
return auth.conn.UnauthenticatedBind(bindPath)
|
||||
}
|
||||
}
|
||||
|
||||
if err := bindFn(); err != nil {
|
||||
auth.log.Info("Initial bind failed", "error", err)
|
||||
|
||||
if ldapErr, ok := err.(*LDAP.Error); ok {
|
||||
if ldapErr.ResultCode == 49 {
|
||||
return ErrInvalidCredentials
|
||||
}
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (auth *Auth) searchForUser(username string) (*UserInfo, error) {
|
||||
var searchResult *LDAP.SearchResult
|
||||
var err error
|
||||
|
||||
for _, searchBase := range auth.server.SearchBaseDNs {
|
||||
attributes := make([]string, 0)
|
||||
inputs := auth.server.Attr
|
||||
attributes = appendIfNotEmpty(attributes,
|
||||
inputs.Username,
|
||||
inputs.Surname,
|
||||
inputs.Email,
|
||||
inputs.Name,
|
||||
inputs.MemberOf)
|
||||
|
||||
searchReq := LDAP.SearchRequest{
|
||||
BaseDN: searchBase,
|
||||
Scope: LDAP.ScopeWholeSubtree,
|
||||
DerefAliases: LDAP.NeverDerefAliases,
|
||||
Attributes: attributes,
|
||||
Filter: strings.Replace(
|
||||
auth.server.SearchFilter,
|
||||
"%s", LDAP.EscapeFilter(username),
|
||||
-1,
|
||||
),
|
||||
}
|
||||
|
||||
auth.log.Debug("Ldap Search For User Request", "info", spew.Sdump(searchReq))
|
||||
|
||||
searchResult, err = auth.conn.Search(&searchReq)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if len(searchResult.Entries) > 0 {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if len(searchResult.Entries) == 0 {
|
||||
return nil, ErrInvalidCredentials
|
||||
}
|
||||
|
||||
if len(searchResult.Entries) > 1 {
|
||||
return nil, errors.New("Ldap search matched more than one entry, please review your filter setting")
|
||||
}
|
||||
|
||||
var memberOf []string
|
||||
if auth.server.GroupSearchFilter == "" {
|
||||
memberOf = getLdapAttrArray(auth.server.Attr.MemberOf, searchResult)
|
||||
} else {
|
||||
// If we are using a POSIX LDAP schema it won't support memberOf, so we manually search the groups
|
||||
var groupSearchResult *LDAP.SearchResult
|
||||
for _, groupSearchBase := range auth.server.GroupSearchBaseDNs {
|
||||
var filter_replace string
|
||||
if auth.server.GroupSearchFilterUserAttribute == "" {
|
||||
filter_replace = getLdapAttr(auth.server.Attr.Username, searchResult)
|
||||
} else {
|
||||
filter_replace = getLdapAttr(auth.server.GroupSearchFilterUserAttribute, searchResult)
|
||||
}
|
||||
|
||||
filter := strings.Replace(
|
||||
auth.server.GroupSearchFilter, "%s",
|
||||
LDAP.EscapeFilter(filter_replace),
|
||||
-1,
|
||||
)
|
||||
|
||||
auth.log.Info("Searching for user's groups", "filter", filter)
|
||||
|
||||
// support old way of reading settings
|
||||
groupIdAttribute := auth.server.Attr.MemberOf
|
||||
// but prefer dn attribute if default settings are used
|
||||
if groupIdAttribute == "" || groupIdAttribute == "memberOf" {
|
||||
groupIdAttribute = "dn"
|
||||
}
|
||||
|
||||
groupSearchReq := LDAP.SearchRequest{
|
||||
BaseDN: groupSearchBase,
|
||||
Scope: LDAP.ScopeWholeSubtree,
|
||||
DerefAliases: LDAP.NeverDerefAliases,
|
||||
Attributes: []string{groupIdAttribute},
|
||||
Filter: filter,
|
||||
}
|
||||
|
||||
groupSearchResult, err = auth.conn.Search(&groupSearchReq)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if len(groupSearchResult.Entries) > 0 {
|
||||
for i := range groupSearchResult.Entries {
|
||||
memberOf = append(memberOf, getLdapAttrN(groupIdAttribute, groupSearchResult, i))
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return &UserInfo{
|
||||
DN: searchResult.Entries[0].DN,
|
||||
LastName: getLdapAttr(auth.server.Attr.Surname, searchResult),
|
||||
FirstName: getLdapAttr(auth.server.Attr.Name, searchResult),
|
||||
Username: getLdapAttr(auth.server.Attr.Username, searchResult),
|
||||
Email: getLdapAttr(auth.server.Attr.Email, searchResult),
|
||||
MemberOf: memberOf,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (ldap *Auth) Users() ([]*UserInfo, error) {
|
||||
var result *LDAP.SearchResult
|
||||
var err error
|
||||
server := ldap.server
|
||||
|
||||
if err := ldap.Dial(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer ldap.conn.Close()
|
||||
|
||||
for _, base := range server.SearchBaseDNs {
|
||||
attributes := make([]string, 0)
|
||||
inputs := server.Attr
|
||||
attributes = appendIfNotEmpty(
|
||||
attributes,
|
||||
inputs.Username,
|
||||
inputs.Surname,
|
||||
inputs.Email,
|
||||
inputs.Name,
|
||||
inputs.MemberOf,
|
||||
)
|
||||
|
||||
req := LDAP.SearchRequest{
|
||||
BaseDN: base,
|
||||
Scope: LDAP.ScopeWholeSubtree,
|
||||
DerefAliases: LDAP.NeverDerefAliases,
|
||||
Attributes: attributes,
|
||||
|
||||
// Doing a star here to get all the users in one go
|
||||
Filter: strings.Replace(server.SearchFilter, "%s", "*", -1),
|
||||
}
|
||||
|
||||
result, err = ldap.conn.Search(&req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if len(result.Entries) > 0 {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return ldap.serializeUsers(result), nil
|
||||
}
|
||||
|
||||
func (ldap *Auth) serializeUsers(users *LDAP.SearchResult) []*UserInfo {
|
||||
var serialized []*UserInfo
|
||||
|
||||
for index := range users.Entries {
|
||||
serialize := &UserInfo{
|
||||
DN: getLdapAttrN(
|
||||
"dn",
|
||||
users,
|
||||
index,
|
||||
),
|
||||
LastName: getLdapAttrN(
|
||||
ldap.server.Attr.Surname,
|
||||
users,
|
||||
index,
|
||||
),
|
||||
FirstName: getLdapAttrN(
|
||||
ldap.server.Attr.Name,
|
||||
users,
|
||||
index,
|
||||
),
|
||||
Username: getLdapAttrN(
|
||||
ldap.server.Attr.Username,
|
||||
users,
|
||||
index,
|
||||
),
|
||||
Email: getLdapAttrN(
|
||||
ldap.server.Attr.Email,
|
||||
users,
|
||||
index,
|
||||
),
|
||||
MemberOf: getLdapAttrArrayN(
|
||||
ldap.server.Attr.MemberOf,
|
||||
users,
|
||||
index,
|
||||
),
|
||||
}
|
||||
|
||||
serialized = append(serialized, serialize)
|
||||
}
|
||||
|
||||
return serialized
|
||||
}
|
||||
|
||||
func appendIfNotEmpty(slice []string, values ...string) []string {
|
||||
for _, v := range values {
|
||||
if v != "" {
|
||||
slice = append(slice, v)
|
||||
}
|
||||
}
|
||||
return slice
|
||||
}
|
||||
|
||||
func getLdapAttr(name string, result *LDAP.SearchResult) string {
|
||||
return getLdapAttrN(name, result, 0)
|
||||
}
|
||||
|
||||
func getLdapAttrN(name string, result *LDAP.SearchResult, n int) string {
|
||||
if strings.ToLower(name) == "dn" {
|
||||
return result.Entries[n].DN
|
||||
}
|
||||
for _, attr := range result.Entries[n].Attributes {
|
||||
if attr.Name == name {
|
||||
if len(attr.Values) > 0 {
|
||||
return attr.Values[0]
|
||||
}
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func getLdapAttrArray(name string, result *LDAP.SearchResult) []string {
|
||||
return getLdapAttrArrayN(name, result, 0)
|
||||
}
|
||||
|
||||
func getLdapAttrArrayN(name string, result *LDAP.SearchResult, n int) []string {
|
||||
for _, attr := range result.Entries[n].Attributes {
|
||||
if attr.Name == name {
|
||||
return attr.Values
|
||||
}
|
||||
}
|
||||
return []string{}
|
||||
}
|
86
pkg/services/ldap/ldap_login_test.go
Normal file
86
pkg/services/ldap/ldap_login_test.go
Normal file
@ -0,0 +1,86 @@
|
||||
package ldap
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
. "github.com/smartystreets/goconvey/convey"
|
||||
"gopkg.in/ldap.v3"
|
||||
|
||||
"github.com/grafana/grafana/pkg/log"
|
||||
)
|
||||
|
||||
func TestLdapLogin(t *testing.T) {
|
||||
Convey("Login using ldap", t, func() {
|
||||
AuthScenario("When login with invalid credentials", func(scenario *scenarioContext) {
|
||||
conn := &mockLdapConn{}
|
||||
entry := ldap.Entry{}
|
||||
result := ldap.SearchResult{Entries: []*ldap.Entry{&entry}}
|
||||
conn.setSearchResult(&result)
|
||||
|
||||
conn.bindProvider = func(username, password string) error {
|
||||
return &ldap.Error{
|
||||
ResultCode: 49,
|
||||
}
|
||||
}
|
||||
auth := &Auth{
|
||||
server: &ServerConfig{
|
||||
Attr: AttributeMap{
|
||||
Username: "username",
|
||||
Name: "name",
|
||||
MemberOf: "memberof",
|
||||
},
|
||||
SearchBaseDNs: []string{"BaseDNHere"},
|
||||
},
|
||||
conn: conn,
|
||||
log: log.New("test-logger"),
|
||||
}
|
||||
|
||||
err := auth.Login(scenario.loginUserQuery)
|
||||
|
||||
Convey("it should return invalid credentials error", func() {
|
||||
So(err, ShouldEqual, ErrInvalidCredentials)
|
||||
})
|
||||
})
|
||||
|
||||
AuthScenario("When login with valid credentials", func(scenario *scenarioContext) {
|
||||
conn := &mockLdapConn{}
|
||||
entry := ldap.Entry{
|
||||
DN: "dn", Attributes: []*ldap.EntryAttribute{
|
||||
{Name: "username", Values: []string{"markelog"}},
|
||||
{Name: "surname", Values: []string{"Gaidarenko"}},
|
||||
{Name: "email", Values: []string{"markelog@gmail.com"}},
|
||||
{Name: "name", Values: []string{"Oleg"}},
|
||||
{Name: "memberof", Values: []string{"admins"}},
|
||||
},
|
||||
}
|
||||
result := ldap.SearchResult{Entries: []*ldap.Entry{&entry}}
|
||||
conn.setSearchResult(&result)
|
||||
|
||||
conn.bindProvider = func(username, password string) error {
|
||||
return nil
|
||||
}
|
||||
auth := &Auth{
|
||||
server: &ServerConfig{
|
||||
Attr: AttributeMap{
|
||||
Username: "username",
|
||||
Name: "name",
|
||||
MemberOf: "memberof",
|
||||
},
|
||||
SearchBaseDNs: []string{"BaseDNHere"},
|
||||
},
|
||||
conn: conn,
|
||||
log: log.New("test-logger"),
|
||||
}
|
||||
|
||||
err := auth.Login(scenario.loginUserQuery)
|
||||
|
||||
Convey("it should not return error", func() {
|
||||
So(err, ShouldBeNil)
|
||||
})
|
||||
|
||||
Convey("it should get user", func() {
|
||||
So(scenario.loginUserQuery.User.Login, ShouldEqual, "markelog")
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
@ -1,18 +1,18 @@
|
||||
package login
|
||||
package ldap
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"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"
|
||||
. "github.com/smartystreets/goconvey/convey"
|
||||
"gopkg.in/ldap.v3"
|
||||
)
|
||||
|
||||
func TestLdapAuther(t *testing.T) {
|
||||
func TestAuth(t *testing.T) {
|
||||
Convey("initialBind", t, func() {
|
||||
Convey("Given bind dn and password configured", func() {
|
||||
conn := &mockLdapConn{}
|
||||
@ -22,16 +22,16 @@ func TestLdapAuther(t *testing.T) {
|
||||
actualPassword = password
|
||||
return nil
|
||||
}
|
||||
ldapAuther := &ldapAuther{
|
||||
Auth := &Auth{
|
||||
conn: conn,
|
||||
server: &LdapServerConf{
|
||||
server: &ServerConfig{
|
||||
BindDN: "cn=%s,o=users,dc=grafana,dc=org",
|
||||
BindPassword: "bindpwd",
|
||||
},
|
||||
}
|
||||
err := ldapAuther.initialBind("user", "pwd")
|
||||
err := Auth.initialBind("user", "pwd")
|
||||
So(err, ShouldBeNil)
|
||||
So(ldapAuther.requireSecondBind, ShouldBeTrue)
|
||||
So(Auth.requireSecondBind, ShouldBeTrue)
|
||||
So(actualUsername, ShouldEqual, "cn=user,o=users,dc=grafana,dc=org")
|
||||
So(actualPassword, ShouldEqual, "bindpwd")
|
||||
})
|
||||
@ -44,15 +44,15 @@ func TestLdapAuther(t *testing.T) {
|
||||
actualPassword = password
|
||||
return nil
|
||||
}
|
||||
ldapAuther := &ldapAuther{
|
||||
Auth := &Auth{
|
||||
conn: conn,
|
||||
server: &LdapServerConf{
|
||||
server: &ServerConfig{
|
||||
BindDN: "cn=%s,o=users,dc=grafana,dc=org",
|
||||
},
|
||||
}
|
||||
err := ldapAuther.initialBind("user", "pwd")
|
||||
err := Auth.initialBind("user", "pwd")
|
||||
So(err, ShouldBeNil)
|
||||
So(ldapAuther.requireSecondBind, ShouldBeFalse)
|
||||
So(Auth.requireSecondBind, ShouldBeFalse)
|
||||
So(actualUsername, ShouldEqual, "cn=user,o=users,dc=grafana,dc=org")
|
||||
So(actualPassword, ShouldEqual, "pwd")
|
||||
})
|
||||
@ -66,13 +66,13 @@ func TestLdapAuther(t *testing.T) {
|
||||
actualUsername = username
|
||||
return nil
|
||||
}
|
||||
ldapAuther := &ldapAuther{
|
||||
Auth := &Auth{
|
||||
conn: conn,
|
||||
server: &LdapServerConf{},
|
||||
server: &ServerConfig{},
|
||||
}
|
||||
err := ldapAuther.initialBind("user", "pwd")
|
||||
err := Auth.initialBind("user", "pwd")
|
||||
So(err, ShouldBeNil)
|
||||
So(ldapAuther.requireSecondBind, ShouldBeTrue)
|
||||
So(Auth.requireSecondBind, ShouldBeTrue)
|
||||
So(unauthenticatedBindWasCalled, ShouldBeTrue)
|
||||
So(actualUsername, ShouldBeEmpty)
|
||||
})
|
||||
@ -87,14 +87,14 @@ func TestLdapAuther(t *testing.T) {
|
||||
actualPassword = password
|
||||
return nil
|
||||
}
|
||||
ldapAuther := &ldapAuther{
|
||||
Auth := &Auth{
|
||||
conn: conn,
|
||||
server: &LdapServerConf{
|
||||
server: &ServerConfig{
|
||||
BindDN: "o=users,dc=grafana,dc=org",
|
||||
BindPassword: "bindpwd",
|
||||
},
|
||||
}
|
||||
err := ldapAuther.serverBind()
|
||||
err := Auth.serverBind()
|
||||
So(err, ShouldBeNil)
|
||||
So(actualUsername, ShouldEqual, "o=users,dc=grafana,dc=org")
|
||||
So(actualPassword, ShouldEqual, "bindpwd")
|
||||
@ -109,13 +109,13 @@ func TestLdapAuther(t *testing.T) {
|
||||
actualUsername = username
|
||||
return nil
|
||||
}
|
||||
ldapAuther := &ldapAuther{
|
||||
Auth := &Auth{
|
||||
conn: conn,
|
||||
server: &LdapServerConf{
|
||||
server: &ServerConfig{
|
||||
BindDN: "o=users,dc=grafana,dc=org",
|
||||
},
|
||||
}
|
||||
err := ldapAuther.serverBind()
|
||||
err := Auth.serverBind()
|
||||
So(err, ShouldBeNil)
|
||||
So(unauthenticatedBindWasCalled, ShouldBeTrue)
|
||||
So(actualUsername, ShouldEqual, "o=users,dc=grafana,dc=org")
|
||||
@ -130,11 +130,11 @@ func TestLdapAuther(t *testing.T) {
|
||||
actualUsername = username
|
||||
return nil
|
||||
}
|
||||
ldapAuther := &ldapAuther{
|
||||
Auth := &Auth{
|
||||
conn: conn,
|
||||
server: &LdapServerConf{},
|
||||
server: &ServerConfig{},
|
||||
}
|
||||
err := ldapAuther.serverBind()
|
||||
err := Auth.serverBind()
|
||||
So(err, ShouldBeNil)
|
||||
So(unauthenticatedBindWasCalled, ShouldBeTrue)
|
||||
So(actualUsername, ShouldBeEmpty)
|
||||
@ -152,59 +152,59 @@ func TestLdapAuther(t *testing.T) {
|
||||
})
|
||||
|
||||
Convey("Given no ldap group map match", func() {
|
||||
ldapAuther := NewLdapAuthenticator(&LdapServerConf{
|
||||
LdapGroups: []*LdapGroupToOrgRole{{}},
|
||||
Auth := New(&ServerConfig{
|
||||
Groups: []*GroupToOrgRole{{}},
|
||||
})
|
||||
_, err := ldapAuther.GetGrafanaUserFor(nil, &LdapUserInfo{})
|
||||
_, err := Auth.GetGrafanaUserFor(nil, &UserInfo{})
|
||||
|
||||
So(err, ShouldEqual, ErrInvalidCredentials)
|
||||
})
|
||||
|
||||
ldapAutherScenario("Given wildcard group match", func(sc *scenarioContext) {
|
||||
ldapAuther := NewLdapAuthenticator(&LdapServerConf{
|
||||
LdapGroups: []*LdapGroupToOrgRole{
|
||||
AuthScenario("Given wildcard group match", func(sc *scenarioContext) {
|
||||
Auth := New(&ServerConfig{
|
||||
Groups: []*GroupToOrgRole{
|
||||
{GroupDN: "*", OrgRole: "Admin"},
|
||||
},
|
||||
})
|
||||
|
||||
sc.userQueryReturns(user1)
|
||||
|
||||
result, err := ldapAuther.GetGrafanaUserFor(nil, &LdapUserInfo{})
|
||||
result, err := Auth.GetGrafanaUserFor(nil, &UserInfo{})
|
||||
So(err, ShouldBeNil)
|
||||
So(result, ShouldEqual, user1)
|
||||
})
|
||||
|
||||
ldapAutherScenario("Given exact group match", func(sc *scenarioContext) {
|
||||
ldapAuther := NewLdapAuthenticator(&LdapServerConf{
|
||||
LdapGroups: []*LdapGroupToOrgRole{
|
||||
AuthScenario("Given exact group match", func(sc *scenarioContext) {
|
||||
Auth := New(&ServerConfig{
|
||||
Groups: []*GroupToOrgRole{
|
||||
{GroupDN: "cn=users", OrgRole: "Admin"},
|
||||
},
|
||||
})
|
||||
|
||||
sc.userQueryReturns(user1)
|
||||
|
||||
result, err := ldapAuther.GetGrafanaUserFor(nil, &LdapUserInfo{MemberOf: []string{"cn=users"}})
|
||||
result, err := Auth.GetGrafanaUserFor(nil, &UserInfo{MemberOf: []string{"cn=users"}})
|
||||
So(err, ShouldBeNil)
|
||||
So(result, ShouldEqual, user1)
|
||||
})
|
||||
|
||||
ldapAutherScenario("Given group match with different case", func(sc *scenarioContext) {
|
||||
ldapAuther := NewLdapAuthenticator(&LdapServerConf{
|
||||
LdapGroups: []*LdapGroupToOrgRole{
|
||||
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 := ldapAuther.GetGrafanaUserFor(nil, &LdapUserInfo{MemberOf: []string{"CN=users"}})
|
||||
result, err := Auth.GetGrafanaUserFor(nil, &UserInfo{MemberOf: []string{"CN=users"}})
|
||||
So(err, ShouldBeNil)
|
||||
So(result, ShouldEqual, user1)
|
||||
})
|
||||
|
||||
ldapAutherScenario("Given no existing grafana user", func(sc *scenarioContext) {
|
||||
ldapAuther := NewLdapAuthenticator(&LdapServerConf{
|
||||
LdapGroups: []*LdapGroupToOrgRole{
|
||||
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"},
|
||||
@ -213,7 +213,7 @@ func TestLdapAuther(t *testing.T) {
|
||||
|
||||
sc.userQueryReturns(nil)
|
||||
|
||||
result, err := ldapAuther.GetGrafanaUserFor(nil, &LdapUserInfo{
|
||||
result, err := Auth.GetGrafanaUserFor(nil, &UserInfo{
|
||||
DN: "torkelo",
|
||||
Username: "torkelo",
|
||||
Email: "my@email.com",
|
||||
@ -235,15 +235,15 @@ func TestLdapAuther(t *testing.T) {
|
||||
})
|
||||
|
||||
Convey("When syncing ldap groups to grafana org roles", t, func() {
|
||||
ldapAutherScenario("given no current user orgs", func(sc *scenarioContext) {
|
||||
ldapAuther := NewLdapAuthenticator(&LdapServerConf{
|
||||
LdapGroups: []*LdapGroupToOrgRole{
|
||||
AuthScenario("given no current user orgs", func(sc *scenarioContext) {
|
||||
Auth := New(&ServerConfig{
|
||||
Groups: []*GroupToOrgRole{
|
||||
{GroupDN: "cn=users", OrgRole: "Admin"},
|
||||
},
|
||||
})
|
||||
|
||||
sc.userOrgsQueryReturns([]*m.UserOrgDTO{})
|
||||
_, err := ldapAuther.GetGrafanaUserFor(nil, &LdapUserInfo{
|
||||
_, err := Auth.GetGrafanaUserFor(nil, &UserInfo{
|
||||
MemberOf: []string{"cn=users"},
|
||||
})
|
||||
|
||||
@ -254,15 +254,15 @@ func TestLdapAuther(t *testing.T) {
|
||||
})
|
||||
})
|
||||
|
||||
ldapAutherScenario("given different current org role", func(sc *scenarioContext) {
|
||||
ldapAuther := NewLdapAuthenticator(&LdapServerConf{
|
||||
LdapGroups: []*LdapGroupToOrgRole{
|
||||
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 := ldapAuther.GetGrafanaUserFor(nil, &LdapUserInfo{
|
||||
_, err := Auth.GetGrafanaUserFor(nil, &UserInfo{
|
||||
MemberOf: []string{"cn=users"},
|
||||
})
|
||||
|
||||
@ -274,9 +274,9 @@ func TestLdapAuther(t *testing.T) {
|
||||
})
|
||||
})
|
||||
|
||||
ldapAutherScenario("given current org role is removed in ldap", func(sc *scenarioContext) {
|
||||
ldapAuther := NewLdapAuthenticator(&LdapServerConf{
|
||||
LdapGroups: []*LdapGroupToOrgRole{
|
||||
AuthScenario("given current org role is removed in ldap", func(sc *scenarioContext) {
|
||||
Auth := New(&ServerConfig{
|
||||
Groups: []*GroupToOrgRole{
|
||||
{GroupDN: "cn=users", OrgId: 2, OrgRole: "Admin"},
|
||||
},
|
||||
})
|
||||
@ -285,7 +285,7 @@ func TestLdapAuther(t *testing.T) {
|
||||
{OrgId: 1, Role: m.ROLE_EDITOR},
|
||||
{OrgId: 2, Role: m.ROLE_EDITOR},
|
||||
})
|
||||
_, err := ldapAuther.GetGrafanaUserFor(nil, &LdapUserInfo{
|
||||
_, err := Auth.GetGrafanaUserFor(nil, &UserInfo{
|
||||
MemberOf: []string{"cn=users"},
|
||||
})
|
||||
|
||||
@ -296,16 +296,16 @@ func TestLdapAuther(t *testing.T) {
|
||||
})
|
||||
})
|
||||
|
||||
ldapAutherScenario("given org role is updated in config", func(sc *scenarioContext) {
|
||||
ldapAuther := NewLdapAuthenticator(&LdapServerConf{
|
||||
LdapGroups: []*LdapGroupToOrgRole{
|
||||
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 := ldapAuther.GetGrafanaUserFor(nil, &LdapUserInfo{
|
||||
_, err := Auth.GetGrafanaUserFor(nil, &UserInfo{
|
||||
MemberOf: []string{"cn=users"},
|
||||
})
|
||||
|
||||
@ -317,16 +317,16 @@ func TestLdapAuther(t *testing.T) {
|
||||
})
|
||||
})
|
||||
|
||||
ldapAutherScenario("given multiple matching ldap groups", func(sc *scenarioContext) {
|
||||
ldapAuther := NewLdapAuthenticator(&LdapServerConf{
|
||||
LdapGroups: []*LdapGroupToOrgRole{
|
||||
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 := ldapAuther.GetGrafanaUserFor(nil, &LdapUserInfo{
|
||||
_, err := Auth.GetGrafanaUserFor(nil, &UserInfo{
|
||||
MemberOf: []string{"cn=admins"},
|
||||
})
|
||||
|
||||
@ -337,16 +337,16 @@ func TestLdapAuther(t *testing.T) {
|
||||
})
|
||||
})
|
||||
|
||||
ldapAutherScenario("given multiple matching ldap groups and no existing groups", func(sc *scenarioContext) {
|
||||
ldapAuther := NewLdapAuthenticator(&LdapServerConf{
|
||||
LdapGroups: []*LdapGroupToOrgRole{
|
||||
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 := ldapAuther.GetGrafanaUserFor(nil, &LdapUserInfo{
|
||||
_, err := Auth.GetGrafanaUserFor(nil, &UserInfo{
|
||||
MemberOf: []string{"cn=admins"},
|
||||
})
|
||||
|
||||
@ -362,17 +362,17 @@ func TestLdapAuther(t *testing.T) {
|
||||
})
|
||||
})
|
||||
|
||||
ldapAutherScenario("given ldap groups with grafana_admin=true", func(sc *scenarioContext) {
|
||||
AuthScenario("given ldap groups with grafana_admin=true", func(sc *scenarioContext) {
|
||||
trueVal := true
|
||||
|
||||
ldapAuther := NewLdapAuthenticator(&LdapServerConf{
|
||||
LdapGroups: []*LdapGroupToOrgRole{
|
||||
Auth := New(&ServerConfig{
|
||||
Groups: []*GroupToOrgRole{
|
||||
{GroupDN: "cn=admins", OrgId: 1, OrgRole: "Admin", IsGrafanaAdmin: &trueVal},
|
||||
},
|
||||
})
|
||||
|
||||
sc.userOrgsQueryReturns([]*m.UserOrgDTO{})
|
||||
_, err := ldapAuther.GetGrafanaUserFor(nil, &LdapUserInfo{
|
||||
_, err := Auth.GetGrafanaUserFor(nil, &UserInfo{
|
||||
MemberOf: []string{"cn=admins"},
|
||||
})
|
||||
|
||||
@ -384,16 +384,16 @@ func TestLdapAuther(t *testing.T) {
|
||||
})
|
||||
|
||||
Convey("When calling SyncUser", t, func() {
|
||||
|
||||
mockLdapConnection := &mockLdapConn{}
|
||||
ldapAuther := NewLdapAuthenticator(
|
||||
&LdapServerConf{
|
||||
|
||||
auth := &Auth{
|
||||
server: &ServerConfig{
|
||||
Host: "",
|
||||
RootCACert: "",
|
||||
LdapGroups: []*LdapGroupToOrgRole{
|
||||
Groups: []*GroupToOrgRole{
|
||||
{GroupDN: "*", OrgRole: "Admin"},
|
||||
},
|
||||
Attr: LdapAttributeMap{
|
||||
Attr: AttributeMap{
|
||||
Username: "username",
|
||||
Surname: "surname",
|
||||
Email: "email",
|
||||
@ -402,10 +402,12 @@ func TestLdapAuther(t *testing.T) {
|
||||
},
|
||||
SearchBaseDNs: []string{"BaseDNHere"},
|
||||
},
|
||||
)
|
||||
conn: mockLdapConnection,
|
||||
log: log.New("test-logger"),
|
||||
}
|
||||
|
||||
dialCalled := false
|
||||
ldapDial = func(network, addr string) (ILdapConn, error) {
|
||||
dial = func(network, addr string) (IConnection, error) {
|
||||
dialCalled = true
|
||||
return mockLdapConnection, nil
|
||||
}
|
||||
@ -421,12 +423,14 @@ func TestLdapAuther(t *testing.T) {
|
||||
result := ldap.SearchResult{Entries: []*ldap.Entry{&entry}}
|
||||
mockLdapConnection.setSearchResult(&result)
|
||||
|
||||
ldapAutherScenario("When ldapUser found call syncInfo and orgRoles", func(sc *scenarioContext) {
|
||||
AuthScenario("When ldapUser found call syncInfo and orgRoles", func(sc *scenarioContext) {
|
||||
// arrange
|
||||
query := &m.LoginUserQuery{
|
||||
Username: "roelgerrits",
|
||||
}
|
||||
|
||||
hookDial = nil
|
||||
|
||||
sc.userQueryReturns(&m.User{
|
||||
Id: 1,
|
||||
Email: "roel@test.net",
|
||||
@ -436,7 +440,7 @@ func TestLdapAuther(t *testing.T) {
|
||||
sc.userOrgsQueryReturns([]*m.UserOrgDTO{})
|
||||
|
||||
// act
|
||||
syncErrResult := ldapAuther.SyncUser(query)
|
||||
syncErrResult := auth.SyncUser(query)
|
||||
|
||||
// assert
|
||||
So(dialCalled, ShouldBeTrue)
|
||||
@ -465,9 +469,9 @@ func TestLdapAuther(t *testing.T) {
|
||||
mockLdapConnection.setSearchResult(&result)
|
||||
|
||||
// Set up attribute map without surname and email
|
||||
ldapAuther := &ldapAuther{
|
||||
server: &LdapServerConf{
|
||||
Attr: LdapAttributeMap{
|
||||
Auth := &Auth{
|
||||
server: &ServerConfig{
|
||||
Attr: AttributeMap{
|
||||
Username: "username",
|
||||
Name: "name",
|
||||
MemberOf: "memberof",
|
||||
@ -478,7 +482,7 @@ func TestLdapAuther(t *testing.T) {
|
||||
log: log.New("test-logger"),
|
||||
}
|
||||
|
||||
searchResult, err := ldapAuther.searchForUser("roelgerrits")
|
||||
searchResult, err := Auth.searchForUser("roelgerrits")
|
||||
|
||||
So(err, ShouldBeNil)
|
||||
So(searchResult, ShouldNotBeNil)
|
||||
@ -490,143 +494,3 @@ func TestLdapAuther(t *testing.T) {
|
||||
So(len(mockLdapConnection.searchAttributes), ShouldEqual, 3)
|
||||
})
|
||||
}
|
||||
|
||||
type mockLdapConn struct {
|
||||
result *ldap.SearchResult
|
||||
searchCalled bool
|
||||
searchAttributes []string
|
||||
bindProvider func(username, password string) error
|
||||
unauthenticatedBindProvider func(username string) error
|
||||
}
|
||||
|
||||
func (c *mockLdapConn) Bind(username, password string) error {
|
||||
if c.bindProvider != nil {
|
||||
return c.bindProvider(username, password)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *mockLdapConn) UnauthenticatedBind(username string) error {
|
||||
if c.unauthenticatedBindProvider != nil {
|
||||
return c.unauthenticatedBindProvider(username)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *mockLdapConn) Close() {}
|
||||
|
||||
func (c *mockLdapConn) setSearchResult(result *ldap.SearchResult) {
|
||||
c.result = result
|
||||
}
|
||||
|
||||
func (c *mockLdapConn) Search(sr *ldap.SearchRequest) (*ldap.SearchResult, error) {
|
||||
c.searchCalled = true
|
||||
c.searchAttributes = sr.Attributes
|
||||
return c.result, nil
|
||||
}
|
||||
|
||||
func (c *mockLdapConn) StartTLS(*tls.Config) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func ldapAutherScenario(desc string, fn scenarioFunc) {
|
||||
Convey(desc, func() {
|
||||
defer bus.ClearBusHandlers()
|
||||
|
||||
sc := &scenarioContext{}
|
||||
loginService := &LoginService{
|
||||
Bus: bus.GetBus(),
|
||||
}
|
||||
|
||||
bus.AddHandler("test", loginService.UpsertUser)
|
||||
|
||||
bus.AddHandlerCtx("test", func(ctx context.Context, cmd *m.SyncTeamsCommand) error {
|
||||
return nil
|
||||
})
|
||||
|
||||
bus.AddHandlerCtx("test", func(ctx context.Context, cmd *m.UpdateUserPermissionsCommand) error {
|
||||
sc.updateUserPermissionsCmd = cmd
|
||||
return nil
|
||||
})
|
||||
|
||||
bus.AddHandler("test", func(cmd *m.GetUserByAuthInfoQuery) error {
|
||||
sc.getUserByAuthInfoQuery = cmd
|
||||
sc.getUserByAuthInfoQuery.Result = &m.User{Login: cmd.Login}
|
||||
return nil
|
||||
})
|
||||
|
||||
bus.AddHandler("test", func(cmd *m.GetUserOrgListQuery) error {
|
||||
sc.getUserOrgListQuery = cmd
|
||||
return nil
|
||||
})
|
||||
|
||||
bus.AddHandler("test", func(cmd *m.CreateUserCommand) error {
|
||||
sc.createUserCmd = cmd
|
||||
sc.createUserCmd.Result = m.User{Login: cmd.Login}
|
||||
return nil
|
||||
})
|
||||
|
||||
bus.AddHandler("test", func(cmd *m.AddOrgUserCommand) error {
|
||||
sc.addOrgUserCmd = cmd
|
||||
return nil
|
||||
})
|
||||
|
||||
bus.AddHandler("test", func(cmd *m.UpdateOrgUserCommand) error {
|
||||
sc.updateOrgUserCmd = cmd
|
||||
return nil
|
||||
})
|
||||
|
||||
bus.AddHandler("test", func(cmd *m.RemoveOrgUserCommand) error {
|
||||
sc.removeOrgUserCmd = cmd
|
||||
return nil
|
||||
})
|
||||
|
||||
bus.AddHandler("test", func(cmd *m.UpdateUserCommand) error {
|
||||
sc.updateUserCmd = cmd
|
||||
return nil
|
||||
})
|
||||
|
||||
bus.AddHandler("test", func(cmd *m.SetUsingOrgCommand) error {
|
||||
sc.setUsingOrgCmd = cmd
|
||||
return nil
|
||||
})
|
||||
|
||||
fn(sc)
|
||||
})
|
||||
}
|
||||
|
||||
type scenarioContext struct {
|
||||
getUserByAuthInfoQuery *m.GetUserByAuthInfoQuery
|
||||
getUserOrgListQuery *m.GetUserOrgListQuery
|
||||
createUserCmd *m.CreateUserCommand
|
||||
addOrgUserCmd *m.AddOrgUserCommand
|
||||
updateOrgUserCmd *m.UpdateOrgUserCommand
|
||||
removeOrgUserCmd *m.RemoveOrgUserCommand
|
||||
updateUserCmd *m.UpdateUserCommand
|
||||
setUsingOrgCmd *m.SetUsingOrgCommand
|
||||
updateUserPermissionsCmd *m.UpdateUserPermissionsCommand
|
||||
}
|
||||
|
||||
func (sc *scenarioContext) userQueryReturns(user *m.User) {
|
||||
bus.AddHandler("test", func(query *m.GetUserByAuthInfoQuery) error {
|
||||
if user == nil {
|
||||
return m.ErrUserNotFound
|
||||
}
|
||||
query.Result = user
|
||||
return nil
|
||||
})
|
||||
bus.AddHandler("test", func(query *m.SetAuthInfoCommand) error {
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func (sc *scenarioContext) userOrgsQueryReturns(orgs []*m.UserOrgDTO) {
|
||||
bus.AddHandler("test", func(query *m.GetUserOrgListQuery) error {
|
||||
query.Result = orgs
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
type scenarioFunc func(c *scenarioContext)
|
126
pkg/services/ldap/settings.go
Normal file
126
pkg/services/ldap/settings.go
Normal file
@ -0,0 +1,126 @@
|
||||
package ldap
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/BurntSushi/toml"
|
||||
|
||||
"github.com/grafana/grafana/pkg/log"
|
||||
m "github.com/grafana/grafana/pkg/models"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
Servers []*ServerConfig `toml:"servers"`
|
||||
}
|
||||
|
||||
type ServerConfig struct {
|
||||
Host string `toml:"host"`
|
||||
Port int `toml:"port"`
|
||||
UseSSL bool `toml:"use_ssl"`
|
||||
StartTLS bool `toml:"start_tls"`
|
||||
SkipVerifySSL bool `toml:"ssl_skip_verify"`
|
||||
RootCACert string `toml:"root_ca_cert"`
|
||||
ClientCert string `toml:"client_cert"`
|
||||
ClientKey string `toml:"client_key"`
|
||||
BindDN string `toml:"bind_dn"`
|
||||
BindPassword string `toml:"bind_password"`
|
||||
Attr AttributeMap `toml:"attributes"`
|
||||
|
||||
SearchFilter string `toml:"search_filter"`
|
||||
SearchBaseDNs []string `toml:"search_base_dns"`
|
||||
|
||||
GroupSearchFilter string `toml:"group_search_filter"`
|
||||
GroupSearchFilterUserAttribute string `toml:"group_search_filter_user_attribute"`
|
||||
GroupSearchBaseDNs []string `toml:"group_search_base_dns"`
|
||||
|
||||
Groups []*GroupToOrgRole `toml:"group_mappings"`
|
||||
}
|
||||
|
||||
type AttributeMap struct {
|
||||
Username string `toml:"username"`
|
||||
Name string `toml:"name"`
|
||||
Surname string `toml:"surname"`
|
||||
Email string `toml:"email"`
|
||||
MemberOf string `toml:"member_of"`
|
||||
}
|
||||
|
||||
type GroupToOrgRole struct {
|
||||
GroupDN string `toml:"group_dn"`
|
||||
OrgId int64 `toml:"org_id"`
|
||||
IsGrafanaAdmin *bool `toml:"grafana_admin"` // This is a pointer to know if it was set or not (for backwards compatibility)
|
||||
OrgRole m.RoleType `toml:"org_role"`
|
||||
}
|
||||
|
||||
var config *Config
|
||||
var logger = log.New("ldap")
|
||||
|
||||
// IsEnabled checks if ldap is enabled
|
||||
func IsEnabled() bool {
|
||||
return setting.LdapEnabled
|
||||
}
|
||||
|
||||
// ReadConfig reads the config if
|
||||
// ldap is enabled otherwise it will return nil
|
||||
func ReadConfig() *Config {
|
||||
if IsEnabled() == false {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Make it a singleton
|
||||
if config != nil {
|
||||
return config
|
||||
}
|
||||
|
||||
config = getConfig(setting.LdapConfigFile)
|
||||
|
||||
return config
|
||||
}
|
||||
func getConfig(configFile string) *Config {
|
||||
result := &Config{}
|
||||
|
||||
logger.Info("Ldap enabled, reading config file", "file", configFile)
|
||||
|
||||
_, err := toml.DecodeFile(configFile, result)
|
||||
if err != nil {
|
||||
logger.Crit("Failed to load ldap config file", "error", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
if len(result.Servers) == 0 {
|
||||
logger.Crit("ldap enabled but no ldap servers defined in config file")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// set default org id
|
||||
for _, server := range result.Servers {
|
||||
assertNotEmptyCfg(server.SearchFilter, "search_filter")
|
||||
assertNotEmptyCfg(server.SearchBaseDNs, "search_base_dns")
|
||||
|
||||
for _, groupMap := range server.Groups {
|
||||
if groupMap.OrgId == 0 {
|
||||
groupMap.OrgId = 1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
func assertNotEmptyCfg(val interface{}, propName string) {
|
||||
switch v := val.(type) {
|
||||
case string:
|
||||
if v == "" {
|
||||
logger.Crit("LDAP config file is missing option", "option", propName)
|
||||
os.Exit(1)
|
||||
}
|
||||
case []string:
|
||||
if len(v) == 0 {
|
||||
logger.Crit("LDAP config file is missing option", "option", propName)
|
||||
os.Exit(1)
|
||||
}
|
||||
default:
|
||||
fmt.Println("unknown")
|
||||
}
|
||||
}
|
165
pkg/services/ldap/test.go
Normal file
165
pkg/services/ldap/test.go
Normal file
@ -0,0 +1,165 @@
|
||||
package ldap
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
|
||||
. "github.com/smartystreets/goconvey/convey"
|
||||
"gopkg.in/ldap.v3"
|
||||
|
||||
"github.com/grafana/grafana/pkg/bus"
|
||||
"github.com/grafana/grafana/pkg/models"
|
||||
"github.com/grafana/grafana/pkg/services/login"
|
||||
)
|
||||
|
||||
type mockLdapConn struct {
|
||||
result *ldap.SearchResult
|
||||
searchCalled bool
|
||||
searchAttributes []string
|
||||
bindProvider func(username, password string) error
|
||||
unauthenticatedBindProvider func(username string) error
|
||||
}
|
||||
|
||||
func (c *mockLdapConn) Bind(username, password string) error {
|
||||
if c.bindProvider != nil {
|
||||
return c.bindProvider(username, password)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *mockLdapConn) UnauthenticatedBind(username string) error {
|
||||
if c.unauthenticatedBindProvider != nil {
|
||||
return c.unauthenticatedBindProvider(username)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *mockLdapConn) Close() {}
|
||||
|
||||
func (c *mockLdapConn) setSearchResult(result *ldap.SearchResult) {
|
||||
c.result = result
|
||||
}
|
||||
|
||||
func (c *mockLdapConn) Search(sr *ldap.SearchRequest) (*ldap.SearchResult, error) {
|
||||
c.searchCalled = true
|
||||
c.searchAttributes = sr.Attributes
|
||||
return c.result, nil
|
||||
}
|
||||
|
||||
func (c *mockLdapConn) StartTLS(*tls.Config) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func AuthScenario(desc string, fn scenarioFunc) {
|
||||
Convey(desc, func() {
|
||||
defer bus.ClearBusHandlers()
|
||||
|
||||
sc := &scenarioContext{
|
||||
loginUserQuery: &models.LoginUserQuery{
|
||||
Username: "user",
|
||||
Password: "pwd",
|
||||
IpAddress: "192.168.1.1:56433",
|
||||
},
|
||||
}
|
||||
|
||||
hookDial = func(auth *Auth) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
loginService := &login.LoginService{
|
||||
Bus: bus.GetBus(),
|
||||
}
|
||||
|
||||
bus.AddHandler("test", loginService.UpsertUser)
|
||||
|
||||
bus.AddHandlerCtx("test", func(ctx context.Context, cmd *models.SyncTeamsCommand) error {
|
||||
return nil
|
||||
})
|
||||
|
||||
bus.AddHandlerCtx("test", func(ctx context.Context, cmd *models.UpdateUserPermissionsCommand) error {
|
||||
sc.updateUserPermissionsCmd = cmd
|
||||
return nil
|
||||
})
|
||||
|
||||
bus.AddHandler("test", func(cmd *models.GetUserByAuthInfoQuery) error {
|
||||
sc.getUserByAuthInfoQuery = cmd
|
||||
sc.getUserByAuthInfoQuery.Result = &models.User{Login: cmd.Login}
|
||||
return nil
|
||||
})
|
||||
|
||||
bus.AddHandler("test", func(cmd *models.GetUserOrgListQuery) error {
|
||||
sc.getUserOrgListQuery = cmd
|
||||
return nil
|
||||
})
|
||||
|
||||
bus.AddHandler("test", func(cmd *models.CreateUserCommand) error {
|
||||
sc.createUserCmd = cmd
|
||||
sc.createUserCmd.Result = models.User{Login: cmd.Login}
|
||||
return nil
|
||||
})
|
||||
|
||||
bus.AddHandler("test", func(cmd *models.AddOrgUserCommand) error {
|
||||
sc.addOrgUserCmd = cmd
|
||||
return nil
|
||||
})
|
||||
|
||||
bus.AddHandler("test", func(cmd *models.UpdateOrgUserCommand) error {
|
||||
sc.updateOrgUserCmd = cmd
|
||||
return nil
|
||||
})
|
||||
|
||||
bus.AddHandler("test", func(cmd *models.RemoveOrgUserCommand) error {
|
||||
sc.removeOrgUserCmd = cmd
|
||||
return nil
|
||||
})
|
||||
|
||||
bus.AddHandler("test", func(cmd *models.UpdateUserCommand) error {
|
||||
sc.updateUserCmd = cmd
|
||||
return nil
|
||||
})
|
||||
|
||||
bus.AddHandler("test", func(cmd *models.SetUsingOrgCommand) error {
|
||||
sc.setUsingOrgCmd = cmd
|
||||
return nil
|
||||
})
|
||||
|
||||
fn(sc)
|
||||
})
|
||||
}
|
||||
|
||||
type scenarioContext struct {
|
||||
loginUserQuery *models.LoginUserQuery
|
||||
getUserByAuthInfoQuery *models.GetUserByAuthInfoQuery
|
||||
getUserOrgListQuery *models.GetUserOrgListQuery
|
||||
createUserCmd *models.CreateUserCommand
|
||||
addOrgUserCmd *models.AddOrgUserCommand
|
||||
updateOrgUserCmd *models.UpdateOrgUserCommand
|
||||
removeOrgUserCmd *models.RemoveOrgUserCommand
|
||||
updateUserCmd *models.UpdateUserCommand
|
||||
setUsingOrgCmd *models.SetUsingOrgCommand
|
||||
updateUserPermissionsCmd *models.UpdateUserPermissionsCommand
|
||||
}
|
||||
|
||||
func (sc *scenarioContext) userQueryReturns(user *models.User) {
|
||||
bus.AddHandler("test", func(query *models.GetUserByAuthInfoQuery) error {
|
||||
if user == nil {
|
||||
return models.ErrUserNotFound
|
||||
}
|
||||
query.Result = user
|
||||
return nil
|
||||
})
|
||||
bus.AddHandler("test", func(query *models.SetAuthInfoCommand) error {
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func (sc *scenarioContext) userOrgsQueryReturns(orgs []*models.UserOrgDTO) {
|
||||
bus.AddHandler("test", func(query *models.GetUserOrgListQuery) error {
|
||||
query.Result = orgs
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
type scenarioFunc func(c *scenarioContext)
|
@ -1,10 +1,10 @@
|
||||
package login
|
||||
package ldap
|
||||
|
||||
import (
|
||||
"strings"
|
||||
)
|
||||
|
||||
type LdapUserInfo struct {
|
||||
type UserInfo struct {
|
||||
DN string
|
||||
FirstName string
|
||||
LastName string
|
||||
@ -13,7 +13,7 @@ type LdapUserInfo struct {
|
||||
MemberOf []string
|
||||
}
|
||||
|
||||
func (u *LdapUserInfo) isMemberOf(group string) bool {
|
||||
func (u *UserInfo) isMemberOf(group string) bool {
|
||||
if group == "*" {
|
||||
return true
|
||||
}
|
15
pkg/services/login/errors.go
Normal file
15
pkg/services/login/errors.go
Normal file
@ -0,0 +1,15 @@
|
||||
package login
|
||||
|
||||
import "errors"
|
||||
|
||||
var (
|
||||
ErrEmailNotAllowed = errors.New("Required email domain not fulfilled")
|
||||
ErrInvalidCredentials = errors.New("Invalid Username or Password")
|
||||
ErrNoEmail = errors.New("Login provider didn't return an email address")
|
||||
ErrProviderDeniedRequest = errors.New("Login provider denied login request")
|
||||
ErrSignUpNotAllowed = errors.New("Signup is not allowed for this adapter")
|
||||
ErrTooManyLoginAttempts = errors.New("Too many consecutive incorrect login attempts for user. Login for user temporarily blocked")
|
||||
ErrPasswordEmpty = errors.New("No password provided.")
|
||||
ErrUsersQuotaReached = errors.New("Users quota reached")
|
||||
ErrGettingUserQuota = errors.New("Error getting user quota")
|
||||
)
|
@ -93,6 +93,7 @@ func (ls *LoginService) UpsertUser(cmd *m.UpsertUserCommand) error {
|
||||
}
|
||||
|
||||
err = syncOrgRoles(cmd.Result, extUser)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
@ -869,14 +869,8 @@ func (cfg *Cfg) Load(args *CommandLineArgs) error {
|
||||
analytics := iniFile.Section("analytics")
|
||||
ReportingEnabled = analytics.Key("reporting_enabled").MustBool(true)
|
||||
CheckForUpdates = analytics.Key("check_for_updates").MustBool(true)
|
||||
GoogleAnalyticsId, err = valueAsString(analytics, "google_analytics_ua_id", "")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
GoogleTagManagerId, err = valueAsString(analytics, "google_tag_manager_id", "")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
GoogleAnalyticsId = analytics.Key("google_analytics_ua_id").String()
|
||||
GoogleTagManagerId = analytics.Key("google_tag_manager_id").String()
|
||||
|
||||
alerting := iniFile.Section("alerting")
|
||||
AlertingEnabled = alerting.Key("enabled").MustBool(true)
|
||||
|
@ -20,7 +20,7 @@ go get -u github.com/golangci/golangci-lint/cmd/golangci-lint
|
||||
|
||||
# use gometalinter when lints are not available in golangci or
|
||||
# when gometalinter is better. Eg. goconst for gometalinter does not lint test files
|
||||
# which is not desired.
|
||||
# which is not desired.
|
||||
exit_if_fail gometalinter --enable-gc --vendor --deadline 10m --disable-all \
|
||||
--enable=goconst\
|
||||
--enable=staticcheck
|
||||
|
22
vendor/github.com/robfig/cron/.gitignore
generated
vendored
Normal file
22
vendor/github.com/robfig/cron/.gitignore
generated
vendored
Normal file
@ -0,0 +1,22 @@
|
||||
# Compiled Object files, Static and Dynamic libs (Shared Objects)
|
||||
*.o
|
||||
*.a
|
||||
*.so
|
||||
|
||||
# Folders
|
||||
_obj
|
||||
_test
|
||||
|
||||
# Architecture specific extensions/prefixes
|
||||
*.[568vq]
|
||||
[568vq].out
|
||||
|
||||
*.cgo1.go
|
||||
*.cgo2.c
|
||||
_cgo_defun.c
|
||||
_cgo_gotypes.go
|
||||
_cgo_export.*
|
||||
|
||||
_testmain.go
|
||||
|
||||
*.exe
|
21
vendor/github.com/robfig/cron/LICENSE
generated
vendored
Normal file
21
vendor/github.com/robfig/cron/LICENSE
generated
vendored
Normal file
@ -0,0 +1,21 @@
|
||||
Copyright (C) 2012 Rob Figueiredo
|
||||
All Rights Reserved.
|
||||
|
||||
MIT LICENSE
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
this software and associated documentation files (the "Software"), to deal in
|
||||
the Software without restriction, including without limitation the rights to
|
||||
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||
the Software, and to permit persons to whom the Software is furnished to do so,
|
||||
subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
6
vendor/github.com/robfig/cron/README.md
generated
vendored
Normal file
6
vendor/github.com/robfig/cron/README.md
generated
vendored
Normal file
@ -0,0 +1,6 @@
|
||||
[](http://godoc.org/github.com/robfig/cron)
|
||||
[](https://travis-ci.org/robfig/cron)
|
||||
|
||||
# cron
|
||||
|
||||
Documentation here: https://godoc.org/github.com/robfig/cron
|
27
vendor/github.com/robfig/cron/constantdelay.go
generated
vendored
Normal file
27
vendor/github.com/robfig/cron/constantdelay.go
generated
vendored
Normal file
@ -0,0 +1,27 @@
|
||||
package cron
|
||||
|
||||
import "time"
|
||||
|
||||
// ConstantDelaySchedule represents a simple recurring duty cycle, e.g. "Every 5 minutes".
|
||||
// It does not support jobs more frequent than once a second.
|
||||
type ConstantDelaySchedule struct {
|
||||
Delay time.Duration
|
||||
}
|
||||
|
||||
// Every returns a crontab Schedule that activates once every duration.
|
||||
// Delays of less than a second are not supported (will round up to 1 second).
|
||||
// Any fields less than a Second are truncated.
|
||||
func Every(duration time.Duration) ConstantDelaySchedule {
|
||||
if duration < time.Second {
|
||||
duration = time.Second
|
||||
}
|
||||
return ConstantDelaySchedule{
|
||||
Delay: duration - time.Duration(duration.Nanoseconds())%time.Second,
|
||||
}
|
||||
}
|
||||
|
||||
// Next returns the next time this should be run.
|
||||
// This rounds so that the next activation time will be on the second.
|
||||
func (schedule ConstantDelaySchedule) Next(t time.Time) time.Time {
|
||||
return t.Add(schedule.Delay - time.Duration(t.Nanosecond())*time.Nanosecond)
|
||||
}
|
259
vendor/github.com/robfig/cron/cron.go
generated
vendored
Normal file
259
vendor/github.com/robfig/cron/cron.go
generated
vendored
Normal file
@ -0,0 +1,259 @@
|
||||
package cron
|
||||
|
||||
import (
|
||||
"log"
|
||||
"runtime"
|
||||
"sort"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Cron keeps track of any number of entries, invoking the associated func as
|
||||
// specified by the schedule. It may be started, stopped, and the entries may
|
||||
// be inspected while running.
|
||||
type Cron struct {
|
||||
entries []*Entry
|
||||
stop chan struct{}
|
||||
add chan *Entry
|
||||
snapshot chan []*Entry
|
||||
running bool
|
||||
ErrorLog *log.Logger
|
||||
location *time.Location
|
||||
}
|
||||
|
||||
// Job is an interface for submitted cron jobs.
|
||||
type Job interface {
|
||||
Run()
|
||||
}
|
||||
|
||||
// The Schedule describes a job's duty cycle.
|
||||
type Schedule interface {
|
||||
// Return the next activation time, later than the given time.
|
||||
// Next is invoked initially, and then each time the job is run.
|
||||
Next(time.Time) time.Time
|
||||
}
|
||||
|
||||
// Entry consists of a schedule and the func to execute on that schedule.
|
||||
type Entry struct {
|
||||
// The schedule on which this job should be run.
|
||||
Schedule Schedule
|
||||
|
||||
// The next time the job will run. This is the zero time if Cron has not been
|
||||
// started or this entry's schedule is unsatisfiable
|
||||
Next time.Time
|
||||
|
||||
// The last time this job was run. This is the zero time if the job has never
|
||||
// been run.
|
||||
Prev time.Time
|
||||
|
||||
// The Job to run.
|
||||
Job Job
|
||||
}
|
||||
|
||||
// byTime is a wrapper for sorting the entry array by time
|
||||
// (with zero time at the end).
|
||||
type byTime []*Entry
|
||||
|
||||
func (s byTime) Len() int { return len(s) }
|
||||
func (s byTime) Swap(i, j int) { s[i], s[j] = s[j], s[i] }
|
||||
func (s byTime) Less(i, j int) bool {
|
||||
// Two zero times should return false.
|
||||
// Otherwise, zero is "greater" than any other time.
|
||||
// (To sort it at the end of the list.)
|
||||
if s[i].Next.IsZero() {
|
||||
return false
|
||||
}
|
||||
if s[j].Next.IsZero() {
|
||||
return true
|
||||
}
|
||||
return s[i].Next.Before(s[j].Next)
|
||||
}
|
||||
|
||||
// New returns a new Cron job runner, in the Local time zone.
|
||||
func New() *Cron {
|
||||
return NewWithLocation(time.Now().Location())
|
||||
}
|
||||
|
||||
// NewWithLocation returns a new Cron job runner.
|
||||
func NewWithLocation(location *time.Location) *Cron {
|
||||
return &Cron{
|
||||
entries: nil,
|
||||
add: make(chan *Entry),
|
||||
stop: make(chan struct{}),
|
||||
snapshot: make(chan []*Entry),
|
||||
running: false,
|
||||
ErrorLog: nil,
|
||||
location: location,
|
||||
}
|
||||
}
|
||||
|
||||
// A wrapper that turns a func() into a cron.Job
|
||||
type FuncJob func()
|
||||
|
||||
func (f FuncJob) Run() { f() }
|
||||
|
||||
// AddFunc adds a func to the Cron to be run on the given schedule.
|
||||
func (c *Cron) AddFunc(spec string, cmd func()) error {
|
||||
return c.AddJob(spec, FuncJob(cmd))
|
||||
}
|
||||
|
||||
// AddJob adds a Job to the Cron to be run on the given schedule.
|
||||
func (c *Cron) AddJob(spec string, cmd Job) error {
|
||||
schedule, err := Parse(spec)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
c.Schedule(schedule, cmd)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Schedule adds a Job to the Cron to be run on the given schedule.
|
||||
func (c *Cron) Schedule(schedule Schedule, cmd Job) {
|
||||
entry := &Entry{
|
||||
Schedule: schedule,
|
||||
Job: cmd,
|
||||
}
|
||||
if !c.running {
|
||||
c.entries = append(c.entries, entry)
|
||||
return
|
||||
}
|
||||
|
||||
c.add <- entry
|
||||
}
|
||||
|
||||
// Entries returns a snapshot of the cron entries.
|
||||
func (c *Cron) Entries() []*Entry {
|
||||
if c.running {
|
||||
c.snapshot <- nil
|
||||
x := <-c.snapshot
|
||||
return x
|
||||
}
|
||||
return c.entrySnapshot()
|
||||
}
|
||||
|
||||
// Location gets the time zone location
|
||||
func (c *Cron) Location() *time.Location {
|
||||
return c.location
|
||||
}
|
||||
|
||||
// Start the cron scheduler in its own go-routine, or no-op if already started.
|
||||
func (c *Cron) Start() {
|
||||
if c.running {
|
||||
return
|
||||
}
|
||||
c.running = true
|
||||
go c.run()
|
||||
}
|
||||
|
||||
// Run the cron scheduler, or no-op if already running.
|
||||
func (c *Cron) Run() {
|
||||
if c.running {
|
||||
return
|
||||
}
|
||||
c.running = true
|
||||
c.run()
|
||||
}
|
||||
|
||||
func (c *Cron) runWithRecovery(j Job) {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
const size = 64 << 10
|
||||
buf := make([]byte, size)
|
||||
buf = buf[:runtime.Stack(buf, false)]
|
||||
c.logf("cron: panic running job: %v\n%s", r, buf)
|
||||
}
|
||||
}()
|
||||
j.Run()
|
||||
}
|
||||
|
||||
// Run the scheduler. this is private just due to the need to synchronize
|
||||
// access to the 'running' state variable.
|
||||
func (c *Cron) run() {
|
||||
// Figure out the next activation times for each entry.
|
||||
now := c.now()
|
||||
for _, entry := range c.entries {
|
||||
entry.Next = entry.Schedule.Next(now)
|
||||
}
|
||||
|
||||
for {
|
||||
// Determine the next entry to run.
|
||||
sort.Sort(byTime(c.entries))
|
||||
|
||||
var timer *time.Timer
|
||||
if len(c.entries) == 0 || c.entries[0].Next.IsZero() {
|
||||
// If there are no entries yet, just sleep - it still handles new entries
|
||||
// and stop requests.
|
||||
timer = time.NewTimer(100000 * time.Hour)
|
||||
} else {
|
||||
timer = time.NewTimer(c.entries[0].Next.Sub(now))
|
||||
}
|
||||
|
||||
for {
|
||||
select {
|
||||
case now = <-timer.C:
|
||||
now = now.In(c.location)
|
||||
// Run every entry whose next time was less than now
|
||||
for _, e := range c.entries {
|
||||
if e.Next.After(now) || e.Next.IsZero() {
|
||||
break
|
||||
}
|
||||
go c.runWithRecovery(e.Job)
|
||||
e.Prev = e.Next
|
||||
e.Next = e.Schedule.Next(now)
|
||||
}
|
||||
|
||||
case newEntry := <-c.add:
|
||||
timer.Stop()
|
||||
now = c.now()
|
||||
newEntry.Next = newEntry.Schedule.Next(now)
|
||||
c.entries = append(c.entries, newEntry)
|
||||
|
||||
case <-c.snapshot:
|
||||
c.snapshot <- c.entrySnapshot()
|
||||
continue
|
||||
|
||||
case <-c.stop:
|
||||
timer.Stop()
|
||||
return
|
||||
}
|
||||
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Logs an error to stderr or to the configured error log
|
||||
func (c *Cron) logf(format string, args ...interface{}) {
|
||||
if c.ErrorLog != nil {
|
||||
c.ErrorLog.Printf(format, args...)
|
||||
} else {
|
||||
log.Printf(format, args...)
|
||||
}
|
||||
}
|
||||
|
||||
// Stop stops the cron scheduler if it is running; otherwise it does nothing.
|
||||
func (c *Cron) Stop() {
|
||||
if !c.running {
|
||||
return
|
||||
}
|
||||
c.stop <- struct{}{}
|
||||
c.running = false
|
||||
}
|
||||
|
||||
// entrySnapshot returns a copy of the current cron entry list.
|
||||
func (c *Cron) entrySnapshot() []*Entry {
|
||||
entries := []*Entry{}
|
||||
for _, e := range c.entries {
|
||||
entries = append(entries, &Entry{
|
||||
Schedule: e.Schedule,
|
||||
Next: e.Next,
|
||||
Prev: e.Prev,
|
||||
Job: e.Job,
|
||||
})
|
||||
}
|
||||
return entries
|
||||
}
|
||||
|
||||
// now returns current time in c location
|
||||
func (c *Cron) now() time.Time {
|
||||
return time.Now().In(c.location)
|
||||
}
|
129
vendor/github.com/robfig/cron/doc.go
generated
vendored
Normal file
129
vendor/github.com/robfig/cron/doc.go
generated
vendored
Normal file
@ -0,0 +1,129 @@
|
||||
/*
|
||||
Package cron implements a cron spec parser and job runner.
|
||||
|
||||
Usage
|
||||
|
||||
Callers may register Funcs to be invoked on a given schedule. Cron will run
|
||||
them in their own goroutines.
|
||||
|
||||
c := cron.New()
|
||||
c.AddFunc("0 30 * * * *", func() { fmt.Println("Every hour on the half hour") })
|
||||
c.AddFunc("@hourly", func() { fmt.Println("Every hour") })
|
||||
c.AddFunc("@every 1h30m", func() { fmt.Println("Every hour thirty") })
|
||||
c.Start()
|
||||
..
|
||||
// Funcs are invoked in their own goroutine, asynchronously.
|
||||
...
|
||||
// Funcs may also be added to a running Cron
|
||||
c.AddFunc("@daily", func() { fmt.Println("Every day") })
|
||||
..
|
||||
// Inspect the cron job entries' next and previous run times.
|
||||
inspect(c.Entries())
|
||||
..
|
||||
c.Stop() // Stop the scheduler (does not stop any jobs already running).
|
||||
|
||||
CRON Expression Format
|
||||
|
||||
A cron expression represents a set of times, using 6 space-separated fields.
|
||||
|
||||
Field name | Mandatory? | Allowed values | Allowed special characters
|
||||
---------- | ---------- | -------------- | --------------------------
|
||||
Seconds | Yes | 0-59 | * / , -
|
||||
Minutes | Yes | 0-59 | * / , -
|
||||
Hours | Yes | 0-23 | * / , -
|
||||
Day of month | Yes | 1-31 | * / , - ?
|
||||
Month | Yes | 1-12 or JAN-DEC | * / , -
|
||||
Day of week | Yes | 0-6 or SUN-SAT | * / , - ?
|
||||
|
||||
Note: Month and Day-of-week field values are case insensitive. "SUN", "Sun",
|
||||
and "sun" are equally accepted.
|
||||
|
||||
Special Characters
|
||||
|
||||
Asterisk ( * )
|
||||
|
||||
The asterisk indicates that the cron expression will match for all values of the
|
||||
field; e.g., using an asterisk in the 5th field (month) would indicate every
|
||||
month.
|
||||
|
||||
Slash ( / )
|
||||
|
||||
Slashes are used to describe increments of ranges. For example 3-59/15 in the
|
||||
1st field (minutes) would indicate the 3rd minute of the hour and every 15
|
||||
minutes thereafter. The form "*\/..." is equivalent to the form "first-last/...",
|
||||
that is, an increment over the largest possible range of the field. The form
|
||||
"N/..." is accepted as meaning "N-MAX/...", that is, starting at N, use the
|
||||
increment until the end of that specific range. It does not wrap around.
|
||||
|
||||
Comma ( , )
|
||||
|
||||
Commas are used to separate items of a list. For example, using "MON,WED,FRI" in
|
||||
the 5th field (day of week) would mean Mondays, Wednesdays and Fridays.
|
||||
|
||||
Hyphen ( - )
|
||||
|
||||
Hyphens are used to define ranges. For example, 9-17 would indicate every
|
||||
hour between 9am and 5pm inclusive.
|
||||
|
||||
Question mark ( ? )
|
||||
|
||||
Question mark may be used instead of '*' for leaving either day-of-month or
|
||||
day-of-week blank.
|
||||
|
||||
Predefined schedules
|
||||
|
||||
You may use one of several pre-defined schedules in place of a cron expression.
|
||||
|
||||
Entry | Description | Equivalent To
|
||||
----- | ----------- | -------------
|
||||
@yearly (or @annually) | Run once a year, midnight, Jan. 1st | 0 0 0 1 1 *
|
||||
@monthly | Run once a month, midnight, first of month | 0 0 0 1 * *
|
||||
@weekly | Run once a week, midnight between Sat/Sun | 0 0 0 * * 0
|
||||
@daily (or @midnight) | Run once a day, midnight | 0 0 0 * * *
|
||||
@hourly | Run once an hour, beginning of hour | 0 0 * * * *
|
||||
|
||||
Intervals
|
||||
|
||||
You may also schedule a job to execute at fixed intervals, starting at the time it's added
|
||||
or cron is run. This is supported by formatting the cron spec like this:
|
||||
|
||||
@every <duration>
|
||||
|
||||
where "duration" is a string accepted by time.ParseDuration
|
||||
(http://golang.org/pkg/time/#ParseDuration).
|
||||
|
||||
For example, "@every 1h30m10s" would indicate a schedule that activates after
|
||||
1 hour, 30 minutes, 10 seconds, and then every interval after that.
|
||||
|
||||
Note: The interval does not take the job runtime into account. For example,
|
||||
if a job takes 3 minutes to run, and it is scheduled to run every 5 minutes,
|
||||
it will have only 2 minutes of idle time between each run.
|
||||
|
||||
Time zones
|
||||
|
||||
All interpretation and scheduling is done in the machine's local time zone (as
|
||||
provided by the Go time package (http://www.golang.org/pkg/time).
|
||||
|
||||
Be aware that jobs scheduled during daylight-savings leap-ahead transitions will
|
||||
not be run!
|
||||
|
||||
Thread safety
|
||||
|
||||
Since the Cron service runs concurrently with the calling code, some amount of
|
||||
care must be taken to ensure proper synchronization.
|
||||
|
||||
All cron methods are designed to be correctly synchronized as long as the caller
|
||||
ensures that invocations have a clear happens-before ordering between them.
|
||||
|
||||
Implementation
|
||||
|
||||
Cron entries are stored in an array, sorted by their next activation time. Cron
|
||||
sleeps until the next job is due to be run.
|
||||
|
||||
Upon waking:
|
||||
- it runs each entry that is active on that second
|
||||
- it calculates the next run times for the jobs that were run
|
||||
- it re-sorts the array of entries by next activation time.
|
||||
- it goes to sleep until the soonest job.
|
||||
*/
|
||||
package cron
|
380
vendor/github.com/robfig/cron/parser.go
generated
vendored
Normal file
380
vendor/github.com/robfig/cron/parser.go
generated
vendored
Normal file
@ -0,0 +1,380 @@
|
||||
package cron
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Configuration options for creating a parser. Most options specify which
|
||||
// fields should be included, while others enable features. If a field is not
|
||||
// included the parser will assume a default value. These options do not change
|
||||
// the order fields are parse in.
|
||||
type ParseOption int
|
||||
|
||||
const (
|
||||
Second ParseOption = 1 << iota // Seconds field, default 0
|
||||
Minute // Minutes field, default 0
|
||||
Hour // Hours field, default 0
|
||||
Dom // Day of month field, default *
|
||||
Month // Month field, default *
|
||||
Dow // Day of week field, default *
|
||||
DowOptional // Optional day of week field, default *
|
||||
Descriptor // Allow descriptors such as @monthly, @weekly, etc.
|
||||
)
|
||||
|
||||
var places = []ParseOption{
|
||||
Second,
|
||||
Minute,
|
||||
Hour,
|
||||
Dom,
|
||||
Month,
|
||||
Dow,
|
||||
}
|
||||
|
||||
var defaults = []string{
|
||||
"0",
|
||||
"0",
|
||||
"0",
|
||||
"*",
|
||||
"*",
|
||||
"*",
|
||||
}
|
||||
|
||||
// A custom Parser that can be configured.
|
||||
type Parser struct {
|
||||
options ParseOption
|
||||
optionals int
|
||||
}
|
||||
|
||||
// Creates a custom Parser with custom options.
|
||||
//
|
||||
// // Standard parser without descriptors
|
||||
// specParser := NewParser(Minute | Hour | Dom | Month | Dow)
|
||||
// sched, err := specParser.Parse("0 0 15 */3 *")
|
||||
//
|
||||
// // Same as above, just excludes time fields
|
||||
// subsParser := NewParser(Dom | Month | Dow)
|
||||
// sched, err := specParser.Parse("15 */3 *")
|
||||
//
|
||||
// // Same as above, just makes Dow optional
|
||||
// subsParser := NewParser(Dom | Month | DowOptional)
|
||||
// sched, err := specParser.Parse("15 */3")
|
||||
//
|
||||
func NewParser(options ParseOption) Parser {
|
||||
optionals := 0
|
||||
if options&DowOptional > 0 {
|
||||
options |= Dow
|
||||
optionals++
|
||||
}
|
||||
return Parser{options, optionals}
|
||||
}
|
||||
|
||||
// Parse returns a new crontab schedule representing the given spec.
|
||||
// It returns a descriptive error if the spec is not valid.
|
||||
// It accepts crontab specs and features configured by NewParser.
|
||||
func (p Parser) Parse(spec string) (Schedule, error) {
|
||||
if len(spec) == 0 {
|
||||
return nil, fmt.Errorf("Empty spec string")
|
||||
}
|
||||
if spec[0] == '@' && p.options&Descriptor > 0 {
|
||||
return parseDescriptor(spec)
|
||||
}
|
||||
|
||||
// Figure out how many fields we need
|
||||
max := 0
|
||||
for _, place := range places {
|
||||
if p.options&place > 0 {
|
||||
max++
|
||||
}
|
||||
}
|
||||
min := max - p.optionals
|
||||
|
||||
// Split fields on whitespace
|
||||
fields := strings.Fields(spec)
|
||||
|
||||
// Validate number of fields
|
||||
if count := len(fields); count < min || count > max {
|
||||
if min == max {
|
||||
return nil, fmt.Errorf("Expected exactly %d fields, found %d: %s", min, count, spec)
|
||||
}
|
||||
return nil, fmt.Errorf("Expected %d to %d fields, found %d: %s", min, max, count, spec)
|
||||
}
|
||||
|
||||
// Fill in missing fields
|
||||
fields = expandFields(fields, p.options)
|
||||
|
||||
var err error
|
||||
field := func(field string, r bounds) uint64 {
|
||||
if err != nil {
|
||||
return 0
|
||||
}
|
||||
var bits uint64
|
||||
bits, err = getField(field, r)
|
||||
return bits
|
||||
}
|
||||
|
||||
var (
|
||||
second = field(fields[0], seconds)
|
||||
minute = field(fields[1], minutes)
|
||||
hour = field(fields[2], hours)
|
||||
dayofmonth = field(fields[3], dom)
|
||||
month = field(fields[4], months)
|
||||
dayofweek = field(fields[5], dow)
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &SpecSchedule{
|
||||
Second: second,
|
||||
Minute: minute,
|
||||
Hour: hour,
|
||||
Dom: dayofmonth,
|
||||
Month: month,
|
||||
Dow: dayofweek,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func expandFields(fields []string, options ParseOption) []string {
|
||||
n := 0
|
||||
count := len(fields)
|
||||
expFields := make([]string, len(places))
|
||||
copy(expFields, defaults)
|
||||
for i, place := range places {
|
||||
if options&place > 0 {
|
||||
expFields[i] = fields[n]
|
||||
n++
|
||||
}
|
||||
if n == count {
|
||||
break
|
||||
}
|
||||
}
|
||||
return expFields
|
||||
}
|
||||
|
||||
var standardParser = NewParser(
|
||||
Minute | Hour | Dom | Month | Dow | Descriptor,
|
||||
)
|
||||
|
||||
// ParseStandard returns a new crontab schedule representing the given standardSpec
|
||||
// (https://en.wikipedia.org/wiki/Cron). It differs from Parse requiring to always
|
||||
// pass 5 entries representing: minute, hour, day of month, month and day of week,
|
||||
// in that order. It returns a descriptive error if the spec is not valid.
|
||||
//
|
||||
// It accepts
|
||||
// - Standard crontab specs, e.g. "* * * * ?"
|
||||
// - Descriptors, e.g. "@midnight", "@every 1h30m"
|
||||
func ParseStandard(standardSpec string) (Schedule, error) {
|
||||
return standardParser.Parse(standardSpec)
|
||||
}
|
||||
|
||||
var defaultParser = NewParser(
|
||||
Second | Minute | Hour | Dom | Month | DowOptional | Descriptor,
|
||||
)
|
||||
|
||||
// Parse returns a new crontab schedule representing the given spec.
|
||||
// It returns a descriptive error if the spec is not valid.
|
||||
//
|
||||
// It accepts
|
||||
// - Full crontab specs, e.g. "* * * * * ?"
|
||||
// - Descriptors, e.g. "@midnight", "@every 1h30m"
|
||||
func Parse(spec string) (Schedule, error) {
|
||||
return defaultParser.Parse(spec)
|
||||
}
|
||||
|
||||
// getField returns an Int with the bits set representing all of the times that
|
||||
// the field represents or error parsing field value. A "field" is a comma-separated
|
||||
// list of "ranges".
|
||||
func getField(field string, r bounds) (uint64, error) {
|
||||
var bits uint64
|
||||
ranges := strings.FieldsFunc(field, func(r rune) bool { return r == ',' })
|
||||
for _, expr := range ranges {
|
||||
bit, err := getRange(expr, r)
|
||||
if err != nil {
|
||||
return bits, err
|
||||
}
|
||||
bits |= bit
|
||||
}
|
||||
return bits, nil
|
||||
}
|
||||
|
||||
// getRange returns the bits indicated by the given expression:
|
||||
// number | number "-" number [ "/" number ]
|
||||
// or error parsing range.
|
||||
func getRange(expr string, r bounds) (uint64, error) {
|
||||
var (
|
||||
start, end, step uint
|
||||
rangeAndStep = strings.Split(expr, "/")
|
||||
lowAndHigh = strings.Split(rangeAndStep[0], "-")
|
||||
singleDigit = len(lowAndHigh) == 1
|
||||
err error
|
||||
)
|
||||
|
||||
var extra uint64
|
||||
if lowAndHigh[0] == "*" || lowAndHigh[0] == "?" {
|
||||
start = r.min
|
||||
end = r.max
|
||||
extra = starBit
|
||||
} else {
|
||||
start, err = parseIntOrName(lowAndHigh[0], r.names)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
switch len(lowAndHigh) {
|
||||
case 1:
|
||||
end = start
|
||||
case 2:
|
||||
end, err = parseIntOrName(lowAndHigh[1], r.names)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
default:
|
||||
return 0, fmt.Errorf("Too many hyphens: %s", expr)
|
||||
}
|
||||
}
|
||||
|
||||
switch len(rangeAndStep) {
|
||||
case 1:
|
||||
step = 1
|
||||
case 2:
|
||||
step, err = mustParseInt(rangeAndStep[1])
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
// Special handling: "N/step" means "N-max/step".
|
||||
if singleDigit {
|
||||
end = r.max
|
||||
}
|
||||
default:
|
||||
return 0, fmt.Errorf("Too many slashes: %s", expr)
|
||||
}
|
||||
|
||||
if start < r.min {
|
||||
return 0, fmt.Errorf("Beginning of range (%d) below minimum (%d): %s", start, r.min, expr)
|
||||
}
|
||||
if end > r.max {
|
||||
return 0, fmt.Errorf("End of range (%d) above maximum (%d): %s", end, r.max, expr)
|
||||
}
|
||||
if start > end {
|
||||
return 0, fmt.Errorf("Beginning of range (%d) beyond end of range (%d): %s", start, end, expr)
|
||||
}
|
||||
if step == 0 {
|
||||
return 0, fmt.Errorf("Step of range should be a positive number: %s", expr)
|
||||
}
|
||||
|
||||
return getBits(start, end, step) | extra, nil
|
||||
}
|
||||
|
||||
// parseIntOrName returns the (possibly-named) integer contained in expr.
|
||||
func parseIntOrName(expr string, names map[string]uint) (uint, error) {
|
||||
if names != nil {
|
||||
if namedInt, ok := names[strings.ToLower(expr)]; ok {
|
||||
return namedInt, nil
|
||||
}
|
||||
}
|
||||
return mustParseInt(expr)
|
||||
}
|
||||
|
||||
// mustParseInt parses the given expression as an int or returns an error.
|
||||
func mustParseInt(expr string) (uint, error) {
|
||||
num, err := strconv.Atoi(expr)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("Failed to parse int from %s: %s", expr, err)
|
||||
}
|
||||
if num < 0 {
|
||||
return 0, fmt.Errorf("Negative number (%d) not allowed: %s", num, expr)
|
||||
}
|
||||
|
||||
return uint(num), nil
|
||||
}
|
||||
|
||||
// getBits sets all bits in the range [min, max], modulo the given step size.
|
||||
func getBits(min, max, step uint) uint64 {
|
||||
var bits uint64
|
||||
|
||||
// If step is 1, use shifts.
|
||||
if step == 1 {
|
||||
return ^(math.MaxUint64 << (max + 1)) & (math.MaxUint64 << min)
|
||||
}
|
||||
|
||||
// Else, use a simple loop.
|
||||
for i := min; i <= max; i += step {
|
||||
bits |= 1 << i
|
||||
}
|
||||
return bits
|
||||
}
|
||||
|
||||
// all returns all bits within the given bounds. (plus the star bit)
|
||||
func all(r bounds) uint64 {
|
||||
return getBits(r.min, r.max, 1) | starBit
|
||||
}
|
||||
|
||||
// parseDescriptor returns a predefined schedule for the expression, or error if none matches.
|
||||
func parseDescriptor(descriptor string) (Schedule, error) {
|
||||
switch descriptor {
|
||||
case "@yearly", "@annually":
|
||||
return &SpecSchedule{
|
||||
Second: 1 << seconds.min,
|
||||
Minute: 1 << minutes.min,
|
||||
Hour: 1 << hours.min,
|
||||
Dom: 1 << dom.min,
|
||||
Month: 1 << months.min,
|
||||
Dow: all(dow),
|
||||
}, nil
|
||||
|
||||
case "@monthly":
|
||||
return &SpecSchedule{
|
||||
Second: 1 << seconds.min,
|
||||
Minute: 1 << minutes.min,
|
||||
Hour: 1 << hours.min,
|
||||
Dom: 1 << dom.min,
|
||||
Month: all(months),
|
||||
Dow: all(dow),
|
||||
}, nil
|
||||
|
||||
case "@weekly":
|
||||
return &SpecSchedule{
|
||||
Second: 1 << seconds.min,
|
||||
Minute: 1 << minutes.min,
|
||||
Hour: 1 << hours.min,
|
||||
Dom: all(dom),
|
||||
Month: all(months),
|
||||
Dow: 1 << dow.min,
|
||||
}, nil
|
||||
|
||||
case "@daily", "@midnight":
|
||||
return &SpecSchedule{
|
||||
Second: 1 << seconds.min,
|
||||
Minute: 1 << minutes.min,
|
||||
Hour: 1 << hours.min,
|
||||
Dom: all(dom),
|
||||
Month: all(months),
|
||||
Dow: all(dow),
|
||||
}, nil
|
||||
|
||||
case "@hourly":
|
||||
return &SpecSchedule{
|
||||
Second: 1 << seconds.min,
|
||||
Minute: 1 << minutes.min,
|
||||
Hour: all(hours),
|
||||
Dom: all(dom),
|
||||
Month: all(months),
|
||||
Dow: all(dow),
|
||||
}, nil
|
||||
}
|
||||
|
||||
const every = "@every "
|
||||
if strings.HasPrefix(descriptor, every) {
|
||||
duration, err := time.ParseDuration(descriptor[len(every):])
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Failed to parse duration %s: %s", descriptor, err)
|
||||
}
|
||||
return Every(duration), nil
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("Unrecognized descriptor: %s", descriptor)
|
||||
}
|
158
vendor/github.com/robfig/cron/spec.go
generated
vendored
Normal file
158
vendor/github.com/robfig/cron/spec.go
generated
vendored
Normal file
@ -0,0 +1,158 @@
|
||||
package cron
|
||||
|
||||
import "time"
|
||||
|
||||
// SpecSchedule specifies a duty cycle (to the second granularity), based on a
|
||||
// traditional crontab specification. It is computed initially and stored as bit sets.
|
||||
type SpecSchedule struct {
|
||||
Second, Minute, Hour, Dom, Month, Dow uint64
|
||||
}
|
||||
|
||||
// bounds provides a range of acceptable values (plus a map of name to value).
|
||||
type bounds struct {
|
||||
min, max uint
|
||||
names map[string]uint
|
||||
}
|
||||
|
||||
// The bounds for each field.
|
||||
var (
|
||||
seconds = bounds{0, 59, nil}
|
||||
minutes = bounds{0, 59, nil}
|
||||
hours = bounds{0, 23, nil}
|
||||
dom = bounds{1, 31, nil}
|
||||
months = bounds{1, 12, map[string]uint{
|
||||
"jan": 1,
|
||||
"feb": 2,
|
||||
"mar": 3,
|
||||
"apr": 4,
|
||||
"may": 5,
|
||||
"jun": 6,
|
||||
"jul": 7,
|
||||
"aug": 8,
|
||||
"sep": 9,
|
||||
"oct": 10,
|
||||
"nov": 11,
|
||||
"dec": 12,
|
||||
}}
|
||||
dow = bounds{0, 6, map[string]uint{
|
||||
"sun": 0,
|
||||
"mon": 1,
|
||||
"tue": 2,
|
||||
"wed": 3,
|
||||
"thu": 4,
|
||||
"fri": 5,
|
||||
"sat": 6,
|
||||
}}
|
||||
)
|
||||
|
||||
const (
|
||||
// Set the top bit if a star was included in the expression.
|
||||
starBit = 1 << 63
|
||||
)
|
||||
|
||||
// Next returns the next time this schedule is activated, greater than the given
|
||||
// time. If no time can be found to satisfy the schedule, return the zero time.
|
||||
func (s *SpecSchedule) Next(t time.Time) time.Time {
|
||||
// General approach:
|
||||
// For Month, Day, Hour, Minute, Second:
|
||||
// Check if the time value matches. If yes, continue to the next field.
|
||||
// If the field doesn't match the schedule, then increment the field until it matches.
|
||||
// While incrementing the field, a wrap-around brings it back to the beginning
|
||||
// of the field list (since it is necessary to re-verify previous field
|
||||
// values)
|
||||
|
||||
// Start at the earliest possible time (the upcoming second).
|
||||
t = t.Add(1*time.Second - time.Duration(t.Nanosecond())*time.Nanosecond)
|
||||
|
||||
// This flag indicates whether a field has been incremented.
|
||||
added := false
|
||||
|
||||
// If no time is found within five years, return zero.
|
||||
yearLimit := t.Year() + 5
|
||||
|
||||
WRAP:
|
||||
if t.Year() > yearLimit {
|
||||
return time.Time{}
|
||||
}
|
||||
|
||||
// Find the first applicable month.
|
||||
// If it's this month, then do nothing.
|
||||
for 1<<uint(t.Month())&s.Month == 0 {
|
||||
// If we have to add a month, reset the other parts to 0.
|
||||
if !added {
|
||||
added = true
|
||||
// Otherwise, set the date at the beginning (since the current time is irrelevant).
|
||||
t = time.Date(t.Year(), t.Month(), 1, 0, 0, 0, 0, t.Location())
|
||||
}
|
||||
t = t.AddDate(0, 1, 0)
|
||||
|
||||
// Wrapped around.
|
||||
if t.Month() == time.January {
|
||||
goto WRAP
|
||||
}
|
||||
}
|
||||
|
||||
// Now get a day in that month.
|
||||
for !dayMatches(s, t) {
|
||||
if !added {
|
||||
added = true
|
||||
t = time.Date(t.Year(), t.Month(), t.Day(), 0, 0, 0, 0, t.Location())
|
||||
}
|
||||
t = t.AddDate(0, 0, 1)
|
||||
|
||||
if t.Day() == 1 {
|
||||
goto WRAP
|
||||
}
|
||||
}
|
||||
|
||||
for 1<<uint(t.Hour())&s.Hour == 0 {
|
||||
if !added {
|
||||
added = true
|
||||
t = time.Date(t.Year(), t.Month(), t.Day(), t.Hour(), 0, 0, 0, t.Location())
|
||||
}
|
||||
t = t.Add(1 * time.Hour)
|
||||
|
||||
if t.Hour() == 0 {
|
||||
goto WRAP
|
||||
}
|
||||
}
|
||||
|
||||
for 1<<uint(t.Minute())&s.Minute == 0 {
|
||||
if !added {
|
||||
added = true
|
||||
t = t.Truncate(time.Minute)
|
||||
}
|
||||
t = t.Add(1 * time.Minute)
|
||||
|
||||
if t.Minute() == 0 {
|
||||
goto WRAP
|
||||
}
|
||||
}
|
||||
|
||||
for 1<<uint(t.Second())&s.Second == 0 {
|
||||
if !added {
|
||||
added = true
|
||||
t = t.Truncate(time.Second)
|
||||
}
|
||||
t = t.Add(1 * time.Second)
|
||||
|
||||
if t.Second() == 0 {
|
||||
goto WRAP
|
||||
}
|
||||
}
|
||||
|
||||
return t
|
||||
}
|
||||
|
||||
// dayMatches returns true if the schedule's day-of-week and day-of-month
|
||||
// restrictions are satisfied by the given time.
|
||||
func dayMatches(s *SpecSchedule, t time.Time) bool {
|
||||
var (
|
||||
domMatch bool = 1<<uint(t.Day())&s.Dom > 0
|
||||
dowMatch bool = 1<<uint(t.Weekday())&s.Dow > 0
|
||||
)
|
||||
if s.Dom&starBit > 0 || s.Dow&starBit > 0 {
|
||||
return domMatch && dowMatch
|
||||
}
|
||||
return domMatch || dowMatch
|
||||
}
|
2
vendor/modules.txt
vendored
2
vendor/modules.txt
vendored
@ -175,6 +175,8 @@ github.com/prometheus/procfs/xfs
|
||||
github.com/prometheus/procfs/internal/util
|
||||
# github.com/rainycape/unidecode v0.0.0-20150907023854-cb7f23ec59be
|
||||
github.com/rainycape/unidecode
|
||||
# github.com/robfig/cron v0.0.0-20180505203441-b41be1df6967
|
||||
github.com/robfig/cron
|
||||
# github.com/sergi/go-diff v1.0.0
|
||||
github.com/sergi/go-diff/diffmatchpatch
|
||||
# github.com/smartystreets/assertions v0.0.0-20190401211740-f487f9de1cd3
|
||||
|
Loading…
Reference in New Issue
Block a user