forgejo/models/auth/source.go
Earl Warren 5455605342
[TESTS] oauth2: add integration test helpers
(cherry picked from commit e11dcc60f291f1b882a993f60f8381fe4561d6d0)

use backticks to avoid backslash

(cherry picked from commit 34212791eef2031ef09ea118a2ee5b98082174dc)
(cherry picked from commit bde9473c69eaf6306457b4218d9704af64cb6cc8)
(cherry picked from commit d4deb43084eec4ce0de786a01acef52921a39b13)
(cherry picked from commit 08e91649b0057258ea5d775447d84093c31ad523)
(cherry picked from commit 2b988e5415b35e608726facb5d23a920334fda1c)

[TESTS] auth LinkAccount test coverage (squash)

(cherry picked from commit a2b2e3066bee46ca15ce66d0deb7ef3e89915248)
(cherry picked from commit 841d1b50731a94b9330b6a623a40f8aa0a6befa8)
(cherry picked from commit 35da630ad884a9ffff5bd873123687af169a6cac)
(cherry picked from commit caf2dc4fa7c6fb45a19edc5a025579d42d8db455)
(cherry picked from commit 6eb81e67ba69aeb9f1290f6717ec6c6a367752c3)
(cherry picked from commit d59757239f4fd6353dafd88f2460145b88ef38a1)
(cherry picked from commit 38a121b6880538f381799fb69666e13abf667502)
(cherry picked from commit 20613874ee04286a5ecb28045ec80af0fd850582)
(cherry picked from commit 6d2705e10858baf5e33df0ced047c544ed826fd3)
(cherry picked from commit f177b728142911fed6709339dd0e686017b610b0)
(cherry picked from commit 75e1fc4c8318b378f94065a268b079ac152657ef)
(cherry picked from commit ba64fa9867b06fb0b390a799ef4c3f39f554bb0b)
(cherry picked from commit 0b8ab0893ec6b6d689534b5e4ac50cdfe36c34e9)
(cherry picked from commit 1419d11435b0cdf7c41cb7175dffaf521ecfacd7)
(cherry picked from commit 38766847e0441f4b3841b05b34e3442f4e23af06)
(cherry picked from commit 6f23426a6ab09df7bb5817d364301975715dc10b)
(cherry picked from commit 9e0ff9ca54505723ad39a3fb221b94cbcef2da66)
(cherry picked from commit 353f3601c318f77a07fba0976fc9e3d28b2fc818)
(cherry picked from commit 6e4ae401d815bf32ca21e2fdada5aa1ac528c756)
(cherry picked from commit 1a7afe41530378cf194ce7c302cfe6bf757a2838)
(cherry picked from commit f9f3e0cc02fda87ef769ee8410e9d926963d2d97)
(cherry picked from commit 22fd0337f3cc57e4365c783b80db553627022f6d)
(cherry picked from commit ee57e138d1a89508f7613d1e6782a9909977b153)
(cherry picked from commit 21f9b7e73ddf12948feb220ec5432e14b75e0baa)
(cherry picked from commit 17c548c09298472af65526f1334fecffd1e72d1e)
(cherry picked from commit 02d31865174d94273e993248aa152f482fa14802)
(cherry picked from commit f02a040fa27afdbcf12d197894e9adc0a8a17734)
(cherry picked from commit 3cf9f82b282fe62d2124e1d3c1d75ea5f92ddce0)
(cherry picked from commit aa9d06dbac2a14cde066f0c1f896c3993a49aae0)
(cherry picked from commit 689421315464c16462938b3dbd710978e1fd14f3)
2024-02-05 14:44:32 +01:00

405 lines
10 KiB
Go

