瀏覽代碼

Feature: LDAP refactoring (#16950)

* incapsulates multipleldap logic under one module

* abstracts users upsert and get logic

* changes some of the text error messages and import sort sequence

* heavily refactors the LDAP module – LDAP module now only deals with LDAP related behaviour

* integrates affected auth_proxy module and their tests

* refactoring of the auth_proxy logic
Oleg Gaidarenko 6 年之前
父節點
當前提交
35f227de11
共有 83 個文件被更改,包括 3346 次插入991 次删除
  1. 1 0
      go.mod
  2. 2 0
      go.sum
  3. 1 0
      pkg/extensions/main.go
  4. 8 7
      pkg/login/auth.go
  5. 16 16
      pkg/login/auth_test.go
  6. 24 14
      pkg/login/ldap_login.go
  7. 60 46
      pkg/login/ldap_login_test.go
  8. 3 3
      pkg/middleware/auth_proxy.go
  9. 63 73
      pkg/middleware/auth_proxy/auth_proxy.go
  10. 59 33
      pkg/middleware/auth_proxy/auth_proxy_test.go
  11. 2 1
      pkg/middleware/middleware.go
  12. 0 5
      pkg/services/ldap/hooks.go
  13. 300 282
      pkg/services/ldap/ldap.go
  14. 140 0
      pkg/services/ldap/ldap_helpers_test.go
  15. 93 27
      pkg/services/ldap/ldap_login_test.go
  16. 122 461
      pkg/services/ldap/ldap_test.go
  17. 4 2
      pkg/services/ldap/settings.go
  18. 32 17
      pkg/services/ldap/test.go
  19. 204 0
      pkg/services/multildap/multildap.go
  20. 8 2
      pkg/services/sqlstore/sqlstore.go
  21. 39 0
      pkg/services/user/user.go
  22. 1 0
      pkg/setting/setting.go
  23. 134 0
      vendor/github.com/brianvoe/gofakeit/BENCHMARKS.md
  24. 46 0
      vendor/github.com/brianvoe/gofakeit/CODE_OF_CONDUCT.md
  25. 1 0
      vendor/github.com/brianvoe/gofakeit/CONTRIBUTING.md
  26. 20 0
      vendor/github.com/brianvoe/gofakeit/LICENSE.txt
  27. 254 0
      vendor/github.com/brianvoe/gofakeit/README.md
  28. 3 0
      vendor/github.com/brianvoe/gofakeit/TODO.txt
  29. 131 0
      vendor/github.com/brianvoe/gofakeit/address.go
  30. 45 0
      vendor/github.com/brianvoe/gofakeit/beer.go
  31. 10 0
      vendor/github.com/brianvoe/gofakeit/bool.go
  32. 44 0
      vendor/github.com/brianvoe/gofakeit/color.go
  33. 30 0
      vendor/github.com/brianvoe/gofakeit/company.go
  34. 40 0
      vendor/github.com/brianvoe/gofakeit/contact.go
  35. 38 0
      vendor/github.com/brianvoe/gofakeit/currency.go
  36. 6 0
      vendor/github.com/brianvoe/gofakeit/data/address.go
  37. 10 0
      vendor/github.com/brianvoe/gofakeit/data/beer.go
  38. 7 0
      vendor/github.com/brianvoe/gofakeit/data/colors.go
  39. 6 0
      vendor/github.com/brianvoe/gofakeit/data/company.go
  40. 8 0
      vendor/github.com/brianvoe/gofakeit/data/computer.go
  41. 6 0
      vendor/github.com/brianvoe/gofakeit/data/contact.go
  42. 5 0
      vendor/github.com/brianvoe/gofakeit/data/currency.go
  43. 28 0
      vendor/github.com/brianvoe/gofakeit/data/data.go
  44. 6 0
      vendor/github.com/brianvoe/gofakeit/data/datetime.go
  45. 4 0
      vendor/github.com/brianvoe/gofakeit/data/files.go
  46. 20 0
      vendor/github.com/brianvoe/gofakeit/data/hacker.go
  47. 4 0
      vendor/github.com/brianvoe/gofakeit/data/hipster.go
  48. 8 0
      vendor/github.com/brianvoe/gofakeit/data/internet.go
  49. 8 0
      vendor/github.com/brianvoe/gofakeit/data/job.go
  50. 8 0
      vendor/github.com/brianvoe/gofakeit/data/log_level.go
  51. 4 0
      vendor/github.com/brianvoe/gofakeit/data/lorem.go
  52. 20 0
      vendor/github.com/brianvoe/gofakeit/data/payment.go
  53. 6 0
      vendor/github.com/brianvoe/gofakeit/data/person.go
  54. 7 0
      vendor/github.com/brianvoe/gofakeit/data/status_code.go
  55. 8 0
      vendor/github.com/brianvoe/gofakeit/data/vehicle.go
  56. 77 0
      vendor/github.com/brianvoe/gofakeit/datetime.go
  57. 10 0
      vendor/github.com/brianvoe/gofakeit/doc.go
  58. 15 0
      vendor/github.com/brianvoe/gofakeit/faker.go
  59. 11 0
      vendor/github.com/brianvoe/gofakeit/file.go
  60. 41 0
      vendor/github.com/brianvoe/gofakeit/generate.go
  61. 35 0
      vendor/github.com/brianvoe/gofakeit/hacker.go
  62. 20 0
      vendor/github.com/brianvoe/gofakeit/hipster.go
  63. 8 0
      vendor/github.com/brianvoe/gofakeit/image.go
  64. 55 0
      vendor/github.com/brianvoe/gofakeit/internet.go
  65. 34 0
      vendor/github.com/brianvoe/gofakeit/job.go
  66. 15 0
      vendor/github.com/brianvoe/gofakeit/log_level.go
  67. 二進制
      vendor/github.com/brianvoe/gofakeit/logo.png
  68. 132 0
      vendor/github.com/brianvoe/gofakeit/misc.go
  69. 26 0
      vendor/github.com/brianvoe/gofakeit/name.go
  70. 84 0
      vendor/github.com/brianvoe/gofakeit/number.go
  71. 68 0
      vendor/github.com/brianvoe/gofakeit/password.go
  72. 81 0
      vendor/github.com/brianvoe/gofakeit/payment.go
  73. 45 0
      vendor/github.com/brianvoe/gofakeit/person.go
  74. 11 0
      vendor/github.com/brianvoe/gofakeit/status_code.go
  75. 48 0
      vendor/github.com/brianvoe/gofakeit/string.go
  76. 87 0
      vendor/github.com/brianvoe/gofakeit/struct.go
  77. 34 0
      vendor/github.com/brianvoe/gofakeit/unique.go
  78. 92 0
      vendor/github.com/brianvoe/gofakeit/user_agent.go
  79. 55 0
      vendor/github.com/brianvoe/gofakeit/vehicle.go
  80. 100 0
      vendor/github.com/brianvoe/gofakeit/words.go
  81. 1 1
      vendor/github.com/robfig/cron/README.md
  82. 1 1
      vendor/github.com/robfig/cron/doc.go
  83. 3 0
      vendor/modules.txt

+ 1 - 0
go.mod

@@ -9,6 +9,7 @@ require (
 	github.com/aws/aws-sdk-go v1.18.5
 	github.com/benbjohnson/clock v0.0.0-20161215174838-7dc76406b6d3
 	github.com/bradfitz/gomemcache v0.0.0-20180710155616-bc664df96737
+	github.com/brianvoe/gofakeit v3.17.0+incompatible
 	github.com/codahale/hdrhistogram v0.0.0-20161010025455-3a0bb77429bd // indirect
 	github.com/codegangsta/cli v1.20.0
 	github.com/davecgh/go-spew v1.1.1

+ 2 - 0
go.sum

@@ -16,6 +16,8 @@ github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973 h1:xJ4a3vCFaGF/jqvzLM
 github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
 github.com/bradfitz/gomemcache v0.0.0-20180710155616-bc664df96737 h1:rRISKWyXfVxvoa702s91Zl5oREZTrR3yv+tXrrX7G/g=
 github.com/bradfitz/gomemcache v0.0.0-20180710155616-bc664df96737/go.mod h1:PmM6Mmwb0LSuEubjR8N7PtNe1KxZLtOUHtbeikc5h60=
+github.com/brianvoe/gofakeit v3.17.0+incompatible h1:C1+30+c0GtjgGDtRC+iePZeP1WMiwsWCELNJhmc7aIc=
+github.com/brianvoe/gofakeit v3.17.0+incompatible/go.mod h1:kfwdRA90vvNhPutZWfH7WPaDzUjz+CZFqG+rPkOjGOc=
 github.com/cockroachdb/apd v1.1.0 h1:3LFP3629v+1aKXU5Q37mxmRxX/pIu1nijXydLShEq5I=
 github.com/cockroachdb/apd v1.1.0/go.mod h1:8Sl8LxpKi29FqWXR16WEFZRNSz3SoPzUzeMeY4+DwBQ=
 github.com/codahale/hdrhistogram v0.0.0-20161010025455-3a0bb77429bd h1:qMd81Ts1T2OTKmB4acZcyKaMtRnY5Y44NuXGX2GFJ1w=

+ 1 - 0
pkg/extensions/main.go

@@ -1,6 +1,7 @@
 package extensions
 
 import (
+	_ "github.com/brianvoe/gofakeit"
 	_ "github.com/gobwas/glob"
 	_ "github.com/robfig/cron"
 	_ "gopkg.in/square/go-jose.v2"

+ 8 - 7
pkg/login/auth.go

@@ -4,8 +4,8 @@ import (
 	"errors"
 
 	"github.com/grafana/grafana/pkg/bus"
-	m "github.com/grafana/grafana/pkg/models"
-	LDAP "github.com/grafana/grafana/pkg/services/ldap"
+	"github.com/grafana/grafana/pkg/models"
+	"github.com/grafana/grafana/pkg/services/ldap"
 )
 
 var (
@@ -25,7 +25,8 @@ func Init() {
 	bus.AddHandler("auth", AuthenticateUser)
 }
 
-func AuthenticateUser(query *m.LoginUserQuery) error {
+// AuthenticateUser authenticates the user via username & password
+func AuthenticateUser(query *models.LoginUserQuery) error {
 	if err := validateLoginAttempts(query.Username); err != nil {
 		return err
 	}
@@ -35,24 +36,24 @@ func AuthenticateUser(query *m.LoginUserQuery) error {
 	}
 
 	err := loginUsingGrafanaDB(query)
-	if err == nil || (err != m.ErrUserNotFound && err != ErrInvalidCredentials) {
+	if err == nil || (err != models.ErrUserNotFound && err != ErrInvalidCredentials) {
 		return err
 	}
 
 	ldapEnabled, ldapErr := loginUsingLdap(query)
 	if ldapEnabled {
-		if ldapErr == nil || ldapErr != LDAP.ErrInvalidCredentials {
+		if ldapErr == nil || ldapErr != ldap.ErrInvalidCredentials {
 			return ldapErr
 		}
 
 		err = ldapErr
 	}
 
-	if err == ErrInvalidCredentials || err == LDAP.ErrInvalidCredentials {
+	if err == ErrInvalidCredentials || err == ldap.ErrInvalidCredentials {
 		saveInvalidLoginAttempt(query)
 	}
 
-	if err == m.ErrUserNotFound {
+	if err == models.ErrUserNotFound {
 		return ErrInvalidCredentials
 	}
 

+ 16 - 16
pkg/login/auth_test.go

@@ -6,8 +6,8 @@ import (
 
 	. "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/models"
+	"github.com/grafana/grafana/pkg/services/ldap"
 )
 
 func TestAuthenticateUser(t *testing.T) {
@@ -17,7 +17,7 @@ func TestAuthenticateUser(t *testing.T) {
 			mockLoginUsingGrafanaDB(nil, sc)
 			mockLoginUsingLdap(false, nil, sc)
 
-			loginQuery := m.LoginUserQuery{
+			loginQuery := models.LoginUserQuery{
 				Username: "user",
 				Password: "",
 			}
@@ -84,7 +84,7 @@ func TestAuthenticateUser(t *testing.T) {
 
 		authScenario("When a non-existing grafana user authenticate and ldap disabled", func(sc *authScenarioContext) {
 			mockLoginAttemptValidation(nil, sc)
-			mockLoginUsingGrafanaDB(m.ErrUserNotFound, sc)
+			mockLoginUsingGrafanaDB(models.ErrUserNotFound, sc)
 			mockLoginUsingLdap(false, nil, sc)
 			mockSaveInvalidLoginAttempt(sc)
 
@@ -101,14 +101,14 @@ 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, LDAP.ErrInvalidCredentials, sc)
+			mockLoginUsingGrafanaDB(models.ErrUserNotFound, sc)
+			mockLoginUsingLdap(true, ldap.ErrInvalidCredentials, sc)
 			mockSaveInvalidLoginAttempt(sc)
 
 			err := AuthenticateUser(sc.loginUserQuery)
 
 			Convey("it should result in", func() {
-				So(err, ShouldEqual, LDAP.ErrInvalidCredentials)
+				So(err, ShouldEqual, ldap.ErrInvalidCredentials)
 				So(sc.loginAttemptValidationWasCalled, ShouldBeTrue)
 				So(sc.grafanaLoginWasCalled, ShouldBeTrue)
 				So(sc.ldapLoginWasCalled, ShouldBeTrue)
@@ -118,7 +118,7 @@ func TestAuthenticateUser(t *testing.T) {
 
 		authScenario("When a non-existing grafana user authenticate and valid ldap credentials", func(sc *authScenarioContext) {
 			mockLoginAttemptValidation(nil, sc)
-			mockLoginUsingGrafanaDB(m.ErrUserNotFound, sc)
+			mockLoginUsingGrafanaDB(models.ErrUserNotFound, sc)
 			mockLoginUsingLdap(true, nil, sc)
 			mockSaveInvalidLoginAttempt(sc)
 
@@ -136,7 +136,7 @@ func TestAuthenticateUser(t *testing.T) {
 		authScenario("When a non-existing grafana user authenticate and ldap returns unexpected error", func(sc *authScenarioContext) {
 			customErr := errors.New("custom")
 			mockLoginAttemptValidation(nil, sc)
-			mockLoginUsingGrafanaDB(m.ErrUserNotFound, sc)
+			mockLoginUsingGrafanaDB(models.ErrUserNotFound, sc)
 			mockLoginUsingLdap(true, customErr, sc)
 			mockSaveInvalidLoginAttempt(sc)
 
@@ -154,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, LDAP.ErrInvalidCredentials, sc)
+			mockLoginUsingLdap(true, ldap.ErrInvalidCredentials, sc)
 			mockSaveInvalidLoginAttempt(sc)
 
 			err := AuthenticateUser(sc.loginUserQuery)
 
 			Convey("it should result in", func() {
-				So(err, ShouldEqual, LDAP.ErrInvalidCredentials)
+				So(err, ShouldEqual, ldap.ErrInvalidCredentials)
 				So(sc.loginAttemptValidationWasCalled, ShouldBeTrue)
 				So(sc.grafanaLoginWasCalled, ShouldBeTrue)
 				So(sc.ldapLoginWasCalled, ShouldBeTrue)
@@ -171,7 +171,7 @@ func TestAuthenticateUser(t *testing.T) {
 }
 
 type authScenarioContext struct {
-	loginUserQuery                   *m.LoginUserQuery
+	loginUserQuery                   *models.LoginUserQuery
 	grafanaLoginWasCalled            bool
 	ldapLoginWasCalled               bool
 	loginAttemptValidationWasCalled  bool
@@ -181,14 +181,14 @@ type authScenarioContext struct {
 type authScenarioFunc func(sc *authScenarioContext)
 
 func mockLoginUsingGrafanaDB(err error, sc *authScenarioContext) {
-	loginUsingGrafanaDB = func(query *m.LoginUserQuery) error {
+	loginUsingGrafanaDB = func(query *models.LoginUserQuery) error {
 		sc.grafanaLoginWasCalled = true
 		return err
 	}
 }
 
 func mockLoginUsingLdap(enabled bool, err error, sc *authScenarioContext) {
-	loginUsingLdap = func(query *m.LoginUserQuery) (bool, error) {
+	loginUsingLdap = func(query *models.LoginUserQuery) (bool, error) {
 		sc.ldapLoginWasCalled = true
 		return enabled, err
 	}
@@ -202,7 +202,7 @@ func mockLoginAttemptValidation(err error, sc *authScenarioContext) {
 }
 
 func mockSaveInvalidLoginAttempt(sc *authScenarioContext) {
-	saveInvalidLoginAttempt = func(query *m.LoginUserQuery) {
+	saveInvalidLoginAttempt = func(query *models.LoginUserQuery) {
 		sc.saveInvalidLoginAttemptWasCalled = true
 	}
 }
@@ -215,7 +215,7 @@ func authScenario(desc string, fn authScenarioFunc) {
 		origSaveInvalidLoginAttempt := saveInvalidLoginAttempt
 
 		sc := &authScenarioContext{
-			loginUserQuery: &m.LoginUserQuery{
+			loginUserQuery: &models.LoginUserQuery{
 				Username:  "user",
 				Password:  "pwd",
 				IpAddress: "192.168.1.1:56433",

+ 24 - 14
pkg/login/ldap_login.go

@@ -2,13 +2,20 @@ package login
 
 import (
 	"github.com/grafana/grafana/pkg/models"
-	LDAP "github.com/grafana/grafana/pkg/services/ldap"
+	"github.com/grafana/grafana/pkg/services/multildap"
+	"github.com/grafana/grafana/pkg/services/user"
+	"github.com/grafana/grafana/pkg/setting"
 	"github.com/grafana/grafana/pkg/util/errutil"
 )
 
-var newLDAP = LDAP.New
-var getLDAPConfig = LDAP.GetConfig
-var isLDAPEnabled = LDAP.IsEnabled
+// getLDAPConfig gets LDAP config
+var getLDAPConfig = multildap.GetConfig
+
+// isLDAPEnabled checks if LDAP is enabled
+var isLDAPEnabled = multildap.IsEnabled
+
+// newLDAP creates multiple LDAP instance
+var newLDAP = multildap.New
 
 // loginUsingLdap logs in user using LDAP. It returns whether LDAP is enabled and optional error and query arg will be
 // populated with the logged in user if successful.
@@ -23,18 +30,21 @@ var loginUsingLdap = func(query *models.LoginUserQuery) (bool, error) {
 	if err != nil {
 		return true, errutil.Wrap("Failed to get LDAP config", err)
 	}
-	if len(config.Servers) == 0 {
-		return true, ErrNoLDAPServers
-	}
 
-	for _, server := range config.Servers {
-		auth := newLDAP(server)
+	externalUser, err := newLDAP(config.Servers).Login(query)
+	if err != nil {
+		return true, err
+	}
 
-		err := auth.Login(query)
-		if err == nil || err != LDAP.ErrInvalidCredentials {
-			return true, err
-		}
+	login, err := user.Upsert(&user.UpsertArgs{
+		ExternalUser:  externalUser,
+		SignupAllowed: setting.LdapAllowSignup,
+	})
+	if err != nil {
+		return true, err
 	}
 
-	return true, LDAP.ErrInvalidCredentials
+	query.User = login
+
+	return true, nil
 }

+ 60 - 46
pkg/login/ldap_login_test.go

@@ -6,8 +6,9 @@ import (
 
 	. "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/models"
+	"github.com/grafana/grafana/pkg/services/ldap"
+	"github.com/grafana/grafana/pkg/services/multildap"
 	"github.com/grafana/grafana/pkg/setting"
 )
 
@@ -18,11 +19,11 @@ func TestLdapLogin(t *testing.T) {
 		Convey("Given ldap enabled and no server configured", func() {
 			setting.LdapEnabled = true
 
-			ldapLoginScenario("When login", func(sc *ldapLoginScenarioContext) {
+			LDAPLoginScenario("When login", func(sc *LDAPLoginScenarioContext) {
 				sc.withLoginResult(false)
-				getLDAPConfig = func() (*LDAP.Config, error) {
-					config := &LDAP.Config{
-						Servers: []*LDAP.ServerConfig{},
+				getLDAPConfig = func() (*ldap.Config, error) {
+					config := &ldap.Config{
+						Servers: []*ldap.ServerConfig{},
 					}
 
 					return config, nil
@@ -35,11 +36,11 @@ func TestLdapLogin(t *testing.T) {
 				})
 
 				Convey("it should return no LDAP servers error", func() {
-					So(err, ShouldEqual, ErrNoLDAPServers)
+					So(err, ShouldEqual, errTest)
 				})
 
 				Convey("it should not call ldap login", func() {
-					So(sc.ldapAuthenticatorMock.loginCalled, ShouldBeFalse)
+					So(sc.LDAPAuthenticatorMock.loginCalled, ShouldBeTrue)
 				})
 			})
 		})
@@ -47,9 +48,9 @@ func TestLdapLogin(t *testing.T) {
 		Convey("Given ldap disabled", func() {
 			setting.LdapEnabled = false
 
-			ldapLoginScenario("When login", func(sc *ldapLoginScenarioContext) {
+			LDAPLoginScenario("When login", func(sc *LDAPLoginScenarioContext) {
 				sc.withLoginResult(false)
-				enabled, err := loginUsingLdap(&m.LoginUserQuery{
+				enabled, err := loginUsingLdap(&models.LoginUserQuery{
 					Username: "user",
 					Password: "pwd",
 				})
@@ -63,75 +64,88 @@ func TestLdapLogin(t *testing.T) {
 				})
 
 				Convey("it should not call ldap login", func() {
-					So(sc.ldapAuthenticatorMock.loginCalled, ShouldBeFalse)
+					So(sc.LDAPAuthenticatorMock.loginCalled, ShouldBeFalse)
 				})
 			})
 		})
 	})
 }
 
-func mockLdapAuthenticator(valid bool) *mockAuth {
-	mock := &mockAuth{
-		validLogin: valid,
-	}
-
-	newLDAP = func(server *LDAP.ServerConfig) LDAP.IAuth {
-		return mock
-	}
-
-	return mock
-}
-
 type mockAuth struct {
 	validLogin  bool
 	loginCalled bool
 }
 
-func (auth *mockAuth) Login(query *m.LoginUserQuery) error {
+func (auth *mockAuth) Login(query *models.LoginUserQuery) (
+	*models.ExternalUserInfo,
+	error,
+) {
 	auth.loginCalled = true
 
 	if !auth.validLogin {
-		return errTest
+		return nil, errTest
 	}
 
-	return nil
+	return nil, nil
 }
 
-func (auth *mockAuth) Users() ([]*LDAP.UserInfo, error) {
+func (auth *mockAuth) Users(logins []string) (
+	[]*models.ExternalUserInfo,
+	error,
+) {
 	return nil, nil
 }
 
-func (auth *mockAuth) SyncUser(query *m.LoginUserQuery) error {
+func (auth *mockAuth) User(login string) (
+	*models.ExternalUserInfo,
+	error,
+) {
+	return nil, nil
+}
+
+func (auth *mockAuth) Add(dn string, values map[string][]string) error {
 	return nil
 }
 
-func (auth *mockAuth) GetGrafanaUserFor(ctx *m.ReqContext, ldapUser *LDAP.UserInfo) (*m.User, error) {
-	return nil, nil
+func (auth *mockAuth) Remove(dn string) error {
+	return nil
+}
+
+func mockLDAPAuthenticator(valid bool) *mockAuth {
+	mock := &mockAuth{
+		validLogin: valid,
+	}
+
+	newLDAP = func(servers []*ldap.ServerConfig) multildap.IMultiLDAP {
+		return mock
+	}
+
+	return mock
 }
 
-type ldapLoginScenarioContext struct {
-	loginUserQuery        *m.LoginUserQuery
-	ldapAuthenticatorMock *mockAuth
+type LDAPLoginScenarioContext struct {
+	loginUserQuery        *models.LoginUserQuery
+	LDAPAuthenticatorMock *mockAuth
 }
 
-type ldapLoginScenarioFunc func(c *ldapLoginScenarioContext)
+type LDAPLoginScenarioFunc func(c *LDAPLoginScenarioContext)
 
-func ldapLoginScenario(desc string, fn ldapLoginScenarioFunc) {
+func LDAPLoginScenario(desc string, fn LDAPLoginScenarioFunc) {
 	Convey(desc, func() {
 		mock := &mockAuth{}
 
-		sc := &ldapLoginScenarioContext{
-			loginUserQuery: &m.LoginUserQuery{
+		sc := &LDAPLoginScenarioContext{
+			loginUserQuery: &models.LoginUserQuery{
 				Username:  "user",
 				Password:  "pwd",
 				IpAddress: "192.168.1.1:56433",
 			},
-			ldapAuthenticatorMock: mock,
+			LDAPAuthenticatorMock: mock,
 		}
 
-		getLDAPConfig = func() (*LDAP.Config, error) {
-			config := &LDAP.Config{
-				Servers: []*LDAP.ServerConfig{
+		getLDAPConfig = func() (*ldap.Config, error) {
+			config := &ldap.Config{
+				Servers: []*ldap.ServerConfig{
 					{
 						Host: "",
 					},
@@ -141,19 +155,19 @@ func ldapLoginScenario(desc string, fn ldapLoginScenarioFunc) {
 			return config, nil
 		}
 
-		newLDAP = func(server *LDAP.ServerConfig) LDAP.IAuth {
+		newLDAP = func(server []*ldap.ServerConfig) multildap.IMultiLDAP {
 			return mock
 		}
 
 		defer func() {
-			newLDAP = LDAP.New
-			getLDAPConfig = LDAP.GetConfig
+			newLDAP = multildap.New
+			getLDAPConfig = multildap.GetConfig
 		}()
 
 		fn(sc)
 	})
 }
 
-func (sc *ldapLoginScenarioContext) withLoginResult(valid bool) {
-	sc.ldapAuthenticatorMock = mockLdapAuthenticator(valid)
+func (sc *LDAPLoginScenarioContext) withLoginResult(valid bool) {
+	sc.LDAPAuthenticatorMock = mockLDAPAuthenticator(valid)
 }

+ 3 - 3
pkg/middleware/auth_proxy.go

@@ -35,8 +35,8 @@ func initContextWithAuthProxy(store *remotecache.RemoteCache, ctx *m.ReqContext,
 		return true
 	}
 
-	// Try to get user id from various sources
-	id, err := auth.GetUserID()
+	// Try to log in user from various providers
+	id, err := auth.Login()
 	if err != nil {
 		ctx.Handle(500, err.Error(), err.DetailsError)
 		return true
@@ -54,7 +54,7 @@ func initContextWithAuthProxy(store *remotecache.RemoteCache, ctx *m.ReqContext,
 	ctx.IsSignedIn = true
 
 	// Remember user data it in cache
-	if err := auth.Remember(); err != nil {
+	if err := auth.Remember(id); err != nil {
 		ctx.Handle(500, err.Error(), err.DetailsError)
 		return true
 	}

+ 63 - 73
pkg/middleware/auth_proxy/auth_proxy.go

@@ -12,6 +12,8 @@ import (
 	"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/services/multildap"
+	"github.com/grafana/grafana/pkg/services/user"
 	"github.com/grafana/grafana/pkg/setting"
 )
 
@@ -21,10 +23,14 @@ const (
 	CachePrefix = "auth-proxy-sync-ttl:%s"
 )
 
-var (
-	getLDAPConfig = ldap.GetConfig
-	isLDAPEnabled = ldap.IsEnabled
-)
+// getLDAPConfig gets LDAP config
+var getLDAPConfig = ldap.GetConfig
+
+// isLDAPEnabled checks if LDAP is enabled
+var isLDAPEnabled = ldap.IsEnabled
+
+// newLDAP creates multiple LDAP instance
+var newLDAP = multildap.New
 
 // AuthProxy struct
 type AuthProxy struct {
@@ -33,13 +39,13 @@ type AuthProxy struct {
 	orgID  int64
 	header string
 
-	LDAP func(server *ldap.ServerConfig) ldap.IAuth
-
-	enabled     bool
-	whitelistIP string
-	headerType  string
-	headers     map[string]string
-	cacheTTL    int
+	enabled             bool
+	LdapAllowSignup     bool
+	AuthProxyAutoSignUp bool
+	whitelistIP         string
+	headerType          string
+	headers             map[string]string
+	cacheTTL            int
 }
 
 // Error auth proxy specific error
@@ -78,13 +84,13 @@ func New(options *Options) *AuthProxy {
 		orgID:  options.OrgID,
 		header: header,
 
-		LDAP: ldap.New,
-
-		enabled:     setting.AuthProxyEnabled,
-		headerType:  setting.AuthProxyHeaderProperty,
-		headers:     setting.AuthProxyHeaders,
-		whitelistIP: setting.AuthProxyWhitelist,
-		cacheTTL:    setting.AuthProxyLdapSyncTtl,
+		enabled:             setting.AuthProxyEnabled,
+		headerType:          setting.AuthProxyHeaderProperty,
+		headers:             setting.AuthProxyHeaders,
+		whitelistIP:         setting.AuthProxyWhitelist,
+		cacheTTL:            setting.AuthProxyLdapSyncTtl,
+		LdapAllowSignup:     setting.LdapAllowSignup,
+		AuthProxyAutoSignUp: setting.AuthProxyAutoSignUp,
 	}
 }
 
@@ -144,34 +150,22 @@ func (auth *AuthProxy) IsAllowedIP() (bool, *Error) {
 	return false, newError("Proxy authentication required", err)
 }
 
-// InCache checks if we have user in cache
-func (auth *AuthProxy) InCache() bool {
-	userID, _ := auth.GetUserIDViaCache()
-
-	if userID == 0 {
-		return false
-	}
-
-	return true
-}
-
 // getKey forms a key for the cache
 func (auth *AuthProxy) getKey() string {
 	return fmt.Sprintf(CachePrefix, auth.header)
 }
 
-// GetUserID gets user id with whatever means possible
-func (auth *AuthProxy) GetUserID() (int64, *Error) {
-	if auth.InCache() {
+// Login logs in user id with whatever means possible
+func (auth *AuthProxy) Login() (int64, *Error) {
 
+	id, _ := auth.GetUserViaCache()
+	if id != 0 {
 		// Error here means absent cache - we don't need to handle that
-		id, _ := auth.GetUserIDViaCache()
-
 		return id, nil
 	}
 
 	if isLDAPEnabled() {
-		id, err := auth.GetUserIDViaLDAP()
+		id, err := auth.LoginViaLDAP()
 
 		if err == ldap.ErrInvalidCredentials {
 			return 0, newError(
@@ -181,16 +175,16 @@ func (auth *AuthProxy) GetUserID() (int64, *Error) {
 		}
 
 		if err != nil {
-			return 0, newError("Failed to sync user", err)
+			return 0, newError("Failed to get the user", err)
 		}
 
 		return id, nil
 	}
 
-	id, err := auth.GetUserIDViaHeader()
+	id, err := auth.LoginViaHeader()
 	if err != nil {
 		return 0, newError(
-			"Failed to login as user specified in auth proxy header",
+			"Failed to log in as user, specified in auth proxy header",
 			err,
 		)
 	}
@@ -198,8 +192,8 @@ func (auth *AuthProxy) GetUserID() (int64, *Error) {
 	return id, nil
 }
 
-// GetUserIDViaCache gets the user from cache
-func (auth *AuthProxy) GetUserIDViaCache() (int64, error) {
+// GetUserViaCache gets user id from cache
+func (auth *AuthProxy) GetUserViaCache() (int64, error) {
 	var (
 		cacheKey    = auth.getKey()
 		userID, err = auth.store.Get(cacheKey)
@@ -212,33 +206,34 @@ func (auth *AuthProxy) GetUserIDViaCache() (int64, error) {
 	return userID.(int64), nil
 }
 
-// GetUserIDViaLDAP gets user via LDAP request
-func (auth *AuthProxy) GetUserIDViaLDAP() (int64, *Error) {
-	query := &models.LoginUserQuery{
-		ReqContext: auth.ctx,
-		Username:   auth.header,
-	}
-
+// LoginViaLDAP logs in user via LDAP request
+func (auth *AuthProxy) LoginViaLDAP() (int64, *Error) {
 	config, err := getLDAPConfig()
 	if err != nil {
 		return 0, newError("Failed to get LDAP config", nil)
 	}
-	if len(config.Servers) == 0 {
-		return 0, newError("No LDAP servers available", nil)
+
+	extUser, err := newLDAP(config.Servers).User(auth.header)
+	if err != nil {
+		return 0, newError(err.Error(), nil)
 	}
 
-	for _, server := range config.Servers {
-		author := auth.LDAP(server)
-		if err := author.SyncUser(query); err != nil {
-			return 0, newError(err.Error(), nil)
-		}
+	// Have to sync grafana and LDAP user during log in
+	user, err := user.Upsert(&user.UpsertArgs{
+		ReqContext:    auth.ctx,
+		SignupAllowed: auth.LdapAllowSignup,
+		ExternalUser:  extUser,
+	})
+	if err != nil {
+		return 0, newError(err.Error(), nil)
 	}
 
-	return query.User.Id, nil
+	return user.Id, nil
 }
 
-// GetUserIDViaHeader gets user from the header only
-func (auth *AuthProxy) GetUserIDViaHeader() (int64, error) {
+// LoginViaHeader logs in user from the header only
+// TODO: refactor - cyclomatic complexity should be much lower
+func (auth *AuthProxy) LoginViaHeader() (int64, error) {
 	extUser := &models.ExternalUserInfo{
 		AuthModule: "authproxy",
 		AuthId:     auth.header,
@@ -269,18 +264,16 @@ func (auth *AuthProxy) GetUserIDViaHeader() (int64, error) {
 		}
 	}
 
-	// add/update user in grafana
-	cmd := &models.UpsertUserCommand{
+	result, err := user.Upsert(&user.UpsertArgs{
 		ReqContext:    auth.ctx,
+		SignupAllowed: true,
 		ExternalUser:  extUser,
-		SignupAllowed: setting.AuthProxyAutoSignUp,
-	}
-	err := bus.Dispatch(cmd)
+	})
 	if err != nil {
 		return 0, err
 	}
 
-	return cmd.Result.Id, nil
+	return result.Id, nil
 }
 
 // GetSignedUser get full signed user info
@@ -298,21 +291,18 @@ func (auth *AuthProxy) GetSignedUser(userID int64) (*models.SignedInUser, *Error
 }
 
 // Remember user in cache
-func (auth *AuthProxy) Remember() *Error {
+func (auth *AuthProxy) Remember(id int64) *Error {
+	key := auth.getKey()
 
-	// Make sure we do not rewrite the expiration time
-	if auth.InCache() {
+	// Check if user already in cache
+	userID, _ := auth.store.Get(key)
+	if userID != nil {
 		return nil
 	}
 
-	var (
-		key        = auth.getKey()
-		value, _   = auth.GetUserIDViaCache()
-		expiration = time.Duration(-auth.cacheTTL) * time.Minute
-
-		err = auth.store.Set(key, value, expiration)
-	)
+	expiration := time.Duration(-auth.cacheTTL) * time.Minute
 
+	err := auth.store.Set(key, id, expiration)
 	if err != nil {
 		return newError(err.Error(), nil)
 	}

+ 59 - 33
pkg/middleware/auth_proxy/auth_proxy_test.go

@@ -1,6 +1,7 @@
 package authproxy
 
 import (
+	"errors"
 	"fmt"
 	"net/http"
 	"testing"
@@ -8,24 +9,40 @@ import (
 	. "github.com/smartystreets/goconvey/convey"
 	"gopkg.in/macaron.v1"
 
+	"github.com/grafana/grafana/pkg/bus"
 	"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/services/multildap"
 	"github.com/grafana/grafana/pkg/setting"
 )
 
-type TestLDAP struct {
-	ldap.Auth
-	ID         int64
-	syncCalled bool
+type TestMultiLDAP struct {
+	multildap.MultiLDAP
+	ID          int64
+	userCalled  bool
+	loginCalled bool
 }
 
-func (stub *TestLDAP) SyncUser(query *models.LoginUserQuery) error {
-	stub.syncCalled = true
-	query.User = &models.User{
-		Id: stub.ID,
+func (stub *TestMultiLDAP) Login(query *models.LoginUserQuery) (
+	*models.ExternalUserInfo, error,
+) {
+	stub.loginCalled = true
+	result := &models.ExternalUserInfo{
+		UserId: stub.ID,
 	}
-	return nil
+	return result, nil
+}
+
+func (stub *TestMultiLDAP) User(login string) (
+	*models.ExternalUserInfo,
+	error,
+) {
+	stub.userCalled = true
+	result := &models.ExternalUserInfo{
+		UserId: stub.ID,
+	}
+	return result, nil
 }
 
 func TestMiddlewareContext(t *testing.T) {
@@ -44,7 +61,7 @@ func TestMiddlewareContext(t *testing.T) {
 			},
 		}
 
-		Convey("gets data from the cache", func() {
+		Convey("logs in user from the cache", func() {
 			store := remotecache.NewFakeStore(t)
 			key := fmt.Sprintf(CachePrefix, name)
 			store.Set(key, int64(33), 0)
@@ -55,53 +72,64 @@ func TestMiddlewareContext(t *testing.T) {
 				OrgID: 4,
 			})
 
-			id, err := auth.GetUserID()
+			id, err := auth.Login()
 
 			So(err, ShouldBeNil)
 			So(id, ShouldEqual, 33)
 		})
 
 		Convey("LDAP", func() {
-			Convey("gets data from the LDAP", func() {
+			Convey("logs in via LDAP", func() {
+				bus.AddHandler("test", func(cmd *models.UpsertUserCommand) error {
+					cmd.Result = &models.User{
+						Id: 42,
+					}
+
+					return nil
+				})
+
 				isLDAPEnabled = func() bool {
 					return true
 				}
 
+				stub := &TestMultiLDAP{
+					ID: 42,
+				}
+
 				getLDAPConfig = func() (*ldap.Config, error) {
 					config := &ldap.Config{
 						Servers: []*ldap.ServerConfig{
-							{},
+							{
+								SearchBaseDNs: []string{"BaseDNHere"},
+							},
 						},
 					}
 					return config, nil
 				}
 
+				newLDAP = func(servers []*ldap.ServerConfig) multildap.IMultiLDAP {
+					return stub
+				}
+
 				defer func() {
+					newLDAP = multildap.New
 					isLDAPEnabled = ldap.IsEnabled
 					getLDAPConfig = ldap.GetConfig
 				}()
 
 				store := remotecache.NewFakeStore(t)
 
-				auth := New(&Options{
+				server := New(&Options{
 					Store: store,
 					Ctx:   ctx,
 					OrgID: 4,
 				})
 
-				stub := &TestLDAP{
-					ID: 42,
-				}
-
-				auth.LDAP = func(server *ldap.ServerConfig) ldap.IAuth {
-					return stub
-				}
-
-				id, err := auth.GetUserID()
+				id, err := server.Login()
 
 				So(err, ShouldBeNil)
 				So(id, ShouldEqual, 42)
-				So(stub.syncCalled, ShouldEqual, true)
+				So(stub.userCalled, ShouldEqual, true)
 			})
 
 			Convey("gets nice error if ldap is enabled but not configured", func() {
@@ -110,13 +138,11 @@ func TestMiddlewareContext(t *testing.T) {
 				}
 
 				getLDAPConfig = func() (*ldap.Config, error) {
-					config := &ldap.Config{
-						Servers: []*ldap.ServerConfig{},
-					}
-					return config, nil
+					return nil, errors.New("Something went wrong")
 				}
 
 				defer func() {
+					newLDAP = multildap.New
 					isLDAPEnabled = ldap.IsEnabled
 					getLDAPConfig = ldap.GetConfig
 				}()
@@ -129,20 +155,20 @@ func TestMiddlewareContext(t *testing.T) {
 					OrgID: 4,
 				})
 
-				stub := &TestLDAP{
+				stub := &TestMultiLDAP{
 					ID: 42,
 				}
 
-				auth.LDAP = func(server *ldap.ServerConfig) ldap.IAuth {
+				newLDAP = func(servers []*ldap.ServerConfig) multildap.IMultiLDAP {
 					return stub
 				}
 
-				id, err := auth.GetUserID()
+				id, err := auth.Login()
 
 				So(err, ShouldNotBeNil)
-				So(err.Error(), ShouldContainSubstring, "Failed to sync user")
+				So(err.Error(), ShouldContainSubstring, "Failed to get the user")
 				So(id, ShouldNotEqual, 42)
-				So(stub.syncCalled, ShouldEqual, false)
+				So(stub.loginCalled, ShouldEqual, false)
 			})
 
 		})

+ 2 - 1
pkg/middleware/middleware.go

@@ -7,6 +7,8 @@ import (
 	"strings"
 	"time"
 
+	macaron "gopkg.in/macaron.v1"
+
 	"github.com/grafana/grafana/pkg/bus"
 	"github.com/grafana/grafana/pkg/components/apikeygen"
 	"github.com/grafana/grafana/pkg/infra/log"
@@ -14,7 +16,6 @@ import (
 	m "github.com/grafana/grafana/pkg/models"
 	"github.com/grafana/grafana/pkg/setting"
 	"github.com/grafana/grafana/pkg/util"
-	macaron "gopkg.in/macaron.v1"
 )
 
 var (

+ 0 - 5
pkg/services/ldap/hooks.go

@@ -1,5 +0,0 @@
-package ldap
-
-var (
-	hookDial func(*Auth) error
-)

+ 300 - 282
pkg/services/ldap/ldap.go

@@ -8,39 +8,38 @@ import (
 	"io/ioutil"
 	"strings"
 
-	"github.com/davecgh/go-spew/spew"
-	LDAP "gopkg.in/ldap.v3"
+	"gopkg.in/ldap.v3"
 
-	"github.com/grafana/grafana/pkg/bus"
 	"github.com/grafana/grafana/pkg/infra/log"
-	models "github.com/grafana/grafana/pkg/models"
-	"github.com/grafana/grafana/pkg/setting"
+	"github.com/grafana/grafana/pkg/models"
 )
 
 // 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)
+	Add(*ldap.AddRequest) error
+	Del(*ldap.DelRequest) 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)
+// IServer is interface for LDAP authorization
+type IServer interface {
+	Login(*models.LoginUserQuery) (*models.ExternalUserInfo, error)
+	Add(string, map[string][]string) error
+	Remove(string) error
+	Users([]string) ([]*models.ExternalUserInfo, error)
+	ExtractGrafanaUser(*UserInfo) (*models.ExternalUserInfo, error)
+	Dial() error
+	Close()
 }
 
-// Auth is basic struct of LDAP authorization
-type Auth struct {
-	server            *ServerConfig
-	conn              IConnection
+// Server is basic struct of LDAP authorization
+type Server struct {
+	config            *ServerConfig
+	connection        IConnection
 	requireSecondBind bool
 	log               log.Logger
 }
@@ -52,28 +51,24 @@ var (
 )
 
 var dial = func(network, addr string) (IConnection, error) {
-	return LDAP.Dial(network, addr)
+	return ldap.Dial(network, addr)
 }
 
 // New creates the new LDAP auth
-func New(server *ServerConfig) IAuth {
-	return &Auth{
-		server: server,
+func New(config *ServerConfig) IServer {
+	return &Server{
+		config: config,
 		log:    log.New("ldap"),
 	}
 }
 
 // Dial dials in the LDAP
-func (auth *Auth) Dial() error {
-	if hookDial != nil {
-		return hookDial(auth)
-	}
-
+func (server *Server) Dial() error {
 	var err error
 	var certPool *x509.CertPool
-	if auth.server.RootCACert != "" {
+	if server.config.RootCACert != "" {
 		certPool = x509.NewCertPool()
-		for _, caCertFile := range strings.Split(auth.server.RootCACert, " ") {
+		for _, caCertFile := range strings.Split(server.config.RootCACert, " ") {
 			pem, err := ioutil.ReadFile(caCertFile)
 			if err != nil {
 				return err
@@ -84,35 +79,35 @@ func (auth *Auth) Dial() error {
 		}
 	}
 	var clientCert tls.Certificate
-	if auth.server.ClientCert != "" && auth.server.ClientKey != "" {
-		clientCert, err = tls.LoadX509KeyPair(auth.server.ClientCert, auth.server.ClientKey)
+	if server.config.ClientCert != "" && server.config.ClientKey != "" {
+		clientCert, err = tls.LoadX509KeyPair(server.config.ClientCert, server.config.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 {
+	for _, host := range strings.Split(server.config.Host, " ") {
+		address := fmt.Sprintf("%s:%d", host, server.config.Port)
+		if server.config.UseSSL {
 			tlsCfg := &tls.Config{
-				InsecureSkipVerify: auth.server.SkipVerifySSL,
+				InsecureSkipVerify: server.config.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 server.config.StartTLS {
+				server.connection, err = dial("tcp", address)
 				if err == nil {
-					if err = auth.conn.StartTLS(tlsCfg); err == nil {
+					if err = server.connection.StartTLS(tlsCfg); err == nil {
 						return nil
 					}
 				}
 			} else {
-				auth.conn, err = LDAP.DialTLS("tcp", address, tlsCfg)
+				server.connection, err = ldap.DialTLS("tcp", address, tlsCfg)
 			}
 		} else {
-			auth.conn, err = dial("tcp", address)
+			server.connection, err = dial("tcp", address)
 		}
 
 		if err == nil {
@@ -122,91 +117,206 @@ func (auth *Auth) Dial() error {
 	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()
+// Close closes the LDAP connection
+func (server *Server) Close() {
+	server.connection.Close()
+}
 
-	// perform initial authentication
-	if err := auth.initialBind(query.Username, query.Password); err != nil {
-		return err
+// Login intialBinds the user, search it and then serialize it
+func (server *Server) Login(query *models.LoginUserQuery) (
+	*models.ExternalUserInfo, error,
+) {
+
+	// Perform initial authentication
+	err := server.intialBind(query.Username, query.Password)
+	if err != nil {
+		return nil, err
 	}
 
-	// find user entry & attributes
-	user, err := auth.searchForUser(query.Username)
+	// Find user entry & attributes
+	users, err := server.Users([]string{query.Username})
 	if err != nil {
-		return err
+		return nil, err
 	}
 
-	auth.log.Debug("Ldap User found", "info", spew.Sdump(user))
+	// If we couldn't find the user -
+	// we should show incorrect credentials err
+	if len(users) == 0 {
+		return nil, ErrInvalidCredentials
+	}
 
-	// check if a second user bind is needed
-	if auth.requireSecondBind {
-		err = auth.secondBind(user, query.Password)
+	// Check if a second user bind is needed
+	user := users[0]
+	if server.requireSecondBind {
+		err = server.secondBind(user, query.Password)
 		if err != nil {
-			return err
+			return nil, err
 		}
 	}
 
-	grafanaUser, err := auth.GetGrafanaUserFor(query.ReqContext, user)
+	return user, nil
+}
+
+// Add adds stuff to LDAP
+func (server *Server) Add(dn string, values map[string][]string) error {
+	err := server.intialBind(
+		server.config.BindDN,
+		server.config.BindPassword,
+	)
 	if err != nil {
 		return err
 	}
 
-	query.User = grafanaUser
-	return nil
-}
+	attributes := make([]ldap.Attribute, 0)
+	for key, value := range values {
+		attributes = append(attributes, ldap.Attribute{
+			Type: key,
+			Vals: value,
+		})
+	}
 
-// SyncUser syncs user with Grafana
-func (auth *Auth) SyncUser(query *models.LoginUserQuery) error {
-	// connect to ldap server
-	err := auth.Dial()
+	request := &ldap.AddRequest{
+		DN:         dn,
+		Attributes: attributes,
+	}
+
+	err = server.connection.Add(request)
 	if err != nil {
 		return err
 	}
-	defer auth.conn.Close()
 
-	err = auth.serverBind()
+	return nil
+}
+
+// Remove removes stuff from LDAP
+func (server *Server) Remove(dn string) error {
+	err := server.intialBind(
+		server.config.BindDN,
+		server.config.BindPassword,
+	)
 	if err != nil {
 		return err
 	}
 
-	// find user entry & attributes
-	user, err := auth.searchForUser(query.Username)
+	request := ldap.NewDelRequest(dn, nil)
+	err = server.connection.Del(request)
 	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))
+	return nil
+}
+
+// Users gets LDAP users
+func (server *Server) Users(logins []string) (
+	[]*models.ExternalUserInfo,
+	error,
+) {
+	var result *ldap.SearchResult
+	var err error
+	var config = server.config
 
-	grafanaUser, err := auth.GetGrafanaUserFor(query.ReqContext, user)
+	for _, base := range config.SearchBaseDNs {
+		result, err = server.connection.Search(
+			server.getSearchRequest(base, logins),
+		)
+		if err != nil {
+			return nil, err
+		}
+
+		if len(result.Entries) > 0 {
+			break
+		}
+	}
+
+	serializedUsers, err := server.serializeUsers(result)
 	if err != nil {
-		return err
+		return nil, err
+	}
+
+	return serializedUsers, nil
+}
+
+// ExtractGrafanaUser extracts external user info from LDAP user
+func (server *Server) ExtractGrafanaUser(user *UserInfo) (*models.ExternalUserInfo, error) {
+	result := server.buildGrafanaUser(user)
+	if err := server.validateGrafanaUser(result); err != nil {
+		return nil, err
+	}
+
+	return result, nil
+}
+
+// validateGrafanaUser validates user access.
+// If there are no ldap group mappings access is true
+// otherwise a single group must match
+func (server *Server) validateGrafanaUser(user *models.ExternalUserInfo) error {
+	if len(server.config.Groups) > 0 && len(user.OrgRoles) < 1 {
+		server.log.Error(
+			"user does not belong in any of the specified LDAP groups",
+			"username", user.Login,
+			"groups", user.Groups,
+		)
+		return ErrInvalidCredentials
 	}
 
-	query.User = grafanaUser
 	return nil
 }
 
-func (auth *Auth) GetGrafanaUserFor(
-	ctx *models.ReqContext,
-	user *UserInfo,
-) (*models.User, error) {
+// getSearchRequest returns LDAP search request for users
+func (server *Server) getSearchRequest(
+	base string,
+	logins []string,
+) *ldap.SearchRequest {
+	attributes := []string{}
+
+	inputs := server.config.Attr
+	attributes = appendIfNotEmpty(
+		attributes,
+		inputs.Username,
+		inputs.Surname,
+		inputs.Email,
+		inputs.Name,
+		inputs.MemberOf,
+	)
+
+	search := ""
+	for _, login := range logins {
+		query := strings.Replace(
+			server.config.SearchFilter,
+			"%s", ldap.EscapeFilter(login),
+			-1,
+		)
+
+		search = search + query
+	}
+
+	filter := fmt.Sprintf("(|%s)", search)
+
+	return &ldap.SearchRequest{
+		BaseDN:       base,
+		Scope:        ldap.ScopeWholeSubtree,
+		DerefAliases: ldap.NeverDerefAliases,
+		Attributes:   attributes,
+		Filter:       filter,
+	}
+}
+
+// buildGrafanaUser extracts info from UserInfo model to ExternalUserInfo
+func (server *Server) buildGrafanaUser(user *UserInfo) *models.ExternalUserInfo {
 	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{},
+		Name: strings.TrimSpace(
+			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 {
+	for _, group := range server.config.Groups {
 		// only use the first match for each org
 		if extUser.OrgRoles[group.OrgId] != "" {
 			continue
@@ -220,49 +330,28 @@ func (auth *Auth) GetGrafanaUserFor(
 		}
 	}
 
-	// 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
+	return extUser
 }
 
-func (auth *Auth) serverBind() error {
+func (server *Server) serverBind() error {
 	bindFn := func() error {
-		return auth.conn.Bind(auth.server.BindDN, auth.server.BindPassword)
+		return server.connection.Bind(
+			server.config.BindDN,
+			server.config.BindPassword,
+		)
 	}
 
-	if auth.server.BindPassword == "" {
+	if server.config.BindPassword == "" {
 		bindFn = func() error {
-			return auth.conn.UnauthenticatedBind(auth.server.BindDN)
+			return server.connection.UnauthenticatedBind(server.config.BindDN)
 		}
 	}
 
 	// bind_dn and bind_password to bind
 	if err := bindFn(); err != nil {
-		auth.log.Info("LDAP initial bind failed, %v", err)
+		server.log.Info("LDAP initial bind failed, %v", err)
 
-		if ldapErr, ok := err.(*LDAP.Error); ok {
+		if ldapErr, ok := err.(*ldap.Error); ok {
 			if ldapErr.ResultCode == 49 {
 				return ErrInvalidCredentials
 			}
@@ -273,11 +362,15 @@ func (auth *Auth) serverBind() error {
 	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)
+func (server *Server) secondBind(
+	user *models.ExternalUserInfo,
+	userPassword string,
+) error {
+	err := server.connection.Bind(user.AuthId, userPassword)
+	if err != nil {
+		server.log.Info("Second bind failed", "error", err)
 
-		if ldapErr, ok := err.(*LDAP.Error); ok {
+		if ldapErr, ok := err.(*ldap.Error); ok {
 			if ldapErr.ResultCode == 49 {
 				return ErrInvalidCredentials
 			}
@@ -288,31 +381,31 @@ func (auth *Auth) secondBind(user *UserInfo, userPassword string) error {
 	return nil
 }
 
-func (auth *Auth) initialBind(username, userPassword string) error {
-	if auth.server.BindPassword != "" || auth.server.BindDN == "" {
-		userPassword = auth.server.BindPassword
-		auth.requireSecondBind = true
+func (server *Server) intialBind(username, userPassword string) error {
+	if server.config.BindPassword != "" || server.config.BindDN == "" {
+		userPassword = server.config.BindPassword
+		server.requireSecondBind = true
 	}
 
-	bindPath := auth.server.BindDN
+	bindPath := server.config.BindDN
 	if strings.Contains(bindPath, "%s") {
-		bindPath = fmt.Sprintf(auth.server.BindDN, username)
+		bindPath = fmt.Sprintf(server.config.BindDN, username)
 	}
 
 	bindFn := func() error {
-		return auth.conn.Bind(bindPath, userPassword)
+		return server.connection.Bind(bindPath, userPassword)
 	}
 
 	if userPassword == "" {
 		bindFn = func() error {
-			return auth.conn.UnauthenticatedBind(bindPath)
+			return server.connection.UnauthenticatedBind(bindPath)
 		}
 	}
 
 	if err := bindFn(); err != nil {
-		auth.log.Info("Initial bind failed", "error", err)
+		server.log.Info("Initial bind failed", "error", err)
 
-		if ldapErr, ok := err.(*LDAP.Error); ok {
+		if ldapErr, ok := err.(*ldap.Error); ok {
 			if ldapErr.ResultCode == 49 {
 				return ErrInvalidCredentials
 			}
@@ -323,199 +416,124 @@ func (auth *Auth) initialBind(username, userPassword string) error {
 	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")
-	}
-
+// requestMemberOf use this function when POSIX LDAP schema does not support memberOf, so it manually search the groups
+func (server *Server) requestMemberOf(searchResult *ldap.SearchResult) ([]string, error) {
 	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
-			}
+	for _, groupSearchBase := range server.config.GroupSearchBaseDNs {
+		var filterReplace string
+		if server.config.GroupSearchFilterUserAttribute == "" {
+			filterReplace = getLdapAttr(server.config.Attr.Username, searchResult)
+		} else {
+			filterReplace = getLdapAttr(server.config.GroupSearchFilterUserAttribute, searchResult)
 		}
-	}
-
-	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,
+		filter := strings.Replace(
+			server.config.GroupSearchFilter, "%s",
+			ldap.EscapeFilter(filterReplace),
+			-1,
 		)
 
-		req := LDAP.SearchRequest{
-			BaseDN:       base,
-			Scope:        LDAP.ScopeWholeSubtree,
-			DerefAliases: LDAP.NeverDerefAliases,
-			Attributes:   attributes,
+		server.log.Info("Searching for user's groups", "filter", filter)
+
+		// support old way of reading settings
+		groupIDAttribute := server.config.Attr.MemberOf
+		// but prefer dn attribute if default settings are used
+		if groupIDAttribute == "" || groupIDAttribute == "memberOf" {
+			groupIDAttribute = "dn"
+		}
 
-			// Doing a star here to get all the users in one go
-			Filter: strings.Replace(server.SearchFilter, "%s", "*", -1),
+		groupSearchReq := ldap.SearchRequest{
+			BaseDN:       groupSearchBase,
+			Scope:        ldap.ScopeWholeSubtree,
+			DerefAliases: ldap.NeverDerefAliases,
+			Attributes:   []string{groupIDAttribute},
+			Filter:       filter,
 		}
 
-		result, err = ldap.conn.Search(&req)
+		groupSearchResult, err := server.connection.Search(&groupSearchReq)
 		if err != nil {
 			return nil, err
 		}
 
-		if len(result.Entries) > 0 {
+		if len(groupSearchResult.Entries) > 0 {
+			for i := range groupSearchResult.Entries {
+				memberOf = append(memberOf, getLdapAttrN(groupIDAttribute, groupSearchResult, i))
+			}
 			break
 		}
 	}
 
-	return ldap.serializeUsers(result), nil
+	return memberOf, nil
 }
 
-func (ldap *Auth) serializeUsers(users *LDAP.SearchResult) []*UserInfo {
-	var serialized []*UserInfo
+// serializeUsers serializes the users
+// from LDAP result to ExternalInfo struct
+func (server *Server) serializeUsers(
+	users *ldap.SearchResult,
+) ([]*models.ExternalUserInfo, error) {
+	var serialized []*models.ExternalUserInfo
 
 	for index := range users.Entries {
-		serialize := &UserInfo{
+		memberOf, err := server.getMemberOf(users)
+		if err != nil {
+			return nil, err
+		}
+
+		userInfo := &UserInfo{
 			DN: getLdapAttrN(
 				"dn",
 				users,
 				index,
 			),
 			LastName: getLdapAttrN(
-				ldap.server.Attr.Surname,
+				server.config.Attr.Surname,
 				users,
 				index,
 			),
 			FirstName: getLdapAttrN(
-				ldap.server.Attr.Name,
+				server.config.Attr.Name,
 				users,
 				index,
 			),
 			Username: getLdapAttrN(
-				ldap.server.Attr.Username,
+				server.config.Attr.Username,
 				users,
 				index,
 			),
 			Email: getLdapAttrN(
-				ldap.server.Attr.Email,
-				users,
-				index,
-			),
-			MemberOf: getLdapAttrArrayN(
-				ldap.server.Attr.MemberOf,
+				server.config.Attr.Email,
 				users,
 				index,
 			),
+			MemberOf: memberOf,
 		}
 
-		serialized = append(serialized, serialize)
+		serialized = append(
+			serialized,
+			server.buildGrafanaUser(userInfo),
+		)
+	}
+
+	return serialized, nil
+}
+
+// getMemberOf finds memberOf property or request it
+func (server *Server) getMemberOf(search *ldap.SearchResult) (
+	[]string, error,
+) {
+	if server.config.GroupSearchFilter == "" {
+		memberOf := getLdapAttrArray(server.config.Attr.MemberOf, search)
+
+		return memberOf, nil
+	}
+
+	memberOf, err := server.requestMemberOf(search)
+	if err != nil {
+		return nil, err
 	}
 
-	return serialized
+	return memberOf, nil
 }
 
 func appendIfNotEmpty(slice []string, values ...string) []string {
@@ -527,11 +545,11 @@ func appendIfNotEmpty(slice []string, values ...string) []string {
 	return slice
 }
 
-func getLdapAttr(name string, result *LDAP.SearchResult) string {
+func getLdapAttr(name string, result *ldap.SearchResult) string {
 	return getLdapAttrN(name, result, 0)
 }
 
-func getLdapAttrN(name string, result *LDAP.SearchResult, n int) string {
+func getLdapAttrN(name string, result *ldap.SearchResult, n int) string {
 	if strings.ToLower(name) == "dn" {
 		return result.Entries[n].DN
 	}
@@ -545,11 +563,11 @@ func getLdapAttrN(name string, result *LDAP.SearchResult, n int) string {
 	return ""
 }
 
-func getLdapAttrArray(name string, result *LDAP.SearchResult) []string {
+func getLdapAttrArray(name string, result *ldap.SearchResult) []string {
 	return getLdapAttrArrayN(name, result, 0)
 }
 
-func getLdapAttrArrayN(name string, result *LDAP.SearchResult, n int) []string {
+func getLdapAttrArrayN(name string, result *ldap.SearchResult, n int) []string {
 	for _, attr := range result.Entries[n].Attributes {
 		if attr.Name == name {
 			return attr.Values

+ 140 - 0
pkg/services/ldap/ldap_helpers_test.go

@@ -0,0 +1,140 @@
+package ldap
+
+import (
+	"testing"
+
+	. "github.com/smartystreets/goconvey/convey"
+	"gopkg.in/ldap.v3"
+
+	"github.com/grafana/grafana/pkg/infra/log"
+)
+
+func TestLDAPHelpers(t *testing.T) {
+	Convey("serializeUsers()", t, func() {
+		Convey("simple case", func() {
+			server := &Server{
+				config: &ServerConfig{
+					Attr: AttributeMap{
+						Username: "username",
+						Name:     "name",
+						MemberOf: "memberof",
+						Email:    "email",
+					},
+					SearchBaseDNs: []string{"BaseDNHere"},
+				},
+				connection: &mockConnection{},
+				log:        log.New("test-logger"),
+			}
+
+			entry := ldap.Entry{
+				DN: "dn", Attributes: []*ldap.EntryAttribute{
+					{Name: "username", Values: []string{"roelgerrits"}},
+					{Name: "surname", Values: []string{"Gerrits"}},
+					{Name: "email", Values: []string{"roel@test.com"}},
+					{Name: "name", Values: []string{"Roel"}},
+					{Name: "memberof", Values: []string{"admins"}},
+				}}
+			users := &ldap.SearchResult{Entries: []*ldap.Entry{&entry}}
+
+			result, err := server.serializeUsers(users)
+
+			So(err, ShouldBeNil)
+			So(result[0].Login, ShouldEqual, "roelgerrits")
+			So(result[0].Email, ShouldEqual, "roel@test.com")
+			So(result[0].Groups, ShouldContain, "admins")
+		})
+
+		Convey("without lastname", func() {
+			server := &Server{
+				config: &ServerConfig{
+					Attr: AttributeMap{
+						Username: "username",
+						Name:     "name",
+						MemberOf: "memberof",
+						Email:    "email",
+					},
+					SearchBaseDNs: []string{"BaseDNHere"},
+				},
+				connection: &mockConnection{},
+				log:        log.New("test-logger"),
+			}
+
+			entry := ldap.Entry{
+				DN: "dn", Attributes: []*ldap.EntryAttribute{
+					{Name: "username", Values: []string{"roelgerrits"}},
+					{Name: "email", Values: []string{"roel@test.com"}},
+					{Name: "name", Values: []string{"Roel"}},
+					{Name: "memberof", Values: []string{"admins"}},
+				}}
+			users := &ldap.SearchResult{Entries: []*ldap.Entry{&entry}}
+
+			result, err := server.serializeUsers(users)
+
+			So(err, ShouldBeNil)
+			So(result[0].Name, ShouldEqual, "Roel")
+		})
+	})
+
+	Convey("serverBind()", t, func() {
+		Convey("Given bind dn and password configured", func() {
+			connection := &mockConnection{}
+			var actualUsername, actualPassword string
+			connection.bindProvider = func(username, password string) error {
+				actualUsername = username
+				actualPassword = password
+				return nil
+			}
+			server := &Server{
+				connection: connection,
+				config: &ServerConfig{
+					BindDN:       "o=users,dc=grafana,dc=org",
+					BindPassword: "bindpwd",
+				},
+			}
+			err := server.serverBind()
+			So(err, ShouldBeNil)
+			So(actualUsername, ShouldEqual, "o=users,dc=grafana,dc=org")
+			So(actualPassword, ShouldEqual, "bindpwd")
+		})
+
+		Convey("Given bind dn configured", func() {
+			connection := &mockConnection{}
+			unauthenticatedBindWasCalled := false
+			var actualUsername string
+			connection.unauthenticatedBindProvider = func(username string) error {
+				unauthenticatedBindWasCalled = true
+				actualUsername = username
+				return nil
+			}
+			server := &Server{
+				connection: connection,
+				config: &ServerConfig{
+					BindDN: "o=users,dc=grafana,dc=org",
+				},
+			}
+			err := server.serverBind()
+			So(err, ShouldBeNil)
+			So(unauthenticatedBindWasCalled, ShouldBeTrue)
+			So(actualUsername, ShouldEqual, "o=users,dc=grafana,dc=org")
+		})
+
+		Convey("Given empty bind dn and password", func() {
+			connection := &mockConnection{}
+			unauthenticatedBindWasCalled := false
+			var actualUsername string
+			connection.unauthenticatedBindProvider = func(username string) error {
+				unauthenticatedBindWasCalled = true
+				actualUsername = username
+				return nil
+			}
+			server := &Server{
+				connection: connection,
+				config:     &ServerConfig{},
+			}
+			err := server.serverBind()
+			So(err, ShouldBeNil)
+			So(unauthenticatedBindWasCalled, ShouldBeTrue)
+			So(actualUsername, ShouldBeEmpty)
+		})
+	})
+}

+ 93 - 27
pkg/services/ldap/ldap_login_test.go

@@ -7,23 +7,94 @@ import (
 	"gopkg.in/ldap.v3"
 
 	"github.com/grafana/grafana/pkg/infra/log"
+	"github.com/grafana/grafana/pkg/models"
+	"github.com/grafana/grafana/pkg/services/user"
 )
 
-func TestLdapLogin(t *testing.T) {
-	Convey("Login using ldap", t, func() {
-		AuthScenario("When login with invalid credentials", func(scenario *scenarioContext) {
-			conn := &mockLdapConn{}
+func TestLDAPLogin(t *testing.T) {
+	Convey("Login()", t, func() {
+		authScenario("When user is log in and updated", func(sc *scenarioContext) {
+			// arrange
+			mockConnection := &mockConnection{}
+
+			auth := &Server{
+				config: &ServerConfig{
+					Host:       "",
+					RootCACert: "",
+					Groups: []*GroupToOrgRole{
+						{GroupDN: "*", OrgRole: "Admin"},
+					},
+					Attr: AttributeMap{
+						Username: "username",
+						Surname:  "surname",
+						Email:    "email",
+						Name:     "name",
+						MemberOf: "memberof",
+					},
+					SearchBaseDNs: []string{"BaseDNHere"},
+				},
+				connection: mockConnection,
+				log:        log.New("test-logger"),
+			}
+
+			entry := ldap.Entry{
+				DN: "dn", Attributes: []*ldap.EntryAttribute{
+					{Name: "username", Values: []string{"roelgerrits"}},
+					{Name: "surname", Values: []string{"Gerrits"}},
+					{Name: "email", Values: []string{"roel@test.com"}},
+					{Name: "name", Values: []string{"Roel"}},
+					{Name: "memberof", Values: []string{"admins"}},
+				}}
+			result := ldap.SearchResult{Entries: []*ldap.Entry{&entry}}
+			mockConnection.setSearchResult(&result)
+
+			query := &models.LoginUserQuery{
+				Username: "roelgerrits",
+			}
+
+			sc.userQueryReturns(&models.User{
+				Id:    1,
+				Email: "roel@test.net",
+				Name:  "Roel Gerrits",
+				Login: "roelgerrits",
+			})
+			sc.userOrgsQueryReturns([]*models.UserOrgDTO{})
+
+			// act
+			extUser, _ := auth.Login(query)
+			userInfo, err := user.Upsert(&user.UpsertArgs{
+				SignupAllowed: true,
+				ExternalUser:  extUser,
+			})
+
+			// assert
+
+			// Check absence of the error
+			So(err, ShouldBeNil)
+
+			// User should be searched in ldap
+			So(mockConnection.searchCalled, ShouldBeTrue)
+
+			// Info should be updated (email differs)
+			So(userInfo.Email, ShouldEqual, "roel@test.com")
+
+			// User should have admin privileges
+			So(sc.addOrgUserCmd.Role, ShouldEqual, "Admin")
+		})
+
+		authScenario("When login with invalid credentials", func(scenario *scenarioContext) {
+			connection := &mockConnection{}
 			entry := ldap.Entry{}
 			result := ldap.SearchResult{Entries: []*ldap.Entry{&entry}}
-			conn.setSearchResult(&result)
+			connection.setSearchResult(&result)
 
-			conn.bindProvider = func(username, password string) error {
+			connection.bindProvider = func(username, password string) error {
 				return &ldap.Error{
 					ResultCode: 49,
 				}
 			}
-			auth := &Auth{
-				server: &ServerConfig{
+			auth := &Server{
+				config: &ServerConfig{
 					Attr: AttributeMap{
 						Username: "username",
 						Name:     "name",
@@ -31,19 +102,19 @@ func TestLdapLogin(t *testing.T) {
 					},
 					SearchBaseDNs: []string{"BaseDNHere"},
 				},
-				conn: conn,
-				log:  log.New("test-logger"),
+				connection: connection,
+				log:        log.New("test-logger"),
 			}
 
-			err := auth.Login(scenario.loginUserQuery)
+			_, 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{}
+		authScenario("When login with valid credentials", func(scenario *scenarioContext) {
+			connection := &mockConnection{}
 			entry := ldap.Entry{
 				DN: "dn", Attributes: []*ldap.EntryAttribute{
 					{Name: "username", Values: []string{"markelog"}},
@@ -54,13 +125,13 @@ func TestLdapLogin(t *testing.T) {
 				},
 			}
 			result := ldap.SearchResult{Entries: []*ldap.Entry{&entry}}
-			conn.setSearchResult(&result)
+			connection.setSearchResult(&result)
 
-			conn.bindProvider = func(username, password string) error {
+			connection.bindProvider = func(username, password string) error {
 				return nil
 			}
-			auth := &Auth{
-				server: &ServerConfig{
+			auth := &Server{
+				config: &ServerConfig{
 					Attr: AttributeMap{
 						Username: "username",
 						Name:     "name",
@@ -68,19 +139,14 @@ func TestLdapLogin(t *testing.T) {
 					},
 					SearchBaseDNs: []string{"BaseDNHere"},
 				},
-				conn: conn,
-				log:  log.New("test-logger"),
+				connection: connection,
+				log:        log.New("test-logger"),
 			}
 
-			err := auth.Login(scenario.loginUserQuery)
+			resp, 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")
-			})
+			So(err, ShouldBeNil)
+			So(resp.Login, ShouldEqual, "markelog")
 		})
 	})
 }

+ 122 - 461
pkg/services/ldap/ldap_test.go

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

+ 4 - 2
pkg/services/ldap/settings.go

@@ -13,10 +13,12 @@ import (
 	"github.com/grafana/grafana/pkg/util/errutil"
 )
 
+// Config holds list of connections to LDAP
 type Config struct {
 	Servers []*ServerConfig `toml:"servers"`
 }
 
+// ServerConfig holds connection data to LDAP
 type ServerConfig struct {
 	Host          string       `toml:"host"`
 	Port          int          `toml:"port"`
@@ -108,11 +110,11 @@ func readConfig(configFile string) (*Config, error) {
 
 	_, err := toml.DecodeFile(configFile, result)
 	if err != nil {
-		return nil, errutil.Wrap("Failed to load ldap config file", err)
+		return nil, errutil.Wrap("Failed to load LDAP config file", err)
 	}
 
 	if len(result.Servers) == 0 {
-		return nil, xerrors.New("ldap enabled but no ldap servers defined in config file")
+		return nil, xerrors.New("LDAP enabled but no LDAP servers defined in config file")
 	}
 
 	// set default org id

+ 32 - 17
pkg/services/ldap/test.go

@@ -12,15 +12,22 @@ import (
 	"github.com/grafana/grafana/pkg/services/login"
 )
 
-type mockLdapConn struct {
-	result                      *ldap.SearchResult
-	searchCalled                bool
-	searchAttributes            []string
+type mockConnection struct {
+	searchResult     *ldap.SearchResult
+	searchCalled     bool
+	searchAttributes []string
+
+	addParams *ldap.AddRequest
+	addCalled bool
+
+	delParams *ldap.DelRequest
+	delCalled bool
+
 	bindProvider                func(username, password string) error
 	unauthenticatedBindProvider func(username string) error
 }
 
-func (c *mockLdapConn) Bind(username, password string) error {
+func (c *mockConnection) Bind(username, password string) error {
 	if c.bindProvider != nil {
 		return c.bindProvider(username, password)
 	}
@@ -28,7 +35,7 @@ func (c *mockLdapConn) Bind(username, password string) error {
 	return nil
 }
 
-func (c *mockLdapConn) UnauthenticatedBind(username string) error {
+func (c *mockConnection) UnauthenticatedBind(username string) error {
 	if c.unauthenticatedBindProvider != nil {
 		return c.unauthenticatedBindProvider(username)
 	}
@@ -36,23 +43,35 @@ func (c *mockLdapConn) UnauthenticatedBind(username string) error {
 	return nil
 }
 
-func (c *mockLdapConn) Close() {}
+func (c *mockConnection) Close() {}
 
-func (c *mockLdapConn) setSearchResult(result *ldap.SearchResult) {
-	c.result = result
+func (c *mockConnection) setSearchResult(result *ldap.SearchResult) {
+	c.searchResult = result
 }
 
-func (c *mockLdapConn) Search(sr *ldap.SearchRequest) (*ldap.SearchResult, error) {
+func (c *mockConnection) Search(sr *ldap.SearchRequest) (*ldap.SearchResult, error) {
 	c.searchCalled = true
 	c.searchAttributes = sr.Attributes
-	return c.result, nil
+	return c.searchResult, nil
+}
+
+func (c *mockConnection) Add(request *ldap.AddRequest) error {
+	c.addCalled = true
+	c.addParams = request
+	return nil
 }
 
-func (c *mockLdapConn) StartTLS(*tls.Config) error {
+func (c *mockConnection) Del(request *ldap.DelRequest) error {
+	c.delCalled = true
+	c.delParams = request
 	return nil
 }
 
-func AuthScenario(desc string, fn scenarioFunc) {
+func (c *mockConnection) StartTLS(*tls.Config) error {
+	return nil
+}
+
+func authScenario(desc string, fn scenarioFunc) {
 	Convey(desc, func() {
 		defer bus.ClearBusHandlers()
 
@@ -64,10 +83,6 @@ func AuthScenario(desc string, fn scenarioFunc) {
 			},
 		}
 
-		hookDial = func(auth *Auth) error {
-			return nil
-		}
-
 		loginService := &login.LoginService{
 			Bus: bus.GetBus(),
 		}

+ 204 - 0
pkg/services/multildap/multildap.go

@@ -0,0 +1,204 @@
+package multildap
+
+import (
+	"errors"
+
+	"github.com/grafana/grafana/pkg/models"
+	"github.com/grafana/grafana/pkg/services/ldap"
+)
+
+// GetConfig gets LDAP config
+var GetConfig = ldap.GetConfig
+
+// IsEnabled checks if LDAP is enabled
+var IsEnabled = ldap.IsEnabled
+
+// ErrInvalidCredentials is returned if username and password do not match
+var ErrInvalidCredentials = ldap.ErrInvalidCredentials
+
+// ErrNoLDAPServers is returned when there is no LDAP servers specified
+var ErrNoLDAPServers = errors.New("No LDAP servers are configured")
+
+// ErrDidNotFindUser if request for user is unsuccessful
+var ErrDidNotFindUser = errors.New("Did not find a user")
+
+// IMultiLDAP is interface for MultiLDAP
+type IMultiLDAP interface {
+	Login(query *models.LoginUserQuery) (
+		*models.ExternalUserInfo, error,
+	)
+
+	Users(logins []string) (
+		[]*models.ExternalUserInfo, error,
+	)
+
+	User(login string) (
+		*models.ExternalUserInfo, error,
+	)
+
+	Add(dn string, values map[string][]string) error
+	Remove(dn string) error
+}
+
+// MultiLDAP is basic struct of LDAP authorization
+type MultiLDAP struct {
+	configs []*ldap.ServerConfig
+}
+
+// New creates the new LDAP auth
+func New(configs []*ldap.ServerConfig) IMultiLDAP {
+	return &MultiLDAP{
+		configs: configs,
+	}
+}
+
+// Add adds user to the *first* defined LDAP
+func (multiples *MultiLDAP) Add(
+	dn string,
+	values map[string][]string,
+) error {
+	if len(multiples.configs) == 0 {
+		return ErrNoLDAPServers
+	}
+
+	config := multiples.configs[0]
+	ldap := ldap.New(config)
+
+	if err := ldap.Dial(); err != nil {
+		return err
+	}
+
+	defer ldap.Close()
+
+	err := ldap.Add(dn, values)
+	if err != nil {
+		return err
+	}
+
+	return nil
+}
+
+// Remove removes user from the *first* defined LDAP
+func (multiples *MultiLDAP) Remove(dn string) error {
+	if len(multiples.configs) == 0 {
+		return ErrNoLDAPServers
+	}
+
+	config := multiples.configs[0]
+	ldap := ldap.New(config)
+
+	if err := ldap.Dial(); err != nil {
+		return err
+	}
+
+	defer ldap.Close()
+
+	err := ldap.Remove(dn)
+	if err != nil {
+		return err
+	}
+
+	return nil
+}
+
+// Login tries to log in the user in multiples LDAP
+func (multiples *MultiLDAP) Login(query *models.LoginUserQuery) (
+	*models.ExternalUserInfo, error,
+) {
+	if len(multiples.configs) == 0 {
+		return nil, ErrNoLDAPServers
+	}
+
+	for _, config := range multiples.configs {
+		server := ldap.New(config)
+
+		if err := server.Dial(); err != nil {
+			return nil, err
+		}
+
+		defer server.Close()
+
+		user, err := server.Login(query)
+
+		if user != nil {
+			return user, nil
+		}
+
+		// Continue if we couldn't find the user
+		if err == ErrInvalidCredentials {
+			continue
+		}
+
+		if err != nil {
+			return nil, err
+		}
+
+		return user, nil
+	}
+
+	// Return invalid credentials if we couldn't find the user anywhere
+	return nil, ErrInvalidCredentials
+}
+
+// User gets a user by login
+func (multiples *MultiLDAP) User(login string) (
+	*models.ExternalUserInfo,
+	error,
+) {
+
+	if len(multiples.configs) == 0 {
+		return nil, ErrNoLDAPServers
+	}
+
+	search := []string{login}
+	for _, config := range multiples.configs {
+		server := ldap.New(config)
+
+		if err := server.Dial(); err != nil {
+			return nil, err
+		}
+
+		defer server.Close()
+
+		users, err := server.Users(search)
+		if err != nil {
+			return nil, err
+		}
+
+		if len(users) != 0 {
+			return users[0], nil
+		}
+	}
+
+	return nil, ErrDidNotFindUser
+}
+
+// Users gets users from multiple LDAP servers
+func (multiples *MultiLDAP) Users(logins []string) (
+	[]*models.ExternalUserInfo,
+	error,
+) {
+	var result []*models.ExternalUserInfo
+
+	if len(multiples.configs) == 0 {
+		return nil, ErrNoLDAPServers
+	}
+
+	for _, config := range multiples.configs {
+		server := ldap.New(config)
+
+		if err := server.Dial(); err != nil {
+			return nil, err
+		}
+
+		defer server.Close()
+
+		users, err := server.Users(logins)
+		if err != nil {
+			return nil, err
+		}
+		result = append(result, users...)
+	}
+
+	return result, nil
+}

+ 8 - 2
pkg/services/sqlstore/sqlstore.go

@@ -8,7 +8,6 @@ import (
 	"path"
 	"path/filepath"
 	"strings"
-	"testing"
 	"time"
 
 	"github.com/go-sql-driver/mysql"
@@ -280,7 +279,14 @@ func (ss *SqlStore) readConfig() {
 	ss.dbCfg.CacheMode = sec.Key("cache_mode").MustString("private")
 }
 
-func InitTestDB(t *testing.T) *SqlStore {
+// Interface of arguments for testing db
+type ITestDB interface {
+	Helper()
+	Fatalf(format string, args ...interface{})
+}
+
+// InitTestDB initiliaze test DB
+func InitTestDB(t ITestDB) *SqlStore {
 	t.Helper()
 	sqlstore := &SqlStore{}
 	sqlstore.skipEnsureAdmin = true

+ 39 - 0
pkg/services/user/user.go

@@ -0,0 +1,39 @@
+package user
+
+import (
+	"github.com/grafana/grafana/pkg/bus"
+	"github.com/grafana/grafana/pkg/models"
+)
+
+// UpsertArgs are object for Upsert method
+type UpsertArgs struct {
+	ReqContext    *models.ReqContext
+	ExternalUser  *models.ExternalUserInfo
+	SignupAllowed bool
+}
+
+// Upsert add/update grafana user
+func Upsert(args *UpsertArgs) (*models.User, error) {
+	query := &models.UpsertUserCommand{
+		ReqContext:    args.ReqContext,
+		ExternalUser:  args.ExternalUser,
+		SignupAllowed: args.SignupAllowed,
+	}
+	err := bus.Dispatch(query)
+	if err != nil {
+		return nil, err
+	}
+
+	return query.Result, nil
+}
+
+// Get the users
+func Get(
+	query *models.SearchUsersQuery,
+) ([]*models.UserSearchHitDTO, error) {
+	if err := bus.Dispatch(query); err != nil {
+		return nil, err
+	}
+
+	return query.Result.Users, nil
+}

+ 1 - 0
pkg/setting/setting.go

@@ -805,6 +805,7 @@ func (cfg *Cfg) Load(args *CommandLineArgs) error {
 	// auth proxy
 	authProxy := iniFile.Section("auth.proxy")
 	AuthProxyEnabled = authProxy.Key("enabled").MustBool(false)
+
 	AuthProxyHeaderName, err = valueAsString(authProxy, "header_name", "")
 	if err != nil {
 		return err

+ 134 - 0
vendor/github.com/brianvoe/gofakeit/BENCHMARKS.md

@@ -0,0 +1,134 @@
+go test -bench=. -benchmem
+goos: darwin
+goarch: amd64
+pkg: github.com/brianvoe/gofakeit
+Table generated with tablesgenerator.com/markdown_tables
+
+| Benchmark              | Ops   | CPU  | MEM   | MEM alloc  |
+|---------------------------------|-----------|-------------|------------|--------------|
+| BenchmarkAddress-4              | 1000000   | 1998 ns/op  | 248 B/op   | 7 allocs/op  |
+| BenchmarkStreet-4               | 1000000   | 1278 ns/op  | 62 B/op    | 3 allocs/op  |
+| BenchmarkStreetNumber-4         | 5000000   | 344 ns/op   | 36 B/op    | 2 allocs/op  |
+| BenchmarkStreetPrefix-4         | 10000000  | 121 ns/op   | 0 B/op     | 0 allocs/op  |
+| BenchmarkStreetName-4           | 10000000  | 122 ns/op   | 0 B/op     | 0 allocs/op  |
+| BenchmarkStreetSuffix-4         | 10000000  | 122 ns/op   | 0 B/op     | 0 allocs/op  |
+| BenchmarkCity-4                 | 5000000   | 326 ns/op   | 15 B/op    | 1 allocs/op  |
+| BenchmarkState-4                | 10000000  | 120 ns/op   | 0 B/op     | 0 allocs/op  |
+| BenchmarkStateAbr-4             | 10000000  | 122 ns/op   | 0 B/op     | 0 allocs/op  |
+| BenchmarkZip-4                  | 5000000   | 315 ns/op   | 5 B/op     | 1 allocs/op  |
+| BenchmarkCountry-4              | 10000000  | 126 ns/op   | 0 B/op     | 0 allocs/op  |
+| BenchmarkCountryAbr-4           | 10000000  | 123 ns/op   | 0 B/op     | 0 allocs/op  |
+| BenchmarkLatitude-4             | 100000000 | 23.6 ns/op  | 0 B/op     | 0 allocs/op  |
+| BenchmarkLongitude-4            | 100000000 | 23.6 ns/op  | 0 B/op     | 0 allocs/op  |
+| BenchmarkLatitudeInRange-4      | 50000000  | 27.7 ns/op  | 0 B/op     | 0 allocs/op  |
+| BenchmarkLongitudeInRange-4     | 50000000  | 27.8 ns/op  | 0 B/op     | 0 allocs/op  |
+| BenchmarkBeerName-4             | 20000000  | 104 ns/op   | 0 B/op     | 0 allocs/op  |
+| BenchmarkBeerStyle-4            | 10000000  | 119 ns/op   | 0 B/op     | 0 allocs/op  |
+| BenchmarkBeerHop-4              | 20000000  | 105 ns/op   | 0 B/op     | 0 allocs/op  |
+| BenchmarkBeerYeast-4            | 20000000  | 106 ns/op   | 0 B/op     | 0 allocs/op  |
+| BenchmarkBeerMalt-4             | 20000000  | 114 ns/op   | 0 B/op     | 0 allocs/op  |
+| BenchmarkBeerIbu-4              | 20000000  | 71.0 ns/op  | 8 B/op     | 1 allocs/op  |
+| BenchmarkBeerAlcohol-4          | 5000000   | 335 ns/op   | 40 B/op    | 3 allocs/op  |
+| BenchmarkBeerBlg-4              | 5000000   | 338 ns/op   | 48 B/op    | 3 allocs/op  |
+| BenchmarkBool-4                 | 50000000  | 34.2 ns/op  | 0 B/op     | 0 allocs/op  |
+| BenchmarkColor-4                | 20000000  | 112 ns/op   | 0 B/op     | 0 allocs/op  |
+| BenchmarkSafeColor-4            | 20000000  | 102 ns/op   | 0 B/op     | 0 allocs/op  |
+| BenchmarkHexColor-4             | 3000000   | 491 ns/op   | 24 B/op    | 3 allocs/op  |
+| BenchmarkRGBColor-4             | 20000000  | 103 ns/op   | 32 B/op    | 1 allocs/op  |
+| BenchmarkCompany-4              | 5000000   | 353 ns/op   | 22 B/op    | 1 allocs/op  |
+| BenchmarkCompanySuffix-4        | 20000000  | 89.6 ns/op  | 0 B/op     | 0 allocs/op  |
+| BenchmarkBuzzWord-4             | 20000000  | 99.0 ns/op  | 0 B/op     | 0 allocs/op  |
+| BenchmarkBS-4                   | 20000000  | 100 ns/op   | 0 B/op     | 0 allocs/op  |
+| BenchmarkContact-4              | 1000000   | 1121 ns/op  | 178 B/op   | 7 allocs/op  |
+| BenchmarkPhone-4                | 5000000   | 346 ns/op   | 16 B/op    | 1 allocs/op  |
+| BenchmarkPhoneFormatted-4       | 3000000   | 456 ns/op   | 16 B/op    | 1 allocs/op  |
+| BenchmarkEmail-4                | 2000000   | 715 ns/op   | 130 B/op   | 5 allocs/op  |
+| BenchmarkCurrency-4             | 10000000  | 125 ns/op   | 32 B/op    | 1 allocs/op  |
+| BenchmarkCurrencyShort-4        | 20000000  | 104 ns/op   | 0 B/op     | 0 allocs/op  |
+| BenchmarkCurrencyLong-4         | 20000000  | 105 ns/op   | 0 B/op     | 0 allocs/op  |
+| BenchmarkPrice-4                | 50000000  | 27.2 ns/op  | 0 B/op     | 0 allocs/op  |
+| BenchmarkDate-4                 | 5000000   | 371 ns/op   | 0 B/op     | 0 allocs/op  |
+| BenchmarkDateRange-4            | 10000000  | 238 ns/op   | 0 B/op     | 0 allocs/op  |
+| BenchmarkMonth-4                | 30000000  | 44.6 ns/op  | 0 B/op     | 0 allocs/op  |
+| BenchmarkDay-4                  | 50000000  | 39.2 ns/op  | 0 B/op     | 0 allocs/op  |
+| BenchmarkWeekDay-4              | 30000000  | 44.7 ns/op  | 0 B/op     | 0 allocs/op  |
+| BenchmarkYear-4                 | 20000000  | 115 ns/op   | 0 B/op     | 0 allocs/op  |
+| BenchmarkHour-4                 | 30000000  | 39.9 ns/op  | 0 B/op     | 0 allocs/op  |
+| BenchmarkMinute-4               | 50000000  | 40.4 ns/op  | 0 B/op     | 0 allocs/op  |
+| BenchmarkSecond-4               | 30000000  | 40.6 ns/op  | 0 B/op     | 0 allocs/op  |
+| BenchmarkNanoSecond-4           | 30000000  | 42.2 ns/op  | 0 B/op     | 0 allocs/op  |
+| BenchmarkTimeZone-4             | 20000000  | 105 ns/op   | 0 B/op     | 0 allocs/op  |
+| BenchmarkTimeZoneFull-4         | 20000000  | 118 ns/op   | 0 B/op     | 0 allocs/op  |
+| BenchmarkTimeZoneAbv-4          | 20000000  | 105 ns/op   | 0 B/op     | 0 allocs/op  |
+| BenchmarkTimeZoneOffset-4       | 10000000  | 147 ns/op   | 0 B/op     | 0 allocs/op  |
+| BenchmarkMimeType-4             | 20000000  | 99.9 ns/op  | 0 B/op     | 0 allocs/op  |
+| BenchmarkExtension-4            | 20000000  | 109 ns/op   | 0 B/op     | 0 allocs/op  |
+| BenchmarkGenerate-4             | 1000000   | 1588 ns/op  | 414 B/op   | 11 allocs/op |
+| BenchmarkHackerPhrase-4         | 300000    | 4576 ns/op  | 2295 B/op  | 26 allocs/op |
+| BenchmarkHackerAbbreviation-4   | 20000000  | 101 ns/op   | 0 B/op     | 0 allocs/op  |
+| BenchmarkHackerAdjective-4      | 20000000  | 101 ns/op   | 0 B/op     | 0 allocs/op  |
+| BenchmarkHackerNoun-4           | 20000000  | 104 ns/op   | 0 B/op     | 0 allocs/op  |
+| BenchmarkHackerVerb-4           | 20000000  | 113 ns/op   | 0 B/op     | 0 allocs/op  |
+| BenchmarkHackerIngverb-4        | 20000000  | 98.6 ns/op  | 0 B/op     | 0 allocs/op  |
+| BenchmarkHipsterWord-4          | 20000000  | 100 ns/op   | 0 B/op     | 0 allocs/op  |
+| BenchmarkHipsterSentence-4      | 1000000   | 1636 ns/op  | 353 B/op   | 3 allocs/op  |
+| BenchmarkHipsterParagraph-4     | 50000     | 31677 ns/op | 12351 B/op | 64 allocs/op |
+| BenchmarkImageURL-4             | 20000000  | 108 ns/op   | 38 B/op    | 3 allocs/op  |
+| BenchmarkDomainName-4           | 3000000   | 491 ns/op   | 76 B/op    | 3 allocs/op  |
+| BenchmarkDomainSuffix-4         | 20000000  | 99.4 ns/op  | 0 B/op     | 0 allocs/op  |
+| BenchmarkURL-4                  | 1000000   | 1201 ns/op  | 278 B/op   | 8 allocs/op  |
+| BenchmarkHTTPMethod-4           | 20000000  | 100 ns/op   | 0 B/op     | 0 allocs/op  |
+| BenchmarkIPv4Address-4          | 3000000   | 407 ns/op   | 48 B/op    | 5 allocs/op  |
+| BenchmarkIPv6Address-4          | 3000000   | 552 ns/op   | 96 B/op    | 7 allocs/op  |
+| BenchmarkUsername-4             | 5000000   | 307 ns/op   | 16 B/op    | 2 allocs/op  |
+| BenchmarkJob-4                  | 2000000   | 726 ns/op   | 86 B/op    | 2 allocs/op  |
+| BenchmarkJobTitle-4             | 20000000  | 98.7 ns/op  | 0 B/op     | 0 allocs/op  |
+| BenchmarkJobDescriptor-4        | 20000000  | 98.9 ns/op  | 0 B/op     | 0 allocs/op  |
+| BenchmarkJobLevel-4             | 20000000  | 110 ns/op   | 0 B/op     | 0 allocs/op  |
+| BenchmarkLogLevel-4             | 20000000  | 107 ns/op   | 0 B/op     | 0 allocs/op  |
+| BenchmarkReplaceWithNumbers-4   | 3000000   | 570 ns/op   | 32 B/op    | 1 allocs/op  |
+| BenchmarkName-4                 | 5000000   | 285 ns/op   | 17 B/op    | 1 allocs/op  |
+| BenchmarkFirstName-4            | 20000000  | 102 ns/op   | 0 B/op     | 0 allocs/op  |
+| BenchmarkLastName-4             | 20000000  | 100 ns/op   | 0 B/op     | 0 allocs/op  |
+| BenchmarkNamePrefix-4           | 20000000  | 98.0 ns/op  | 0 B/op     | 0 allocs/op  |
+| BenchmarkNameSuffix-4           | 20000000  | 109 ns/op   | 0 B/op     | 0 allocs/op  |
+| BenchmarkNumber-4               | 50000000  | 34.5 ns/op  | 0 B/op     | 0 allocs/op  |
+| BenchmarkUint8-4                | 50000000  | 28.5 ns/op  | 0 B/op     | 0 allocs/op  |
+| BenchmarkUint16-4               | 50000000  | 28.5 ns/op  | 0 B/op     | 0 allocs/op  |
+| BenchmarkUint32-4               | 50000000  | 27.0 ns/op  | 0 B/op     | 0 allocs/op  |
+| BenchmarkUint64-4               | 50000000  | 34.6 ns/op  | 0 B/op     | 0 allocs/op  |
+| BenchmarkInt8-4                 | 50000000  | 28.5 ns/op  | 0 B/op     | 0 allocs/op  |
+| BenchmarkInt16-4                | 50000000  | 28.4 ns/op  | 0 B/op     | 0 allocs/op  |
+| BenchmarkInt32-4                | 50000000  | 27.0 ns/op  | 0 B/op     | 0 allocs/op  |
+| BenchmarkInt64-4                | 50000000  | 34.9 ns/op  | 0 B/op     | 0 allocs/op  |
+| BenchmarkFloat32-4              | 50000000  | 27.7 ns/op  | 0 B/op     | 0 allocs/op  |
+| BenchmarkFloat32Range-4         | 50000000  | 27.9 ns/op  | 0 B/op     | 0 allocs/op  |
+| BenchmarkFloat64-4              | 50000000  | 25.9 ns/op  | 0 B/op     | 0 allocs/op  |
+| BenchmarkFloat64Range-4         | 50000000  | 26.5 ns/op  | 0 B/op     | 0 allocs/op  |
+| BenchmarkNumerify-4             | 5000000   | 354 ns/op   | 16 B/op    | 1 allocs/op  |
+| BenchmarkShuffleInts-4          | 10000000  | 226 ns/op   | 0 B/op     | 0 allocs/op  |
+| BenchmarkPassword-4             | 2000000   | 655 ns/op   | 304 B/op   | 6 allocs/op  |
+| BenchmarkCreditCard-4           | 2000000   | 997 ns/op   | 88 B/op    | 4 allocs/op  |
+| BenchmarkCreditCardType-4       | 20000000  | 92.7 ns/op  | 0 B/op     | 0 allocs/op  |
+| BenchmarkCreditCardNumber-4     | 3000000   | 572 ns/op   | 16 B/op    | 1 allocs/op  |
+| BenchmarkCreditCardNumberLuhn-4 | 300000    | 5815 ns/op  | 159 B/op   | 9 allocs/op  |
+| BenchmarkCreditCardExp-4        | 10000000  | 129 ns/op   | 5 B/op     | 1 allocs/op  |
+| BenchmarkCreditCardCvv-4        | 10000000  | 128 ns/op   | 3 B/op     | 1 allocs/op  |
+| BenchmarkSSN-4                  | 20000000  | 84.2 ns/op  | 16 B/op    | 1 allocs/op  |
+| BenchmarkGender-4               | 50000000  | 38.0 ns/op  | 0 B/op     | 0 allocs/op  |
+| BenchmarkPerson-4               | 300000    | 5563 ns/op  | 805 B/op   | 26 allocs/op |
+| BenchmarkSimpleStatusCode-4     | 20000000  | 72.9 ns/op  | 0 B/op     | 0 allocs/op  |
+| BenchmarkStatusCode-4           | 20000000  | 75.8 ns/op  | 0 B/op     | 0 allocs/op  |
+| BenchmarkLetter-4               | 50000000  | 38.4 ns/op  | 0 B/op     | 0 allocs/op  |
+| BenchmarkDigit-4                | 50000000  | 38.2 ns/op  | 0 B/op     | 0 allocs/op  |
+| BenchmarkLexify-4               | 10000000  | 222 ns/op   | 8 B/op     | 1 allocs/op  |
+| BenchmarkShuffleStrings-4       | 10000000  | 197 ns/op   | 0 B/op     | 0 allocs/op  |
+| BenchmarkUUID-4                 | 20000000  | 106 ns/op   | 48 B/op    | 1 allocs/op  |
+| BenchmarkUserAgent-4            | 1000000   | 1236 ns/op  | 305 B/op   | 5 allocs/op  |
+| BenchmarkChromeUserAgent-4      | 2000000   | 881 ns/op   | 188 B/op   | 5 allocs/op  |
+| BenchmarkFirefoxUserAgent-4     | 1000000   | 1595 ns/op  | 386 B/op   | 7 allocs/op  |
+| BenchmarkSafariUserAgent-4      | 1000000   | 1396 ns/op  | 551 B/op   | 7 allocs/op  |
+| BenchmarkOperaUserAgent-4       | 2000000   | 950 ns/op   | 216 B/op   | 5 allocs/op  |
+| BenchmarkWord-4                 | 20000000  | 99.1 ns/op  | 0 B/op     | 0 allocs/op  |
+| BenchmarkSentence-4             | 1000000   | 1540 ns/op  | 277 B/op   | 2 allocs/op  |
+| BenchmarkParagraph-4            | 50000     | 30978 ns/op | 11006 B/op | 61 allocs/op |

+ 46 - 0
vendor/github.com/brianvoe/gofakeit/CODE_OF_CONDUCT.md

@@ -0,0 +1,46 @@
+# Contributor Covenant Code of Conduct
+
+## Our Pledge
+
+In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation.
+
+## Our Standards
+
+Examples of behavior that contributes to creating a positive environment include:
+
+* Using welcoming and inclusive language
+* Being respectful of differing viewpoints and experiences
+* Gracefully accepting constructive criticism
+* Focusing on what is best for the community
+* Showing empathy towards other community members
+
+Examples of unacceptable behavior by participants include:
+
+* The use of sexualized language or imagery and unwelcome sexual attention or advances
+* Trolling, insulting/derogatory comments, and personal or political attacks
+* Public or private harassment
+* Publishing others' private information, such as a physical or electronic address, without explicit permission
+* Other conduct which could reasonably be considered inappropriate in a professional setting
+
+## Our Responsibilities
+
+Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior.
+
+Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful.
+
+## Scope
+
+This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers.
+
+## Enforcement
+
+Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at brian@webiswhatido.com. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately.
+
+Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership.
+
+## Attribution
+
+This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version]
+
+[homepage]: http://contributor-covenant.org
+[version]: http://contributor-covenant.org/version/1/4/

+ 1 - 0
vendor/github.com/brianvoe/gofakeit/CONTRIBUTING.md

@@ -0,0 +1 @@
+# Make a pull request and submit it and ill take a look at it. Thanks!

+ 20 - 0
vendor/github.com/brianvoe/gofakeit/LICENSE.txt

@@ -0,0 +1,20 @@
+The MIT License (MIT)
+
+Copyright (c) [year] [fullname]
+
+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.

+ 254 - 0
vendor/github.com/brianvoe/gofakeit/README.md

@@ -0,0 +1,254 @@
+![alt text](https://raw.githubusercontent.com/brianvoe/gofakeit/master/logo.png)
+
+# gofakeit [![Go Report Card](https://goreportcard.com/badge/github.com/brianvoe/gofakeit)](https://goreportcard.com/report/github.com/brianvoe/gofakeit) [![Build Status](https://travis-ci.org/brianvoe/gofakeit.svg?branch=master)](https://travis-ci.org/brianvoe/gofakeit) [![codecov.io](https://codecov.io/github/brianvoe/gofakeit/branch/master/graph/badge.svg)](https://codecov.io/github/brianvoe/gofakeit) [![GoDoc](https://godoc.org/github.com/brianvoe/gofakeit?status.svg)](https://godoc.org/github.com/brianvoe/gofakeit) [![license](http://img.shields.io/badge/license-MIT-green.svg?style=flat)](https://raw.githubusercontent.com/brianvoe/gofakeit/master/LICENSE.txt)
+Random data generator written in go
+
+<a href="https://www.buymeacoffee.com/brianvoe" target="_blank"><img src="https://www.buymeacoffee.com/assets/img/custom_images/orange_img.png" alt="Buy Me A Coffee" style="height: auto !important;width: auto !important;" ></a>
+
+### Features
+- Every function has an example and a benchmark,
+[see benchmarks](https://github.com/brianvoe/gofakeit/blob/master/BENCHMARKS.md)
+- Zero dependencies
+- Randomizes user defined structs
+- Numerous functions for regular use
+
+### 120+ Functions!!!
+If there is something that is generic enough missing from this package [add an issue](https://github.com/brianvoe/gofakeit/issues) and let me know what you need.
+Most of the time i'll add it!
+
+## Person
+```go
+Person() *PersonInfo
+Name() string
+NamePrefix() string
+NameSuffix() string
+FirstName() string
+LastName() string
+Gender() string
+SSN() string
+Contact() *ContactInfo
+Email() string
+Phone() string
+PhoneFormatted() string
+Username() string
+Password(lower bool, upper bool, numeric bool, special bool, space bool, num int) string
+```
+
+## Address
+```go
+Address() *AddressInfo
+City() string
+Country() string
+CountryAbr() string
+State() string
+StateAbr() string
+StatusCode() string
+Street() string
+StreetName() string
+StreetNumber() string
+StreetPrefix() string
+StreetSuffix() string
+Zip() string
+Latitude() float64
+LatitudeInRange() (float64, error)
+Longitude() float64
+LongitudeInRange() (float64, error)
+```
+
+## Beer
+```go
+BeerAlcohol() string
+BeerBlg() string
+BeerHop() string
+BeerIbu() string
+BeerMalt() string
+BeerName() string
+BeerStyle() string
+BeerYeast() string
+```
+
+## Cars
+```go
+Vehicle() *VehicleInfo
+CarMaker() string
+CarModel() string
+VehicleType() string
+FuelType() string
+TransmissionGearType() string
+```
+
+## Words
+```go
+Word() string
+Sentence(wordCount int) string
+Paragraph(paragraphCount int, sentenceCount int, wordCount int, separator string) string
+Question() string
+Quote() string
+```
+
+## Misc
+```go
+Struct(v interface{})
+Generate() string
+Bool() bool
+UUID() string
+```
+
+## Colors
+```go
+Color() string
+HexColor() string
+RGBColor() string
+SafeColor() string
+```
+
+## Internet
+```go
+URL() string
+ImageURL(width int, height int) string
+DomainName() string
+DomainSuffix() string
+IPv4Address() string
+IPv6Address() string
+SimpleStatusCode() int
+LogLevel(logType string) string
+HTTPMethod() string
+UserAgent() string
+ChromeUserAgent() string
+FirefoxUserAgent() string
+OperaUserAgent() string
+SafariUserAgent() string
+```
+
+## Date/Time
+```go
+Date() time.Time
+DateRange(start, end time.Time) time.Time
+NanoSecond() int
+Second() int
+Minute() int
+Hour() int
+Month() string
+Day() int
+WeekDay() string
+Year() int
+TimeZone() string
+TimeZoneAbv() string
+TimeZoneFull() string
+TimeZoneOffset() float32
+```
+
+## Payment
+```go
+Price(min, max float64) float64
+CreditCard() *CreditCardInfo
+CreditCardCvv() string
+CreditCardExp() string
+CreditCardNumber() int
+CreditCardNumberLuhn() int
+CreditCardType() string
+Currency() *CurrencyInfo
+CurrencyLong() string
+CurrencyShort() string
+```
+
+## Company
+```go
+BS() string
+BuzzWord() string
+Company() string
+CompanySuffix() string
+Job() *JobInfo
+JobDescriptor() string
+JobLevel() string
+JobTitle() string
+```
+
+## Hacker
+```go
+HackerAbbreviation() string
+HackerAdjective() string
+HackerIngverb() string
+HackerNoun() string
+HackerPhrase() string
+HackerVerb() string
+```
+
+## Hipster
+```go
+HipsterWord() string
+HipsterSentence(wordCount int) string
+HipsterParagraph(paragraphCount int, sentenceCount int, wordCount int, separator string) string
+```
+
+## File
+```go
+Extension() string
+MimeType() string
+```
+
+## Numbers
+```go
+Number(min int, max int) int
+Numerify(str string) string
+Int8() int8
+Int16() int16
+Int32() int32
+Int64() int64
+Uint8() uint8
+Uint16() uint16
+Uint32() uint32
+Uint64() uint64
+Float32() float32
+Float32Range(min, max float32) float32
+Float64() float64
+Float64Range(min, max float64) float64
+ShuffleInts(a []int)
+```
+
+## String
+```go
+Digit() string
+Letter() string
+Lexify(str string) string
+RandString(a []string) string
+ShuffleStrings(a []string)
+```
+
+## Documentation
+[![GoDoc](https://godoc.org/github.com/brianvoe/gofakeit?status.svg)](https://godoc.org/github.com/brianvoe/gofakeit)
+
+## Example
+```go
+import "github.com/brianvoe/gofakeit"
+
+gofakeit.Name() // Markus Moen
+gofakeit.Email() // alaynawuckert@kozey.biz
+gofakeit.Phone() // (570)245-7485
+gofakeit.BS() // front-end
+gofakeit.BeerName() // Duvel
+gofakeit.Color() // MediumOrchid
+gofakeit.Company() // Moen, Pagac and Wuckert
+gofakeit.CreditCardNumber() // 4287271570245748
+gofakeit.HackerPhrase() // Connecting the array won't do anything, we need to generate the haptic COM driver!
+gofakeit.JobTitle() // Director
+gofakeit.Password(true, true, true, true, true, 32) // WV10MzLxq2DX79w1omH97_0ga59j8!kj
+gofakeit.CurrencyShort() // USD
+// 120+ more!!!
+
+// Create structs with random injected data
+type Foo struct {
+	Bar     string
+	Baz     string
+	Int     int
+	Pointer *int
+	Skip    *string `fake:"skip"` // Set to "skip" to not generate data for
+}
+var f Foo
+gofakeit.Struct(&f)
+fmt.Printf("f.Bar:%s\n", f.Bar) // f.Bar:hrukpttuezptneuvunh
+fmt.Printf("f.Baz:%s\n", f.Baz) // f.Baz:uksqvgzadxlgghejkmv
+fmt.Printf("f.Int:%d\n", f.Int) // f.Int:-7825289004089916589
+fmt.Printf("f.Pointer:%d\n", *f.Pointer) // f.Pointer:-343806609094473732
+fmt.Printf("f.Skip:%v\n", f.Skip) // f.Skip:<nil>
+```

+ 3 - 0
vendor/github.com/brianvoe/gofakeit/TODO.txt

@@ -0,0 +1,3 @@
+* Take a look at [chance.js](http://chancejs.com/) and see if i missed anything.
+* Look into [National Baby Name List](http://www.ssa.gov/oact/babynames/limits.html) and see if that makes sense to replace over what we currently have.
+* Look at [data list](https://github.com/dariusk/corpora/tree/master/data) and see if it makes sense to add that data in or if it seems unncessary.

+ 131 - 0
vendor/github.com/brianvoe/gofakeit/address.go

@@ -0,0 +1,131 @@
+package gofakeit
+
+import (
+	"errors"
+	"math/rand"
+	"strings"
+)
+
+// AddressInfo is a struct full of address information
+type AddressInfo struct {
+	Address   string
+	Street    string
+	City      string
+	State     string
+	Zip       string
+	Country   string
+	Latitude  float64
+	Longitude float64
+}
+
+// Address will generate a struct of address information
+func Address() *AddressInfo {
+	street := Street()
+	city := City()
+	state := State()
+	zip := Zip()
+
+	return &AddressInfo{
+		Address:   street + ", " + city + ", " + state + " " + zip,
+		Street:    street,
+		City:      city,
+		State:     state,
+		Zip:       zip,
+		Country:   Country(),
+		Latitude:  Latitude(),
+		Longitude: Longitude(),
+	}
+}
+
+// Street will generate a random address street string
+func Street() (street string) {
+	switch randInt := randIntRange(1, 2); randInt {
+	case 1:
+		street = StreetNumber() + " " + StreetPrefix() + " " + StreetName() + StreetSuffix()
+	case 2:
+		street = StreetNumber() + " " + StreetName() + StreetSuffix()
+	}
+
+	return
+}
+
+// StreetNumber will generate a random address street number string
+func StreetNumber() string {
+	return strings.TrimLeft(replaceWithNumbers(getRandValue([]string{"address", "number"})), "0")
+}
+
+// StreetPrefix will generate a random address street prefix string
+func StreetPrefix() string {
+	return getRandValue([]string{"address", "street_prefix"})
+}
+
+// StreetName will generate a random address street name string
+func StreetName() string {
+	return getRandValue([]string{"address", "street_name"})
+}
+
+// StreetSuffix will generate a random address street suffix string
+func StreetSuffix() string {
+	return getRandValue([]string{"address", "street_suffix"})
+}
+
+// City will generate a random city string
+func City() (city string) {
+	switch randInt := randIntRange(1, 3); randInt {
+	case 1:
+		city = FirstName() + StreetSuffix()
+	case 2:
+		city = LastName() + StreetSuffix()
+	case 3:
+		city = StreetPrefix() + " " + LastName()
+	}
+
+	return
+}
+
+// State will generate a random state string
+func State() string {
+	return getRandValue([]string{"address", "state"})
+}
+
+// StateAbr will generate a random abbreviated state string
+func StateAbr() string {
+	return getRandValue([]string{"address", "state_abr"})
+}
+
+// Zip will generate a random Zip code string
+func Zip() string {
+	return replaceWithNumbers(getRandValue([]string{"address", "zip"}))
+}
+
+// Country will generate a random country string
+func Country() string {
+	return getRandValue([]string{"address", "country"})
+}
+
+// CountryAbr will generate a random abbreviated country string
+func CountryAbr() string {
+	return getRandValue([]string{"address", "country_abr"})
+}
+
+// Latitude will generate a random latitude float64
+func Latitude() float64 { return (rand.Float64() * 180) - 90 }
+
+// LatitudeInRange will generate a random latitude within the input range
+func LatitudeInRange(min, max float64) (float64, error) {
+	if min > max || min < -90 || min > 90 || max < -90 || max > 90 {
+		return 0, errors.New("input range is invalid")
+	}
+	return randFloat64Range(min, max), nil
+}
+
+// Longitude will generate a random longitude float64
+func Longitude() float64 { return (rand.Float64() * 360) - 180 }
+
+// LongitudeInRange will generate a random longitude within the input range
+func LongitudeInRange(min, max float64) (float64, error) {
+	if min > max || min < -180 || min > 180 || max < -180 || max > 180 {
+		return 0, errors.New("input range is invalid")
+	}
+	return randFloat64Range(min, max), nil
+}

+ 45 - 0
vendor/github.com/brianvoe/gofakeit/beer.go

@@ -0,0 +1,45 @@
+package gofakeit
+
+import "strconv"
+
+// Faker::Beer.blg #=> "18.5°Blg"
+
+// BeerName will return a random beer name
+func BeerName() string {
+	return getRandValue([]string{"beer", "name"})
+}
+
+// BeerStyle will return a random beer style
+func BeerStyle() string {
+	return getRandValue([]string{"beer", "style"})
+}
+
+// BeerHop will return a random beer hop
+func BeerHop() string {
+	return getRandValue([]string{"beer", "hop"})
+}
+
+// BeerYeast will return a random beer yeast
+func BeerYeast() string {
+	return getRandValue([]string{"beer", "yeast"})
+}
+
+// BeerMalt will return a random beer malt
+func BeerMalt() string {
+	return getRandValue([]string{"beer", "malt"})
+}
+
+// BeerIbu will return a random beer ibu value between 10 and 100
+func BeerIbu() string {
+	return strconv.Itoa(randIntRange(10, 100)) + " IBU"
+}
+
+// BeerAlcohol will return a random beer alcohol level between 2.0 and 10.0
+func BeerAlcohol() string {
+	return strconv.FormatFloat(randFloat64Range(2.0, 10.0), 'f', 1, 64) + "%"
+}
+
+// BeerBlg will return a random beer blg between 5.0 and 20.0
+func BeerBlg() string {
+	return strconv.FormatFloat(randFloat64Range(5.0, 20.0), 'f', 1, 64) + "°Blg"
+}

+ 10 - 0
vendor/github.com/brianvoe/gofakeit/bool.go

@@ -0,0 +1,10 @@
+package gofakeit
+
+// Bool will generate a random boolean value
+func Bool() bool {
+	if randIntRange(0, 1) == 1 {
+		return true
+	}
+
+	return false
+}

+ 44 - 0
vendor/github.com/brianvoe/gofakeit/color.go

@@ -0,0 +1,44 @@
+package gofakeit
+
+import "math/rand"
+
+// Color will generate a random color string
+func Color() string {
+	return getRandValue([]string{"color", "full"})
+}
+
+// SafeColor will generate a random safe color string
+func SafeColor() string {
+	return getRandValue([]string{"color", "safe"})
+}
+
+// HexColor will generate a random hexadecimal color string
+func HexColor() string {
+	color := make([]byte, 6)
+	hashQuestion := []byte("?#")
+	for i := 0; i < 6; i++ {
+		color[i] = hashQuestion[rand.Intn(2)]
+	}
+
+	return "#" + replaceWithLetters(replaceWithNumbers(string(color)))
+
+	// color := ""
+	// for i := 1; i <= 6; i++ {
+	// 	color += RandString([]string{"?", "#"})
+	// }
+
+	// // Replace # with number
+	// color = replaceWithNumbers(color)
+
+	// // Replace ? with letter
+	// for strings.Count(color, "?") > 0 {
+	// 	color = strings.Replace(color, "?", RandString(letters), 1)
+	// }
+
+	// return "#" + color
+}
+
+// RGBColor will generate a random int slice color
+func RGBColor() []int {
+	return []int{randIntRange(0, 255), randIntRange(0, 255), randIntRange(0, 255)}
+}

+ 30 - 0
vendor/github.com/brianvoe/gofakeit/company.go

@@ -0,0 +1,30 @@
+package gofakeit
+
+// Company will generate a random company name string
+func Company() (company string) {
+	switch randInt := randIntRange(1, 3); randInt {
+	case 1:
+		company = LastName() + ", " + LastName() + " and " + LastName()
+	case 2:
+		company = LastName() + "-" + LastName()
+	case 3:
+		company = LastName() + " " + CompanySuffix()
+	}
+
+	return
+}
+
+// CompanySuffix will generate a random company suffix string
+func CompanySuffix() string {
+	return getRandValue([]string{"company", "suffix"})
+}
+
+// BuzzWord will generate a random company buzz word string
+func BuzzWord() string {
+	return getRandValue([]string{"company", "buzzwords"})
+}
+
+// BS will generate a random company bs string
+func BS() string {
+	return getRandValue([]string{"company", "bs"})
+}

+ 40 - 0
vendor/github.com/brianvoe/gofakeit/contact.go

@@ -0,0 +1,40 @@
+package gofakeit
+
+import (
+	"strings"
+)
+
+// ContactInfo struct full of contact info
+type ContactInfo struct {
+	Phone string
+	Email string
+}
+
+// Contact will generate a struct with information randomly populated contact information
+func Contact() *ContactInfo {
+	return &ContactInfo{
+		Phone: Phone(),
+		Email: Email(),
+	}
+}
+
+// Phone will generate a random phone number string
+func Phone() string {
+	return replaceWithNumbers("##########")
+}
+
+// PhoneFormatted will generate a random phone number string
+func PhoneFormatted() string {
+	return replaceWithNumbers(getRandValue([]string{"contact", "phone"}))
+}
+
+// Email will generate a random email string
+func Email() string {
+	var email string
+
+	email = getRandValue([]string{"person", "first"}) + getRandValue([]string{"person", "last"})
+	email += "@"
+	email += getRandValue([]string{"person", "last"}) + "." + getRandValue([]string{"internet", "domain_suffix"})
+
+	return strings.ToLower(email)
+}

+ 38 - 0
vendor/github.com/brianvoe/gofakeit/currency.go

@@ -0,0 +1,38 @@
+package gofakeit
+
+import (
+	"math"
+	"math/rand"
+
+	"github.com/brianvoe/gofakeit/data"
+)
+
+// CurrencyInfo is a struct of currency information
+type CurrencyInfo struct {
+	Short string
+	Long  string
+}
+
+// Currency will generate a struct with random currency information
+func Currency() *CurrencyInfo {
+	index := rand.Intn(len(data.Data["currency"]["short"]))
+	return &CurrencyInfo{
+		Short: data.Data["currency"]["short"][index],
+		Long:  data.Data["currency"]["long"][index],
+	}
+}
+
+// CurrencyShort will generate a random short currency value
+func CurrencyShort() string {
+	return getRandValue([]string{"currency", "short"})
+}
+
+// CurrencyLong will generate a random long currency name
+func CurrencyLong() string {
+	return getRandValue([]string{"currency", "long"})
+}
+
+// Price will take in a min and max value and return a formatted price
+func Price(min, max float64) float64 {
+	return math.Floor(randFloat64Range(min, max)*100) / 100
+}

文件差異過大導致無法顯示
+ 6 - 0
vendor/github.com/brianvoe/gofakeit/data/address.go


+ 10 - 0
vendor/github.com/brianvoe/gofakeit/data/beer.go

@@ -0,0 +1,10 @@
+package data
+
+// Beer consists of various beer information
+var Beer = map[string][]string{
+	"name":  {"Pliny The Elder", "Founders Kentucky Breakfast", "Trappistes Rochefort 10", "HopSlam Ale", "Stone Imperial Russian Stout", "St. Bernardus Abt 12", "Founders Breakfast Stout", "Weihenstephaner Hefeweissbier", "Péché Mortel", "Celebrator Doppelbock", "Duvel", "Dreadnaught IPA", "Nugget Nectar", "La Fin Du Monde", "Bourbon County Stout", "Old Rasputin Russian Imperial Stout", "Two Hearted Ale", "Ruination IPA", "Schneider Aventinus", "Double Bastard Ale", "90 Minute IPA", "Hop Rod Rye", "Trappistes Rochefort 8", "Chimay Grande Réserve", "Stone IPA", "Arrogant Bastard Ale", "Edmund Fitzgerald Porter", "Chocolate St", "Oak Aged Yeti Imperial Stout", "Ten FIDY", "Storm King Stout", "Shakespeare Oatmeal", "Alpha King Pale Ale", "Westmalle Trappist Tripel", "Samuel Smith’s Imperial IPA", "Yeti Imperial Stout", "Hennepin", "Samuel Smith’s Oatmeal Stout", "Brooklyn Black", "Oaked Arrogant Bastard Ale", "Sublimely Self-Righteous Ale", "Trois Pistoles", "Bell’s Expedition", "Sierra Nevada Celebration Ale", "Sierra Nevada Bigfoot Barleywine Style Ale", "Racer 5 India Pale Ale, Bear Republic Bre", "Orval Trappist Ale", "Hercules Double IPA", "Maharaj", "Maudite"},
+	"hop":   {"Ahtanum", "Amarillo", "Bitter Gold", "Bravo", "Brewer’s Gold", "Bullion", "Cascade", "Cashmere", "Centennial", "Chelan", "Chinook", "Citra", "Cluster", "Columbia", "Columbus", "Comet", "Crystal", "Equinox", "Eroica", "Fuggle", "Galena", "Glacier", "Golding", "Hallertau", "Horizon", "Liberty", "Magnum", "Millennium", "Mosaic", "Mt. Hood", "Mt. Rainier", "Newport", "Northern Brewer", "Nugget", "Olympic", "Palisade", "Perle", "Saaz", "Santiam", "Simcoe", "Sorachi Ace", "Sterling", "Summit", "Tahoma", "Tettnang", "TriplePearl", "Ultra", "Vanguard", "Warrior", "Willamette", "Yakima Gol"},
+	"yeast": {"1007 - German Ale", "1010 - American Wheat", "1028 - London Ale", "1056 - American Ale", "1084 - Irish Ale", "1098 - British Ale", "1099 - Whitbread Ale", "1187 - Ringwood Ale", "1272 - American Ale II", "1275 - Thames Valley Ale", "1318 - London Ale III", "1332 - Northwest Ale", "1335 - British Ale II", "1450 - Dennys Favorite 50", "1469 - West Yorkshire Ale", "1728 - Scottish Ale", "1968 - London ESB Ale", "2565 - Kölsch", "1214 - Belgian Abbey", "1388 - Belgian Strong Ale", "1762 - Belgian Abbey II", "3056 - Bavarian Wheat Blend", "3068 - Weihenstephan Weizen", "3278 - Belgian Lambic Blend", "3333 - German Wheat", "3463 - Forbidden Fruit", "3522 - Belgian Ardennes", "3638 - Bavarian Wheat", "3711 - French Saison", "3724 - Belgian Saison", "3763 - Roeselare Ale Blend", "3787 - Trappist High Gravity", "3942 - Belgian Wheat", "3944 - Belgian Witbier", "2000 - Budvar Lager", "2001 - Urquell Lager", "2007 - Pilsen Lager", "2035 - American Lager", "2042 - Danish Lager", "2112 - California Lager", "2124 - Bohemian Lager", "2206 - Bavarian Lager", "2278 - Czech Pils", "2308 - Munich Lager", "2633 - Octoberfest Lager Blend", "5112 - Brettanomyces bruxellensis", "5335 - Lactobacillus", "5526 - Brettanomyces lambicus", "5733 - Pediococcus"},
+	"malt":  {"Black malt", "Caramel", "Carapils", "Chocolate", "Munich", "Caramel", "Carapils", "Chocolate malt", "Munich", "Pale", "Roasted barley", "Rye malt", "Special roast", "Victory", "Vienna", "Wheat mal"},
+	"style": {"Light Lager", "Pilsner", "European Amber Lager", "Dark Lager", "Bock", "Light Hybrid Beer", "Amber Hybrid Beer", "English Pale Ale", "Scottish And Irish Ale", "Merican Ale", "English Brown Ale", "Porter", "Stout", "India Pale Ale", "German Wheat And Rye Beer", "Belgian And French Ale", "Sour Ale", "Belgian Strong Ale", "Strong Ale", "Fruit Beer", "Vegetable Beer", "Smoke-flavored", "Wood-aged Beer"},
+}

+ 7 - 0
vendor/github.com/brianvoe/gofakeit/data/colors.go

@@ -0,0 +1,7 @@
+package data
+
+// Colors consists of color information
+var Colors = map[string][]string{
+	"safe": {"black", "maroon", "green", "navy", "olive", "purple", "teal", "lime", "blue", "silver", "gray", "yellow", "fuchsia", "aqua", "white"},
+	"full": {"AliceBlue", "AntiqueWhite", "Aqua", "Aquamarine", "Azure", "Beige", "Bisque", "Black", "BlanchedAlmond", "Blue", "BlueViolet", "Brown", "BurlyWood", "CadetBlue", "Chartreuse", "Chocolate", "Coral", "CornflowerBlue", "Cornsilk", "Crimson", "Cyan", "DarkBlue", "DarkCyan", "DarkGoldenRod", "DarkGray", "DarkGreen", "DarkKhaki", "DarkMagenta", "DarkOliveGreen", "Darkorange", "DarkOrchid", "DarkRed", "DarkSalmon", "DarkSeaGreen", "DarkSlateBlue", "DarkSlateGray", "DarkTurquoise", "DarkViolet", "DeepPink", "DeepSkyBlue", "DimGray", "DimGrey", "DodgerBlue", "FireBrick", "FloralWhite", "ForestGreen", "Fuchsia", "Gainsboro", "GhostWhite", "Gold", "GoldenRod", "Gray", "Green", "GreenYellow", "HoneyDew", "HotPink", "IndianRed ", "Indigo ", "Ivory", "Khaki", "Lavender", "LavenderBlush", "LawnGreen", "LemonChiffon", "LightBlue", "LightCoral", "LightCyan", "LightGoldenRodYellow", "LightGray", "LightGreen", "LightPink", "LightSalmon", "LightSeaGreen", "LightSkyBlue", "LightSlateGray", "LightSteelBlue", "LightYellow", "Lime", "LimeGreen", "Linen", "Magenta", "Maroon", "MediumAquaMarine", "MediumBlue", "MediumOrchid", "MediumPurple", "MediumSeaGreen", "MediumSlateBlue", "MediumSpringGreen", "MediumTurquoise", "MediumVioletRed", "MidnightBlue", "MintCream", "MistyRose", "Moccasin", "NavajoWhite", "Navy", "OldLace", "Olive", "OliveDrab", "Orange", "OrangeRed", "Orchid", "PaleGoldenRod", "PaleGreen", "PaleTurquoise", "PaleVioletRed", "PapayaWhip", "PeachPuff", "Peru", "Pink", "Plum", "PowderBlue", "Purple", "Red", "RosyBrown", "RoyalBlue", "SaddleBrown", "Salmon", "SandyBrown", "SeaGreen", "SeaShell", "Sienna", "Silver", "SkyBlue", "SlateBlue", "SlateGray", "Snow", "SpringGreen", "SteelBlue", "Tan", "Teal", "Thistle", "Tomato", "Turquoise", "Violet", "Wheat", "White", "WhiteSmoke", "Yellow", "YellowGreen"},
+}

文件差異過大導致無法顯示
+ 6 - 0
vendor/github.com/brianvoe/gofakeit/data/company.go


+ 8 - 0
vendor/github.com/brianvoe/gofakeit/data/computer.go

@@ -0,0 +1,8 @@
+package data
+
+// Computer consists of computer information
+var Computer = map[string][]string{
+	"linux_processor":  {"i686", "x86_64"},
+	"mac_processor":    {"Intel", "PPC", "U; Intel", "U; PPC"},
+	"windows_platform": {"Windows NT 6.2", "Windows NT 6.1", "Windows NT 6.0", "Windows NT 5.2", "Windows NT 5.1", "Windows NT 5.01", "Windows NT 5.0", "Windows NT 4.0", "Windows 98; Win 9x 4.90", "Windows 98", "Windows 95", "Windows CE"},
+}

+ 6 - 0
vendor/github.com/brianvoe/gofakeit/data/contact.go

@@ -0,0 +1,6 @@
+package data
+
+// Contact consists of contact information
+var Contact = map[string][]string{
+	"phone": {"###-###-####", "(###)###-####", "1-###-###-####", "###.###.####"},
+}

文件差異過大導致無法顯示
+ 5 - 0
vendor/github.com/brianvoe/gofakeit/data/currency.go


+ 28 - 0
vendor/github.com/brianvoe/gofakeit/data/data.go

@@ -0,0 +1,28 @@
+package data
+
+// Data consists of the main set of fake information
+var Data = map[string]map[string][]string{
+	"person":    Person,
+	"contact":   Contact,
+	"address":   Address,
+	"company":   Company,
+	"job":       Job,
+	"lorem":     Lorem,
+	"internet":  Internet,
+	"file":      Files,
+	"color":     Colors,
+	"computer":  Computer,
+	"payment":   Payment,
+	"hipster":   Hipster,
+	"beer":      Beer,
+	"hacker":    Hacker,
+	"currency":  Currency,
+	"log_level": LogLevels,
+	"timezone":  TimeZone,
+	"vehicle":   Vehicle,
+}
+
+// IntData consists of the main set of fake information (integer only)
+var IntData = map[string]map[string][]int{
+	"status_code": StatusCodes,
+}

文件差異過大導致無法顯示
+ 6 - 0
vendor/github.com/brianvoe/gofakeit/data/datetime.go


文件差異過大導致無法顯示
+ 4 - 0
vendor/github.com/brianvoe/gofakeit/data/files.go


+ 20 - 0
vendor/github.com/brianvoe/gofakeit/data/hacker.go

@@ -0,0 +1,20 @@
+package data
+
+// Hacker consists of random hacker phrases
+var Hacker = map[string][]string{
+	"abbreviation": {"TCP", "HTTP", "SDD", "RAM", "GB", "CSS", "SSL", "AGP", "SQL", "FTP", "PCI", "AI", "ADP", "RSS", "XML", "EXE", "COM", "HDD", "THX", "SMTP", "SMS", "USB", "PNG", "SAS", "IB", "SCSI", "JSON", "XSS", "JBOD"},
+	"adjective":    {"auxiliary", "primary", "back-end", "digital", "open-source", "virtual", "cross-platform", "redundant", "online", "haptic", "multi-byte", "bluetooth", "wireless", "1080p", "neural", "optical", "solid state", "mobile"},
+	"noun":         {"driver", "protocol", "bandwidth", "panel", "microchip", "program", "port", "card", "array", "interface", "system", "sensor", "firewall", "hard drive", "pixel", "alarm", "feed", "monitor", "application", "transmitter", "bus", "circuit", "capacitor", "matrix"},
+	"verb":         {"back up", "bypass", "hack", "override", "compress", "copy", "navigate", "index", "connect", "generate", "quantify", "calculate", "synthesize", "input", "transmit", "program", "reboot", "parse"},
+	"ingverb":      {"backing up", "bypassing", "hacking", "overriding", "compressing", "copying", "navigating", "indexing", "connecting", "generating", "quantifying", "calculating", "synthesizing", "transmitting", "programming", "parsing"},
+	"phrase": {
+		"If we {hacker.verb} the {hacker.noun}, we can get to the {hacker.abbreviation} {hacker.noun} through the {hacker.adjective} {hacker.abbreviation} {hacker.noun}!",
+		"We need to {hacker.verb} the {hacker.adjective} {hacker.abbreviation} {hacker.noun}!",
+		"Try to {hacker.verb} the {hacker.abbreviation} {hacker.noun}, maybe it will {hacker.verb} the {hacker.adjective} {hacker.noun}!",
+		"You can't {hacker.verb} the {hacker.noun} without {hacker.ingverb} the {hacker.adjective} {hacker.abbreviation} {hacker.noun}!",
+		"Use the {hacker.adjective} {hacker.abbreviation} {hacker.noun}, then you can {hacker.verb} the {hacker.adjective} {hacker.noun}!",
+		"The {hacker.abbreviation} {hacker.noun} is down, {hacker.verb} the {hacker.adjective} {hacker.noun} so we can {hacker.verb} the {hacker.abbreviation} {hacker.noun}!",
+		"{hacker.ingverb} the {hacker.noun} won't do anything, we need to {hacker.verb} the {hacker.adjective} {hacker.abbreviation} {hacker.noun}!",
+		"I'll {hacker.verb} the {hacker.adjective} {hacker.abbreviation} {hacker.noun}, that should {hacker.verb} the {hacker.abbreviation} {hacker.noun}!",
+	},
+}

文件差異過大導致無法顯示
+ 4 - 0
vendor/github.com/brianvoe/gofakeit/data/hipster.go


+ 8 - 0
vendor/github.com/brianvoe/gofakeit/data/internet.go

@@ -0,0 +1,8 @@
+package data
+
+// Internet consists of various internet information
+var Internet = map[string][]string{
+	"browser":       {"firefox", "chrome", "internetExplorer", "opera", "safari"},
+	"domain_suffix": {"com", "biz", "info", "name", "net", "org", "io"},
+	"http_method":   {"HEAD", "GET", "POST", "PUT", "PATCH", "DELETE"},
+}

+ 8 - 0
vendor/github.com/brianvoe/gofakeit/data/job.go

@@ -0,0 +1,8 @@
+package data
+
+// Job consists of job data
+var Job = map[string][]string{
+	"title":      {"Administrator", "Agent", "Analyst", "Architect", "Assistant", "Associate", "Consultant", "Coordinator", "Designer", "Developer", "Director", "Engineer", "Executive", "Facilitator", "Liaison", "Manager", "Officer", "Orchestrator", "Planner", "Producer", "Representative", "Specialist", "Strategist", "Supervisor", "Technician"},
+	"descriptor": {"Central", "Chief", "Corporate", "Customer", "Direct", "District", "Dynamic", "Dynamic", "Forward", "Future", "Global", "Human", "Internal", "International", "Investor", "Lead", "Legacy", "National", "Principal", "Product", "Regional", "Senior"},
+	"level":      {"Accountability", "Accounts", "Applications", "Assurance", "Brand", "Branding", "Communications", "Configuration", "Creative", "Data", "Directives", "Division", "Factors", "Functionality", "Group", "Identity", "Implementation", "Infrastructure", "Integration", "Interactions", "Intranet", "Marketing", "Markets", "Metrics", "Mobility", "Operations", "Optimization", "Paradigm", "Program", "Quality", "Research", "Response", "Security", "Solutions", "Tactics", "Usability", "Web"},
+}

+ 8 - 0
vendor/github.com/brianvoe/gofakeit/data/log_level.go

@@ -0,0 +1,8 @@
+package data
+
+// LogLevels consists of log levels for several types
+var LogLevels = map[string][]string{
+	"general": {"error", "warning", "info", "fatal", "trace", "debug"},
+	"syslog":  {"emerg", "alert", "crit", "err", "warning", "notice", "info", "debug"},
+	"apache":  {"emerg", "alert", "crit", "error", "warn", "notice", "info", "debug", "trace1-8"},
+}

文件差異過大導致無法顯示
+ 4 - 0
vendor/github.com/brianvoe/gofakeit/data/lorem.go


+ 20 - 0
vendor/github.com/brianvoe/gofakeit/data/payment.go

@@ -0,0 +1,20 @@
+package data
+
+// Payment contains payment information
+var Payment = map[string][]string{
+	"card_type": {"Visa", "MasterCard", "American Express", "Discover"},
+	"number": {
+		// Visa
+		"4###############",
+		"4###############",
+		// Mastercard
+		"222100##########",
+		"272099##########",
+		// American Express
+		"34#############",
+		"37#############",
+		// Discover
+		"65##############",
+		"65##############",
+	},
+}

文件差異過大導致無法顯示
+ 6 - 0
vendor/github.com/brianvoe/gofakeit/data/person.go


+ 7 - 0
vendor/github.com/brianvoe/gofakeit/data/status_code.go

@@ -0,0 +1,7 @@
+package data
+
+// StatusCodes consists of commonly used HTTP status codes
+var StatusCodes = map[string][]int{
+	"simple":  {200, 301, 302, 400, 404, 500},
+	"general": {100, 200, 201, 203, 204, 205, 301, 302, 304, 400, 401, 403, 404, 405, 406, 416, 500, 501, 502, 503, 504},
+}

文件差異過大導致無法顯示
+ 8 - 0
vendor/github.com/brianvoe/gofakeit/data/vehicle.go


+ 77 - 0
vendor/github.com/brianvoe/gofakeit/datetime.go

@@ -0,0 +1,77 @@
+package gofakeit
+
+import (
+	"strconv"
+	"time"
+)
+
+// Date will generate a random time.Time struct
+func Date() time.Time {
+	return time.Date(Year(), time.Month(Number(0, 12)), Day(), Hour(), Minute(), Second(), NanoSecond(), time.UTC)
+}
+
+// DateRange will generate a random time.Time struct between a start and end date
+func DateRange(start, end time.Time) time.Time {
+	return time.Unix(0, int64(Number(int(start.UnixNano()), int(end.UnixNano())))).UTC()
+}
+
+// Month will generate a random month string
+func Month() string {
+	return time.Month(Number(1, 12)).String()
+}
+
+// Day will generate a random day between 1 - 31
+func Day() int {
+	return Number(1, 31)
+}
+
+// WeekDay will generate a random weekday string (Monday-Sunday)
+func WeekDay() string {
+	return time.Weekday(Number(0, 6)).String()
+}
+
+// Year will generate a random year between 1900 - current year
+func Year() int {
+	return Number(1900, time.Now().Year())
+}
+
+// Hour will generate a random hour - in military time
+func Hour() int {
+	return Number(0, 23)
+}
+
+// Minute will generate a random minute
+func Minute() int {
+	return Number(0, 59)
+}
+
+// Second will generate a random second
+func Second() int {
+	return Number(0, 59)
+}
+
+// NanoSecond will generate a random nano second
+func NanoSecond() int {
+	return Number(0, 999999999)
+}
+
+// TimeZone will select a random timezone string
+func TimeZone() string {
+	return getRandValue([]string{"timezone", "text"})
+}
+
+// TimeZoneFull will select a random full timezone string
+func TimeZoneFull() string {
+	return getRandValue([]string{"timezone", "full"})
+}
+
+// TimeZoneAbv will select a random timezone abbreviation string
+func TimeZoneAbv() string {
+	return getRandValue([]string{"timezone", "abr"})
+}
+
+// TimeZoneOffset will select a random timezone offset
+func TimeZoneOffset() float32 {
+	value, _ := strconv.ParseFloat(getRandValue([]string{"timezone", "offset"}), 32)
+	return float32(value)
+}

+ 10 - 0
vendor/github.com/brianvoe/gofakeit/doc.go

@@ -0,0 +1,10 @@
+/*
+Package gofakeit is a random data generator written in go
+
+Every function has an example and a benchmark
+
+See the full list here https://godoc.org/github.com/brianvoe/gofakeit
+
+80+ Functions!!!
+*/
+package gofakeit

+ 15 - 0
vendor/github.com/brianvoe/gofakeit/faker.go

@@ -0,0 +1,15 @@
+package gofakeit
+
+import (
+	"math/rand"
+	"time"
+)
+
+// Seed random. Setting seed to 0 will use time.Now().UnixNano()
+func Seed(seed int64) {
+	if seed == 0 {
+		rand.Seed(time.Now().UTC().UnixNano())
+	} else {
+		rand.Seed(seed)
+	}
+}

+ 11 - 0
vendor/github.com/brianvoe/gofakeit/file.go

@@ -0,0 +1,11 @@
+package gofakeit
+
+// MimeType will generate a random mime file type
+func MimeType() string {
+	return getRandValue([]string{"file", "mime_type"})
+}
+
+// Extension will generate a random file extension
+func Extension() string {
+	return getRandValue([]string{"file", "extension"})
+}

+ 41 - 0
vendor/github.com/brianvoe/gofakeit/generate.go

@@ -0,0 +1,41 @@
+package gofakeit
+
+import (
+	"strings"
+)
+
+// Generate fake information from given string. String should contain {category.subcategory}
+//
+// Ex: {person.first} - random firstname
+//
+// Ex: {person.first}###{person.last}@{person.last}.{internet.domain_suffix} - billy834smith@smith.com
+//
+// Ex: ### - 481 - random numbers
+//
+// Ex: ??? - fda - random letters
+//
+// For a complete list possible categories use the Categories() function.
+func Generate(dataVal string) string {
+	// Identify items between brackets: {person.first}
+	for strings.Count(dataVal, "{") > 0 && strings.Count(dataVal, "}") > 0 {
+		catValue := ""
+		startIndex := strings.Index(dataVal, "{")
+		endIndex := strings.Index(dataVal, "}")
+		replace := dataVal[(startIndex + 1):endIndex]
+		categories := strings.Split(replace, ".")
+
+		if len(categories) >= 2 && dataCheck([]string{categories[0], categories[1]}) {
+			catValue = getRandValue([]string{categories[0], categories[1]})
+		}
+
+		dataVal = strings.Replace(dataVal, "{"+replace+"}", catValue, 1)
+	}
+
+	// Replace # with numbers
+	dataVal = replaceWithNumbers(dataVal)
+
+	// Replace ? with letters
+	dataVal = replaceWithLetters(dataVal)
+
+	return dataVal
+}

+ 35 - 0
vendor/github.com/brianvoe/gofakeit/hacker.go

@@ -0,0 +1,35 @@
+package gofakeit
+
+import "strings"
+
+// HackerPhrase will return a random hacker sentence
+func HackerPhrase() string {
+	words := strings.Split(Generate(getRandValue([]string{"hacker", "phrase"})), " ")
+	words[0] = strings.Title(words[0])
+	return strings.Join(words, " ")
+}
+
+// HackerAbbreviation will return a random hacker abbreviation
+func HackerAbbreviation() string {
+	return getRandValue([]string{"hacker", "abbreviation"})
+}
+
+// HackerAdjective will return a random hacker adjective
+func HackerAdjective() string {
+	return getRandValue([]string{"hacker", "adjective"})
+}
+
+// HackerNoun will return a random hacker noun
+func HackerNoun() string {
+	return getRandValue([]string{"hacker", "noun"})
+}
+
+// HackerVerb will return a random hacker verb
+func HackerVerb() string {
+	return getRandValue([]string{"hacker", "verb"})
+}
+
+// HackerIngverb will return a random hacker ingverb
+func HackerIngverb() string {
+	return getRandValue([]string{"hacker", "ingverb"})
+}

+ 20 - 0
vendor/github.com/brianvoe/gofakeit/hipster.go

@@ -0,0 +1,20 @@
+package gofakeit
+
+// HipsterWord will return a single hipster word
+func HipsterWord() string {
+	return getRandValue([]string{"hipster", "word"})
+}
+
+// HipsterSentence will generate a random sentence
+func HipsterSentence(wordCount int) string {
+	return sentence(wordCount, HipsterWord)
+}
+
+// HipsterParagraph will generate a random paragraphGenerator
+// Set Paragraph Count
+// Set Sentence Count
+// Set Word Count
+// Set Paragraph Separator
+func HipsterParagraph(paragraphCount int, sentenceCount int, wordCount int, separator string) string {
+	return paragraphGenerator(paragrapOptions{paragraphCount, sentenceCount, wordCount, separator}, HipsterSentence)
+}

+ 8 - 0
vendor/github.com/brianvoe/gofakeit/image.go

@@ -0,0 +1,8 @@
+package gofakeit
+
+import "strconv"
+
+// ImageURL will generate a random Image Based Upon Height And Width. https://picsum.photos/
+func ImageURL(width int, height int) string {
+	return "https://picsum.photos/" + strconv.Itoa(width) + "/" + strconv.Itoa(height)
+}

+ 55 - 0
vendor/github.com/brianvoe/gofakeit/internet.go

@@ -0,0 +1,55 @@
+package gofakeit
+
+import (
+	"fmt"
+	"math/rand"
+	"strings"
+)
+
+// DomainName will generate a random url domain name
+func DomainName() string {
+	return strings.ToLower(JobDescriptor()+BS()) + "." + DomainSuffix()
+}
+
+// DomainSuffix will generate a random domain suffix
+func DomainSuffix() string {
+	return getRandValue([]string{"internet", "domain_suffix"})
+}
+
+// URL will generate a random url string
+func URL() string {
+	url := "http" + RandString([]string{"s", ""}) + "://www."
+	url += DomainName()
+
+	// Slugs
+	num := Number(1, 4)
+	slug := make([]string, num)
+	for i := 0; i < num; i++ {
+		slug[i] = BS()
+	}
+	url += "/" + strings.ToLower(strings.Join(slug, "/"))
+
+	return url
+}
+
+// HTTPMethod will generate a random http method
+func HTTPMethod() string {
+	return getRandValue([]string{"internet", "http_method"})
+}
+
+// IPv4Address will generate a random version 4 ip address
+func IPv4Address() string {
+	num := func() int { return 2 + rand.Intn(254) }
+	return fmt.Sprintf("%d.%d.%d.%d", num(), num(), num(), num())
+}
+
+// IPv6Address will generate a random version 6 ip address
+func IPv6Address() string {
+	num := 65536
+	return fmt.Sprintf("2001:cafe:%x:%x:%x:%x:%x:%x", rand.Intn(num), rand.Intn(num), rand.Intn(num), rand.Intn(num), rand.Intn(num), rand.Intn(num))
+}
+
+// Username will genrate a random username based upon picking a random lastname and random numbers at the end
+func Username() string {
+	return getRandValue([]string{"person", "last"}) + replaceWithNumbers("####")
+}

+ 34 - 0
vendor/github.com/brianvoe/gofakeit/job.go

@@ -0,0 +1,34 @@
+package gofakeit
+
+// JobInfo is a struct of job information
+type JobInfo struct {
+	Company    string
+	Title      string
+	Descriptor string
+	Level      string
+}
+
+// Job will generate a struct with random job information
+func Job() *JobInfo {
+	return &JobInfo{
+		Company:    Company(),
+		Title:      JobTitle(),
+		Descriptor: JobDescriptor(),
+		Level:      JobLevel(),
+	}
+}
+
+// JobTitle will generate a random job title string
+func JobTitle() string {
+	return getRandValue([]string{"job", "title"})
+}
+
+// JobDescriptor will generate a random job descriptor string
+func JobDescriptor() string {
+	return getRandValue([]string{"job", "descriptor"})
+}
+
+// JobLevel will generate a random job level string
+func JobLevel() string {
+	return getRandValue([]string{"job", "level"})
+}

+ 15 - 0
vendor/github.com/brianvoe/gofakeit/log_level.go

@@ -0,0 +1,15 @@
+package gofakeit
+
+import (
+	"github.com/brianvoe/gofakeit/data"
+)
+
+// LogLevel will generate a random log level
+// See data/LogLevels for list of available levels
+func LogLevel(logType string) string {
+	if _, ok := data.LogLevels[logType]; ok {
+		return getRandValue([]string{"log_level", logType})
+	}
+
+	return getRandValue([]string{"log_level", "general"})
+}

二進制
vendor/github.com/brianvoe/gofakeit/logo.png


+ 132 - 0
vendor/github.com/brianvoe/gofakeit/misc.go

@@ -0,0 +1,132 @@
+package gofakeit
+
+import (
+	"math/rand"
+
+	"github.com/brianvoe/gofakeit/data"
+)
+
+const hashtag = '#'
+const questionmark = '?'
+
+// Check if in lib
+func dataCheck(dataVal []string) bool {
+	var checkOk bool
+
+	if len(dataVal) == 2 {
+		_, checkOk = data.Data[dataVal[0]]
+		if checkOk {
+			_, checkOk = data.Data[dataVal[0]][dataVal[1]]
+		}
+	}
+
+	return checkOk
+}
+
+// Check if in lib
+func intDataCheck(dataVal []string) bool {
+	if len(dataVal) != 2 {
+		return false
+	}
+
+	_, checkOk := data.IntData[dataVal[0]]
+	if checkOk {
+		_, checkOk = data.IntData[dataVal[0]][dataVal[1]]
+	}
+
+	return checkOk
+}
+
+// Get Random Value
+func getRandValue(dataVal []string) string {
+	if !dataCheck(dataVal) {
+		return ""
+	}
+	return data.Data[dataVal[0]][dataVal[1]][rand.Intn(len(data.Data[dataVal[0]][dataVal[1]]))]
+}
+
+// Get Random Integer Value
+func getRandIntValue(dataVal []string) int {
+	if !intDataCheck(dataVal) {
+		return 0
+	}
+	return data.IntData[dataVal[0]][dataVal[1]][rand.Intn(len(data.IntData[dataVal[0]][dataVal[1]]))]
+}
+
+// Replace # with numbers
+func replaceWithNumbers(str string) string {
+	if str == "" {
+		return str
+	}
+	bytestr := []byte(str)
+	for i := 0; i < len(bytestr); i++ {
+		if bytestr[i] == hashtag {
+			bytestr[i] = byte(randDigit())
+		}
+	}
+	if bytestr[0] == '0' {
+		bytestr[0] = byte(rand.Intn(8)+1) + '0'
+	}
+
+	return string(bytestr)
+}
+
+// Replace ? with ASCII lowercase letters
+func replaceWithLetters(str string) string {
+	if str == "" {
+		return str
+	}
+	bytestr := []byte(str)
+	for i := 0; i < len(bytestr); i++ {
+		if bytestr[i] == questionmark {
+			bytestr[i] = byte(randLetter())
+		}
+	}
+
+	return string(bytestr)
+}
+
+// Generate random lowercase ASCII letter
+func randLetter() rune {
+	return rune(byte(rand.Intn(26)) + 'a')
+}
+
+// Generate random ASCII digit
+func randDigit() rune {
+	return rune(byte(rand.Intn(10)) + '0')
+}
+
+// Generate random integer between min and max
+func randIntRange(min, max int) int {
+	if min == max {
+		return min
+	}
+	return rand.Intn((max+1)-min) + min
+}
+
+func randFloat32Range(min, max float32) float32 {
+	if min == max {
+		return min
+	}
+	return rand.Float32()*(max-min) + min
+}
+
+func randFloat64Range(min, max float64) float64 {
+	if min == max {
+		return min
+	}
+	return rand.Float64()*(max-min) + min
+}
+
+// Categories will return a map string array of available data categories and sub categories
+func Categories() map[string][]string {
+	types := make(map[string][]string)
+	for category, subCategoriesMap := range data.Data {
+		subCategories := make([]string, 0)
+		for subType := range subCategoriesMap {
+			subCategories = append(subCategories, subType)
+		}
+		types[category] = subCategories
+	}
+	return types
+}

+ 26 - 0
vendor/github.com/brianvoe/gofakeit/name.go

@@ -0,0 +1,26 @@
+package gofakeit
+
+// Name will generate a random First and Last Name
+func Name() string {
+	return getRandValue([]string{"person", "first"}) + " " + getRandValue([]string{"person", "last"})
+}
+
+// FirstName will generate a random first name
+func FirstName() string {
+	return getRandValue([]string{"person", "first"})
+}
+
+// LastName will generate a random last name
+func LastName() string {
+	return getRandValue([]string{"person", "last"})
+}
+
+// NamePrefix will generate a random name prefix
+func NamePrefix() string {
+	return getRandValue([]string{"person", "prefix"})
+}
+
+// NameSuffix will generate a random name suffix
+func NameSuffix() string {
+	return getRandValue([]string{"person", "suffix"})
+}

+ 84 - 0
vendor/github.com/brianvoe/gofakeit/number.go

@@ -0,0 +1,84 @@
+package gofakeit
+
+import (
+	"math"
+	"math/rand"
+)
+
+// Number will generate a random number between given min And max
+func Number(min int, max int) int {
+	return randIntRange(min, max)
+}
+
+// Uint8 will generate a random uint8 value
+func Uint8() uint8 {
+	return uint8(randIntRange(0, math.MaxUint8))
+}
+
+// Uint16 will generate a random uint16 value
+func Uint16() uint16 {
+	return uint16(randIntRange(0, math.MaxUint16))
+}
+
+// Uint32 will generate a random uint32 value
+func Uint32() uint32 {
+	return uint32(randIntRange(0, math.MaxInt32))
+}
+
+// Uint64 will generate a random uint64 value
+func Uint64() uint64 {
+	return uint64(rand.Int63n(math.MaxInt64))
+}
+
+// Int8 will generate a random Int8 value
+func Int8() int8 {
+	return int8(randIntRange(math.MinInt8, math.MaxInt8))
+}
+
+// Int16 will generate a random int16 value
+func Int16() int16 {
+	return int16(randIntRange(math.MinInt16, math.MaxInt16))
+}
+
+// Int32 will generate a random int32 value
+func Int32() int32 {
+	return int32(randIntRange(math.MinInt32, math.MaxInt32))
+}
+
+// Int64 will generate a random int64 value
+func Int64() int64 {
+	return rand.Int63n(math.MaxInt64) + math.MinInt64
+}
+
+// Float32 will generate a random float32 value
+func Float32() float32 {
+	return randFloat32Range(math.SmallestNonzeroFloat32, math.MaxFloat32)
+}
+
+// Float32Range will generate a random float32 value between min and max
+func Float32Range(min, max float32) float32 {
+	return randFloat32Range(min, max)
+}
+
+// Float64 will generate a random float64 value
+func Float64() float64 {
+	return randFloat64Range(math.SmallestNonzeroFloat64, math.MaxFloat64)
+}
+
+// Float64Range will generate a random float64 value between min and max
+func Float64Range(min, max float64) float64 {
+	return randFloat64Range(min, max)
+}
+
+// Numerify will replace # with random numerical values
+func Numerify(str string) string {
+	return replaceWithNumbers(str)
+}
+
+// ShuffleInts will randomize a slice of ints
+func ShuffleInts(a []int) {
+	for i := range a {
+		j := rand.Intn(i + 1)
+		a[i], a[j] = a[j], a[i]
+	}
+}

+ 68 - 0
vendor/github.com/brianvoe/gofakeit/password.go

@@ -0,0 +1,68 @@
+package gofakeit
+
+import (
+	"math/rand"
+)
+
+const lowerStr = "abcdefghijklmnopqrstuvwxyz"
+const upperStr = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
+const numericStr = "0123456789"
+const specialStr = "!@#$%&*+-=?"
+const spaceStr = "   "
+
+// Password will generate a random password
+// Minimum number length of 5 if less than
+func Password(lower bool, upper bool, numeric bool, special bool, space bool, num int) string {
+	// Make sure the num minimun is at least 5
+	if num < 5 {
+		num = 5
+	}
+	i := 0
+	b := make([]byte, num)
+	var passString string
+
+	if lower {
+		passString += lowerStr
+		b[i] = lowerStr[rand.Int63()%int64(len(lowerStr))]
+		i++
+	}
+	if upper {
+		passString += upperStr
+		b[i] = upperStr[rand.Int63()%int64(len(upperStr))]
+		i++
+	}
+	if numeric {
+		passString += numericStr
+		b[i] = numericStr[rand.Int63()%int64(len(numericStr))]
+		i++
+	}
+	if special {
+		passString += specialStr
+		b[i] = specialStr[rand.Int63()%int64(len(specialStr))]
+		i++
+	}
+	if space {
+		passString += spaceStr
+		b[i] = spaceStr[rand.Int63()%int64(len(spaceStr))]
+		i++
+	}
+
+	// Set default if empty
+	if passString == "" {
+		passString = lowerStr + numericStr
+	}
+
+	// Loop through and add it up
+	for i <= num-1 {
+		b[i] = passString[rand.Int63()%int64(len(passString))]
+		i++
+	}
+
+	// Shuffle bytes
+	for i := range b {
+		j := rand.Intn(i + 1)
+		b[i], b[j] = b[j], b[i]
+	}
+
+	return string(b)
+}

+ 81 - 0
vendor/github.com/brianvoe/gofakeit/payment.go

@@ -0,0 +1,81 @@
+package gofakeit
+
+import "strconv"
+import "time"
+
+var currentYear = time.Now().Year() - 2000
+
+// CreditCardInfo is a struct containing credit variables
+type CreditCardInfo struct {
+	Type   string
+	Number int
+	Exp    string
+	Cvv    string
+}
+
+// CreditCard will generate a struct full of credit card information
+func CreditCard() *CreditCardInfo {
+	return &CreditCardInfo{
+		Type:   CreditCardType(),
+		Number: CreditCardNumber(),
+		Exp:    CreditCardExp(),
+		Cvv:    CreditCardCvv(),
+	}
+}
+
+// CreditCardType will generate a random credit card type string
+func CreditCardType() string {
+	return getRandValue([]string{"payment", "card_type"})
+}
+
+// CreditCardNumber will generate a random credit card number int
+func CreditCardNumber() int {
+	integer, _ := strconv.Atoi(replaceWithNumbers(getRandValue([]string{"payment", "number"})))
+	return integer
+}
+
+// CreditCardNumberLuhn will generate a random credit card number int that passes luhn test
+func CreditCardNumberLuhn() int {
+	cc := ""
+	for i := 0; i < 100000; i++ {
+		cc = replaceWithNumbers(getRandValue([]string{"payment", "number"}))
+		if luhn(cc) {
+			break
+		}
+	}
+	integer, _ := strconv.Atoi(cc)
+	return integer
+}
+
+// CreditCardExp will generate a random credit card expiration date string
+// Exp date will always be a future date
+func CreditCardExp() string {
+	month := strconv.Itoa(randIntRange(1, 12))
+	if len(month) == 1 {
+		month = "0" + month
+	}
+	return month + "/" + strconv.Itoa(randIntRange(currentYear+1, currentYear+10))
+}
+
+// CreditCardCvv will generate a random CVV number - Its a string because you could have 017 as an exp date
+func CreditCardCvv() string {
+	return Numerify("###")
+}
+
+// luhn check is used for checking if credit card is valid
+func luhn(s string) bool {
+	var t = [...]int{0, 2, 4, 6, 8, 1, 3, 5, 7, 9}
+	odd := len(s) & 1
+	var sum int
+	for i, c := range s {
+		if c < '0' || c > '9' {
+			return false
+		}
+		if i&1 == odd {
+			sum += t[c-'0']
+		} else {
+			sum += int(c - '0')
+		}
+	}
+	return sum%10 == 0
+}

+ 45 - 0
vendor/github.com/brianvoe/gofakeit/person.go

@@ -0,0 +1,45 @@
+package gofakeit
+
+import "strconv"
+
+// SSN will generate a random Social Security Number
+func SSN() string {
+	return strconv.Itoa(randIntRange(100000000, 999999999))
+}
+
+// Gender will generate a random gender string
+func Gender() string {
+	if Bool() == true {
+		return "male"
+	}
+
+	return "female"
+}
+
+// PersonInfo is a struct of person information
+type PersonInfo struct {
+	FirstName  string
+	LastName   string
+	Gender     string
+	SSN        string
+	Image      string
+	Job        *JobInfo
+	Address    *AddressInfo
+	Contact    *ContactInfo
+	CreditCard *CreditCardInfo
+}
+
+// Person will generate a struct with person information
+func Person() *PersonInfo {
+	return &PersonInfo{
+		FirstName:  FirstName(),
+		LastName:   LastName(),
+		Gender:     Gender(),
+		SSN:        SSN(),
+		Image:      ImageURL(300, 300) + "/people",
+		Job:        Job(),
+		Address:    Address(),
+		Contact:    Contact(),
+		CreditCard: CreditCard(),
+	}
+}

+ 11 - 0
vendor/github.com/brianvoe/gofakeit/status_code.go

@@ -0,0 +1,11 @@
+package gofakeit
+
+// SimpleStatusCode will generate a random simple status code
+func SimpleStatusCode() int {
+	return getRandIntValue([]string{"status_code", "simple"})
+}
+
+// StatusCode will generate a random status code
+func StatusCode() int {
+	return getRandIntValue([]string{"status_code", "general"})
+}

+ 48 - 0
vendor/github.com/brianvoe/gofakeit/string.go

@@ -0,0 +1,48 @@
+package gofakeit
+
+import (
+	"math/rand"
+)
+
+// Letter will generate a single random lower case ASCII letter
+func Letter() string {
+	return string(randLetter())
+}
+
+// Digit will generate a single ASCII digit
+func Digit() string {
+	return string(randDigit())
+}
+
+// Lexify will replace ? will random generated letters
+func Lexify(str string) string {
+	return replaceWithLetters(str)
+}
+
+// ShuffleStrings will randomize a slice of strings
+func ShuffleStrings(a []string) {
+	swap := func(i, j int) {
+		a[i], a[j] = a[j], a[i]
+	}
+	//to avoid upgrading to 1.10 I copied the algorithm
+	n := len(a)
+	if n <= 1 {
+		return
+	}
+
+	//if size is > int32 probably it will never finish, or ran out of entropy
+	i := n - 1
+	for ; i > 0; i-- {
+		j := int(rand.Int31n(int32(i + 1)))
+		swap(i, j)
+	}
+}
+
+// RandString will take in a slice of string and return a randomly selected value
+func RandString(a []string) string {
+	size := len(a)
+	if size == 0 {
+		return ""
+	}
+	return a[rand.Intn(size)]
+}

+ 87 - 0
vendor/github.com/brianvoe/gofakeit/struct.go

@@ -0,0 +1,87 @@
+package gofakeit
+
+import (
+	"reflect"
+)
+
+// Struct fills in exported elements of a struct with random data
+// based on the value of `fake` tag of exported elements.
+// Use `fake:"skip"` to explicitly skip an element.
+// All built-in types are supported, with templating support
+// for string types.
+func Struct(v interface{}) {
+	r(reflect.TypeOf(v), reflect.ValueOf(v), "")
+}
+
+func r(t reflect.Type, v reflect.Value, template string) {
+	switch t.Kind() {
+	case reflect.Ptr:
+		rPointer(t, v, template)
+	case reflect.Struct:
+		rStruct(t, v)
+	case reflect.String:
+		rString(template, v)
+	case reflect.Uint8:
+		v.SetUint(uint64(Uint8()))
+	case reflect.Uint16:
+		v.SetUint(uint64(Uint16()))
+	case reflect.Uint32:
+		v.SetUint(uint64(Uint32()))
+	case reflect.Uint64:
+		//capped at [0, math.MaxInt64)
+		v.SetUint(uint64(Uint64()))
+	case reflect.Int:
+		v.SetInt(int64(Int64()))
+	case reflect.Int8:
+		v.SetInt(int64(Int8()))
+	case reflect.Int16:
+		v.SetInt(int64(Int16()))
+	case reflect.Int32:
+		v.SetInt(int64(Int32()))
+	case reflect.Int64:
+		v.SetInt(int64(Int64()))
+	case reflect.Float64:
+		v.SetFloat(Float64())
+	case reflect.Float32:
+		v.SetFloat(float64(Float32()))
+	case reflect.Bool:
+		v.SetBool(Bool())
+	}
+}
+
+func rString(template string, v reflect.Value) {
+	if template != "" {
+		r := Generate(template)
+		v.SetString(r)
+	} else {
+		v.SetString(Generate("???????????????????"))
+		// we don't have a String(len int) string function!!
+	}
+}
+
+func rStruct(t reflect.Type, v reflect.Value) {
+	n := t.NumField()
+	for i := 0; i < n; i++ {
+		elementT := t.Field(i)
+		elementV := v.Field(i)
+		fake := true
+		t, ok := elementT.Tag.Lookup("fake")
+		if ok && t == "skip" {
+			fake = false
+		}
+		if fake && elementV.CanSet() {
+			r(elementT.Type, elementV, t)
+		}
+	}
+}
+
+func rPointer(t reflect.Type, v reflect.Value, template string) {
+	elemT := t.Elem()
+	if v.IsNil() {
+		nv := reflect.New(elemT)
+		r(elemT, nv.Elem(), template)
+		v.Set(nv)
+	} else {
+		r(elemT, v.Elem(), template)
+	}
+}

+ 34 - 0
vendor/github.com/brianvoe/gofakeit/unique.go

@@ -0,0 +1,34 @@
+package gofakeit
+
+import (
+	"encoding/hex"
+	"math/rand"
+)
+
+// UUID (version 4) will generate a random unique identifier based upon random nunbers
+// Format: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
+func UUID() string {
+	version := byte(4)
+	uuid := make([]byte, 16)
+	rand.Read(uuid)
+
+	// Set version
+	uuid[6] = (uuid[6] & 0x0f) | (version << 4)
+
+	// Set variant
+	uuid[8] = (uuid[8] & 0xbf) | 0x80
+
+	buf := make([]byte, 36)
+	var dash byte = '-'
+	hex.Encode(buf[0:8], uuid[0:4])
+	buf[8] = dash
+	hex.Encode(buf[9:13], uuid[4:6])
+	buf[13] = dash
+	hex.Encode(buf[14:18], uuid[6:8])
+	buf[18] = dash
+	hex.Encode(buf[19:23], uuid[8:10])
+	buf[23] = dash
+	hex.Encode(buf[24:], uuid[10:])
+
+	return string(buf)
+}

+ 92 - 0
vendor/github.com/brianvoe/gofakeit/user_agent.go

@@ -0,0 +1,92 @@
+package gofakeit
+
+import "strconv"
+
+// UserAgent will generate a random broswer user agent
+func UserAgent() string {
+	randNum := randIntRange(0, 4)
+	switch randNum {
+	case 0:
+		return ChromeUserAgent()
+	case 1:
+		return FirefoxUserAgent()
+	case 2:
+		return SafariUserAgent()
+	case 3:
+		return OperaUserAgent()
+	default:
+		return ChromeUserAgent()
+	}
+}
+
+// ChromeUserAgent will generate a random chrome browser user agent string
+func ChromeUserAgent() string {
+	randNum1 := strconv.Itoa(randIntRange(531, 536)) + strconv.Itoa(randIntRange(0, 2))
+	randNum2 := strconv.Itoa(randIntRange(36, 40))
+	randNum3 := strconv.Itoa(randIntRange(800, 899))
+	return "Mozilla/5.0 " + "(" + randomPlatform() + ") AppleWebKit/" + randNum1 + " (KHTML, like Gecko) Chrome/" + randNum2 + ".0." + randNum3 + ".0 Mobile Safari/" + randNum1
+}
+
+// FirefoxUserAgent will generate a random firefox broswer user agent string
+func FirefoxUserAgent() string {
+	ver := "Gecko/" + Date().Format("2006-02-01") + " Firefox/" + strconv.Itoa(randIntRange(35, 37)) + ".0"
+	platforms := []string{
+		"(" + windowsPlatformToken() + "; " + "en-US" + "; rv:1.9." + strconv.Itoa(randIntRange(0, 3)) + ".20) " + ver,
+		"(" + linuxPlatformToken() + "; rv:" + strconv.Itoa(randIntRange(5, 8)) + ".0) " + ver,
+		"(" + macPlatformToken() + " rv:" + strconv.Itoa(randIntRange(2, 7)) + ".0) " + ver,
+	}
+
+	return "Mozilla/5.0 " + RandString(platforms)
+}
+
+// SafariUserAgent will generate a random safari browser user agent string
+func SafariUserAgent() string {
+	randNum := strconv.Itoa(randIntRange(531, 536)) + "." + strconv.Itoa(randIntRange(1, 51)) + "." + strconv.Itoa(randIntRange(1, 8))
+	ver := strconv.Itoa(randIntRange(4, 6)) + "." + strconv.Itoa(randIntRange(0, 2))
+
+	mobileDevices := []string{
+		"iPhone; CPU iPhone OS",
+		"iPad; CPU OS",
+	}
+
+	platforms := []string{
+		"(Windows; U; " + windowsPlatformToken() + ") AppleWebKit/" + randNum + " (KHTML, like Gecko) Version/" + ver + " Safari/" + randNum,
+		"(" + macPlatformToken() + " rv:" + strconv.Itoa(randIntRange(4, 7)) + ".0; en-US) AppleWebKit/" + randNum + " (KHTML, like Gecko) Version/" + ver + " Safari/" + randNum,
+		"(" + RandString(mobileDevices) + " " + strconv.Itoa(randIntRange(7, 9)) + "_" + strconv.Itoa(randIntRange(0, 3)) + "_" + strconv.Itoa(randIntRange(1, 3)) + " like Mac OS X; " + "en-US" + ") AppleWebKit/" + randNum + " (KHTML, like Gecko) Version/" + strconv.Itoa(randIntRange(3, 5)) + ".0.5 Mobile/8B" + strconv.Itoa(randIntRange(111, 120)) + " Safari/6" + randNum,
+	}
+
+	return "Mozilla/5.0 " + RandString(platforms)
+}
+
+// OperaUserAgent will generate a random opera browser user agent string
+func OperaUserAgent() string {
+	platform := "(" + randomPlatform() + "; en-US) Presto/2." + strconv.Itoa(randIntRange(8, 13)) + "." + strconv.Itoa(randIntRange(160, 355)) + " Version/" + strconv.Itoa(randIntRange(10, 13)) + ".00"
+
+	return "Opera/" + strconv.Itoa(randIntRange(8, 10)) + "." + strconv.Itoa(randIntRange(10, 99)) + " " + platform
+}
+
+// linuxPlatformToken will generate a random linux platform
+func linuxPlatformToken() string {
+	return "X11; Linux " + getRandValue([]string{"computer", "linux_processor"})
+}
+
+// macPlatformToken will generate a random mac platform
+func macPlatformToken() string {
+	return "Macintosh; " + getRandValue([]string{"computer", "mac_processor"}) + " Mac OS X 10_" + strconv.Itoa(randIntRange(5, 9)) + "_" + strconv.Itoa(randIntRange(0, 10))
+}
+
+// windowsPlatformToken will generate a random windows platform
+func windowsPlatformToken() string {
+	return getRandValue([]string{"computer", "windows_platform"})
+}
+
+// randomPlatform will generate a random platform
+func randomPlatform() string {
+	platforms := []string{
+		linuxPlatformToken(),
+		macPlatformToken(),
+		windowsPlatformToken(),
+	}
+
+	return RandString(platforms)
+}

+ 55 - 0
vendor/github.com/brianvoe/gofakeit/vehicle.go

@@ -0,0 +1,55 @@
+package gofakeit
+
+// VehicleInfo is a struct dataset of all vehicle information
+type VehicleInfo struct {
+	// Vehicle type
+	VehicleType string
+	// Fuel type
+	Fuel string
+	// Transmission type
+	TransmissionGear string
+	// Brand name
+	Brand string
+	// Vehicle model
+	Model string
+	// Vehicle model year
+	Year int
+}
+
+// Vehicle will generate a struct with vehicle information
+func Vehicle() *VehicleInfo {
+	return &VehicleInfo{
+		VehicleType:      VehicleType(),
+		Fuel:             FuelType(),
+		TransmissionGear: TransmissionGearType(),
+		Brand:            CarMaker(),
+		Model:            CarModel(),
+		Year:             Year(),
+	}
+
+}
+
+// VehicleType will generate a random vehicle type string
+func VehicleType() string {
+	return getRandValue([]string{"vehicle", "vehicle_type"})
+}
+
+// FuelType will return a random fuel type
+func FuelType() string {
+	return getRandValue([]string{"vehicle", "fuel_type"})
+}
+
+// TransmissionGearType will return a random transmission gear type
+func TransmissionGearType() string {
+	return getRandValue([]string{"vehicle", "transmission_type"})
+}
+
+// CarMaker will return a random car maker
+func CarMaker() string {
+	return getRandValue([]string{"vehicle", "maker"})
+}
+
+// CarModel will return a random car model
+func CarModel() string {
+	return getRandValue([]string{"vehicle", "model"})
+}

+ 100 - 0
vendor/github.com/brianvoe/gofakeit/words.go

@@ -0,0 +1,100 @@
+package gofakeit
+
+import (
+	"bytes"
+	"strings"
+	"unicode"
+)
+
+type paragrapOptions struct {
+	paragraphCount int
+	sentenceCount  int
+	wordCount      int
+	separator      string
+}
+
+const bytesPerWordEstimation = 6
+
+type sentenceGenerator func(wordCount int) string
+type wordGenerator func() string
+
+// Word will generate a random word
+func Word() string {
+	return getRandValue([]string{"lorem", "word"})
+}
+
+// Sentence will generate a random sentence
+func Sentence(wordCount int) string {
+	return sentence(wordCount, Word)
+}
+
+// Paragraph will generate a random paragraphGenerator
+// Set Paragraph Count
+// Set Sentence Count
+// Set Word Count
+// Set Paragraph Separator
+func Paragraph(paragraphCount int, sentenceCount int, wordCount int, separator string) string {
+	return paragraphGenerator(paragrapOptions{paragraphCount, sentenceCount, wordCount, separator}, Sentence)
+}
+
+func sentence(wordCount int, word wordGenerator) string {
+	if wordCount <= 0 {
+		return ""
+	}
+
+	wordSeparator := ' '
+	sentence := bytes.Buffer{}
+	sentence.Grow(wordCount * bytesPerWordEstimation)
+
+	for i := 0; i < wordCount; i++ {
+		word := word()
+		if i == 0 {
+			runes := []rune(word)
+			runes[0] = unicode.ToTitle(runes[0])
+			word = string(runes)
+		}
+		sentence.WriteString(word)
+		if i < wordCount-1 {
+			sentence.WriteRune(wordSeparator)
+		}
+	}
+	sentence.WriteRune('.')
+	return sentence.String()
+}
+
+func paragraphGenerator(opts paragrapOptions, sentecer sentenceGenerator) string {
+	if opts.paragraphCount <= 0 || opts.sentenceCount <= 0 || opts.wordCount <= 0 {
+		return ""
+	}
+
+	//to avoid making Go 1.10 dependency, we cannot use strings.Builder
+	paragraphs := bytes.Buffer{}
+	//we presume the length
+	paragraphs.Grow(opts.paragraphCount * opts.sentenceCount * opts.wordCount * bytesPerWordEstimation)
+	wordSeparator := ' '
+
+	for i := 0; i < opts.paragraphCount; i++ {
+		for e := 0; e < opts.sentenceCount; e++ {
+			paragraphs.WriteString(sentecer(opts.wordCount))
+			if e < opts.sentenceCount-1 {
+				paragraphs.WriteRune(wordSeparator)
+			}
+		}
+
+		if i < opts.paragraphCount-1 {
+			paragraphs.WriteString(opts.separator)
+		}
+	}
+
+	return paragraphs.String()
+}
+
+// Question will return a random question
+func Question() string {
+	return strings.Replace(HipsterSentence(Number(3, 10)), ".", "?", 1)
+}
+
+// Quote will return a random quote from a random person
+func Quote() string {
+	return `"` + HipsterSentence(Number(3, 10)) + `" - ` + FirstName() + " " + LastName()
+}

+ 1 - 1
vendor/github.com/robfig/cron/README.md

@@ -1,4 +1,4 @@
-[![GoDoc](http://godoc.org/github.com/robfig/cron?status.png)](http://godoc.org/github.com/robfig/cron) 
+[![GoDoc](http://godoc.org/github.com/robfig/cron?status.png)](http://godoc.org/github.com/robfig/cron)
 [![Build Status](https://travis-ci.org/robfig/cron.svg?branch=master)](https://travis-ci.org/robfig/cron)
 
 # cron

+ 1 - 1
vendor/github.com/robfig/cron/doc.go

@@ -84,7 +84,7 @@ You may use one of several pre-defined schedules in place of a cron expression.
 
 Intervals
 
-You may also schedule a job to execute at fixed intervals, starting at the time it's added 
+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>

+ 3 - 0
vendor/modules.txt

@@ -56,6 +56,9 @@ github.com/benbjohnson/clock
 github.com/beorn7/perks/quantile
 # github.com/bradfitz/gomemcache v0.0.0-20180710155616-bc664df96737
 github.com/bradfitz/gomemcache/memcache
+# github.com/brianvoe/gofakeit v3.17.0+incompatible
+github.com/brianvoe/gofakeit
+github.com/brianvoe/gofakeit/data
 # github.com/codegangsta/cli v1.20.0
 github.com/codegangsta/cli
 # github.com/davecgh/go-spew v1.1.1

部分文件因文件數量過多而無法顯示