Refactor: Move login out of models (#16199)
`models` does far too much. In particular it handles all `UserSignin`. It shouldn't be responsible for calling LDAP, SMTP or PAM for signing in. Therefore we should move this code out of `models`. This code has to depend on `models` - therefore it belongs in `services`. There is a package in `services` called `auth` and clearly this functionality belongs in there. Plan: - [x] Change `auth.Auth` to `auth.Method` - as they represent methods of authentication. - [x] Move `models.UserSignIn` into `auth` - [x] Move `models.ExternalUserLogin` - [x] Move most of the `LoginVia*` methods to `auth` or subpackages - [x] Move Resynchronize functionality to `auth` - Involved some restructuring of `models/ssh_key.go` to reduce the size of this massive file and simplify its files. - [x] Move the rest of the LDAP functionality in to the ldap subpackage - [x] Re-factor the login sources to express an interfaces `auth.Source`? - I've done this through some smaller interfaces Authenticator and Synchronizable - which would allow us to extend things in future - [x] Now LDAP is out of models - need to think about modules/auth/ldap and I think all of that functionality might just be moveable - [x] Similarly a lot Oauth2 functionality need not be in models too and should be moved to services/auth/source/oauth2 - [x] modules/auth/oauth2/oauth2.go uses xorm... This is naughty - probably need to move this into models. - [x] models/oauth2.go - mostly should be in modules/auth/oauth2 or services/auth/source/oauth2 - [x] More simplifications of login_source.go may need to be done - Allow wiring in of notify registration - *this can now easily be done - but I think we should do it in another PR* - see #16178 - More refactors...? - OpenID should probably become an auth Method but I think that can be left for another PR - Methods should also probably be cleaned up - again another PR I think. - SSPI still needs more refactors.* Rename auth.Auth auth.Method * Restructure ssh_key.go - move functions from models/user.go that relate to ssh_key to ssh_key - split ssh_key.go to try create clearer function domains for allow for future refactors here. Signed-off-by: Andrew Thornton <art27@cantab.net>forgejo
parent
f135a818f5
commit
5d2e11eedb
@ -0,0 +1,48 @@
|
||||
# type LoginSource struct {
|
||||
# ID int64 `xorm:"pk autoincr"`
|
||||
# Type int
|
||||
# Cfg []byte `xorm:"TEXT"`
|
||||
# Expected []byte `xorm:"TEXT"`
|
||||
# }
|
||||
-
|
||||
id: 1
|
||||
type: 1
|
||||
is_actived: false
|
||||
cfg: "{\"Source\":{\"A\":\"string\",\"B\":1}}"
|
||||
expected: "{\"Source\":{\"A\":\"string\",\"B\":1}}"
|
||||
-
|
||||
id: 2
|
||||
type: 2
|
||||
is_actived: true
|
||||
cfg: "{\"Source\":{\"A\":\"string2\",\"B\":2}}"
|
||||
expected: "{\"A\":\"string2\",\"B\":2}"
|
||||
-
|
||||
id: 3
|
||||
type: 3
|
||||
is_actived: false
|
||||
cfg: "{\"Source\":{\"A\":\"string3\",\"B\":3}}"
|
||||
expected: "{\"Source\":{\"A\":\"string3\",\"B\":3}}"
|
||||
-
|
||||
id: 4
|
||||
type: 4
|
||||
is_actived: true
|
||||
cfg: "{\"Source\":{\"A\":\"string4\",\"B\":4}}"
|
||||
expected: "{\"Source\":{\"A\":\"string4\",\"B\":4}}"
|
||||
-
|
||||
id: 5
|
||||
type: 5
|
||||
is_actived: false
|
||||
cfg: "{\"Source\":{\"A\":\"string5\",\"B\":5}}"
|
||||
expected: "{\"A\":\"string5\",\"B\":5}"
|
||||
-
|
||||
id: 6
|
||||
type: 2
|
||||
is_actived: true
|
||||
cfg: "{\"A\":\"string6\",\"B\":6}"
|
||||
expected: "{\"A\":\"string6\",\"B\":6}"
|
||||
-
|
||||
id: 7
|
||||
type: 5
|
||||
is_actived: false
|
||||
cfg: "{\"A\":\"string7\",\"B\":7}"
|
||||
expected: "{\"A\":\"string7\",\"B\":7}"
|
@ -0,0 +1,111 @@
|
||||
// Copyright 2021 The Gitea Authors. All rights reserved.
|
||||
// Use of this source code is governed by a MIT-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package migrations
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
|
||||
jsoniter "github.com/json-iterator/go"
|
||||
"xorm.io/xorm"
|
||||
)
|
||||
|
||||
func unwrapLDAPSourceCfg(x *xorm.Engine) error {
|
||||
jsonUnmarshalHandleDoubleEncode := func(bs []byte, v interface{}) error {
|
||||
json := jsoniter.ConfigCompatibleWithStandardLibrary
|
||||
err := json.Unmarshal(bs, v)
|
||||
if err != nil {
|
||||
ok := true
|
||||
rs := []byte{}
|
||||
temp := make([]byte, 2)
|
||||
for _, rn := range string(bs) {
|
||||
if rn > 0xffff {
|
||||
ok = false
|
||||
break
|
||||
}
|
||||
binary.LittleEndian.PutUint16(temp, uint16(rn))
|
||||
rs = append(rs, temp...)
|
||||
}
|
||||
if ok {
|
||||
if rs[0] == 0xff && rs[1] == 0xfe {
|
||||
rs = rs[2:]
|
||||
}
|
||||
err = json.Unmarshal(rs, v)
|
||||
}
|
||||
}
|
||||
if err != nil && len(bs) > 2 && bs[0] == 0xff && bs[1] == 0xfe {
|
||||
err = json.Unmarshal(bs[2:], v)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// LoginSource represents an external way for authorizing users.
|
||||
type LoginSource struct {
|
||||
ID int64 `xorm:"pk autoincr"`
|
||||
Type int
|
||||
IsActived bool `xorm:"INDEX NOT NULL DEFAULT false"`
|
||||
IsActive bool `xorm:"INDEX NOT NULL DEFAULT false"`
|
||||
Cfg string `xorm:"TEXT"`
|
||||
}
|
||||
|
||||
const ldapType = 2
|
||||
const dldapType = 5
|
||||
|
||||
type WrappedSource struct {
|
||||
Source map[string]interface{}
|
||||
}
|
||||
|
||||
// change lower_email as unique
|
||||
if err := x.Sync2(new(LoginSource)); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
sess := x.NewSession()
|
||||
defer sess.Close()
|
||||
|
||||
const batchSize = 100
|
||||
for start := 0; ; start += batchSize {
|
||||
sources := make([]*LoginSource, 0, batchSize)
|
||||
if err := sess.Limit(batchSize, start).Where("`type` = ? OR `type` = ?", ldapType, dldapType).Find(&sources); err != nil {
|
||||
return err
|
||||
}
|
||||
if len(sources) == 0 {
|
||||
break
|
||||
}
|
||||
|
||||
for _, source := range sources {
|
||||
wrapped := &WrappedSource{
|
||||
Source: map[string]interface{}{},
|
||||
}
|
||||
err := jsonUnmarshalHandleDoubleEncode([]byte(source.Cfg), &wrapped)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to unmarshal %s: %w", string(source.Cfg), err)
|
||||
}
|
||||
if wrapped.Source != nil && len(wrapped.Source) > 0 {
|
||||
bs, err := jsoniter.Marshal(wrapped.Source)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
source.Cfg = string(bs)
|
||||
if _, err := sess.ID(source.ID).Cols("cfg").Update(source); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if _, err := x.SetExpr("is_active", "is_actived").Update(&LoginSource{}); err != nil {
|
||||
return fmt.Errorf("SetExpr Update failed: %w", err)
|
||||
}
|
||||
|
||||
if err := sess.Begin(); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := dropTableColumns(sess, "login_source", "is_actived"); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return sess.Commit()
|
||||
}
|
@ -0,0 +1,83 @@
|
||||
// Copyright 2021 The Gitea Authors. All rights reserved.
|
||||
// Use of this source code is governed by a MIT-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package migrations
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
jsoniter "github.com/json-iterator/go"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
// LoginSource represents an external way for authorizing users.
|
||||
type LoginSourceOriginalV189 struct {
|
||||
ID int64 `xorm:"pk autoincr"`
|
||||
Type int
|
||||
IsActived bool `xorm:"INDEX NOT NULL DEFAULT false"`
|
||||
Cfg string `xorm:"TEXT"`
|
||||
Expected string `xorm:"TEXT"`
|
||||
}
|
||||
|
||||
func (ls *LoginSourceOriginalV189) TableName() string {
|
||||
return "login_source"
|
||||
}
|
||||
|
||||
func Test_unwrapLDAPSourceCfg(t *testing.T) {
|
||||
|
||||
// Prepare and load the testing database
|
||||
x, deferable := prepareTestEnv(t, 0, new(LoginSourceOriginalV189))
|
||||
if x == nil || t.Failed() {
|
||||
defer deferable()
|
||||
return
|
||||
}
|
||||
defer deferable()
|
||||
|
||||
// LoginSource represents an external way for authorizing users.
|
||||
type LoginSource struct {
|
||||
ID int64 `xorm:"pk autoincr"`
|
||||
Type int
|
||||
IsActive bool `xorm:"INDEX NOT NULL DEFAULT false"`
|
||||
Cfg string `xorm:"TEXT"`
|
||||
Expected string `xorm:"TEXT"`
|
||||
}
|
||||
|
||||
// Run the migration
|
||||
if err := unwrapLDAPSourceCfg(x); err != nil {
|
||||
assert.NoError(t, err)
|
||||
return
|
||||
}
|
||||
|
||||
const batchSize = 100
|
||||
for start := 0; ; start += batchSize {
|
||||
sources := make([]*LoginSource, 0, batchSize)
|
||||
if err := x.Table("login_source").Limit(batchSize, start).Find(&sources); err != nil {
|
||||
assert.NoError(t, err)
|
||||
return
|
||||
}
|
||||
|
||||
if len(sources) == 0 {
|
||||
break
|
||||
}
|
||||
|
||||
for _, source := range sources {
|
||||
converted := map[string]interface{}{}
|
||||
expected := map[string]interface{}{}
|
||||
|
||||
if err := jsoniter.Unmarshal([]byte(source.Cfg), &converted); err != nil {
|
||||
assert.NoError(t, err)
|
||||
return
|
||||
}
|
||||
|
||||
if err := jsoniter.Unmarshal([]byte(source.Expected), &expected); err != nil {
|
||||
assert.NoError(t, err)
|
||||
return
|
||||
}
|
||||
|
||||
assert.EqualValues(t, expected, converted, "unwrapLDAPSourceCfg failed for %d", source.ID)
|
||||
assert.EqualValues(t, source.ID%2 == 0, source.IsActive, "unwrapLDAPSourceCfg failed for %d", source.ID)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,219 @@
|
||||
// Copyright 2021 The Gitea Authors. All rights reserved.
|
||||
// Use of this source code is governed by a MIT-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package models
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
)
|
||||
|
||||
// _____ __ .__ .__ .___
|
||||
// / _ \ __ ___/ |_| |__ ___________|__|_______ ____ __| _/
|
||||
// / /_\ \| | \ __\ | \ / _ \_ __ \ \___ // __ \ / __ |
|
||||
// / | \ | /| | | Y ( <_> ) | \/ |/ /\ ___// /_/ |
|
||||
// \____|__ /____/ |__| |___| /\____/|__| |__/_____ \\___ >____ |
|
||||
// \/ \/ \/ \/ \/
|
||||
// ____ __.
|
||||
// | |/ _|____ ___.__. ______
|
||||
// | <_/ __ < | |/ ___/
|
||||
// | | \ ___/\___ |\___ \
|
||||
// |____|__ \___ > ____/____ >
|
||||
// \/ \/\/ \/
|
||||
//
|
||||
// This file contains functions for creating authorized_keys files
|
||||
//
|
||||
// There is a dependence on the database within RegeneratePublicKeys however most of these functions probably belong in a module
|
||||
|
||||
const (
|
||||
tplCommentPrefix = `# gitea public key`
|
||||
tplPublicKey = tplCommentPrefix + "\n" + `command=%s,no-port-forwarding,no-X11-forwarding,no-agent-forwarding,no-pty %s` + "\n"
|
||||
)
|
||||
|
||||
var sshOpLocker sync.Mutex
|
||||
|
||||
// AuthorizedStringForKey creates the authorized keys string appropriate for the provided key
|
||||
func AuthorizedStringForKey(key *PublicKey) string {
|
||||
sb := &strings.Builder{}
|
||||
_ = setting.SSH.AuthorizedKeysCommandTemplateTemplate.Execute(sb, map[string]interface{}{
|
||||
"AppPath": util.ShellEscape(setting.AppPath),
|
||||
"AppWorkPath": util.ShellEscape(setting.AppWorkPath),
|
||||
"CustomConf": util.ShellEscape(setting.CustomConf),
|
||||
"CustomPath": util.ShellEscape(setting.CustomPath),
|
||||
"Key": key,
|
||||
})
|
||||
|
||||
return fmt.Sprintf(tplPublicKey, util.ShellEscape(sb.String()), key.Content)
|
||||
}
|
||||
|
||||
// appendAuthorizedKeysToFile appends new SSH keys' content to authorized_keys file.
|
||||
func appendAuthorizedKeysToFile(keys ...*PublicKey) error {
|
||||
// Don't need to rewrite this file if builtin SSH server is enabled.
|
||||
if setting.SSH.StartBuiltinServer || !setting.SSH.CreateAuthorizedKeysFile {
|
||||
return nil
|
||||
}
|
||||
|
||||
sshOpLocker.Lock()
|
||||
defer sshOpLocker.Unlock()
|
||||
|
||||
if setting.SSH.RootPath != "" {
|
||||
// First of ensure that the RootPath is present, and if not make it with 0700 permissions
|
||||
// This of course doesn't guarantee that this is the right directory for authorized_keys
|
||||
// but at least if it's supposed to be this directory and it doesn't exist and we're the
|
||||
// right user it will at least be created properly.
|
||||
err := os.MkdirAll(setting.SSH.RootPath, 0o700)
|
||||
if err != nil {
|
||||
log.Error("Unable to MkdirAll(%s): %v", setting.SSH.RootPath, err)
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
fPath := filepath.Join(setting.SSH.RootPath, "authorized_keys")
|
||||
f, err := os.OpenFile(fPath, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0o600)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
// Note: chmod command does not support in Windows.
|
||||
if !setting.IsWindows {
|
||||
fi, err := f.Stat()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// .ssh directory should have mode 700, and authorized_keys file should have mode 600.
|
||||
if fi.Mode().Perm() > 0o600 {
|
||||
log.Error("authorized_keys file has unusual permission flags: %s - setting to -rw-------", fi.Mode().Perm().String())
|
||||
if err = f.Chmod(0o600); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for _, key := range keys {
|
||||
if key.Type == KeyTypePrincipal {
|
||||
continue
|
||||
}
|
||||
if _, err = f.WriteString(key.AuthorizedString()); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// RewriteAllPublicKeys removes any authorized key and rewrite all keys from database again.
|
||||
// Note: x.Iterate does not get latest data after insert/delete, so we have to call this function
|
||||
// outside any session scope independently.
|
||||
func RewriteAllPublicKeys() error {
|
||||
return rewriteAllPublicKeys(x)
|
||||
}
|
||||
|
||||
func rewriteAllPublicKeys(e Engine) error {
|
||||
// Don't rewrite key if internal server
|
||||
if setting.SSH.StartBuiltinServer || !setting.SSH.CreateAuthorizedKeysFile {
|
||||
return nil
|
||||
}
|
||||
|
||||
sshOpLocker.Lock()
|
||||
defer sshOpLocker.Unlock()
|
||||
|
||||
if setting.SSH.RootPath != "" {
|
||||
// First of ensure that the RootPath is present, and if not make it with 0700 permissions
|
||||
// This of course doesn't guarantee that this is the right directory for authorized_keys
|
||||
// but at least if it's supposed to be this directory and it doesn't exist and we're the
|
||||
// right user it will at least be created properly.
|
||||
err := os.MkdirAll(setting.SSH.RootPath, 0o700)
|
||||
if err != nil {
|
||||
log.Error("Unable to MkdirAll(%s): %v", setting.SSH.RootPath, err)
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
fPath := filepath.Join(setting.SSH.RootPath, "authorized_keys")
|
||||
tmpPath := fPath + ".tmp"
|
||||
t, err := os.OpenFile(tmpPath, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0o600)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer func() {
|
||||
t.Close()
|
||||
if err := util.Remove(tmpPath); err != nil {
|
||||
log.Warn("Unable to remove temporary authorized keys file: %s: Error: %v", tmpPath, err)
|
||||
}
|
||||
}()
|
||||
|
||||
if setting.SSH.AuthorizedKeysBackup {
|
||||
isExist, err := util.IsExist(fPath)
|
||||
if err != nil {
|
||||
log.Error("Unable to check if %s exists. Error: %v", fPath, err)
|
||||
return err
|
||||
}
|
||||
if isExist {
|
||||
bakPath := fmt.Sprintf("%s_%d.gitea_bak", fPath, time.Now().Unix())
|
||||
if err = util.CopyFile(fPath, bakPath); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if err := regeneratePublicKeys(e, t); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
t.Close()
|
||||
return util.Rename(tmpPath, fPath)
|
||||
}
|
||||
|
||||
// RegeneratePublicKeys regenerates the authorized_keys file
|
||||
func RegeneratePublicKeys(t io.StringWriter) error {
|
||||
return regeneratePublicKeys(x, t)
|
||||
}
|
||||
|
||||
func regeneratePublicKeys(e Engine, t io.StringWriter) error {
|
||||
if err := e.Where("type != ?", KeyTypePrincipal).Iterate(new(PublicKey), func(idx int, bean interface{}) (err error) {
|
||||
_, err = t.WriteString((bean.(*PublicKey)).AuthorizedString())
|
||||
return err
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fPath := filepath.Join(setting.SSH.RootPath, "authorized_keys")
|
||||
isExist, err := util.IsExist(fPath)
|
||||
if err != nil {
|
||||
log.Error("Unable to check if %s exists. Error: %v", fPath, err)
|
||||
return err
|
||||
}
|
||||
if isExist {
|
||||
f, err := os.Open(fPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
scanner := bufio.NewScanner(f)
|
||||
for scanner.Scan() {
|
||||
line := scanner.Text()
|
||||
if strings.HasPrefix(line, tplCommentPrefix) {
|
||||
scanner.Scan()
|
||||
continue
|
||||
}
|
||||
_, err = t.WriteString(line + "\n")
|
||||
if err != nil {
|
||||
f.Close()
|
||||
return err
|
||||
}
|
||||
}
|
||||
f.Close()
|
||||
}
|
||||
return nil
|
||||
}
|
@ -0,0 +1,142 @@
|
||||
// Copyright 2021 The Gitea Authors. All rights reserved.
|
||||
// Use of this source code is governed by a MIT-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package models
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
)
|
||||
|
||||
// _____ __ .__ .__ .___
|
||||
// / _ \ __ ___/ |_| |__ ___________|__|_______ ____ __| _/
|
||||
// / /_\ \| | \ __\ | \ / _ \_ __ \ \___ // __ \ / __ |
|
||||
// / | \ | /| | | Y ( <_> ) | \/ |/ /\ ___// /_/ |
|
||||
// \____|__ /____/ |__| |___| /\____/|__| |__/_____ \\___ >____ |
|
||||
// \/ \/ \/ \/ \/
|
||||
// __________ .__ .__ .__
|
||||
// \______ _______|__| ____ ____ |_____________ | | ______
|
||||
// | ___\_ __ | |/ \_/ ___\| \____ \__ \ | | / ___/
|
||||
// | | | | \| | | \ \___| | |_> / __ \| |__\___ \
|
||||
// |____| |__| |__|___| /\___ |__| __(____ |____/____ >
|
||||
// \/ \/ |__| \/ \/
|
||||
//
|
||||
// This file contains functions for creating authorized_principals files
|
||||
//
|
||||
// There is a dependence on the database within RewriteAllPrincipalKeys & RegeneratePrincipalKeys
|
||||
// The sshOpLocker is used from ssh_key_authorized_keys.go
|
||||
|
||||
const authorizedPrincipalsFile = "authorized_principals"
|
||||
|
||||
// RewriteAllPrincipalKeys removes any authorized principal and rewrite all keys from database again.
|
||||
// Note: x.Iterate does not get latest data after insert/delete, so we have to call this function
|
||||
// outside any session scope independently.
|
||||
func RewriteAllPrincipalKeys() error {
|
||||
return rewriteAllPrincipalKeys(x)
|
||||
}
|
||||
|
||||
func rewriteAllPrincipalKeys(e Engine) error {
|
||||
// Don't rewrite key if internal server
|
||||
if setting.SSH.StartBuiltinServer || !setting.SSH.CreateAuthorizedPrincipalsFile {
|
||||
return nil
|
||||
}
|
||||
|
||||
sshOpLocker.Lock()
|
||||
defer sshOpLocker.Unlock()
|
||||
|
||||
if setting.SSH.RootPath != "" {
|
||||
// First of ensure that the RootPath is present, and if not make it with 0700 permissions
|
||||
// This of course doesn't guarantee that this is the right directory for authorized_keys
|
||||
// but at least if it's supposed to be this directory and it doesn't exist and we're the
|
||||
// right user it will at least be created properly.
|
||||
err := os.MkdirAll(setting.SSH.RootPath, 0o700)
|
||||
if err != nil {
|
||||
log.Error("Unable to MkdirAll(%s): %v", setting.SSH.RootPath, err)
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
fPath := filepath.Join(setting.SSH.RootPath, authorizedPrincipalsFile)
|
||||
tmpPath := fPath + ".tmp"
|
||||
t, err := os.OpenFile(tmpPath, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0o600)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer func() {
|
||||
t.Close()
|
||||
os.Remove(tmpPath)
|
||||
}()
|
||||
|
||||
if setting.SSH.AuthorizedPrincipalsBackup {
|
||||
isExist, err := util.IsExist(fPath)
|
||||
if err != nil {
|
||||
log.Error("Unable to check if %s exists. Error: %v", fPath, err)
|
||||
return err
|
||||
}
|
||||
if isExist {
|
||||
bakPath := fmt.Sprintf("%s_%d.gitea_bak", fPath, time.Now().Unix())
|
||||
if err = util.CopyFile(fPath, bakPath); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if err := regeneratePrincipalKeys(e, t); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
t.Close()
|
||||
return util.Rename(tmpPath, fPath)
|
||||
}
|
||||
|
||||
// RegeneratePrincipalKeys regenerates the authorized_principals file
|
||||
func RegeneratePrincipalKeys(t io.StringWriter) error {
|
||||
return regeneratePrincipalKeys(x, t)
|
||||
}
|
||||
|
||||
func regeneratePrincipalKeys(e Engine, t io.StringWriter) error {
|
||||
if err := e.Where("type = ?", KeyTypePrincipal).Iterate(new(PublicKey), func(idx int, bean interface{}) (err error) {
|
||||
_, err = t.WriteString((bean.(*PublicKey)).AuthorizedString())
|
||||
return err
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fPath := filepath.Join(setting.SSH.RootPath, authorizedPrincipalsFile)
|
||||
isExist, err := util.IsExist(fPath)
|
||||
if err != nil {
|
||||
log.Error("Unable to check if %s exists. Error: %v", fPath, err)
|
||||
return err
|
||||
}
|
||||
if isExist {
|
||||
f, err := os.Open(fPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
scanner := bufio.NewScanner(f)
|
||||
for scanner.Scan() {
|
||||
line := scanner.Text()
|
||||
if strings.HasPrefix(line, tplCommentPrefix) {
|
||||
scanner.Scan()
|
||||
continue
|
||||
}
|
||||
_, err = t.WriteString(line + "\n")
|
||||
if err != nil {
|
||||
f.Close()
|
||||
return err
|
||||
}
|
||||
}
|
||||
f.Close()
|
||||
}
|
||||
return nil
|
||||
}
|
@ -0,0 +1,299 @@
|
||||
// Copyright 2021 The Gitea Authors. All rights reserved.
|
||||
// Use of this source code is governed by a MIT-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package models
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"code.gitea.io/gitea/modules/timeutil"
|
||||
"xorm.io/builder"
|
||||
"xorm.io/xorm"
|
||||
)
|
||||
|
||||
// ________ .__ ____ __.
|
||||
// \______ \ ____ ______ | | ____ ___.__.| |/ _|____ ___.__.
|
||||
// | | \_/ __ \\____ \| | / _ < | || <_/ __ < | |
|
||||
// | ` \ ___/| |_> > |_( <_> )___ || | \ ___/\___ |
|
||||
// /_______ /\___ > __/|____/\____// ____||____|__ \___ > ____|
|
||||
// \/ \/|__| \/ \/ \/\/
|
||||
//
|
||||
// This file contains functions specific to DeployKeys
|
||||
|
||||
// DeployKey represents deploy key information and its relation with repository.
|
||||
type DeployKey struct {
|
||||
ID int64 `xorm:"pk autoincr"`
|
||||
KeyID int64 `xorm:"UNIQUE(s) INDEX"`
|
||||
RepoID int64 `xorm:"UNIQUE(s) INDEX"`
|
||||
Name string
|
||||
Fingerprint string
|
||||
Content string `xorm:"-"`
|
||||
|
||||
Mode AccessMode `xorm:"NOT NULL DEFAULT 1"`
|
||||
|
||||
CreatedUnix timeutil.TimeStamp `xorm:"created"`
|
||||
UpdatedUnix timeutil.TimeStamp `xorm:"updated"`
|
||||
HasRecentActivity bool `xorm:"-"`
|
||||
HasUsed bool `xorm:"-"`
|
||||
}
|
||||
|
||||
// AfterLoad is invoked from XORM after setting the values of all fields of this object.
|
||||
func (key *DeployKey) AfterLoad() {
|
||||
key.HasUsed = key.UpdatedUnix > key.CreatedUnix
|
||||
key.HasRecentActivity = key.UpdatedUnix.AddDuration(7*24*time.Hour) > timeutil.TimeStampNow()
|
||||
}
|
||||
|
||||
// GetContent gets associated public key content.
|
||||
func (key *DeployKey) GetContent() error {
|
||||
pkey, err := GetPublicKeyByID(key.KeyID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
key.Content = pkey.Content
|
||||
return nil
|
||||
}
|
||||
|
||||
// IsReadOnly checks if the key can only be used for read operations
|
||||
func (key *DeployKey) IsReadOnly() bool {
|
||||
return key.Mode == AccessModeRead
|
||||
}
|
||||
|
||||
func checkDeployKey(e Engine, keyID, repoID int64, name string) error {
|
||||
// Note: We want error detail, not just true or false here.
|
||||
has, err := e.
|
||||
Where("key_id = ? AND repo_id = ?", keyID, repoID).
|
||||
Get(new(DeployKey))
|
||||
if err != nil {
|
||||
return err
|
||||
} else if has {
|
||||
return ErrDeployKeyAlreadyExist{keyID, repoID}
|
||||
}
|
||||
|
||||
has, err = e.
|
||||
Where("repo_id = ? AND name = ?", repoID, name).
|
||||
Get(new(DeployKey))
|
||||
if err != nil {
|
||||
return err
|
||||
} else if has {
|
||||
return ErrDeployKeyNameAlreadyUsed{repoID, name}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// addDeployKey adds new key-repo relation.
|
||||
func addDeployKey(e *xorm.Session, keyID, repoID int64, name, fingerprint string, mode AccessMode) (*DeployKey, error) {
|
||||
if err := checkDeployKey(e, keyID, repoID, name); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
key := &DeployKey{
|
||||
KeyID: keyID,
|
||||
RepoID: repoID,
|
||||
Name: name,
|
||||
Fingerprint: fingerprint,
|
||||
Mode: mode,
|
||||
}
|
||||
_, err := e.Insert(key)
|
||||
return key, err
|
||||
}
|
||||
|
||||
// HasDeployKey returns true if public key is a deploy key of given repository.
|
||||
func HasDeployKey(keyID, repoID int64) bool {
|
||||
has, _ := x.
|
||||
Where("key_id = ? AND repo_id = ?", keyID, repoID).
|
||||
Get(new(DeployKey))
|
||||
return has
|
||||
}
|
||||
|
||||
// AddDeployKey add new deploy key to database and authorized_keys file.
|
||||
func AddDeployKey(repoID int64, name, content string, readOnly bool) (*DeployKey, error) {
|
||||
fingerprint, err := calcFingerprint(content)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
accessMode := AccessModeRead
|
||||
if !readOnly {
|
||||
accessMode = AccessModeWrite
|
||||
}
|
||||
|
||||
sess := x.NewSession()
|
||||
defer sess.Close()
|
||||
if err = sess.Begin(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
pkey := &PublicKey{
|
||||
Fingerprint: fingerprint,
|
||||
}
|
||||
has, err := sess.Get(pkey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if has {
|
||||
if pkey.Type != KeyTypeDeploy {
|
||||
return nil, ErrKeyAlreadyExist{0, fingerprint, ""}
|
||||
}
|
||||
} else {
|
||||
// First time use this deploy key.
|
||||
pkey.Mode = accessMode
|
||||
pkey.Type = KeyTypeDeploy
|
||||
pkey.Content = content
|
||||
pkey.Name = name
|
||||
if err = addKey(sess, pkey); err != nil {
|
||||
return nil, fmt.Errorf("addKey: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
key, err := addDeployKey(sess, pkey.ID, repoID, name, pkey.Fingerprint, accessMode)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return key, sess.Commit()
|
||||
}
|
||||
|
||||
// GetDeployKeyByID returns deploy key by given ID.
|
||||
func GetDeployKeyByID(id int64) (*DeployKey, error) {
|
||||
return getDeployKeyByID(x, id)
|
||||
}
|
||||
|
||||
func getDeployKeyByID(e Engine, id int64) (*DeployKey, error) {
|
||||
key := new(DeployKey)
|
||||
has, err := e.ID(id).Get(key)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
} else if !has {
|
||||
return nil, ErrDeployKeyNotExist{id, 0, 0}
|
||||
}
|
||||
return key, nil
|
||||
}
|
||||
|
||||
// GetDeployKeyByRepo returns deploy key by given public key ID and repository ID.
|
||||
func GetDeployKeyByRepo(keyID, repoID int64) (*DeployKey, error) {
|
||||
return getDeployKeyByRepo(x, keyID, repoID)
|
||||
}
|
||||
|
||||
func getDeployKeyByRepo(e Engine, keyID, repoID int64) (*DeployKey, error) {
|
||||
key := &DeployKey{
|
||||
KeyID: keyID,
|
||||
RepoID: repoID,
|
||||
}
|
||||
has, err := e.Get(key)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
} else if !has {
|
||||
return nil, ErrDeployKeyNotExist{0, keyID, repoID}
|
||||
}
|
||||
return key, nil
|
||||
}
|
||||
|
||||
// UpdateDeployKeyCols updates deploy key information in the specified columns.
|
||||
func UpdateDeployKeyCols(key *DeployKey, cols ...string) error {
|
||||
_, err := x.ID(key.ID).Cols(cols...).Update(key)
|
||||
return err
|
||||
}
|
||||
|
||||
// UpdateDeployKey updates deploy key information.
|
||||
func UpdateDeployKey(key *DeployKey) error {
|
||||
_, err := x.ID(key.ID).AllCols().Update(key)
|
||||
return err
|
||||
}
|
||||
|
||||
// DeleteDeployKey deletes deploy key from its repository authorized_keys file if needed.
|
||||
func DeleteDeployKey(doer *User, id int64) error {
|
||||
sess := x.NewSession()
|
||||
defer sess.Close()
|
||||
if err := sess.Begin(); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := deleteDeployKey(sess, doer, id); err != nil {
|
||||
return err
|
||||
}
|
||||
return sess.Commit()
|
||||
}
|
||||
|
||||
func deleteDeployKey(sess Engine, doer *User, id int64) error {
|
||||
key, err := getDeployKeyByID(sess, id)
|
||||
if err != nil {
|
||||
if IsErrDeployKeyNotExist(err) {
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf("GetDeployKeyByID: %v", err)
|
||||
}
|
||||
|
||||
// Check if user has access to delete this key.
|
||||
if !doer.IsAdmin {
|
||||
repo, err := getRepositoryByID(sess, key.RepoID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("GetRepositoryByID: %v", err)
|
||||
}
|
||||
has, err := isUserRepoAdmin(sess, repo, doer)
|
||||
if err != nil {
|
||||
return fmt.Errorf("GetUserRepoPermission: %v", err)
|
||||
} else if !has {
|
||||
return ErrKeyAccessDenied{doer.ID, key.ID, "deploy"}
|
||||
}
|
||||
}
|
||||
|
||||
if _, err = sess.ID(key.ID).Delete(new(DeployKey)); err != nil {
|
||||
return fmt.Errorf("delete deploy key [%d]: %v", key.ID, err)
|
||||
}
|
||||
|
||||
// Check if this is the last reference to same key content.
|
||||
has, err := sess.
|
||||
Where("key_id = ?", key.KeyID).
|
||||
Get(new(DeployKey))
|
||||
if err != nil {
|
||||
return err
|
||||
} else if !has {
|
||||
if err = deletePublicKeys(sess, key.KeyID); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// after deleted the public keys, should rewrite the public keys file
|
||||
if err = rewriteAllPublicKeys(sess); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ListDeployKeys returns all deploy keys by given repository ID.
|
||||
func ListDeployKeys(repoID int64, listOptions ListOptions) ([]*DeployKey, error) {
|
||||
return listDeployKeys(x, repoID, listOptions)
|
||||
}
|
||||
|
||||
func listDeployKeys(e Engine, repoID int64, listOptions ListOptions) ([]*DeployKey, error) {
|
||||
sess := e.Where("repo_id = ?", repoID)
|
||||
if listOptions.Page != 0 {
|
||||
sess = listOptions.setSessionPagination(sess)
|
||||
|
||||
keys := make([]*DeployKey, 0, listOptions.PageSize)
|
||||
return keys, sess.Find(&keys)
|
||||
}
|
||||
|
||||
keys := make([]*DeployKey, 0, 5)
|
||||
return keys, sess.Find(&keys)
|
||||
}
|
||||
|
||||
// SearchDeployKeys returns a list of deploy keys matching the provided arguments.
|
||||
func SearchDeployKeys(repoID, keyID int64, fingerprint string) ([]*DeployKey, error) {
|
||||
keys := make([]*DeployKey, 0, 5)
|
||||
cond := builder.NewCond()
|
||||
if repoID != 0 {
|
||||
cond = cond.And(builder.Eq{"repo_id": repoID})
|
||||
}
|
||||
if keyID != 0 {
|
||||
cond = cond.And(builder.Eq{"key_id": keyID})
|
||||
}
|
||||
if fingerprint != "" {
|
||||
cond = cond.And(builder.Eq{"fingerprint": fingerprint})
|
||||
}
|
||||
return keys, x.Where(cond).Find(&keys)
|
||||
}
|
@ -0,0 +1,97 @@
|
||||
// Copyright 2021 The Gitea Authors. All rights reserved.
|
||||
// Use of this source code is governed by a MIT-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package models
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
"code.gitea.io/gitea/modules/process"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
"golang.org/x/crypto/ssh"
|
||||
)
|
||||
|
||||
// ___________.__ .__ __
|
||||
// \_ _____/|__| ____ ____ ________________________|__| _____/ |_
|
||||
// | __) | |/ \ / ___\_/ __ \_ __ \____ \_ __ \ |/ \ __\
|
||||
// | \ | | | \/ /_/ > ___/| | \/ |_> > | \/ | | \ |
|
||||
// \___ / |__|___| /\___ / \___ >__| | __/|__| |__|___| /__|
|
||||
// \/ \//_____/ \/ |__| \/
|
||||
//
|
||||
// This file contains functions for fingerprinting SSH keys
|
||||
//
|
||||
// The database is used in checkKeyFingerprint however most of these functions probably belong in a module
|
||||
|
||||
// checkKeyFingerprint only checks if key fingerprint has been used as public key,
|
||||
// it is OK to use same key as deploy key for multiple repositories/users.
|
||||
func checkKeyFingerprint(e Engine, fingerprint string) error {
|
||||
has, err := e.Get(&PublicKey{
|
||||
Fingerprint: fingerprint,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
} else if has {
|
||||
return ErrKeyAlreadyExist{0, fingerprint, ""}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func calcFingerprintSSHKeygen(publicKeyContent string) (string, error) {
|
||||
// Calculate fingerprint.
|
||||
tmpPath, err := writeTmpKeyFile(publicKeyContent)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer func() {
|
||||
if err := util.Remove(tmpPath); err != nil {
|
||||
log.Warn("Unable to remove temporary key file: %s: Error: %v", tmpPath, err)
|
||||
}
|
||||
}()
|
||||
stdout, stderr, err := process.GetManager().Exec("AddPublicKey", "ssh-keygen", "-lf", tmpPath)
|
||||
if err != nil {
|
||||
if strings.Contains(stderr, "is not a public key file") {
|
||||
return "", ErrKeyUnableVerify{stderr}
|
||||
}
|
||||
return "", fmt.Errorf("'ssh-keygen -lf %s' failed with error '%s': %s", tmpPath, err, stderr)
|
||||
} else if len(stdout) < 2 {
|
||||
return "", errors.New("not enough output for calculating fingerprint: " + stdout)
|
||||
}
|
||||
return strings.Split(stdout, " ")[1], nil
|
||||
}
|
||||
|
||||
func calcFingerprintNative(publicKeyContent string) (string, error) {
|
||||
// Calculate fingerprint.
|
||||
pk, _, _, _, err := ssh.ParseAuthorizedKey([]byte(publicKeyContent))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return ssh.FingerprintSHA256(pk), nil
|
||||
}
|
||||
|
||||
func calcFingerprint(publicKeyContent string) (string, error) {
|
||||
// Call the method based on configuration
|
||||
var (
|
||||
fnName, fp string
|
||||
err error
|
||||
)
|
||||
if setting.SSH.StartBuiltinServer {
|
||||
fnName = "calcFingerprintNative"
|
||||
fp, err = calcFingerprintNative(publicKeyContent)
|
||||
} else {
|
||||
fnName = "calcFingerprintSSHKeygen"
|
||||
fp, err = calcFingerprintSSHKeygen(publicKeyContent)
|
||||
}
|
||||
if err != nil {
|
||||
if IsErrKeyUnableVerify(err) {
|
||||
log.Info("%s", publicKeyContent)
|
||||
return "", err
|
||||
}
|
||||
return "", fmt.Errorf("%s: %v", fnName, err)
|
||||
}
|
||||
return fp, nil
|
||||
}
|
@ -0,0 +1,309 @@
|
||||
// Copyright 2021 The Gitea Authors. All rights reserved.
|
||||
// Use of this source code is governed by a MIT-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package models
|
||||
|
||||
import (
|
||||
"crypto/rsa"
|
||||
"crypto/x509"
|
||||
"encoding/asn1"
|
||||
"encoding/base64"
|
||||
"encoding/binary"
|
||||
"encoding/pem"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"math/big"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
"code.gitea.io/gitea/modules/process"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
"golang.org/x/crypto/ssh"
|
||||
)
|
||||
|
||||
// ____ __. __________
|
||||
// | |/ _|____ ___.__. \______ \_____ _______ ______ ___________
|
||||
// | <_/ __ < | | | ___/\__ \\_ __ \/ ___// __ \_ __ \
|
||||
// | | \ ___/\___ | | | / __ \| | \/\___ \\ ___/| | \/
|
||||
// |____|__ \___ > ____| |____| (____ /__| /____ >\___ >__|
|
||||
// \/ \/\/ \/ \/ \/
|
||||
//
|
||||
// This file contains functiosn for parsing ssh-keys
|
||||
//
|
||||
// TODO: Consider if these functions belong in models - no other models function call them or are called by them
|
||||
// They may belong in a service or a module
|
||||
|
||||
const ssh2keyStart = "---- BEGIN SSH2 PUBLIC KEY ----"
|
||||
|
||||
func extractTypeFromBase64Key(key string) (string, error) {
|
||||
b, err := base64.StdEncoding.DecodeString(key)
|
||||
if err != nil || len(b) < 4 {
|
||||
return "", fmt.Errorf("invalid key format: %v", err)
|
||||
}
|
||||
|
||||
keyLength := int(binary.BigEndian.Uint32(b))
|
||||
if len(b) < 4+keyLength {
|
||||
return "", fmt.Errorf("invalid key format: not enough length %d", keyLength)
|
||||
}
|
||||
|
||||
return string(b[4 : 4+keyLength]), nil
|
||||
}
|
||||
|
||||
// parseKeyString parses any key string in OpenSSH or SSH2 format to clean OpenSSH string (RFC4253).
|
||||
func parseKeyString(content string) (string, error) {
|
||||
// remove whitespace at start and end
|
||||
content = strings.TrimSpace(content)
|
||||
|
||||
var keyType, keyContent, keyComment string
|
||||
|
||||
if strings.HasPrefix(content, ssh2keyStart) {
|
||||
// Parse SSH2 file format.
|
||||
|
||||
// Transform all legal line endings to a single "\n".
|
||||
content = strings.NewReplacer("\r\n", "\n", "\r", "\n").Replace(content)
|
||||
|
||||
lines := strings.Split(content, "\n")
|
||||
continuationLine := false
|
||||
|
||||
for _, line := range lines {
|
||||
// Skip lines that:
|
||||
// 1) are a continuation of the previous line,
|
||||
// 2) contain ":" as that are comment lines
|
||||
// 3) contain "-" as that are begin and end tags
|
||||
if continuationLine || strings.ContainsAny(line, ":-") {
|
||||
continuationLine = strings.HasSuffix(line, "\\")
|
||||
} else {
|
||||
keyContent += line
|
||||
}
|
||||
}
|
||||
|
||||
t, err := extractTypeFromBase64Key(keyContent)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("extractTypeFromBase64Key: %v", err)
|
||||
}
|
||||
keyType = t
|
||||
} else {
|
||||
if strings.Contains(content, "-----BEGIN") {
|
||||
// Convert PEM Keys to OpenSSH format
|
||||
// Transform all legal line endings to a single "\n".
|
||||
content = strings.NewReplacer("\r\n", "\n", "\r", "\n").Replace(content)
|
||||
|
||||
block, _ := pem.Decode([]byte(content))
|
||||
if block == nil {
|
||||
return "", fmt.Errorf("failed to parse PEM block containing the public key")
|
||||
}
|
||||
|
||||
pub, err := x509.ParsePKIXPublicKey(block.Bytes)
|
||||
if err != nil {
|
||||
var pk rsa.PublicKey
|
||||
_, err2 := asn1.Unmarshal(block.Bytes, &pk)
|
||||
if err2 != nil {
|
||||
return "", fmt.Errorf("failed to parse DER encoded public key as either PKIX or PEM RSA Key: %v %v", err, err2)
|
||||
}
|
||||
pub = &pk
|
||||
}
|
||||
|
||||
sshKey, err := ssh.NewPublicKey(pub)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("unable to convert to ssh public key: %v", err)
|
||||
}
|
||||
content = string(ssh.MarshalAuthorizedKey(sshKey))
|
||||
}
|
||||
// Parse OpenSSH format.
|
||||
|
||||
// Remove all newlines
|
||||
content = strings.NewReplacer("\r\n", "", "\n", "").Replace(content)
|
||||
|
||||
parts := strings.SplitN(content, " ", 3)
|
||||
switch len(parts) {
|
||||
case 0:
|
||||
return "", errors.New("empty key")
|
||||
case 1:
|
||||
keyContent = parts[0]
|
||||
case 2:
|
||||
keyType = parts[0]
|
||||
keyContent = parts[1]
|
||||
default:
|
||||
keyType = parts[0]
|
||||
keyContent = parts[1]
|
||||
keyComment = parts[2]
|
||||
}
|
||||
|
||||
// If keyType is not given, extract it from content. If given, validate it.
|
||||
t, err := extractTypeFromBase64Key(keyContent)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("extractTypeFromBase64Key: %v", err)
|
||||
}
|
||||
if len(keyType) == 0 {
|
||||
keyType = t
|
||||
} else if keyType != t {
|
||||
return "", fmt.Errorf("key type and content does not match: %s - %s", keyType, t)
|
||||
}
|
||||
}
|
||||
// Finally we need to check whether we can actually read the proposed key:
|
||||
_, _, _, _, err := ssh.ParseAuthorizedKey([]byte(keyType + " " + keyContent + " " + keyComment))
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("invalid ssh public key: %v", err)
|
||||
}
|
||||
return keyType + " " + keyContent + " " + keyComment, nil
|
||||
}
|
||||
|
||||
// CheckPublicKeyString checks if the given public key string is recognized by SSH.
|
||||
// It returns the actual public key line on success.
|
||||
func CheckPublicKeyString(content string) (_ string, err error) {
|
||||
if setting.SSH.Disabled {
|
||||
return "", ErrSSHDisabled{}
|
||||
}
|
||||
|
||||
content, err = parseKeyString(content)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
content = strings.TrimRight(content, "\n\r")
|
||||
if strings.ContainsAny(content, "\n\r") {
|
||||
return "", errors.New("only a single line with a single key please")
|
||||
}
|
||||
|
||||
// remove any unnecessary whitespace now
|
||||
content = strings.TrimSpace(content)
|
||||
|
||||
if !setting.SSH.MinimumKeySizeCheck {
|
||||
return content, nil
|
||||
}
|
||||
|
||||
var (
|
||||
fnName string
|
||||
keyType string
|
||||
length int
|
||||
)
|
||||
if setting.SSH.StartBuiltinServer {
|
||||
fnName = "SSHNativeParsePublicKey"
|
||||
keyType, length, err = SSHNativeParsePublicKey(content)
|
||||
} else {
|
||||
fnName = "SSHKeyGenParsePublicKey"
|
||||
keyType, length, err = SSHKeyGenParsePublicKey(content)
|
||||
}
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("%s: %v", fnName, err)
|
||||
}
|
||||
log.Trace("Key info [native: %v]: %s-%d", setting.SSH.StartBuiltinServer, keyType, length)
|
||||
|
||||
if minLen, found := setting.SSH.MinimumKeySizes[keyType]; found && length >= minLen {
|
||||
return content, nil
|
||||
} else if found && length < minLen {
|
||||
return "", fmt.Errorf("key length is not enough: got %d, needs %d", length, minLen)
|
||||
}
|
||||
return "", fmt.Errorf("key type is not allowed: %s", keyType)
|
||||
}
|
||||
|
||||
// SSHNativeParsePublicKey extracts the key type and length using the golang SSH library.
|
||||
func SSHNativeParsePublicKey(keyLine string) (string, int, error) {
|
||||
fields := strings.Fields(keyLine)
|
||||
if len(fields) < 2 {
|
||||
return "", 0, fmt.Errorf("not enough fields in public key line: %s", keyLine)
|
||||
}
|
||||
|
||||
raw, err := base64.StdEncoding.DecodeString(fields[1])
|
||||
if err != nil {
|
||||
return "", 0, err
|
||||
}
|
||||
|
||||
pkey, err := ssh.ParsePublicKey(raw)
|
||||
if err != nil {
|
||||
if strings.Contains(err.Error(), "ssh: unknown key algorithm") {
|
||||
return "", 0, ErrKeyUnableVerify{err.Error()}
|
||||
}
|
||||
return "", 0, fmt.Errorf("ParsePublicKey: %v", err)
|
||||
}
|
||||
|
||||
// The ssh library can parse the key, so next we find out what key exactly we have.
|
||||
switch pkey.Type() {
|
||||
case ssh.KeyAlgoDSA:
|
||||
rawPub := struct {
|
||||
Name string
|
||||
P, Q, G, Y *big.Int
|
||||
}{}
|
||||
if err := ssh.Unmarshal(pkey.Marshal(), &rawPub); err != nil {
|
||||
return "", 0, err
|
||||
}
|
||||
// as per https://bugzilla.mindrot.org/show_bug.cgi?id=1647 we should never
|
||||
// see dsa keys != 1024 bit, but as it seems to work, we will not check here
|
||||
return "dsa", rawPub.P.BitLen(), nil // use P as per crypto/dsa/dsa.go (is L)
|
||||
case ssh.KeyAlgoRSA:
|
||||
rawPub := struct {
|
||||
Name string
|
||||
E *big.Int
|
||||
N *big.Int
|
||||
}{}
|
||||
if err := ssh.Unmarshal(pkey.Marshal(), &rawPub); err != nil {
|
||||
return "", 0, err
|
||||
}
|
||||
return "rsa", rawPub.N.BitLen(), nil // use N as per crypto/rsa/rsa.go (is bits)
|
||||
case ssh.KeyAlgoECDSA256:
|
||||
return "ecdsa", 256, nil
|
||||
case ssh.KeyAlgoECDSA384:
|
||||
return "ecdsa", 384, nil
|
||||
case ssh.KeyAlgoECDSA521:
|
||||
return "ecdsa", 521, nil
|
||||
case ssh.KeyAlgoED25519:
|
||||
return "ed25519", 256, nil
|
||||
case ssh.KeyAlgoSKECDSA256:
|
||||
return "ecdsa-sk", 256, nil
|
||||
case ssh.KeyAlgoSKED25519:
|
||||
return "ed25519-sk", 256, nil
|
||||
}
|
||||
return "", 0, fmt.Errorf("unsupported key length detection for type: %s", pkey.Type())
|
||||
}
|
||||
|
||||
// writeTmpKeyFile writes key content to a temporary file
|
||||
// and returns the name of that file, along with any possible errors.
|
||||
func writeTmpKeyFile(content string) (string, error) {
|
||||
tmpFile, err := ioutil.TempFile(setting.SSH.KeyTestPath, "gitea_keytest")
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("TempFile: %v", err)
|
||||
}
|
||||
defer tmpFile.Close()
|
||||
|
||||
if _, err = tmpFile.WriteString(content); err != nil {
|
||||
return "", fmt.Errorf("WriteString: %v", err)
|
||||
}
|
||||
return tmpFile.Name(), nil
|
||||
}
|
||||
|
||||
// SSHKeyGenParsePublicKey extracts key type and length using ssh-keygen.
|
||||
func SSHKeyGenParsePublicKey(key string) (string, int, error) {
|
||||
tmpName, err := writeTmpKeyFile(key)
|
||||
if err != nil {
|
||||
return "", 0, fmt.Errorf("writeTmpKeyFile: %v", err)
|
||||
}
|
||||
defer func() {
|
||||
if err := util.Remove(tmpName); err != nil {
|
||||
log.Warn("Unable to remove temporary key file: %s: Error: %v", tmpName, err)
|
||||
}
|
||||
}()
|
||||
|
||||
stdout, stderr, err := process.GetManager().Exec("SSHKeyGenParsePublicKey", setting.SSH.KeygenPath, "-lf", tmpName)
|
||||
if err != nil {
|
||||
return "", 0, fmt.Errorf("fail to parse public key: %s - %s", err, stderr)
|
||||
}
|
||||
if strings.Contains(stdout, "is not a public key file") {
|
||||
return "", 0, ErrKeyUnableVerify{stdout}
|
||||
}
|
||||
|
||||
fields := strings.Split(stdout, " ")
|
||||
if len(fields) < 4 {
|
||||
return "", 0, fmt.Errorf("invalid public key line: %s", stdout)
|
||||
}
|
||||
|
||||
keyType := strings.Trim(fields[len(fields)-1], "()\r\n")
|
||||
length, err := strconv.ParseInt(fields[0], 10, 32)
|
||||
if err != nil {
|
||||
return "", 0, err
|
||||
}
|
||||
return strings.ToLower(keyType), int(length), nil
|
||||
}
|
@ -0,0 +1,125 @@
|
||||
// Copyright 2021 The Gitea Authors. All rights reserved.
|
||||
// Use of this source code is governed by a MIT-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package models
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
)
|
||||
|
||||
// __________ .__ .__ .__
|
||||
// \______ _______|__| ____ ____ |_____________ | | ______
|
||||
// | ___\_ __ | |/ \_/ ___\| \____ \__ \ | | / ___/
|
||||
// | | | | \| | | \ \___| | |_> / __ \| |__\___ \
|
||||
// |____| |__| |__|___| /\___ |__| __(____ |____/____ >
|
||||
// \/ \/ |__| \/ \/
|
||||
//
|
||||
// This file contains functions related to principals
|
||||
|
||||
// AddPrincipalKey adds new principal to database and authorized_principals file.
|
||||
func AddPrincipalKey(ownerID int64, content string, loginSourceID int64) (*PublicKey, error) {
|
||||
sess := x.NewSession()
|
||||
defer sess.Close()
|
||||
if err := sess.Begin(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Principals cannot be duplicated.
|
||||
has, err := sess.
|
||||
Where("content = ? AND type = ?", content, KeyTypePrincipal).
|
||||
Get(new(PublicKey))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
} else if has {
|
||||
return nil, ErrKeyAlreadyExist{0, "", content}
|
||||
}
|
||||
|
||||
key := &PublicKey{
|
||||
OwnerID: ownerID,
|
||||
Name: content,
|
||||
Content: content,
|
||||
Mode: AccessModeWrite,
|
||||
Type: KeyTypePrincipal,
|
||||
LoginSourceID: loginSourceID,
|
||||
}
|
||||
if err = addPrincipalKey(sess, key); err != nil {
|
||||
return nil, fmt.Errorf("addKey: %v", err)
|
||||
}
|
||||
|
||||
if err = sess.Commit(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
sess.Close()
|
||||
|
||||
return key, RewriteAllPrincipalKeys()
|
||||
}
|
||||
|
||||
func addPrincipalKey(e Engine, key *PublicKey) (err error) {
|
||||
// Save Key representing a principal.
|
||||
if _, err = e.Insert(key); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// CheckPrincipalKeyString strips spaces and returns an error if the given principal contains newlines
|
||||
func CheckPrincipalKeyString(user *User, content string) (_ string, err error) {
|
||||
if setting.SSH.Disabled {
|
||||
return "", ErrSSHDisabled{}
|
||||
}
|
||||
|
||||
content = strings.TrimSpace(content)
|
||||
if strings.ContainsAny(content, "\r\n") {
|
||||
return "", errors.New("only a single line with a single principal please")
|
||||
}
|
||||
|
||||
// check all the allowed principals, email, username or anything
|
||||
// if any matches, return ok
|
||||
for _, v := range setting.SSH.AuthorizedPrincipalsAllow {
|
||||
switch v {
|
||||
case "anything":
|
||||
return content, nil
|
||||
case "email":
|
||||
emails, err := GetEmailAddresses(user.ID)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
for _, email := range emails {
|
||||
if !email.IsActivated {
|
||||
continue
|
||||
}
|
||||
if content == email.Email {
|
||||
return content, nil
|
||||
}
|
||||
}
|
||||
|
||||
case "username":
|
||||
if content == user.Name {
|
||||
return content, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return "", fmt.Errorf("didn't match allowed principals: %s", setting.SSH.AuthorizedPrincipalsAllow)
|
||||
}
|
||||
|
||||
// ListPrincipalKeys returns a list of principals belongs to given user.
|
||||
func ListPrincipalKeys(uid int64, listOptions ListOptions) ([]*PublicKey, error) {
|
||||
sess := x.Where("owner_id = ? AND type = ?", uid, KeyTypePrincipal)
|
||||
if listOptions.Page != 0 {
|
||||
sess = listOptions.setSessionPagination(sess)
|
||||
|
||||
keys := make([]*PublicKey, 0, listOptions.PageSize)
|
||||
return keys, sess.Find(&keys)
|
||||
}
|
||||
|
||||
keys := make([]*PublicKey, 0, 5)
|
||||
return keys, sess.Find(&keys)
|
||||
}
|
@ -0,0 +1,16 @@
|
||||
// Copyright 2021 The Gitea Authors. All rights reserved.
|
||||
// Use of this source code is governed by a MIT-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package models
|
||||
|
||||
import "github.com/lafriks/xormstore"
|
||||
|
||||
// CreateStore creates a xormstore for the provided table and key
|
||||
func CreateStore(table, key string) (*xormstore.Store, error) {
|
||||
store, err := xormstore.NewOptions(x, xormstore.Options{
|
||||
TableName: table,
|
||||
}, []byte(key))
|
||||
|
||||
return store, err
|
||||
}
|
@ -0,0 +1,113 @@
|
||||
// Copyright 2021 The Gitea Authors. All rights reserved.
|
||||
// Use of this source code is governed by a MIT-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package auth
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"code.gitea.io/gitea/models"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
|
||||
// Register the sources
|
||||
_ "code.gitea.io/gitea/services/auth/source/db"
|
||||
_ "code.gitea.io/gitea/services/auth/source/ldap"
|
||||
_ "code.gitea.io/gitea/services/auth/source/oauth2"
|
||||
_ "code.gitea.io/gitea/services/auth/source/pam"
|
||||
_ "code.gitea.io/gitea/services/auth/source/smtp"
|
||||
_ "code.gitea.io/gitea/services/auth/source/sspi"
|
||||
)
|
||||
|
||||
// UserSignIn validates user name and password.
|
||||
func UserSignIn(username, password string) (*models.User, error) {
|
||||
var user *models.User
|
||||
if strings.Contains(username, "@") {
|
||||
user = &models.User{Email: strings.ToLower(strings.TrimSpace(username))}
|
||||
// check same email
|
||||
cnt, err := models.Count(user)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if cnt > 1 {
|
||||
return nil, models.ErrEmailAlreadyUsed{
|
||||
Email: user.Email,
|
||||
}
|
||||
}
|
||||
} else {
|
||||
trimmedUsername := strings.TrimSpace(username)
|
||||
if len(trimmedUsername) == 0 {
|
||||
return nil, models.ErrUserNotExist{Name: username}
|
||||
}
|
||||
|
||||
user = &models.User{LowerName: strings.ToLower(trimmedUsername)}
|
||||
}
|
||||
|
||||
hasUser, err := models.GetUser(user)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if hasUser {
|
||||
source, err := models.GetLoginSourceByID(user.LoginSource)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if !source.IsActive {
|
||||
return nil, models.ErrLoginSourceNotActived
|
||||
}
|
||||
|
||||
authenticator, ok := source.Cfg.(PasswordAuthenticator)
|
||||
if !ok {
|
||||
return nil, models.ErrUnsupportedLoginType
|
||||
}
|
||||
|
||||
user, err := authenticator.Authenticate(user, username, password)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// WARN: DON'T check user.IsActive, that will be checked on reqSign so that
|
||||
// user could be hint to resend confirm email.
|
||||
if user.ProhibitLogin {
|
||||
return nil, models.ErrUserProhibitLogin{UID: user.ID, Name: user.Name}
|
||||
}
|
||||
|
||||
return user, nil
|
||||
}
|
||||
|
||||
sources, err := models.AllActiveLoginSources()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, source := range sources {
|
||||
if !source.IsActive {
|
||||
// don't try to authenticate non-active sources
|
||||
continue
|
||||
}
|
||||
|
||||
authenticator, ok := source.Cfg.(PasswordAuthenticator)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
authUser, err := authenticator.Authenticate(nil, username, password)
|
||||
|
||||
if err == nil {
|
||||
if !authUser.ProhibitLogin {
|
||||
return authUser, nil
|
||||
}
|
||||
err = models.ErrUserProhibitLogin{UID: authUser.ID, Name: authUser.Name}
|
||||
}
|
||||
|
||||
if models.IsErrUserNotExist(err) {
|
||||
log.Debug("Failed to login '%s' via '%s': %v", username, source.Name, err)
|
||||
} else {
|
||||
log.Warn("Failed to login '%s' via '%s': %v", username, source.Name, err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil, models.ErrUserNotExist{Name: username}
|
||||
}
|
@ -0,0 +1,21 @@
|
||||
// Copyright 2021 The Gitea Authors. All rights reserved.
|
||||
// Use of this source code is governed by a MIT-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package db_test
|
||||
|
||||
import (
|
||||
"code.gitea.io/gitea/models"
|
||||
"code.gitea.io/gitea/services/auth"
|
||||
"code.gitea.io/gitea/services/auth/source/db"
|
||||
)
|
||||
|
||||
// This test file exists to assert that our Source exposes the interfaces that we expect
|
||||
// It tightly binds the interfaces and implementation without breaking go import cycles
|
||||
|
||||
type sourceInterface interface {
|
||||
auth.PasswordAuthenticator
|
||||
models.LoginConfig
|
||||
}
|
||||
|
||||
var _ (sourceInterface) = &db.Source{}
|
@ -0,0 +1,42 @@
|
||||
// Copyright 2021 The Gitea Authors. All rights reserved.
|
||||
// Use of this source code is governed by a MIT-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package db
|
||||
|
||||
import (
|
||||
"code.gitea.io/gitea/models"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
)
|
||||
|
||||
// Authenticate authenticates the provided user against the DB
|
||||
func Authenticate(user *models.User, login, password string) (*models.User, error) {
|
||||
if user == nil {
|
||||
return nil, models.ErrUserNotExist{Name: login}
|
||||
}
|
||||
|
||||
if !user.IsPasswordSet() || !user.ValidatePassword(password) {
|
||||
return nil, models.ErrUserNotExist{UID: user.ID, Name: user.Name}
|
||||
}
|
||||
|
||||
// Update password hash if server password hash algorithm have changed
|
||||
if user.PasswdHashAlgo != setting.PasswordHashAlgo {
|
||||
if err := user.SetPassword(password); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := models.UpdateUserCols(user, "passwd", "passwd_hash_algo", "salt"); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
// WARN: DON'T check user.IsActive, that will be checked on reqSign so that
|
||||
// user could be hint to resend confirm email.
|
||||
if user.ProhibitLogin {
|
||||
return nil, models.ErrUserProhibitLogin{
|
||||
UID: user.ID,
|
||||
Name: user.Name,
|
||||
}
|
||||
}
|
||||
|
||||
return user, nil
|
||||
}
|
@ -0,0 +1,31 @@
|
||||
// Copyright 2021 The Gitea Authors. All rights reserved.
|
||||
// Use of this source code is governed by a MIT-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package db
|
||||
|
||||
import "code.gitea.io/gitea/models"
|
||||
|
||||
// Source is a password authentication service
|
||||
type Source struct{}
|
||||
|
||||
// FromDB fills up an OAuth2Config from serialized format.
|
||||
func (source *Source) FromDB(bs []byte) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// ToDB exports an SMTPConfig to a serialized format.
|
||||
func (source *Source) ToDB() ([]byte, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// Authenticate queries if login/password is valid against the PAM,
|
||||
// and create a local user if success when enabled.
|
||||
func (source *Source) Authenticate(user *models.User, login, password string) (*models.User, error) {
|
||||
return Authenticate(user, login, password)
|
||||
}
|
||||
|
||||
func init() {
|
||||
models.RegisterLoginTypeConfig(models.LoginNoType, &Source{})
|
||||
models.RegisterLoginTypeConfig(models.LoginPlain, &Source{})
|
||||
}
|
@ -0,0 +1,27 @@
|
||||
// Copyright 2021 The Gitea Authors. All rights reserved.
|
||||
// Use of this source code is governed by a MIT-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package ldap_test
|
||||
|
||||
import (
|
||||
"code.gitea.io/gitea/models"
|
||||
"code.gitea.io/gitea/services/auth"
|
||||
"code.gitea.io/gitea/services/auth/source/ldap"
|
||||
)
|
||||
|
||||
// This test file exists to assert that our Source exposes the interfaces that we expect
|
||||
// It tightly binds the interfaces and implementation without breaking go import cycles
|
||||
|
||||
type sourceInterface interface {
|
||||
auth.PasswordAuthenticator
|
||||
auth.SynchronizableSource
|
||||
models.SSHKeyProvider
|
||||
models.LoginConfig
|
||||
models.SkipVerifiable
|
||||
models.HasTLSer
|
||||
models.UseTLSer
|
||||
models.LoginSourceSettable
|
||||
}
|
||||
|
||||
var _ (sourceInterface) = &ldap.Source{}
|
@ -0,0 +1,27 @@
|
||||
// Copyright 2021 The Gitea Authors. All rights reserved.
|
||||
// Use of this source code is governed by a MIT-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package ldap
|
||||
|
||||
// SecurityProtocol protocol type
|
||||
type SecurityProtocol int
|
||||
|
||||
// Note: new type must be added at the end of list to maintain compatibility.
|
||||
const (
|
||||
SecurityProtocolUnencrypted SecurityProtocol = iota
|
||||
SecurityProtocolLDAPS
|
||||
SecurityProtocolStartTLS
|
||||
)
|
||||
|
||||
// String returns the name of the SecurityProtocol
|
||||
func (s SecurityProtocol) String() string {
|
||||
return SecurityProtocolNames[s]
|
||||
}
|
||||
|
||||
// SecurityProtocolNames contains the name of SecurityProtocol values.
|
||||
var SecurityProtocolNames = map[SecurityProtocol]string{
|
||||
SecurityProtocolUnencrypted: "Unencrypted",
|
||||
SecurityProtocolLDAPS: "LDAPS",
|
||||
SecurityProtocolStartTLS: "StartTLS",
|
||||
}
|
@ -0,0 +1,120 @@
|
||||
// Copyright 2021 The Gitea Authors. All rights reserved.
|
||||
// Use of this source code is governed by a MIT-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package ldap
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"code.gitea.io/gitea/models"
|
||||
"code.gitea.io/gitea/modules/secret"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
|
||||
jsoniter "github.com/json-iterator/go"
|
||||
)
|
||||
|
||||
// .____ ________ _____ __________
|
||||
// | | \______ \ / _ \\______ \
|
||||
// | | | | \ / /_\ \| ___/
|
||||
// | |___ | ` \/ | \ |
|
||||
// |_______ \/_______ /\____|__ /____|
|
||||
// \/ \/ \/
|
||||
|
||||
// Package ldap provide functions & structure to query a LDAP ldap directory
|
||||
// For now, it's mainly tested again an MS Active Directory service, see README.md for more information
|
||||
|
||||
// Source Basic LDAP authentication service
|
||||
type Source struct {
|
||||
Name string // canonical name (ie. corporate.ad)
|
||||
Host string // LDAP host
|
||||
Port int // port number
|
||||
SecurityProtocol SecurityProtocol
|
||||
SkipVerify bool
|
||||
BindDN string // DN to bind with
|
||||
BindPasswordEncrypt string // Encrypted Bind BN password
|
||||
BindPassword string // Bind DN password
|
||||
UserBase string // Base search path for users
|
||||
UserDN string // Template for the DN of the user for simple auth
|
||||
AttributeUsername string // Username attribute
|
||||
AttributeName string // First name attribute
|
||||
AttributeSurname string // Surname attribute
|
||||
AttributeMail string // E-mail attribute
|
||||
AttributesInBind bool // fetch attributes in bind context (not user)
|
||||
AttributeSSHPublicKey string // LDAP SSH Public Key attribute
|
||||
SearchPageSize uint32 // Search with paging page size
|
||||
Filter string // Query filter to validate entry
|
||||
AdminFilter string // Query filter to check if user is admin
|
||||
RestrictedFilter string // Query filter to check if user is restricted
|
||||
Enabled bool // if this source is disabled
|
||||
AllowDeactivateAll bool // Allow an empty search response to deactivate all users from this source
|
||||
GroupsEnabled bool // if the group checking is enabled
|
||||
GroupDN string // Group Search Base
|
||||
GroupFilter string // Group Name Filter
|
||||
GroupMemberUID string // Group Attribute containing array of UserUID
|
||||
UserUID string // User Attribute listed in Group
|
||||
|
||||
// reference to the loginSource
|
||||
loginSource *models.LoginSource
|
||||
}
|
||||
|
||||
// FromDB fills up a LDAPConfig from serialized format.
|
||||
func (source *Source) FromDB(bs []byte) error {
|
||||
err := models.JSONUnmarshalHandleDoubleEncode(bs, &source)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if source.BindPasswordEncrypt != "" {
|
||||
source.BindPassword, err = secret.DecryptSecret(setting.SecretKey, source.BindPasswordEncrypt)
|
||||
source.BindPasswordEncrypt = ""
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// ToDB exports a LDAPConfig to a serialized format.
|
||||
func (source *Source) ToDB() ([]byte, error) {
|
||||
var err error
|
||||
source.BindPasswordEncrypt, err = secret.EncryptSecret(setting.SecretKey, source.BindPassword)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
source.BindPassword = ""
|
||||
json := jsoniter.ConfigCompatibleWithStandardLibrary
|
||||
return json.Marshal(source)
|
||||
}
|
||||
|
||||
// SecurityProtocolName returns the name of configured security
|
||||
// protocol.
|
||||
func (source *Source) SecurityProtocolName() string {
|
||||
return SecurityProtocolNames[source.SecurityProtocol]
|
||||
}
|
||||
|
||||
// IsSkipVerify returns if SkipVerify is set
|
||||
func (source *Source) IsSkipVerify() bool {
|
||||
return source.SkipVerify
|
||||
}
|
||||
|
||||
// HasTLS returns if HasTLS
|
||||
func (source *Source) HasTLS() bool {
|
||||
return source.SecurityProtocol > SecurityProtocolUnencrypted
|
||||
}
|
||||
|
||||
// UseTLS returns if UseTLS
|
||||
func (source *Source) UseTLS() bool {
|
||||
return source.SecurityProtocol != SecurityProtocolUnencrypted
|
||||
}
|
||||
|
||||
// ProvidesSSHKeys returns if this source provides SSH Keys
|
||||
func (source *Source) ProvidesSSHKeys() bool {
|
||||
return len(strings.TrimSpace(source.AttributeSSHPublicKey)) > 0
|
||||
}
|
||||
|
||||
// SetLoginSource sets the related LoginSource
|
||||
func (source *Source) SetLoginSource(loginSource *models.LoginSource) {
|
||||
source.loginSource = loginSource
|
||||
}
|
||||
|
||||
func init() {
|
||||
models.RegisterLoginTypeConfig(models.LoginLDAP, &Source{})
|
||||
models.RegisterLoginTypeConfig(models.LoginDLDAP, &Source{})
|
||||
}
|
@ -0,0 +1,93 @@
|
||||
// Copyright 2021 The Gitea Authors. All rights reserved.
|
||||
// Use of this source code is governed by a MIT-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package ldap
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"code.gitea.io/gitea/models"
|
||||
)
|
||||
|
||||
// Authenticate queries if login/password is valid against the LDAP directory pool,
|
||||
// and create a local user if success when enabled.
|
||||
func (source *Source) Authenticate(user *models.User, login, password string) (*models.User, error) {
|
||||
sr := source.SearchEntry(login, password, source.loginSource.Type == models.LoginDLDAP)
|
||||
if sr == nil {
|
||||
// User not in LDAP, do nothing
|
||||
return nil, models.ErrUserNotExist{Name: login}
|
||||
}
|
||||
|
||||
isAttributeSSHPublicKeySet := len(strings.TrimSpace(source.AttributeSSHPublicKey)) > 0
|
||||
|
||||
// Update User admin flag if exist
|
||||
if isExist, err := models.IsUserExist(0, sr.Username); err != nil {
|
||||
return nil, err
|
||||
} else if isExist {
|
||||
if user == nil {
|
||||
user, err = models.GetUserByName(sr.Username)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
if user != nil && !user.ProhibitLogin {
|
||||
cols := make([]string, 0)
|
||||
if len(source.AdminFilter) > 0 && user.IsAdmin != sr.IsAdmin {
|
||||
// Change existing admin flag only if AdminFilter option is set
|
||||
user.IsAdmin = sr.IsAdmin
|
||||
cols = append(cols, "is_admin")
|
||||
}
|
||||
if !user.IsAdmin && len(source.RestrictedFilter) > 0 && user.IsRestricted != sr.IsRestricted {
|
||||
// Change existing restricted flag only if RestrictedFilter option is set
|
||||
user.IsRestricted = sr.IsRestricted
|
||||
cols = append(cols, "is_restricted")
|
||||
}
|
||||
if len(cols) > 0 {
|
||||
err = models.UpdateUserCols(user, cols...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if user != nil {
|
||||
if isAttributeSSHPublicKeySet && models.SynchronizePublicKeys(user, source.loginSource, sr.SSHPublicKey) {
|
||||
return user, models.RewriteAllPublicKeys()
|
||||
}
|
||||
|
||||
return user, nil
|
||||
}
|
||||
|
||||
// Fallback.
|
||||
if len(sr.Username) == 0 {
|
||||
sr.Username = login
|
||||
}
|
||||
|
||||
if len(sr.Mail) == 0 {
|
||||
sr.Mail = fmt.Sprintf("%s@localhost", sr.Username)
|
||||
}
|
||||
|
||||
user = &models.User{
|
||||
LowerName: strings.ToLower(sr.Username),
|
||||
Name: sr.Username,
|
||||
FullName: composeFullName(sr.Name, sr.Surname, sr.Username),
|
||||
Email: sr.Mail,
|
||||
LoginType: source.loginSource.Type,
|
||||
LoginSource: source.loginSource.ID,
|
||||
LoginName: login,
|
||||
IsActive: true,
|
||||
IsAdmin: sr.IsAdmin,
|
||||
IsRestricted: sr.IsRestricted,
|
||||
}
|
||||
|
||||
err := models.CreateUser(user)
|
||||
|
||||
if err == nil && isAttributeSSHPublicKeySet && models.AddPublicKeysBySource(user, source.loginSource, sr.SSHPublicKey) {
|
||||
err = models.RewriteAllPublicKeys()
|
||||
}
|
||||
|
||||
return user, err
|
||||
}
|
@ -0,0 +1,184 @@
|
||||
// Copyright 2021 The Gitea Authors. All rights reserved.
|
||||
// Use of this source code is governed by a MIT-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package ldap
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"code.gitea.io/gitea/models"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
)
|
||||
|
||||
// Sync causes this ldap source to synchronize its users with the db
|
||||
func (source *Source) Sync(ctx context.Context, updateExisting bool) error {
|
||||
log.Trace("Doing: SyncExternalUsers[%s]", source.loginSource.Name)
|
||||
|
||||
var existingUsers []int64
|
||||
isAttributeSSHPublicKeySet := len(strings.TrimSpace(source.AttributeSSHPublicKey)) > 0
|
||||
var sshKeysNeedUpdate bool
|
||||
|
||||
// Find all users with this login type - FIXME: Should this be an iterator?
|
||||
users, err := models.GetUsersBySource(source.loginSource)
|
||||
if err != nil {
|
||||
log.Error("SyncExternalUsers: %v", err)
|
||||
return err
|
||||
}
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
log.Warn("SyncExternalUsers: Cancelled before update of %s", source.loginSource.Name)
|
||||
return models.ErrCancelledf("Before update of %s", source.loginSource.Name)
|
||||
default:
|
||||
}
|
||||
|
||||
sr, err := source.SearchEntries()
|
||||
if err != nil {
|
||||
log.Error("SyncExternalUsers LDAP source failure [%s], skipped", source.loginSource.Name)
|
||||
return nil
|
||||
}
|
||||
|
||||
if len(sr) == 0 {
|
||||
if !source.AllowDeactivateAll {
|
||||
log.Error("LDAP search found no entries but did not report an error. Refusing to deactivate all users")
|
||||
return nil
|
||||
}
|
||||
log.Warn("LDAP search found no entries but did not report an error. All users will be deactivated as per settings")
|
||||
}
|
||||
|
||||
for _, su := range sr {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
log.Warn("SyncExternalUsers: Cancelled at update of %s before completed update of users", source.loginSource.Name)
|
||||
// Rewrite authorized_keys file if LDAP Public SSH Key attribute is set and any key was added or removed
|
||||
if sshKeysNeedUpdate {
|
||||
err = models.RewriteAllPublicKeys()
|
||||
if err != nil {
|
||||
log.Error("RewriteAllPublicKeys: %v", err)
|
||||
}
|
||||
}
|
||||
return models.ErrCancelledf("During update of %s before completed update of users", source.loginSource.Name)
|
||||
default:
|
||||
}
|
||||
if len(su.Username) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
if len(su.Mail) == 0 {
|
||||
su.Mail = fmt.Sprintf("%s@localhost", su.Username)
|
||||
}
|
||||
|
||||
var usr *models.User
|
||||
// Search for existing user
|
||||
for _, du := range users {
|
||||
if du.LowerName == strings.ToLower(su.Username) {
|
||||
usr = du
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
fullName := composeFullName(su.Name, su.Surname, su.Username)
|
||||
// If no existing user found, create one
|
||||
if usr == nil {
|
||||
log.Trace("SyncExternalUsers[%s]: Creating user %s", source.loginSource.Name, su.Username)
|
||||
|
||||
usr = &models.User{
|
||||
LowerName: strings.ToLower(su.Username),
|
||||
Name: su.Username,
|
||||
FullName: fullName,
|
||||
LoginType: source.loginSource.Type,
|
||||
LoginSource: source.loginSource.ID,
|
||||
LoginName: su.Username,
|
||||
Email: su.Mail,
|
||||
IsAdmin: su.IsAdmin,
|
||||
IsRestricted: su.IsRestricted,
|
||||
IsActive: true,
|
||||
}
|
||||
|
||||
err = models.CreateUser(usr)
|
||||
|
||||
if err != nil {
|
||||
log.Error("SyncExternalUsers[%s]: Error creating user %s: %v", source.loginSource.Name, su.Username, err)
|
||||
} else if isAttributeSSHPublicKeySet {
|
||||
log.Trace("SyncExternalUsers[%s]: Adding LDAP Public SSH Keys for user %s", source.loginSource.Name, usr.Name)
|
||||
if models.AddPublicKeysBySource(usr, source.loginSource, su.SSHPublicKey) {
|
||||
sshKeysNeedUpdate = true
|
||||
}
|
||||
}
|
||||
} else if updateExisting {
|
||||
existingUsers = append(existingUsers, usr.ID)
|
||||
|
||||
// Synchronize SSH Public Key if that attribute is set
|
||||
if isAttributeSSHPublicKeySet && models.SynchronizePublicKeys(usr, source.loginSource, su.SSHPublicKey) {
|
||||
sshKeysNeedUpdate = true
|
||||
}
|
||||
|
||||
// Check if user data has changed
|
||||
if (len(source.AdminFilter) > 0 && usr.IsAdmin != su.IsAdmin) ||
|
||||
(len(source.RestrictedFilter) > 0 && usr.IsRestricted != su.IsRestricted) ||
|
||||
!strings.EqualFold(usr.Email, su.Mail) ||
|
||||
usr.FullName != fullName ||
|
||||
!usr.IsActive {
|
||||
|
||||
log.Trace("SyncExternalUsers[%s]: Updating user %s", source.loginSource.Name, usr.Name)
|
||||
|
||||
usr.FullName = fullName
|
||||
usr.Email = su.Mail
|
||||
// Change existing admin flag only if AdminFilter option is set
|
||||
if len(source.AdminFilter) > 0 {
|
||||
usr.IsAdmin = su.IsAdmin
|
||||
}
|
||||
// Change existing restricted flag only if RestrictedFilter option is set
|
||||
if !usr.IsAdmin && len(source.RestrictedFilter) > 0 {
|
||||
usr.IsRestricted = su.IsRestricted
|
||||
}
|
||||
usr.IsActive = true
|
||||
|
||||
err = models.UpdateUserCols(usr, "full_name", "email", "is_admin", "is_restricted", "is_active")
|
||||
if err != nil {
|
||||
log.Error("SyncExternalUsers[%s]: Error updating user %s: %v", source.loginSource.Name, usr.Name, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Rewrite authorized_keys file if LDAP Public SSH Key attribute is set and any key was added or removed
|
||||
if sshKeysNeedUpdate {
|
||||
err = models.RewriteAllPublicKeys()
|
||||
if err != nil {
|
||||
log.Error("RewriteAllPublicKeys: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
log.Warn("SyncExternalUsers: Cancelled during update of %s before delete users", source.loginSource.Name)
|
||||
return models.ErrCancelledf("During update of %s before delete users", source.loginSource.Name)
|
||||
default:
|
||||
}
|
||||
|
||||
// Deactivate users not present in LDAP
|
||||
if updateExisting {
|
||||
for _, usr := range users {
|
||||
found := false
|
||||
for _, uid := range existingUsers {
|
||||
if usr.ID == uid {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
log.Trace("SyncExternalUsers[%s]: Deactivating user %s", source.loginSource.Name, usr.Name)
|
||||
|
||||
usr.IsActive = false
|
||||
err = models.UpdateUserCols(usr, "is_active")
|
||||
if err != nil {
|
||||
log.Error("SyncExternalUsers[%s]: Error deactivating user %s: %v", source.loginSource.Name, usr.Name, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
@ -0,0 +1,19 @@
|
||||
// Copyright 2021 The Gitea Authors. All rights reserved.
|
||||
// Use of this source code is governed by a MIT-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package ldap
|
||||
|
||||
// composeFullName composes a firstname surname or username
|
||||
func composeFullName(firstname, surname, username string) string {
|
||||
switch {
|
||||
case len(firstname) == 0 && len(surname) == 0:
|
||||
return username
|
||||
case len(firstname) == 0:
|
||||
return surname
|
||||
case len(surname) == 0:
|
||||
return firstname
|
||||
default:
|
||||
return firstname + " " + surname
|
||||
}
|
||||
}
|
@ -0,0 +1,23 @@
|
||||
// Copyright 2021 The Gitea Authors. All rights reserved.
|
||||
// Use of this source code is governed by a MIT-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package oauth2_test
|
||||
|
||||
import (
|
||||
"code.gitea.io/gitea/models"
|
||||
"code.gitea.io/gitea/services/auth"
|
||||
"code.gitea.io/gitea/services/auth/source/oauth2"
|
||||
)
|
||||
|
||||
// This test file exists to assert that our Source exposes the interfaces that we expect
|
||||
// It tightly binds the interfaces and implementation without breaking go import cycles
|
||||
|
||||
type sourceInterface interface {
|
||||
models.LoginConfig
|
||||
models.LoginSourceSettable
|
||||
models.RegisterableSource
|
||||
auth.PasswordAuthenticator
|
||||
}
|
||||
|
||||
var _ (sourceInterface) = &oauth2.Source{}
|
@ -0,0 +1,83 @@
|
||||
// Copyright 2021 The Gitea Authors. All rights reserved.
|
||||
// Use of this source code is governed by a MIT-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package oauth2
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"code.gitea.io/gitea/models"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/markbates/goth/gothic"
|
||||
)
|
||||
|
||||
// SessionTableName is the table name that OAuth2 will use to store things
|
||||
const SessionTableName = "oauth2_session"
|
||||
|
||||
// UsersStoreKey is the key for the store
|
||||
const UsersStoreKey = "gitea-oauth2-sessions"
|
||||
|
||||
// ProviderHeaderKey is the HTTP header key
|
||||
const ProviderHeaderKey = "gitea-oauth2-provider"
|
||||
|
||||
// Init initializes the oauth source
|
||||
func Init() error {
|
||||
if err := InitSigningKey(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
store, err := models.CreateStore(SessionTableName, UsersStoreKey)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// according to the Goth lib:
|
||||
// set the maxLength of the cookies stored on the disk to a larger number to prevent issues with:
|
||||
// securecookie: the value is too long
|
||||
// when using OpenID Connect , since this can contain a large amount of extra information in the id_token
|
||||
|
||||
// Note, when using the FilesystemStore only the session.ID is written to a browser cookie, so this is explicit for the storage on disk
|
||||
store.MaxLength(setting.OAuth2.MaxTokenLength)
|
||||
gothic.Store = store
|
||||
|
||||
gothic.SetState = func(req *http.Request) string {
|
||||
return uuid.New().String()
|
||||
}
|
||||
|
||||
gothic.GetProviderName = func(req *http.Request) (string, error) {
|
||||
return req.Header.Get(ProviderHeaderKey), nil
|
||||
}
|
||||
|
||||
return initOAuth2LoginSources()
|
||||
}
|
||||
|
||||
// ResetOAuth2 clears existing OAuth2 providers and loads them from DB
|
||||
func ResetOAuth2() error {
|
||||
ClearProviders()
|
||||
return initOAuth2LoginSources()
|
||||
}
|
||||
|
||||
// initOAuth2LoginSources is used to load and register all active OAuth2 providers
|
||||
func initOAuth2LoginSources() error {
|
||||
loginSources, _ := models.GetActiveOAuth2ProviderLoginSources()
|
||||
for _, source := range loginSources {
|
||||
oauth2Source, ok := source.Cfg.(*Source)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
err := oauth2Source.RegisterSource()
|
||||
if err != nil {
|
||||
log.Critical("Unable to register source: %s due to Error: %v. This source will be disabled.", source.Name, err)
|
||||
source.IsActive = false
|
||||
if err = models.UpdateSource(source); err != nil {
|
||||
log.Critical("Unable to update source %s to disable it. Error: %v", err)
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
@ -0,0 +1,51 @@
|
||||
// Copyright 2021 The Gitea Authors. All rights reserved.
|
||||
// Use of this source code is governed by a MIT-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package oauth2
|
||||
|
||||
import (
|
||||
"code.gitea.io/gitea/models"
|
||||
|
||||
jsoniter "github.com/json-iterator/go"
|
||||
)
|
||||
|
||||
// ________ _____ __ .__ ________
|
||||
// \_____ \ / _ \ __ ___/ |_| |__ \_____ \
|
||||
// / | \ / /_\ \| | \ __\ | \ / ____/
|
||||
// / | \/ | \ | /| | | Y \/ \
|
||||
// \_______ /\____|__ /____/ |__| |___| /\_______ \
|
||||
// \/ \/ \/ \/
|
||||
|
||||
// Source holds configuration for the OAuth2 login source.
|
||||
type Source struct {
|
||||
Provider string
|
||||
ClientID string
|
||||
ClientSecret string
|
||||
OpenIDConnectAutoDiscoveryURL string
|
||||
CustomURLMapping *CustomURLMapping
|
||||
IconURL string
|
||||
|
||||
// reference to the loginSource
|
||||
loginSource *models.LoginSource
|
||||
}
|
||||
|
||||
// FromDB fills up an OAuth2Config from serialized format.
|
||||
func (source *Source) FromDB(bs []byte) error {
|
||||
return models.JSONUnmarshalHandleDoubleEncode(bs, &source)
|
||||
}
|
||||
|
||||
// ToDB exports an SMTPConfig to a serialized format.
|
||||
func (source *Source) ToDB() ([]byte, error) {
|
||||
json := jsoniter.ConfigCompatibleWithStandardLibrary
|
||||
return json.Marshal(source)
|
||||
}
|
||||
|
||||
// SetLoginSource sets the related LoginSource
|
||||
func (source *Source) SetLoginSource(loginSource *models.LoginSource) {
|
||||
source.loginSource = loginSource
|
||||
}
|
||||
|
||||
func init() {
|
||||
models.RegisterLoginTypeConfig(models.LoginOAuth2, &Source{})
|
||||
}
|
@ -0,0 +1,15 @@
|
||||
// Copyright 2021 The Gitea Authors. All rights reserved.
|
||||
// Use of this source code is governed by a MIT-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package oauth2
|
||||
|
||||
import (
|
||||
"code.gitea.io/gitea/models"
|
||||
"code.gitea.io/gitea/services/auth/source/db"
|
||||
)
|
||||
|
||||
// Authenticate falls back to the db authenticator
|
||||
func (source *Source) Authenticate(user *models.User, login, password string) (*models.User, error) {
|
||||
return db.Authenticate(user, login, password)
|
||||
}
|
@ -0,0 +1,42 @@
|
||||
// Copyright 2021 The Gitea Authors. All rights reserved.
|
||||
// Use of this source code is governed by a MIT-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package oauth2
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/markbates/goth"
|
||||
"github.com/markbates/goth/gothic"
|
||||
)
|
||||
|
||||
// Callout redirects request/response pair to authenticate against the provider
|
||||
func (source *Source) Callout(request *http.Request, response http.ResponseWriter) error {
|
||||
// not sure if goth is thread safe (?) when using multiple providers
|
||||
request.Header.Set(ProviderHeaderKey, source.loginSource.Name)
|
||||
|
||||
// don't use the default gothic begin handler to prevent issues when some error occurs
|
||||
// normally the gothic library will write some custom stuff to the response instead of our own nice error page
|
||||
//gothic.BeginAuthHandler(response, request)
|
||||
|
||||
url, err := gothic.GetAuthURL(response, request)
|
||||
if err == nil {
|
||||
http.Redirect(response, request, url, http.StatusTemporaryRedirect)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// Callback handles OAuth callback, resolve to a goth user and send back to original url
|
||||
// this will trigger a new authentication request, but because we save it in the session we can use that
|
||||
func (source *Source) Callback(request *http.Request, response http.ResponseWriter) (goth.User, error) {
|
||||
// not sure if goth is thread safe (?) when using multiple providers
|
||||
request.Header.Set(ProviderHeaderKey, source.loginSource.Name)
|
||||
|
||||
user, err := gothic.CompleteUserAuth(response, request)
|
||||
if err != nil {
|
||||
return user, err
|
||||
}
|
||||
|
||||
return user, nil
|
||||
}
|
@ -0,0 +1,30 @@
|
||||
// Copyright 2021 The Gitea Authors. All rights reserved.
|
||||
// Use of this source code is governed by a MIT-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package oauth2
|
||||
|
||||
import (
|
||||
"code.gitea.io/gitea/models"
|
||||
)
|
||||
|
||||
// RegisterSource causes an OAuth2 configuration to be registered
|
||||
func (source *Source) RegisterSource() error {
|
||||
err := RegisterProvider(source.loginSource.Name, source.Provider, source.ClientID, source.ClientSecret, source.OpenIDConnectAutoDiscoveryURL, source.CustomURLMapping)
|
||||
return wrapOpenIDConnectInitializeError(err, source.loginSource.Name, source)
|
||||
}
|
||||
|
||||
// UnregisterSource causes an OAuth2 configuration to be unregistered
|
||||
func (source *Source) UnregisterSource() error {
|
||||
RemoveProvider(source.loginSource.Name)
|
||||
return nil
|
||||
}
|
||||
|
||||
// wrapOpenIDConnectInitializeError is used to wrap the error but this cannot be done in modules/auth/oauth2
|
||||
// inside oauth2: import cycle not allowed models -> modules/auth/oauth2 -> models
|
||||
func wrapOpenIDConnectInitializeError(err error, providerName string, source *Source) error {
|
||||
if err != nil && source.Provider == "openidConnect" {
|
||||
err = models.ErrOpenIDConnectInitialize{ProviderName: providerName, OpenIDConnectAutoDiscoveryURL: source.OpenIDConnectAutoDiscoveryURL, Cause: err}
|
||||
}
|
||||
return err
|
||||
}
|
@ -0,0 +1,94 @@
|
||||
// Copyright 2021 The Gitea Authors. All rights reserved.
|
||||
// Use of this source code is governed by a MIT-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package oauth2
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"code.gitea.io/gitea/modules/timeutil"
|
||||
"github.com/dgrijalva/jwt-go"
|
||||
)
|
||||
|
||||
// ___________ __
|
||||
// \__ ___/___ | | __ ____ ____
|
||||
// | | / _ \| |/ // __ \ / \
|
||||
// | |( <_> ) <\ ___/| | \
|
||||
// |____| \____/|__|_ \\___ >___| /
|
||||
// \/ \/ \/
|
||||
|
||||
// Token represents an Oauth grant
|
||||
|
||||
// TokenType represents the type of token for an oauth application
|
||||
type TokenType int
|
||||
|
||||
const (
|
||||
// TypeAccessToken is a token with short lifetime to access the api
|
||||
TypeAccessToken TokenType = 0
|
||||
// TypeRefreshToken is token with long lifetime to refresh access tokens obtained by the client
|
||||
TypeRefreshToken = iota
|
||||
)
|
||||
|
||||
// Token represents a JWT token used to authenticate a client
|
||||
type Token struct {
|
||||
GrantID int64 `json:"gnt"`
|
||||
Type TokenType `json:"tt"`
|
||||
Counter int64 `json:"cnt,omitempty"`
|
||||
jwt.StandardClaims
|
||||
}
|
||||
|
||||
// ParseToken parses a signed jwt string
|
||||
func ParseToken(jwtToken string) (*Token, error) {
|
||||
parsedToken, err := jwt.ParseWithClaims(jwtToken, &Token{}, func(token *jwt.Token) (interface{}, error) {
|
||||
if token.Method == nil || token.Method.Alg() != DefaultSigningKey.SigningMethod().Alg() {
|
||||
return nil, fmt.Errorf("unexpected signing algo: %v", token.Header["alg"])
|
||||
}
|
||||
return DefaultSigningKey.VerifyKey(), nil
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var token *Token
|
||||
var ok bool
|
||||
if token, ok = parsedToken.Claims.(*Token); !ok || !parsedToken.Valid {
|
||||
return nil, fmt.Errorf("invalid token")
|
||||
}
|
||||
return token, nil
|
||||
}
|
||||
|
||||
// SignToken signs the token with the JWT secret
|
||||
func (token *Token) SignToken() (string, error) {
|
||||
token.IssuedAt = time.Now().Unix()
|
||||
jwtToken := jwt.NewWithClaims(DefaultSigningKey.SigningMethod(), token)
|
||||
DefaultSigningKey.PreProcessToken(jwtToken)
|
||||
return jwtToken.SignedString(DefaultSigningKey.SignKey())
|
||||
}
|
||||
|
||||
// OIDCToken represents an OpenID Connect id_token
|
||||
type OIDCToken struct {
|
||||
jwt.StandardClaims
|
||||
Nonce string `json:"nonce,omitempty"`
|
||||
|
||||
// Scope profile
|
||||
Name string `json:"name,omitempty"`
|
||||
PreferredUsername string `json:"preferred_username,omitempty"`
|
||||
Profile string `json:"profile,omitempty"`
|
||||
Picture string `json:"picture,omitempty"`
|
||||
Website string `json:"website,omitempty"`
|
||||
Locale string `json:"locale,omitempty"`
|
||||
UpdatedAt timeutil.TimeStamp `json:"updated_at,omitempty"`
|
||||
|
||||
// Scope email
|
||||
Email string `json:"email,omitempty"`
|
||||
EmailVerified bool `json:"email_verified,omitempty"`
|
||||
}
|
||||
|
||||
// SignToken signs an id_token with the (symmetric) client secret key
|
||||
func (token *OIDCToken) SignToken(signingKey JWTSigningKey) (string, error) {
|
||||
token.IssuedAt = time.Now().Unix()
|
||||
jwtToken := jwt.NewWithClaims(signingKey.SigningMethod(), token)
|
||||
signingKey.PreProcessToken(jwtToken)
|
||||
return jwtToken.SignedString(signingKey.SignKey())
|
||||
}
|
@ -0,0 +1,24 @@
|
||||
// Copyright 2021 The Gitea Authors. All rights reserved.
|
||||
// Use of this source code is governed by a MIT-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package oauth2
|
||||
|
||||
// CustomURLMapping describes the urls values to use when customizing OAuth2 provider URLs
|
||||
type CustomURLMapping struct {
|
||||
AuthURL string
|
||||
TokenURL string
|
||||
ProfileURL string
|
||||
EmailURL string
|
||||
}
|
||||
|
||||
// DefaultCustomURLMappings contains the map of default URL's for OAuth2 providers that are allowed to have custom urls
|
||||
// key is used to map the OAuth2Provider
|
||||
// value is the mapping as defined for the OAuth2Provider
|
||||
var DefaultCustomURLMappings = map[string]*CustomURLMapping{
|
||||
"github": Providers["github"].CustomURLMapping,
|
||||
"gitlab": Providers["gitlab"].CustomURLMapping,
|
||||
"gitea": Providers["gitea"].CustomURLMapping,
|
||||
"nextcloud": Providers["nextcloud"].CustomURLMapping,
|
||||
"mastodon": Providers["mastodon"].CustomURLMapping,
|
||||
}
|
@ -0,0 +1,22 @@
|
||||
// Copyright 2021 The Gitea Authors. All rights reserved.
|
||||
// Use of this source code is governed by a MIT-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package pam_test
|
||||
|
||||
import (
|
||||
"code.gitea.io/gitea/models"
|
||||
"code.gitea.io/gitea/services/auth"
|
||||
"code.gitea.io/gitea/services/auth/source/pam"
|
||||
)
|
||||
|
||||
// This test file exists to assert that our Source exposes the interfaces that we expect
|
||||
// It tightly binds the interfaces and implementation without breaking go import cycles
|
||||
|
||||
type sourceInterface interface {
|
||||
auth.PasswordAuthenticator
|
||||
models.LoginConfig
|
||||
models.LoginSourceSettable
|
||||
}
|
||||
|
||||
var _ (sourceInterface) = &pam.Source{}
|
@ -0,0 +1,47 @@
|
||||
// Copyright 2021 The Gitea Authors. All rights reserved.
|
||||
// Use of this source code is governed by a MIT-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package pam
|
||||
|
||||
import (
|
||||
"code.gitea.io/gitea/models"
|
||||
|
||||
jsoniter "github.com/json-iterator/go"
|
||||
)
|
||||
|
||||
// __________ _____ _____
|
||||
// \______ \/ _ \ / \
|
||||
// | ___/ /_\ \ / \ / \
|
||||
// | | / | \/ Y \
|
||||
// |____| \____|__ /\____|__ /
|
||||
// \/ \/
|
||||
|
||||
// Source holds configuration for the PAM login source.
|
||||
type Source struct {
|
||||
ServiceName string // pam service (e.g. system-auth)
|
||||
EmailDomain string
|
||||
|
||||
// reference to the loginSource
|
||||
loginSource *models.LoginSource
|
||||
}
|
||||
|
||||
// FromDB fills up a PAMConfig from serialized format.
|
||||
func (source *Source) FromDB(bs []byte) error {
|
||||
return models.JSONUnmarshalHandleDoubleEncode(bs, &source)
|
||||
}
|
||||
|
||||
// ToDB exports a PAMConfig to a serialized format.
|
||||
func (source *Source) ToDB() ([]byte, error) {
|
||||
json := jsoniter.ConfigCompatibleWithStandardLibrary
|
||||
return json.Marshal(source)
|
||||
}
|
||||
|
||||
// SetLoginSource sets the related LoginSource
|
||||
func (source *Source) SetLoginSource(loginSource *models.LoginSource) {
|
||||
source.loginSource = loginSource
|
||||
}
|
||||
|
||||
func init() {
|
||||
models.RegisterLoginTypeConfig(models.LoginPAM, &Source{})
|
||||
}
|
@ -0,0 +1,62 @@
|
||||
// Copyright 2021 The Gitea Authors. All rights reserved.
|
||||
// Use of this source code is governed by a MIT-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package pam
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"code.gitea.io/gitea/models"
|
||||
"code.gitea.io/gitea/modules/auth/pam"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
// Authenticate queries if login/password is valid against the PAM,
|
||||
// and create a local user if success when enabled.
|
||||
func (source *Source) Authenticate(user *models.User, login, password string) (*models.User, error) {
|
||||
pamLogin, err := pam.Auth(source.ServiceName, login, password)
|
||||
if err != nil {
|
||||
if strings.Contains(err.Error(), "Authentication failure") {
|
||||
return nil, models.ErrUserNotExist{Name: login}
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if user != nil {
|
||||
return user, nil
|
||||
}
|
||||
|
||||
// Allow PAM sources with `@` in their name, like from Active Directory
|
||||
username := pamLogin
|
||||
email := pamLogin
|
||||
idx := strings.Index(pamLogin, "@")
|
||||
if idx > -1 {
|
||||
username = pamLogin[:idx]
|
||||
}
|
||||
if models.ValidateEmail(email) != nil {
|
||||
if source.EmailDomain != "" {
|
||||
email = fmt.Sprintf("%s@%s", username, source.EmailDomain)
|
||||
} else {
|
||||
email = fmt.Sprintf("%s@%s", username, setting.Service.NoReplyAddress)
|
||||
}
|
||||
if models.ValidateEmail(email) != nil {
|
||||
email = uuid.New().String() + "@localhost"
|
||||
}
|
||||
}
|
||||
|
||||
user = &models.User{
|
||||
LowerName: strings.ToLower(username),
|
||||
Name: username,
|
||||
Email: email,
|
||||
Passwd: password,
|
||||
LoginType: models.LoginPAM,
|
||||
LoginSource: source.loginSource.ID,
|
||||
LoginName: login, // This is what the user typed in
|
||||
IsActive: true,
|
||||
}
|
||||
return user, models.CreateUser(user)
|
||||
}
|
@ -0,0 +1,25 @@
|
||||
// Copyright 2021 The Gitea Authors. All rights reserved.
|
||||
// Use of this source code is governed by a MIT-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package smtp_test
|
||||
|
||||
import (
|
||||
"code.gitea.io/gitea/models"
|
||||
"code.gitea.io/gitea/services/auth"
|
||||
"code.gitea.io/gitea/services/auth/source/smtp"
|
||||
)
|
||||
|
||||
// This test file exists to assert that our Source exposes the interfaces that we expect
|
||||
// It tightly binds the interfaces and implementation without breaking go import cycles
|
||||
|
||||
type sourceInterface interface {
|
||||
auth.PasswordAuthenticator
|
||||
models.LoginConfig
|
||||
models.SkipVerifiable
|
||||
models.HasTLSer
|
||||
models.UseTLSer
|
||||
models.LoginSourceSettable
|
||||
}
|
||||
|
||||
var _ (sourceInterface) = &smtp.Source{}
|
@ -0,0 +1,81 @@
|
||||
// Copyright 2021 The Gitea Authors. All rights reserved.
|
||||
// Use of this source code is governed by a MIT-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package smtp
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/smtp"
|
||||
|
||||
"code.gitea.io/gitea/models"
|
||||
)
|
||||
|
||||
// _________ __________________________
|
||||
// / _____/ / \__ ___/\______ \
|
||||
// \_____ \ / \ / \| | | ___/
|
||||
// / \/ Y \ | | |
|
||||
// /_______ /\____|__ /____| |____|
|
||||
// \/ \/
|
||||
|
||||
type loginAuthenticator struct {
|
||||
username, password string
|
||||
}
|
||||
|
||||
func (auth *loginAuthenticator) Start(server *smtp.ServerInfo) (string, []byte, error) {
|
||||
return "LOGIN", []byte(auth.username), nil
|
||||
}
|
||||
|
||||
func (auth *loginAuthenticator) Next(fromServer []byte, more bool) ([]byte, error) {
|
||||
if more {
|
||||
switch string(fromServer) {
|
||||
case "Username:":
|
||||
return []byte(auth.username), nil
|
||||
case "Password:":
|
||||
return []byte(auth.password), nil
|
||||
}
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// SMTP authentication type names.
|
||||
const (
|
||||
PlainAuthentication = "PLAIN"
|
||||
LoginAuthentication = "LOGIN"
|
||||
)
|
||||
|
||||
// Authenticators contains available SMTP authentication type names.
|
||||
var Authenticators = []string{PlainAuthentication, LoginAuthentication}
|
||||
|
||||
// Authenticate performs an SMTP authentication.
|
||||
func Authenticate(a smtp.Auth, source *Source) error {
|
||||
c, err := smtp.Dial(fmt.Sprintf("%s:%d", source.Host, source.Port))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer c.Close()
|
||||
|
||||
if err = c.Hello("gogs"); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if source.TLS {
|
||||
if ok, _ := c.Extension("STARTTLS"); ok {
|
||||
if err = c.StartTLS(&tls.Config{
|
||||
InsecureSkipVerify: source.SkipVerify,
|
||||
ServerName: source.Host,
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
return errors.New("SMTP server unsupports TLS")
|
||||
}
|
||||
}
|
||||
|
||||
if ok, _ := c.Extension("AUTH"); ok {
|
||||
return c.Auth(a)
|
||||
}
|
||||
return models.ErrUnsupportedLoginType
|
||||
}
|
@ -0,0 +1,66 @@
|
||||
// Copyright 2021 The Gitea Authors. All rights reserved.
|
||||
// Use of this source code is governed by a MIT-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package smtp
|
||||
|
||||
import (
|
||||
"code.gitea.io/gitea/models"
|
||||
|
||||
jsoniter "github.com/json-iterator/go"
|
||||
)
|
||||
|
||||
// _________ __________________________
|
||||
// / _____/ / \__ ___/\______ \
|
||||
// \_____ \ / \ / \| | | ___/
|
||||
// / \/ Y \ | | |
|
||||
// /_______ /\____|__ /____| |____|
|
||||
// \/ \/
|
||||
|
||||
// Source holds configuration for the SMTP login source.
|
||||
type Source struct {
|
||||
Auth string
|
||||
Host string
|
||||
Port int
|
||||
AllowedDomains string `xorm:"TEXT"`
|
||||
TLS bool
|
||||
SkipVerify bool
|
||||
|
||||
// reference to the loginSource
|
||||
loginSource *models.LoginSource
|
||||
}
|
||||
|
||||
// FromDB fills up an SMTPConfig from serialized format.
|
||||
func (source *Source) FromDB(bs []byte) error {
|
||||
return models.JSONUnmarshalHandleDoubleEncode(bs, &source)
|
||||
}
|
||||
|
||||
// ToDB exports an SMTPConfig to a serialized format.
|
||||
func (source *Source) ToDB() ([]byte, error) {
|
||||
json := jsoniter.ConfigCompatibleWithStandardLibrary
|
||||
return json.Marshal(source)
|
||||
}
|
||||
|
||||
// IsSkipVerify returns if SkipVerify is set
|
||||
func (source *Source) IsSkipVerify() bool {
|
||||
return source.SkipVerify
|
||||
}
|
||||
|
||||
// HasTLS returns true for SMTP
|
||||
func (source *Source) HasTLS() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
// UseTLS returns if TLS is set
|
||||
func (source *Source) UseTLS() bool {
|
||||
return source.TLS
|
||||
}
|
||||
|
||||
// SetLoginSource sets the related LoginSource
|
||||
func (source *Source) SetLoginSource(loginSource *models.LoginSource) {
|
||||
source.loginSource = loginSource
|
||||
}
|
||||
|
||||
func init() {
|
||||
models.RegisterLoginTypeConfig(models.LoginSMTP, &Source{})
|
||||
}
|
@ -0,0 +1,71 @@
|
||||
// Copyright 2021 The Gitea Authors. All rights reserved.
|
||||
// Use of this source code is governed by a MIT-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package smtp
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/smtp"
|
||||
"net/textproto"
|
||||
"strings"
|
||||
|
||||
"code.gitea.io/gitea/models"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
)
|
||||
|
||||
// Authenticate queries if the provided login/password is authenticates against the SMTP server
|
||||
// Users will be autoregistered as required
|
||||
func (source *Source) Authenticate(user *models.User, login, password string) (*models.User, error) {
|
||||
// Verify allowed domains.
|
||||
if len(source.AllowedDomains) > 0 {
|
||||
idx := strings.Index(login, "@")
|
||||
if idx == -1 {
|
||||
return nil, models.ErrUserNotExist{Name: login}
|
||||
} else if !util.IsStringInSlice(login[idx+1:], strings.Split(source.AllowedDomains, ","), true) {
|
||||
return nil, models.ErrUserNotExist{Name: login}
|
||||
}
|
||||
}
|
||||
|
||||
var auth smtp.Auth
|
||||
if source.Auth == PlainAuthentication {
|
||||
auth = smtp.PlainAuth("", login, password, source.Host)
|
||||
} else if source.Auth == LoginAuthentication {
|
||||
auth = &loginAuthenticator{login, password}
|
||||
} else {
|
||||
return nil, errors.New("Unsupported SMTP auth type")
|
||||
}
|
||||
|
||||
if err := Authenticate(auth, source); err != nil {
|
||||
// Check standard error format first,
|
||||
// then fallback to worse case.
|
||||
tperr, ok := err.(*textproto.Error)
|
||||
if (ok && tperr.Code == 535) ||
|
||||
strings.Contains(err.Error(), "Username and Password not accepted") {
|
||||
return nil, models.ErrUserNotExist{Name: login}
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if user != nil {
|
||||
return user, nil
|
||||
}
|
||||
|
||||
username := login
|
||||
idx := strings.Index(login, "@")
|
||||
if idx > -1 {
|
||||
username = login[:idx]
|
||||
}
|
||||
|
||||
user = &models.User{
|
||||
LowerName: strings.ToLower(username),
|
||||
Name: strings.ToLower(username),
|
||||
Email: login,
|
||||
Passwd: password,
|
||||
LoginType: models.LoginSMTP,
|
||||
LoginSource: source.loginSource.ID,
|
||||
LoginName: login,
|
||||
IsActive: true,
|
||||
}
|
||||
return user, models.CreateUser(user)
|
||||
}
|
@ -0,0 +1,19 @@
|
||||
// Copyright 2021 The Gitea Authors. All rights reserved.
|
||||
// Use of this source code is governed by a MIT-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package sspi_test
|
||||
|
||||
import (
|
||||
"code.gitea.io/gitea/models"
|
||||
"code.gitea.io/gitea/services/auth/source/sspi"
|
||||
)
|
||||
|
||||
// This test file exists to assert that our Source exposes the interfaces that we expect
|
||||
// It tightly binds the interfaces and implementation without breaking go import cycles
|
||||
|
||||
type sourceInterface interface {
|
||||
models.LoginConfig
|
||||
}
|
||||
|
||||
var _ (sourceInterface) = &sspi.Source{}
|
@ -0,0 +1,41 @@
|
||||
// Copyright 2021 The Gitea Authors. All rights reserved.
|
||||
// Use of this source code is governed by a MIT-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package sspi
|
||||
|
||||
import (
|
||||
"code.gitea.io/gitea/models"
|
||||
jsoniter "github.com/json-iterator/go"
|
||||
)
|
||||
|
||||
// _________ ___________________.___
|
||||
// / _____// _____/\______ \ |
|
||||
// \_____ \ \_____ \ | ___/ |
|
||||
// / \/ \ | | | |
|
||||
// /_______ /_______ / |____| |___|
|
||||
// \/ \/
|
||||
|
||||
// Source holds configuration for SSPI single sign-on.
|
||||
type Source struct {
|
||||
AutoCreateUsers bool
|
||||
AutoActivateUsers bool
|
||||
StripDomainNames bool
|
||||
SeparatorReplacement string
|
||||
DefaultLanguage string
|
||||
}
|
||||
|
||||
// FromDB fills up an SSPIConfig from serialized format.
|
||||
func (cfg *Source) FromDB(bs []byte) error {
|
||||
return models.JSONUnmarshalHandleDoubleEncode(bs, &cfg)
|
||||
}
|
||||
|
||||
// ToDB exports an SSPIConfig to a serialized format.
|
||||
func (cfg *Source) ToDB() ([]byte, error) {
|
||||
json := jsoniter.ConfigCompatibleWithStandardLibrary
|
||||
return json.Marshal(cfg)
|
||||
}
|
||||
|
||||
func init() {
|
||||
models.RegisterLoginTypeConfig(models.LoginSSPI, &Source{})
|
||||
}
|
@ -0,0 +1,43 @@
|
||||
// Copyright 2021 The Gitea Authors. All rights reserved.
|
||||
// Use of this source code is governed by a MIT-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package auth
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"code.gitea.io/gitea/models"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
)
|
||||
|
||||
// SyncExternalUsers is used to synchronize users with external authorization source
|
||||
func SyncExternalUsers(ctx context.Context, updateExisting bool) error {
|
||||
log.Trace("Doing: SyncExternalUsers")
|
||||
|
||||
ls, err := models.LoginSources()
|
||||
if err != nil {
|
||||
log.Error("SyncExternalUsers: %v", err)
|
||||
return err
|
||||
}
|
||||
|
||||
for _, s := range ls {
|
||||
if !s.IsActive || !s.IsSyncEnabled {
|
||||
continue
|
||||
}
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
log.Warn("SyncExternalUsers: Cancelled before update of %s", s.Name)
|
||||
return models.ErrCancelledf("Before update of %s", s.Name)
|
||||
default:
|
||||
}
|
||||
|
||||
if syncable, ok := s.Cfg.(SynchronizableSource); ok {
|
||||
err := syncable.Sync(ctx, updateExisting)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
Loading…
Reference in New Issue