// Copyright 2014 The Gogs Authors. All rights reserved.
// Copyright 2019 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package auth
import (
"context"
"fmt"
"reflect"
"code.gitea.io/gitea/models/db"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/timeutil"
"code.gitea.io/gitea/modules/util"
"xorm.io/builder"
"xorm.io/xorm"
"xorm.io/xorm/convert"
)
// Type represents an login type.
type Type int
// Note: new type must append to the end of list to maintain compatibility.
const (
NoType Type = iota
Plain // 1
LDAP // 2
SMTP // 3
PAM // 4
DLDAP // 5
OAuth2 // 6
SSPI // 7
)
// String returns the string name of the LoginType
func (typ Type) String() string {
return Names[typ]
}
// Int returns the int value of the LoginType
func (typ Type) Int() int {
return int(typ)
}
// Names contains the name of LoginType values.
var Names = map[Type]string{
LDAP: "LDAP (via BindDN)",
DLDAP: "LDAP (simple auth)", // Via direct bind
SMTP: "SMTP",
PAM: "PAM",
OAuth2: "OAuth2",
SSPI: "SPNEGO with SSPI",
}
// Config represents login config as far as the db is concerned
type Config interface {
convert.Conversion
}
// SkipVerifiable configurations provide a IsSkipVerify to check if SkipVerify is set
type SkipVerifiable interface {
IsSkipVerify() bool
}
// HasTLSer configurations provide a HasTLS to check if TLS can be enabled
type HasTLSer interface {
HasTLS() bool
}
// UseTLSer configurations provide a HasTLS to check if TLS is enabled
type UseTLSer interface {
UseTLS() bool
}
// SSHKeyProvider configurations provide ProvidesSSHKeys to check if they provide SSHKeys
type SSHKeyProvider interface {
ProvidesSSHKeys() bool
}
// RegisterableSource configurations provide RegisterSource which needs to be run on creation
type RegisterableSource interface {
RegisterSource() error
UnregisterSource() error
}
var registeredConfigs = map[Type]func() Config{}
// RegisterTypeConfig register a config for a provided type
func RegisterTypeConfig(typ Type, exemplar Config) {
if reflect.TypeOf(exemplar).Kind() == reflect.Ptr {
// Pointer:
registeredConfigs[typ] = func() Config {
return reflect.New(reflect.ValueOf(exemplar).Elem().Type()).Interface().(Config)
}
return
}
// Not a Pointer
registeredConfigs[typ] = func() Config {
return reflect.New(reflect.TypeOf(exemplar)).Elem().Interface().(Config)
}
}
// SourceSettable configurations can have their authSource set on them
type SourceSettable interface {
SetAuthSource(*Source)
}
// Source represents an external way for authorizing users.
type Source struct {
ID int64 `xorm:"pk autoincr"`
Type Type
Name string `xorm:"UNIQUE"`
IsActive bool `xorm:"INDEX NOT NULL DEFAULT false"`
IsSyncEnabled bool `xorm:"INDEX NOT NULL DEFAULT false"`
Cfg convert.Conversion `xorm:"TEXT"`
CreatedUnix timeutil.TimeStamp `xorm:"INDEX created"`
UpdatedUnix timeutil.TimeStamp `xorm:"INDEX updated"`
}
// TableName xorm will read the table name from this method
func (Source) TableName() string {
return "login_source"
}
func init() {
db.RegisterModel(new(Source))
}
// BeforeSet is invoked from XORM before setting the value of a field of this object.
func (source *Source) BeforeSet(colName string, val xorm.Cell) {
if colName == "type" {
typ := Type(db.Cell2Int64(val))
constructor, ok := registeredConfigs[typ]
if !ok {
return
}
source.Cfg = constructor()
if settable, ok := source.Cfg.(SourceSettable); ok {
settable.SetAuthSource(source)
}
}
}
// TypeName return name of this login source type.
func (source *Source) TypeName() string {
return Names[source.Type]
}
// IsLDAP returns true of this source is of the LDAP type.
func (source *Source) IsLDAP() bool {
return source.Type == LDAP
}
// IsDLDAP returns true of this source is of the DLDAP type.
func (source *Source) IsDLDAP() bool {
return source.Type == DLDAP
}
// IsSMTP returns true of this source is of the SMTP type.
func (source *Source) IsSMTP() bool {
return source.Type == SMTP
}
// IsPAM returns true of this source is of the PAM type.
func (source *Source) IsPAM() bool {
return source.Type == PAM
}
// IsOAuth2 returns true of this source is of the OAuth2 type.
func (source *Source) IsOAuth2() bool {
return source.Type == OAuth2
}
// IsSSPI returns true of this source is of the SSPI type.
func (source *Source) IsSSPI() bool {
return source.Type == SSPI
}
// HasTLS returns true of this source supports TLS.
func (source *Source) HasTLS() bool {
hasTLSer, ok := source.Cfg.(HasTLSer)
return ok && hasTLSer.HasTLS()
}
// UseTLS returns true of this source is configured to use TLS.
func (source *Source) UseTLS() bool {
useTLSer, ok := source.Cfg.(UseTLSer)
return ok && useTLSer.UseTLS()
}
// SkipVerify returns true if this source is configured to skip SSL
// verification.
func (source *Source) SkipVerify() bool {
skipVerifiable, ok := source.Cfg.(SkipVerifiable)
return ok && skipVerifiable.IsSkipVerify()
}
// CreateSource inserts a AuthSource in the DB if not already
// existing with the given name.
func CreateSource(ctx context.Context, source *Source) error {
has, err := db.GetEngine(ctx).Where("name=?", source.Name).Exist(new(Source))
if err != nil {
return err
} else if has {
return ErrSourceAlreadyExist{source.Name}
}
// Synchronization is only available with LDAP for now
if !source.IsLDAP() {
source.IsSyncEnabled = false
}
_, err = db.GetEngine(ctx).Insert(source)
if err != nil {
return err
}
if !source.IsActive {
return nil
}
if settable, ok := source.Cfg.(SourceSettable); ok {
settable.SetAuthSource(source)
}
registerableSource, ok := source.Cfg.(RegisterableSource)
if !ok {
return nil
}
err = registerableSource.RegisterSource()
if err != nil {
// remove the AuthSource in case of errors while registering configuration
if _, err := db.GetEngine(ctx).ID(source.ID).Delete(new(Source)); err != nil {
log.Error("CreateSource: Error while wrapOpenIDConnectInitializeError: %v", err)
}
}
return err
}
type FindSourcesOptions struct {
db.ListOptions
IsActive util.OptionalBool
LoginType Type
}
func (opts FindSourcesOptions) ToConds() builder.Cond {
conds := builder.NewCond()
if !opts.IsActive.IsNone() {
conds = conds.And(builder.Eq{"is_active": opts.IsActive.IsTrue()})
}
if opts.LoginType != NoType {
conds = conds.And(builder.Eq{"`type`": opts.LoginType})
}
return conds
}
// IsSSPIEnabled returns true if there is at least one activated login
// source of type LoginSSPI
func IsSSPIEnabled(ctx context.Context) bool {
exist, err := db.Exist[Source](ctx, FindSourcesOptions{
IsActive: util.OptionalBoolTrue,
LoginType: SSPI,
}.ToConds())
if err != nil {
log.Error("IsSSPIEnabled: failed to query active SSPI sources: %v", err)
return false
}
return exist
}
// GetSourceByID returns login source by given ID.
func GetSourceByID(ctx context.Context, id int64) (*Source, error) {
source := new(Source)
if id == 0 {
source.Cfg = registeredConfigs[NoType]()
// Set this source to active
// FIXME: allow disabling of db based password authentication in future
source.IsActive = true
return source, nil
}
has, err := db.GetEngine(ctx).ID(id).Get(source)
if err != nil {
return nil, err
} else if !has {
return nil, ErrSourceNotExist{id}
}
return source, nil
}
func GetSourceByName(ctx context.Context, name string) (*Source, error) {
source := &Source{}
has, err := db.GetEngine(ctx).Where("name = ?", name).Get(source)
if err != nil {
return nil, err
} else if !has {
return nil, ErrSourceNotExist{}
}
return source, nil
}
// UpdateSource updates a Source record in DB.
func UpdateSource(ctx context.Context, source *Source) error {
var originalSource *Source
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 originalSource, err = GetSourceByID(ctx, source.ID); err != nil {
return err
}
}
has, err := db.GetEngine(ctx).Where("name=? AND id!=?", source.Name, source.ID).Exist(new(Source))
if err != nil {
return err
} else if has {
return ErrSourceAlreadyExist{source.Name}
}
_, err = db.GetEngine(ctx).ID(source.ID).AllCols().Update(source)
if err != nil {
return err
}
if !source.IsActive {
return nil
}
if settable, ok := source.Cfg.(SourceSettable); ok {
settable.SetAuthSource(source)
}
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 := db.GetEngine(ctx).ID(source.ID).AllCols().Update(originalSource); err != nil {
log.Error("UpdateSource: Error while wrapOpenIDConnectInitializeError: %v", err)
}
}
return err
}
// ErrSourceNotExist represents a "SourceNotExist" kind of error.
type ErrSourceNotExist struct {
ID int64
}
// IsErrSourceNotExist checks if an error is a ErrSourceNotExist.
func IsErrSourceNotExist(err error) bool {
_, ok := err.(ErrSourceNotExist)
return ok
}
func (err ErrSourceNotExist) Error() string {
return fmt.Sprintf("login source does not exist [id: %d]", err.ID)
}
// Unwrap unwraps this as a ErrNotExist err
func (err ErrSourceNotExist) Unwrap() error {
return util.ErrNotExist
}
// ErrSourceAlreadyExist represents a "SourceAlreadyExist" kind of error.
type ErrSourceAlreadyExist struct {
Name string
}
// IsErrSourceAlreadyExist checks if an error is a ErrSourceAlreadyExist.
func IsErrSourceAlreadyExist(err error) bool {
_, ok := err.(ErrSourceAlreadyExist)
return ok
}
func (err ErrSourceAlreadyExist) Error() string {
return fmt.Sprintf("login source already exists [name: %s]", err.Name)
}
// Unwrap unwraps this as a ErrExist err
func (err ErrSourceAlreadyExist) Unwrap() error {
return util.ErrAlreadyExist
}
// ErrSourceInUse represents a "SourceInUse" kind of error.
type ErrSourceInUse struct {
ID int64
}
// IsErrSourceInUse checks if an error is a ErrSourceInUse.
func IsErrSourceInUse(err error) bool {
_, ok := err.(ErrSourceInUse)
return ok
}
func (err ErrSourceInUse) Error() string {
return fmt.Sprintf("login source is still used by some users [id: %d]", err.ID)
}