2014-06-08 21:53:53 +00:00
|
|
|
// Copyright 2014 The Gogs Authors. All rights reserved.
|
2019-11-14 19:10:23 +00:00
|
|
|
// Copyright 2019 The Gitea Authors. All rights reserved.
|
2014-05-05 09:32:47 +00:00
|
|
|
// Use of this source code is governed by a MIT-style
|
|
|
|
// license that can be found in the LICENSE file.
|
|
|
|
|
2014-04-26 06:21:04 +00:00
|
|
|
package models
|
|
|
|
|
2014-05-03 02:48:14 +00:00
|
|
|
import (
|
2021-07-24 10:16:34 +00:00
|
|
|
"reflect"
|
2020-12-25 09:59:32 +00:00
|
|
|
"strconv"
|
2014-04-26 06:21:04 +00:00
|
|
|
|
2016-11-10 16:24:48 +00:00
|
|
|
"code.gitea.io/gitea/modules/log"
|
2019-08-15 14:46:21 +00:00
|
|
|
"code.gitea.io/gitea/modules/timeutil"
|
|
|
|
|
2019-10-17 09:26:49 +00:00
|
|
|
"xorm.io/xorm"
|
2020-03-22 15:12:55 +00:00
|
|
|
"xorm.io/xorm/convert"
|
2014-05-03 02:48:14 +00:00
|
|
|
)
|
2014-04-26 06:21:04 +00:00
|
|
|
|
2016-11-24 11:34:38 +00:00
|
|
|
// LoginType represents an login type.
|
2014-06-08 21:53:53 +00:00
|
|
|
type LoginType int
|
|
|
|
|
2016-08-31 08:22:41 +00:00
|
|
|
// Note: new type must append to the end of list to maintain compatibility.
|
2014-05-05 08:40:25 +00:00
|
|
|
const (
|
2016-11-07 20:58:22 +00:00
|
|
|
LoginNoType LoginType = iota
|
2017-02-25 14:57:06 +00:00
|
|
|
LoginPlain // 1
|
|
|
|
LoginLDAP // 2
|
|
|
|
LoginSMTP // 3
|
|
|
|
LoginPAM // 4
|
|
|
|
LoginDLDAP // 5
|
|
|
|
LoginOAuth2 // 6
|
2019-11-22 23:33:31 +00:00
|
|
|
LoginSSPI // 7
|
2014-05-05 08:40:25 +00:00
|
|
|
)
|
|
|
|
|
2021-07-24 10:16:34 +00:00
|
|
|
// String returns the string name of the LoginType
|
|
|
|
func (typ LoginType) String() string {
|
|
|
|
return LoginNames[typ]
|
|
|
|
}
|
|
|
|
|
2021-07-25 07:09:52 +00:00
|
|
|
// Int returns the int value of the LoginType
|
|
|
|
func (typ LoginType) Int() int {
|
|
|
|
return int(typ)
|
|
|
|
}
|
|
|
|
|
2016-11-24 11:34:38 +00:00
|
|
|
// LoginNames contains the name of LoginType values.
|
2015-09-10 21:11:41 +00:00
|
|
|
var LoginNames = map[LoginType]string{
|
2017-02-22 07:14:37 +00:00
|
|
|
LoginLDAP: "LDAP (via BindDN)",
|
|
|
|
LoginDLDAP: "LDAP (simple auth)", // Via direct bind
|
|
|
|
LoginSMTP: "SMTP",
|
|
|
|
LoginPAM: "PAM",
|
|
|
|
LoginOAuth2: "OAuth2",
|
2019-11-22 23:33:31 +00:00
|
|
|
LoginSSPI: "SPNEGO with SSPI",
|
2014-05-05 08:40:25 +00:00
|
|
|
}
|
2014-04-26 06:21:04 +00:00
|
|
|
|
2021-07-24 10:16:34 +00:00
|
|
|
// LoginConfig represents login config as far as the db is concerned
|
|
|
|
type LoginConfig interface {
|
|
|
|
convert.Conversion
|
2014-04-26 06:21:04 +00:00
|
|
|
}
|
|
|
|
|
2021-07-24 10:16:34 +00:00
|
|
|
// SkipVerifiable configurations provide a IsSkipVerify to check if SkipVerify is set
|
|
|
|
type SkipVerifiable interface {
|
|
|
|
IsSkipVerify() bool
|
2016-07-07 23:25:09 +00:00
|
|
|
}
|
|
|
|
|
2021-07-24 10:16:34 +00:00
|
|
|
// HasTLSer configurations provide a HasTLS to check if TLS can be enabled
|
|
|
|
type HasTLSer interface {
|
|
|
|
HasTLS() bool
|
2014-05-11 07:49:36 +00:00
|
|
|
}
|
|
|
|
|
2021-07-24 10:16:34 +00:00
|
|
|
// UseTLSer configurations provide a HasTLS to check if TLS is enabled
|
|
|
|
type UseTLSer interface {
|
|
|
|
UseTLS() bool
|
2014-05-11 07:49:36 +00:00
|
|
|
}
|
|
|
|
|
2021-07-24 10:16:34 +00:00
|
|
|
// SSHKeyProvider configurations provide ProvidesSSHKeys to check if they provide SSHKeys
|
|
|
|
type SSHKeyProvider interface {
|
|
|
|
ProvidesSSHKeys() bool
|
2014-05-11 07:49:36 +00:00
|
|
|
}
|
|
|
|
|
2021-07-24 10:16:34 +00:00
|
|
|
// RegisterableSource configurations provide RegisterSource which needs to be run on creation
|
|
|
|
type RegisterableSource interface {
|
|
|
|
RegisterSource() error
|
|
|
|
UnregisterSource() error
|
2015-04-23 11:58:57 +00:00
|
|
|
}
|
|
|
|
|
2021-07-24 10:16:34 +00:00
|
|
|
// LoginSourceSettable configurations can have their loginSource set on them
|
|
|
|
type LoginSourceSettable interface {
|
|
|
|
SetLoginSource(*LoginSource)
|
2015-04-23 11:58:57 +00:00
|
|
|
}
|
|
|
|
|
2021-07-24 10:16:34 +00:00
|
|
|
// RegisterLoginTypeConfig register a config for a provided type
|
|
|
|
func RegisterLoginTypeConfig(typ LoginType, exemplar LoginConfig) {
|
|
|
|
if reflect.TypeOf(exemplar).Kind() == reflect.Ptr {
|
|
|
|
// Pointer:
|
|
|
|
registeredLoginConfigs[typ] = func() LoginConfig {
|
|
|
|
return reflect.New(reflect.ValueOf(exemplar).Elem().Type()).Interface().(LoginConfig)
|
|
|
|
}
|
|
|
|
return
|
|
|
|
}
|
2019-11-22 23:33:31 +00:00
|
|
|
|
2021-07-24 10:16:34 +00:00
|
|
|
// Not a Pointer
|
|
|
|
registeredLoginConfigs[typ] = func() LoginConfig {
|
|
|
|
return reflect.New(reflect.TypeOf(exemplar)).Elem().Interface().(LoginConfig)
|
|
|
|
}
|
2019-11-22 23:33:31 +00:00
|
|
|
}
|
|
|
|
|
2021-07-24 10:16:34 +00:00
|
|
|
var registeredLoginConfigs = map[LoginType]func() LoginConfig{}
|
2019-11-22 23:33:31 +00:00
|
|
|
|
2016-08-31 08:22:41 +00:00
|
|
|
// LoginSource represents an external way for authorizing users.
|
2014-04-26 06:21:04 +00:00
|
|
|
type LoginSource struct {
|
2017-05-10 13:10:18 +00:00
|
|
|
ID int64 `xorm:"pk autoincr"`
|
|
|
|
Type LoginType
|
2020-03-22 15:12:55 +00:00
|
|
|
Name string `xorm:"UNIQUE"`
|
2021-07-24 10:16:34 +00:00
|
|
|
IsActive bool `xorm:"INDEX NOT NULL DEFAULT false"`
|
2020-03-22 15:12:55 +00:00
|
|
|
IsSyncEnabled bool `xorm:"INDEX NOT NULL DEFAULT false"`
|
|
|
|
Cfg convert.Conversion `xorm:"TEXT"`
|
2016-03-10 00:53:30 +00:00
|
|
|
|
2019-08-15 14:46:21 +00:00
|
|
|
CreatedUnix timeutil.TimeStamp `xorm:"INDEX created"`
|
|
|
|
UpdatedUnix timeutil.TimeStamp `xorm:"INDEX updated"`
|
2014-05-03 02:48:14 +00:00
|
|
|
}
|
|
|
|
|
2016-01-11 06:34:32 +00:00
|
|
|
// Cell2Int64 converts a xorm.Cell type to int64,
|
|
|
|
// and handles possible irregular cases.
|
|
|
|
func Cell2Int64(val xorm.Cell) int64 {
|
|
|
|
switch (*val).(type) {
|
2016-01-11 07:47:23 +00:00
|
|
|
case []uint8:
|
|
|
|
log.Trace("Cell2Int64 ([]uint8): %v", *val)
|
2020-12-25 09:59:32 +00:00
|
|
|
|
|
|
|
v, _ := strconv.ParseInt(string((*val).([]uint8)), 10, 64)
|
|
|
|
return v
|
2016-01-11 06:34:32 +00:00
|
|
|
}
|
|
|
|
return (*val).(int64)
|
|
|
|
}
|
|
|
|
|
2016-11-24 11:34:38 +00:00
|
|
|
// BeforeSet is invoked from XORM before setting the value of a field of this object.
|
2015-08-29 07:45:58 +00:00
|
|
|
func (source *LoginSource) BeforeSet(colName string, val xorm.Cell) {
|
2019-06-12 19:41:28 +00:00
|
|
|
if colName == "type" {
|
2021-07-24 10:16:34 +00:00
|
|
|
typ := LoginType(Cell2Int64(val))
|
|
|
|
constructor, ok := registeredLoginConfigs[typ]
|
|
|
|
if !ok {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
source.Cfg = constructor()
|
|
|
|
if settable, ok := source.Cfg.(LoginSourceSettable); ok {
|
|
|
|
settable.SetLoginSource(source)
|
2015-08-29 07:45:58 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2016-11-24 11:34:38 +00:00
|
|
|
// TypeName return name of this login source type.
|
2015-09-10 21:11:41 +00:00
|
|
|
func (source *LoginSource) TypeName() string {
|
|
|
|
return LoginNames[source.Type]
|
2014-05-05 08:40:25 +00:00
|
|
|
}
|
|
|
|
|
2016-11-24 11:34:38 +00:00
|
|
|
// IsLDAP returns true of this source is of the LDAP type.
|
2015-09-11 16:03:08 +00:00
|
|
|
func (source *LoginSource) IsLDAP() bool {
|
2016-11-07 20:58:22 +00:00
|
|
|
return source.Type == LoginLDAP
|
2015-09-11 16:03:08 +00:00
|
|
|
}
|
|
|
|
|
2016-11-24 11:34:38 +00:00
|
|
|
// IsDLDAP returns true of this source is of the DLDAP type.
|
2015-09-11 16:03:08 +00:00
|
|
|
func (source *LoginSource) IsDLDAP() bool {
|
2016-11-07 20:58:22 +00:00
|
|
|
return source.Type == LoginDLDAP
|
2015-09-11 16:03:08 +00:00
|
|
|
}
|
|
|
|
|
2016-11-24 11:34:38 +00:00
|
|
|
// IsSMTP returns true of this source is of the SMTP type.
|
2015-09-11 16:03:08 +00:00
|
|
|
func (source *LoginSource) IsSMTP() bool {
|
2016-11-07 20:58:22 +00:00
|
|
|
return source.Type == LoginSMTP
|
2015-09-11 16:03:08 +00:00
|
|
|
}
|
|
|
|
|
2016-11-24 11:34:38 +00:00
|
|
|
// IsPAM returns true of this source is of the PAM type.
|
2015-09-11 16:03:08 +00:00
|
|
|
func (source *LoginSource) IsPAM() bool {
|
2016-11-07 20:58:22 +00:00
|
|
|
return source.Type == LoginPAM
|
2015-09-11 16:03:08 +00:00
|
|
|
}
|
|
|
|
|
2017-02-22 07:14:37 +00:00
|
|
|
// IsOAuth2 returns true of this source is of the OAuth2 type.
|
|
|
|
func (source *LoginSource) IsOAuth2() bool {
|
|
|
|
return source.Type == LoginOAuth2
|
|
|
|
}
|
|
|
|
|
2019-11-22 23:33:31 +00:00
|
|
|
// IsSSPI returns true of this source is of the SSPI type.
|
|
|
|
func (source *LoginSource) IsSSPI() bool {
|
|
|
|
return source.Type == LoginSSPI
|
|
|
|
}
|
|
|
|
|
2016-11-24 11:34:38 +00:00
|
|
|
// HasTLS returns true of this source supports TLS.
|
2016-07-07 23:25:09 +00:00
|
|
|
func (source *LoginSource) HasTLS() bool {
|
2021-07-24 10:16:34 +00:00
|
|
|
hasTLSer, ok := source.Cfg.(HasTLSer)
|
|
|
|
return ok && hasTLSer.HasTLS()
|
2016-07-07 23:25:09 +00:00
|
|
|
}
|
|
|
|
|
2016-11-24 11:34:38 +00:00
|
|
|
// UseTLS returns true of this source is configured to use TLS.
|
2015-09-11 16:03:08 +00:00
|
|
|
func (source *LoginSource) UseTLS() bool {
|
2021-07-24 10:16:34 +00:00
|
|
|
useTLSer, ok := source.Cfg.(UseTLSer)
|
|
|
|
return ok && useTLSer.UseTLS()
|
2015-09-11 16:03:08 +00:00
|
|
|
}
|
|
|
|
|
2016-11-24 11:34:38 +00:00
|
|
|
// SkipVerify returns true if this source is configured to skip SSL
|
|
|
|
// verification.
|
2015-09-14 19:48:51 +00:00
|
|
|
func (source *LoginSource) SkipVerify() bool {
|
2021-07-24 10:16:34 +00:00
|
|
|
skipVerifiable, ok := source.Cfg.(SkipVerifiable)
|
|
|
|
return ok && skipVerifiable.IsSkipVerify()
|
2019-11-22 23:33:31 +00:00
|
|
|
}
|
|
|
|
|
2016-11-24 11:34:38 +00:00
|
|
|
// CreateLoginSource inserts a LoginSource in the DB if not already
|
|
|
|
// existing with the given name.
|
2016-08-31 07:56:10 +00:00
|
|
|
func CreateLoginSource(source *LoginSource) error {
|
2020-06-17 17:50:11 +00:00
|
|
|
has, err := x.Where("name=?", source.Name).Exist(new(LoginSource))
|
2016-08-31 07:56:10 +00:00
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
} else if has {
|
|
|
|
return ErrLoginSourceAlreadyExist{source.Name}
|
|
|
|
}
|
2021-07-08 11:38:13 +00:00
|
|
|
// Synchronization is only available with LDAP for now
|
2017-05-10 13:10:18 +00:00
|
|
|
if !source.IsLDAP() {
|
|
|
|
source.IsSyncEnabled = false
|
|
|
|
}
|
2016-08-31 07:56:10 +00:00
|
|
|
|
|
|
|
_, err = x.Insert(source)
|
2021-07-24 10:16:34 +00:00
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
if !source.IsActive {
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2021-07-25 07:09:52 +00:00
|
|
|
if settable, ok := source.Cfg.(LoginSourceSettable); ok {
|
|
|
|
settable.SetLoginSource(source)
|
|
|
|
}
|
|
|
|
|
2021-07-24 10:16:34 +00:00
|
|
|
registerableSource, ok := source.Cfg.(RegisterableSource)
|
|
|
|
if !ok {
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
err = registerableSource.RegisterSource()
|
|
|
|
if err != nil {
|
|
|
|
// remove the LoginSource in case of errors while registering configuration
|
|
|
|
if _, err := x.Delete(source); err != nil {
|
|
|
|
log.Error("CreateLoginSource: Error while wrapOpenIDConnectInitializeError: %v", err)
|
2017-05-01 13:26:53 +00:00
|
|
|
}
|
2017-02-22 07:14:37 +00:00
|
|
|
}
|
2014-06-08 21:53:53 +00:00
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
2016-11-24 11:34:38 +00:00
|
|
|
// LoginSources returns a slice of all login sources found in DB.
|
2015-09-13 13:51:51 +00:00
|
|
|
func LoginSources() ([]*LoginSource, error) {
|
2017-02-22 07:14:37 +00:00
|
|
|
auths := make([]*LoginSource, 0, 6)
|
2015-08-29 07:45:58 +00:00
|
|
|
return auths, x.Find(&auths)
|
2014-05-03 02:48:14 +00:00
|
|
|
}
|
|
|
|
|
2019-11-22 23:33:31 +00:00
|
|
|
// LoginSourcesByType returns all sources of the specified type
|
|
|
|
func LoginSourcesByType(loginType LoginType) ([]*LoginSource, error) {
|
|
|
|
sources := make([]*LoginSource, 0, 1)
|
|
|
|
if err := x.Where("type = ?", loginType).Find(&sources); err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
return sources, nil
|
|
|
|
}
|
|
|
|
|
2021-07-24 10:16:34 +00:00
|
|
|
// AllActiveLoginSources returns all active sources
|
|
|
|
func AllActiveLoginSources() ([]*LoginSource, error) {
|
|
|
|
sources := make([]*LoginSource, 0, 5)
|
|
|
|
if err := x.Where("is_active = ?", true).Find(&sources); err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
return sources, nil
|
|
|
|
}
|
|
|
|
|
2019-11-22 23:33:31 +00:00
|
|
|
// ActiveLoginSources returns all active sources of the specified type
|
|
|
|
func ActiveLoginSources(loginType LoginType) ([]*LoginSource, error) {
|
|
|
|
sources := make([]*LoginSource, 0, 1)
|
2021-07-24 10:16:34 +00:00
|
|
|
if err := x.Where("is_active = ? and type = ?", true, loginType).Find(&sources); err != nil {
|
2019-11-22 23:33:31 +00:00
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
return sources, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// IsSSPIEnabled returns true if there is at least one activated login
|
|
|
|
// source of type LoginSSPI
|
|
|
|
func IsSSPIEnabled() bool {
|
|
|
|
if !HasEngine {
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
sources, err := ActiveLoginSources(LoginSSPI)
|
|
|
|
if err != nil {
|
|
|
|
log.Error("ActiveLoginSources: %v", err)
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
return len(sources) > 0
|
|
|
|
}
|
|
|
|
|
2015-12-05 22:13:13 +00:00
|
|
|
// GetLoginSourceByID returns login source by given ID.
|
2015-08-29 07:45:58 +00:00
|
|
|
func GetLoginSourceByID(id int64) (*LoginSource, error) {
|
2014-05-05 08:40:25 +00:00
|
|
|
source := new(LoginSource)
|
2021-07-24 10:16:34 +00:00
|
|
|
if id == 0 {
|
|
|
|
source.Cfg = registeredLoginConfigs[LoginNoType]()
|
|
|
|
// Set this source to active
|
|
|
|
// FIXME: allow disabling of db based password authentication in future
|
|
|
|
source.IsActive = true
|
|
|
|
return source, nil
|
|
|
|
}
|
|
|
|
|
2017-10-05 04:43:04 +00:00
|
|
|
has, err := x.ID(id).Get(source)
|
2014-05-05 08:40:25 +00:00
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
2014-06-08 21:53:53 +00:00
|
|
|
} else if !has {
|
2016-08-31 07:56:10 +00:00
|
|
|
return nil, ErrLoginSourceNotExist{id}
|
2014-05-05 08:40:25 +00:00
|
|
|
}
|
|
|
|
return source, nil
|
|
|
|
}
|
|
|
|
|
2016-11-24 11:34:38 +00:00
|
|
|
// UpdateSource updates a LoginSource record in DB.
|
2014-05-11 10:10:37 +00:00
|
|
|
func UpdateSource(source *LoginSource) error {
|
2017-05-01 13:26:53 +00:00
|
|
|
var originalLoginSource *LoginSource
|
|
|
|
if source.IsOAuth2() {
|
|
|
|
// keep track of the original values so we can restore in case of errors while registering OAuth2 providers
|
|
|
|
var err error
|
|
|
|
if originalLoginSource, err = GetLoginSourceByID(source.ID); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2017-10-05 04:43:04 +00:00
|
|
|
_, err := x.ID(source.ID).AllCols().Update(source)
|
2021-07-24 10:16:34 +00:00
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
if !source.IsActive {
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2021-07-25 07:09:52 +00:00
|
|
|
if settable, ok := source.Cfg.(LoginSourceSettable); ok {
|
|
|
|
settable.SetLoginSource(source)
|
|
|
|
}
|
|
|
|
|
2021-07-24 10:16:34 +00:00
|
|
|
registerableSource, ok := source.Cfg.(RegisterableSource)
|
|
|
|
if !ok {
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
err = registerableSource.RegisterSource()
|
|
|
|
if err != nil {
|
|
|
|
// restore original values since we cannot update the provider it self
|
|
|
|
if _, err := x.ID(source.ID).AllCols().Update(originalLoginSource); err != nil {
|
|
|
|
log.Error("UpdateSource: Error while wrapOpenIDConnectInitializeError: %v", err)
|
2017-05-01 13:26:53 +00:00
|
|
|
}
|
2017-02-22 07:14:37 +00:00
|
|
|
}
|
2014-05-03 02:48:14 +00:00
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
2016-11-24 11:34:38 +00:00
|
|
|
// DeleteSource deletes a LoginSource record in DB.
|
2015-09-11 16:03:08 +00:00
|
|
|
func DeleteSource(source *LoginSource) error {
|
|
|
|
count, err := x.Count(&User{LoginSource: source.ID})
|
2014-05-05 08:40:25 +00:00
|
|
|
if err != nil {
|
|
|
|
return err
|
2015-09-11 16:03:08 +00:00
|
|
|
} else if count > 0 {
|
2016-08-31 08:22:41 +00:00
|
|
|
return ErrLoginSourceInUse{source.ID}
|
2014-05-05 08:40:25 +00:00
|
|
|
}
|
2017-02-22 07:14:37 +00:00
|
|
|
|
|
|
|
count, err = x.Count(&ExternalLoginUser{LoginSourceID: source.ID})
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
} else if count > 0 {
|
|
|
|
return ErrLoginSourceInUse{source.ID}
|
|
|
|
}
|
|
|
|
|
2021-07-24 10:16:34 +00:00
|
|
|
if registerableSource, ok := source.Cfg.(RegisterableSource); ok {
|
|
|
|
if err := registerableSource.UnregisterSource(); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
2017-02-22 07:14:37 +00:00
|
|
|
}
|
|
|
|
|
2017-10-05 04:43:04 +00:00
|
|
|
_, err = x.ID(source.ID).Delete(new(LoginSource))
|
2014-05-03 02:48:14 +00:00
|
|
|
return err
|
2014-04-26 06:21:04 +00:00
|
|
|
}
|
2014-05-11 06:12:45 +00:00
|
|
|
|
2016-08-31 08:22:41 +00:00
|
|
|
// CountLoginSources returns number of login sources.
|
|
|
|
func CountLoginSources() int64 {
|
|
|
|
count, _ := x.Count(new(LoginSource))
|
|
|
|
return count
|
|
|
|
}
|