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