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>
This commit is contained in:
12
cmd/admin.go
12
cmd/admin.go
@ -14,7 +14,6 @@ import (
|
||||
"text/tabwriter"
|
||||
|
||||
"code.gitea.io/gitea/models"
|
||||
"code.gitea.io/gitea/modules/auth/oauth2"
|
||||
"code.gitea.io/gitea/modules/git"
|
||||
"code.gitea.io/gitea/modules/graceful"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
@ -22,6 +21,7 @@ import (
|
||||
repo_module "code.gitea.io/gitea/modules/repository"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/modules/storage"
|
||||
"code.gitea.io/gitea/services/auth/source/oauth2"
|
||||
|
||||
"github.com/urfave/cli"
|
||||
)
|
||||
@ -597,7 +597,7 @@ func runRegenerateKeys(_ *cli.Context) error {
|
||||
return models.RewriteAllPublicKeys()
|
||||
}
|
||||
|
||||
func parseOAuth2Config(c *cli.Context) *models.OAuth2Config {
|
||||
func parseOAuth2Config(c *cli.Context) *oauth2.Source {
|
||||
var customURLMapping *oauth2.CustomURLMapping
|
||||
if c.IsSet("use-custom-urls") {
|
||||
customURLMapping = &oauth2.CustomURLMapping{
|
||||
@ -609,7 +609,7 @@ func parseOAuth2Config(c *cli.Context) *models.OAuth2Config {
|
||||
} else {
|
||||
customURLMapping = nil
|
||||
}
|
||||
return &models.OAuth2Config{
|
||||
return &oauth2.Source{
|
||||
Provider: c.String("provider"),
|
||||
ClientID: c.String("key"),
|
||||
ClientSecret: c.String("secret"),
|
||||
@ -627,7 +627,7 @@ func runAddOauth(c *cli.Context) error {
|
||||
return models.CreateLoginSource(&models.LoginSource{
|
||||
Type: models.LoginOAuth2,
|
||||
Name: c.String("name"),
|
||||
IsActived: true,
|
||||
IsActive: true,
|
||||
Cfg: parseOAuth2Config(c),
|
||||
})
|
||||
}
|
||||
@ -646,7 +646,7 @@ func runUpdateOauth(c *cli.Context) error {
|
||||
return err
|
||||
}
|
||||
|
||||
oAuth2Config := source.OAuth2()
|
||||
oAuth2Config := source.Cfg.(*oauth2.Source)
|
||||
|
||||
if c.IsSet("name") {
|
||||
source.Name = c.String("name")
|
||||
@ -728,7 +728,7 @@ func runListAuth(c *cli.Context) error {
|
||||
w := tabwriter.NewWriter(os.Stdout, c.Int("min-width"), c.Int("tab-width"), c.Int("padding"), padChar, flags)
|
||||
fmt.Fprintf(w, "ID\tName\tType\tEnabled\n")
|
||||
for _, source := range loginSources {
|
||||
fmt.Fprintf(w, "%d\t%s\t%s\t%t\n", source.ID, source.Name, models.LoginNames[source.Type], source.IsActived)
|
||||
fmt.Fprintf(w, "%d\t%s\t%s\t%t\n", source.ID, source.Name, models.LoginNames[source.Type], source.IsActive)
|
||||
}
|
||||
w.Flush()
|
||||
|
||||
|
@ -9,7 +9,7 @@ import (
|
||||
"strings"
|
||||
|
||||
"code.gitea.io/gitea/models"
|
||||
"code.gitea.io/gitea/modules/auth/ldap"
|
||||
"code.gitea.io/gitea/services/auth/source/ldap"
|
||||
|
||||
"github.com/urfave/cli"
|
||||
)
|
||||
@ -172,7 +172,7 @@ func parseLoginSource(c *cli.Context, loginSource *models.LoginSource) {
|
||||
loginSource.Name = c.String("name")
|
||||
}
|
||||
if c.IsSet("not-active") {
|
||||
loginSource.IsActived = !c.Bool("not-active")
|
||||
loginSource.IsActive = !c.Bool("not-active")
|
||||
}
|
||||
if c.IsSet("synchronize-users") {
|
||||
loginSource.IsSyncEnabled = c.Bool("synchronize-users")
|
||||
@ -180,70 +180,70 @@ func parseLoginSource(c *cli.Context, loginSource *models.LoginSource) {
|
||||
}
|
||||
|
||||
// parseLdapConfig assigns values on config according to command line flags.
|
||||
func parseLdapConfig(c *cli.Context, config *models.LDAPConfig) error {
|
||||
func parseLdapConfig(c *cli.Context, config *ldap.Source) error {
|
||||
if c.IsSet("name") {
|
||||
config.Source.Name = c.String("name")
|
||||
config.Name = c.String("name")
|
||||
}
|
||||
if c.IsSet("host") {
|
||||
config.Source.Host = c.String("host")
|
||||
config.Host = c.String("host")
|
||||
}
|
||||
if c.IsSet("port") {
|
||||
config.Source.Port = c.Int("port")
|
||||
config.Port = c.Int("port")
|
||||
}
|
||||
if c.IsSet("security-protocol") {
|
||||
p, ok := findLdapSecurityProtocolByName(c.String("security-protocol"))
|
||||
if !ok {
|
||||
return fmt.Errorf("Unknown security protocol name: %s", c.String("security-protocol"))
|
||||
}
|
||||
config.Source.SecurityProtocol = p
|
||||
config.SecurityProtocol = p
|
||||
}
|
||||
if c.IsSet("skip-tls-verify") {
|
||||
config.Source.SkipVerify = c.Bool("skip-tls-verify")
|
||||
config.SkipVerify = c.Bool("skip-tls-verify")
|
||||
}
|
||||
if c.IsSet("bind-dn") {
|
||||
config.Source.BindDN = c.String("bind-dn")
|
||||
config.BindDN = c.String("bind-dn")
|
||||
}
|
||||
if c.IsSet("user-dn") {
|
||||
config.Source.UserDN = c.String("user-dn")
|
||||
config.UserDN = c.String("user-dn")
|
||||
}
|
||||
if c.IsSet("bind-password") {
|
||||
config.Source.BindPassword = c.String("bind-password")
|
||||
config.BindPassword = c.String("bind-password")
|
||||
}
|
||||
if c.IsSet("user-search-base") {
|
||||
config.Source.UserBase = c.String("user-search-base")
|
||||
config.UserBase = c.String("user-search-base")
|
||||
}
|
||||
if c.IsSet("username-attribute") {
|
||||
config.Source.AttributeUsername = c.String("username-attribute")
|
||||
config.AttributeUsername = c.String("username-attribute")
|
||||
}
|
||||
if c.IsSet("firstname-attribute") {
|
||||
config.Source.AttributeName = c.String("firstname-attribute")
|
||||
config.AttributeName = c.String("firstname-attribute")
|
||||
}
|
||||
if c.IsSet("surname-attribute") {
|
||||
config.Source.AttributeSurname = c.String("surname-attribute")
|
||||
config.AttributeSurname = c.String("surname-attribute")
|
||||
}
|
||||
if c.IsSet("email-attribute") {
|
||||
config.Source.AttributeMail = c.String("email-attribute")
|
||||
config.AttributeMail = c.String("email-attribute")
|
||||
}
|
||||
if c.IsSet("attributes-in-bind") {
|
||||
config.Source.AttributesInBind = c.Bool("attributes-in-bind")
|
||||
config.AttributesInBind = c.Bool("attributes-in-bind")
|
||||
}
|
||||
if c.IsSet("public-ssh-key-attribute") {
|
||||
config.Source.AttributeSSHPublicKey = c.String("public-ssh-key-attribute")
|
||||
config.AttributeSSHPublicKey = c.String("public-ssh-key-attribute")
|
||||
}
|
||||
if c.IsSet("page-size") {
|
||||
config.Source.SearchPageSize = uint32(c.Uint("page-size"))
|
||||
config.SearchPageSize = uint32(c.Uint("page-size"))
|
||||
}
|
||||
if c.IsSet("user-filter") {
|
||||
config.Source.Filter = c.String("user-filter")
|
||||
config.Filter = c.String("user-filter")
|
||||
}
|
||||
if c.IsSet("admin-filter") {
|
||||
config.Source.AdminFilter = c.String("admin-filter")
|
||||
config.AdminFilter = c.String("admin-filter")
|
||||
}
|
||||
if c.IsSet("restricted-filter") {
|
||||
config.Source.RestrictedFilter = c.String("restricted-filter")
|
||||
config.RestrictedFilter = c.String("restricted-filter")
|
||||
}
|
||||
if c.IsSet("allow-deactivate-all") {
|
||||
config.Source.AllowDeactivateAll = c.Bool("allow-deactivate-all")
|
||||
config.AllowDeactivateAll = c.Bool("allow-deactivate-all")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@ -251,7 +251,7 @@ func parseLdapConfig(c *cli.Context, config *models.LDAPConfig) error {
|
||||
// findLdapSecurityProtocolByName finds security protocol by its name ignoring case.
|
||||
// It returns the value of the security protocol and if it was found.
|
||||
func findLdapSecurityProtocolByName(name string) (ldap.SecurityProtocol, bool) {
|
||||
for i, n := range models.SecurityProtocolNames {
|
||||
for i, n := range ldap.SecurityProtocolNames {
|
||||
if strings.EqualFold(name, n) {
|
||||
return i, true
|
||||
}
|
||||
@ -290,16 +290,14 @@ func (a *authService) addLdapBindDn(c *cli.Context) error {
|
||||
|
||||
loginSource := &models.LoginSource{
|
||||
Type: models.LoginLDAP,
|
||||
IsActived: true, // active by default
|
||||
Cfg: &models.LDAPConfig{
|
||||
Source: &ldap.Source{
|
||||
IsActive: true, // active by default
|
||||
Cfg: &ldap.Source{
|
||||
Enabled: true, // always true
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
parseLoginSource(c, loginSource)
|
||||
if err := parseLdapConfig(c, loginSource.LDAP()); err != nil {
|
||||
if err := parseLdapConfig(c, loginSource.Cfg.(*ldap.Source)); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@ -318,7 +316,7 @@ func (a *authService) updateLdapBindDn(c *cli.Context) error {
|
||||
}
|
||||
|
||||
parseLoginSource(c, loginSource)
|
||||
if err := parseLdapConfig(c, loginSource.LDAP()); err != nil {
|
||||
if err := parseLdapConfig(c, loginSource.Cfg.(*ldap.Source)); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@ -337,16 +335,14 @@ func (a *authService) addLdapSimpleAuth(c *cli.Context) error {
|
||||
|
||||
loginSource := &models.LoginSource{
|
||||
Type: models.LoginDLDAP,
|
||||
IsActived: true, // active by default
|
||||
Cfg: &models.LDAPConfig{
|
||||
Source: &ldap.Source{
|
||||
IsActive: true, // active by default
|
||||
Cfg: &ldap.Source{
|
||||
Enabled: true, // always true
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
parseLoginSource(c, loginSource)
|
||||
if err := parseLdapConfig(c, loginSource.LDAP()); err != nil {
|
||||
if err := parseLdapConfig(c, loginSource.Cfg.(*ldap.Source)); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@ -365,7 +361,7 @@ func (a *authService) updateLdapSimpleAuth(c *cli.Context) error {
|
||||
}
|
||||
|
||||
parseLoginSource(c, loginSource)
|
||||
if err := parseLdapConfig(c, loginSource.LDAP()); err != nil {
|
||||
if err := parseLdapConfig(c, loginSource.Cfg.(*ldap.Source)); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -11,7 +11,7 @@ import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"code.gitea.io/gitea/models"
|
||||
"code.gitea.io/gitea/services/auth"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/unknwon/i18n"
|
||||
@ -205,7 +205,7 @@ func TestLDAPUserSync(t *testing.T) {
|
||||
}
|
||||
defer prepareTestEnv(t)()
|
||||
addAuthSourceLDAP(t, "")
|
||||
models.SyncExternalUsers(context.Background(), true)
|
||||
auth.SyncExternalUsers(context.Background(), true)
|
||||
|
||||
session := loginUser(t, "user1")
|
||||
// Check if users exists
|
||||
@ -270,7 +270,7 @@ func TestLDAPUserSSHKeySync(t *testing.T) {
|
||||
defer prepareTestEnv(t)()
|
||||
addAuthSourceLDAP(t, "sshPublicKey")
|
||||
|
||||
models.SyncExternalUsers(context.Background(), true)
|
||||
auth.SyncExternalUsers(context.Background(), true)
|
||||
|
||||
// Check if users has SSH keys synced
|
||||
for _, u := range gitLDAPUsers {
|
||||
|
@ -4,6 +4,12 @@
|
||||
|
||||
package models
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
|
||||
jsoniter "github.com/json-iterator/go"
|
||||
)
|
||||
|
||||
func keysInt64(m map[int64]struct{}) []int64 {
|
||||
keys := make([]int64, 0, len(m))
|
||||
for k := range m {
|
||||
@ -27,3 +33,33 @@ func valuesUser(m map[int64]*User) []*User {
|
||||
}
|
||||
return values
|
||||
}
|
||||
|
||||
// JSONUnmarshalHandleDoubleEncode - due to a bug in xorm (see https://gitea.com/xorm/xorm/pulls/1957) - it's
|
||||
// possible that a Blob may be double encoded or gain an unwanted prefix of 0xff 0xfe.
|
||||
func JSONUnmarshalHandleDoubleEncode(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
|
||||
}
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -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}"
|
@ -327,6 +327,8 @@ var migrations = []Migration{
|
||||
NewMigration("Drop unneeded webhook related columns", dropWebhookColumns),
|
||||
// v188 -> v189
|
||||
NewMigration("Add key is verified to gpg key", addKeyIsVerified),
|
||||
// v189 -> v190
|
||||
NewMigration("Unwrap ldap.Sources", unwrapLDAPSourceCfg),
|
||||
}
|
||||
|
||||
// GetCurrentDBVersion returns the current db version
|
||||
|
@ -220,6 +220,9 @@ func prepareTestEnv(t *testing.T, skip int, syncModels ...interface{}) (*xorm.En
|
||||
if err := x.Close(); err != nil {
|
||||
t.Errorf("error during close: %v", err)
|
||||
}
|
||||
if err := deleteDB(); err != nil {
|
||||
t.Errorf("unable to reset database: %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
if err != nil {
|
||||
|
111
models/migrations/v189.go
Normal file
111
models/migrations/v189.go
Normal file
@ -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()
|
||||
}
|
83
models/migrations/v189_test.go
Normal file
83
models/migrations/v189_test.go
Normal file
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
154
models/oauth2.go
154
models/oauth2.go
@ -4,89 +4,10 @@
|
||||
|
||||
package models
|
||||
|
||||
import (
|
||||
"sort"
|
||||
|
||||
"code.gitea.io/gitea/modules/auth/oauth2"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
)
|
||||
|
||||
// OAuth2Provider describes the display values of a single OAuth2 provider
|
||||
type OAuth2Provider struct {
|
||||
Name string
|
||||
DisplayName string
|
||||
Image string
|
||||
CustomURLMapping *oauth2.CustomURLMapping
|
||||
}
|
||||
|
||||
// OAuth2Providers contains the map of registered OAuth2 providers in Gitea (based on goth)
|
||||
// key is used to map the OAuth2Provider with the goth provider type (also in LoginSource.OAuth2Config.Provider)
|
||||
// value is used to store display data
|
||||
var OAuth2Providers = map[string]OAuth2Provider{
|
||||
"bitbucket": {Name: "bitbucket", DisplayName: "Bitbucket", Image: "/assets/img/auth/bitbucket.png"},
|
||||
"dropbox": {Name: "dropbox", DisplayName: "Dropbox", Image: "/assets/img/auth/dropbox.png"},
|
||||
"facebook": {Name: "facebook", DisplayName: "Facebook", Image: "/assets/img/auth/facebook.png"},
|
||||
"github": {
|
||||
Name: "github", DisplayName: "GitHub", Image: "/assets/img/auth/github.png",
|
||||
CustomURLMapping: &oauth2.CustomURLMapping{
|
||||
TokenURL: oauth2.GetDefaultTokenURL("github"),
|
||||
AuthURL: oauth2.GetDefaultAuthURL("github"),
|
||||
ProfileURL: oauth2.GetDefaultProfileURL("github"),
|
||||
EmailURL: oauth2.GetDefaultEmailURL("github"),
|
||||
},
|
||||
},
|
||||
"gitlab": {
|
||||
Name: "gitlab", DisplayName: "GitLab", Image: "/assets/img/auth/gitlab.png",
|
||||
CustomURLMapping: &oauth2.CustomURLMapping{
|
||||
TokenURL: oauth2.GetDefaultTokenURL("gitlab"),
|
||||
AuthURL: oauth2.GetDefaultAuthURL("gitlab"),
|
||||
ProfileURL: oauth2.GetDefaultProfileURL("gitlab"),
|
||||
},
|
||||
},
|
||||
"gplus": {Name: "gplus", DisplayName: "Google", Image: "/assets/img/auth/google.png"},
|
||||
"openidConnect": {Name: "openidConnect", DisplayName: "OpenID Connect", Image: "/assets/img/auth/openid_connect.svg"},
|
||||
"twitter": {Name: "twitter", DisplayName: "Twitter", Image: "/assets/img/auth/twitter.png"},
|
||||
"discord": {Name: "discord", DisplayName: "Discord", Image: "/assets/img/auth/discord.png"},
|
||||
"gitea": {
|
||||
Name: "gitea", DisplayName: "Gitea", Image: "/assets/img/auth/gitea.png",
|
||||
CustomURLMapping: &oauth2.CustomURLMapping{
|
||||
TokenURL: oauth2.GetDefaultTokenURL("gitea"),
|
||||
AuthURL: oauth2.GetDefaultAuthURL("gitea"),
|
||||
ProfileURL: oauth2.GetDefaultProfileURL("gitea"),
|
||||
},
|
||||
},
|
||||
"nextcloud": {
|
||||
Name: "nextcloud", DisplayName: "Nextcloud", Image: "/assets/img/auth/nextcloud.png",
|
||||
CustomURLMapping: &oauth2.CustomURLMapping{
|
||||
TokenURL: oauth2.GetDefaultTokenURL("nextcloud"),
|
||||
AuthURL: oauth2.GetDefaultAuthURL("nextcloud"),
|
||||
ProfileURL: oauth2.GetDefaultProfileURL("nextcloud"),
|
||||
},
|
||||
},
|
||||
"yandex": {Name: "yandex", DisplayName: "Yandex", Image: "/assets/img/auth/yandex.png"},
|
||||
"mastodon": {
|
||||
Name: "mastodon", DisplayName: "Mastodon", Image: "/assets/img/auth/mastodon.png",
|
||||
CustomURLMapping: &oauth2.CustomURLMapping{
|
||||
AuthURL: oauth2.GetDefaultAuthURL("mastodon"),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// OAuth2DefaultCustomURLMappings 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 OAuth2DefaultCustomURLMappings = map[string]*oauth2.CustomURLMapping{
|
||||
"github": OAuth2Providers["github"].CustomURLMapping,
|
||||
"gitlab": OAuth2Providers["gitlab"].CustomURLMapping,
|
||||
"gitea": OAuth2Providers["gitea"].CustomURLMapping,
|
||||
"nextcloud": OAuth2Providers["nextcloud"].CustomURLMapping,
|
||||
"mastodon": OAuth2Providers["mastodon"].CustomURLMapping,
|
||||
}
|
||||
|
||||
// GetActiveOAuth2ProviderLoginSources returns all actived LoginOAuth2 sources
|
||||
func GetActiveOAuth2ProviderLoginSources() ([]*LoginSource, error) {
|
||||
sources := make([]*LoginSource, 0, 1)
|
||||
if err := x.Where("is_actived = ? and type = ?", true, LoginOAuth2).Find(&sources); err != nil {
|
||||
if err := x.Where("is_active = ? and type = ?", true, LoginOAuth2).Find(&sources); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return sources, nil
|
||||
@ -95,81 +16,10 @@ func GetActiveOAuth2ProviderLoginSources() ([]*LoginSource, error) {
|
||||
// GetActiveOAuth2LoginSourceByName returns a OAuth2 LoginSource based on the given name
|
||||
func GetActiveOAuth2LoginSourceByName(name string) (*LoginSource, error) {
|
||||
loginSource := new(LoginSource)
|
||||
has, err := x.Where("name = ? and type = ? and is_actived = ?", name, LoginOAuth2, true).Get(loginSource)
|
||||
has, err := x.Where("name = ? and type = ? and is_active = ?", name, LoginOAuth2, true).Get(loginSource)
|
||||
if !has || err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return loginSource, nil
|
||||
}
|
||||
|
||||
// GetActiveOAuth2Providers returns the map of configured active OAuth2 providers
|
||||
// key is used as technical name (like in the callbackURL)
|
||||
// values to display
|
||||
func GetActiveOAuth2Providers() ([]string, map[string]OAuth2Provider, error) {
|
||||
// Maybe also separate used and unused providers so we can force the registration of only 1 active provider for each type
|
||||
|
||||
loginSources, err := GetActiveOAuth2ProviderLoginSources()
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
var orderedKeys []string
|
||||
providers := make(map[string]OAuth2Provider)
|
||||
for _, source := range loginSources {
|
||||
prov := OAuth2Providers[source.OAuth2().Provider]
|
||||
if source.OAuth2().IconURL != "" {
|
||||
prov.Image = source.OAuth2().IconURL
|
||||
}
|
||||
providers[source.Name] = prov
|
||||
orderedKeys = append(orderedKeys, source.Name)
|
||||
}
|
||||
|
||||
sort.Strings(orderedKeys)
|
||||
|
||||
return orderedKeys, providers, nil
|
||||
}
|
||||
|
||||
// InitOAuth2 initialize the OAuth2 lib and register all active OAuth2 providers in the library
|
||||
func InitOAuth2() error {
|
||||
if err := oauth2.InitSigningKey(); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := oauth2.Init(x); err != nil {
|
||||
return err
|
||||
}
|
||||
return initOAuth2LoginSources()
|
||||
}
|
||||
|
||||
// ResetOAuth2 clears existing OAuth2 providers and loads them from DB
|
||||
func ResetOAuth2() error {
|
||||
oauth2.ClearProviders()
|
||||
return initOAuth2LoginSources()
|
||||
}
|
||||
|
||||
// initOAuth2LoginSources is used to load and register all active OAuth2 providers
|
||||
func initOAuth2LoginSources() error {
|
||||
loginSources, _ := GetActiveOAuth2ProviderLoginSources()
|
||||
for _, source := range loginSources {
|
||||
oAuth2Config := source.OAuth2()
|
||||
err := oauth2.RegisterProvider(source.Name, oAuth2Config.Provider, oAuth2Config.ClientID, oAuth2Config.ClientSecret, oAuth2Config.OpenIDConnectAutoDiscoveryURL, oAuth2Config.CustomURLMapping)
|
||||
if err != nil {
|
||||
log.Critical("Unable to register source: %s due to Error: %v. This source will be disabled.", source.Name, err)
|
||||
source.IsActived = false
|
||||
if err = UpdateSource(source); err != nil {
|
||||
log.Critical("Unable to update source %s to disable it. Error: %v", err)
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
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, oAuth2Config *OAuth2Config) error {
|
||||
if err != nil && "openidConnect" == oAuth2Config.Provider {
|
||||
err = ErrOpenIDConnectInitialize{ProviderName: providerName, OpenIDConnectAutoDiscoveryURL: oAuth2Config.OpenIDConnectAutoDiscoveryURL, Cause: err}
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
@ -10,14 +10,11 @@ import (
|
||||
"fmt"
|
||||
"net/url"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"code.gitea.io/gitea/modules/auth/oauth2"
|
||||
"code.gitea.io/gitea/modules/secret"
|
||||
"code.gitea.io/gitea/modules/timeutil"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
|
||||
"github.com/dgrijalva/jwt-go"
|
||||
uuid "github.com/google/uuid"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
"xorm.io/xorm"
|
||||
@ -516,77 +513,3 @@ func revokeOAuth2Grant(e Engine, grantID, userID int64) error {
|
||||
_, err := e.Delete(&OAuth2Grant{ID: grantID, UserID: userID})
|
||||
return err
|
||||
}
|
||||
|
||||
//////////////////////////////////////////////////////////////
|
||||
|
||||
// OAuth2TokenType represents the type of token for an oauth application
|
||||
type OAuth2TokenType int
|
||||
|
||||
const (
|
||||
// TypeAccessToken is a token with short lifetime to access the api
|
||||
TypeAccessToken OAuth2TokenType = 0
|
||||
// TypeRefreshToken is token with long lifetime to refresh access tokens obtained by the client
|
||||
TypeRefreshToken = iota
|
||||
)
|
||||
|
||||
// OAuth2Token represents a JWT token used to authenticate a client
|
||||
type OAuth2Token struct {
|
||||
GrantID int64 `json:"gnt"`
|
||||
Type OAuth2TokenType `json:"tt"`
|
||||
Counter int64 `json:"cnt,omitempty"`
|
||||
jwt.StandardClaims
|
||||
}
|
||||
|
||||
// ParseOAuth2Token parses a signed jwt string
|
||||
func ParseOAuth2Token(jwtToken string) (*OAuth2Token, error) {
|
||||
parsedToken, err := jwt.ParseWithClaims(jwtToken, &OAuth2Token{}, func(token *jwt.Token) (interface{}, error) {
|
||||
if token.Method == nil || token.Method.Alg() != oauth2.DefaultSigningKey.SigningMethod().Alg() {
|
||||
return nil, fmt.Errorf("unexpected signing algo: %v", token.Header["alg"])
|
||||
}
|
||||
return oauth2.DefaultSigningKey.VerifyKey(), nil
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var token *OAuth2Token
|
||||
var ok bool
|
||||
if token, ok = parsedToken.Claims.(*OAuth2Token); !ok || !parsedToken.Valid {
|
||||
return nil, fmt.Errorf("invalid token")
|
||||
}
|
||||
return token, nil
|
||||
}
|
||||
|
||||
// SignToken signs the token with the JWT secret
|
||||
func (token *OAuth2Token) SignToken() (string, error) {
|
||||
token.IssuedAt = time.Now().Unix()
|
||||
jwtToken := jwt.NewWithClaims(oauth2.DefaultSigningKey.SigningMethod(), token)
|
||||
oauth2.DefaultSigningKey.PreProcessToken(jwtToken)
|
||||
return jwtToken.SignedString(oauth2.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 oauth2.JWTSigningKey) (string, error) {
|
||||
token.IssuedAt = time.Now().Unix()
|
||||
jwtToken := jwt.NewWithClaims(signingKey.SigningMethod(), token)
|
||||
signingKey.PreProcessToken(jwtToken)
|
||||
return jwtToken.SignedString(signingKey.SignKey())
|
||||
}
|
||||
|
@ -28,7 +28,7 @@ type UnitConfig struct{}
|
||||
|
||||
// FromDB fills up a UnitConfig from serialized format.
|
||||
func (cfg *UnitConfig) FromDB(bs []byte) error {
|
||||
return jsonUnmarshalHandleDoubleEncode(bs, &cfg)
|
||||
return JSONUnmarshalHandleDoubleEncode(bs, &cfg)
|
||||
}
|
||||
|
||||
// ToDB exports a UnitConfig to a serialized format.
|
||||
@ -44,7 +44,7 @@ type ExternalWikiConfig struct {
|
||||
|
||||
// FromDB fills up a ExternalWikiConfig from serialized format.
|
||||
func (cfg *ExternalWikiConfig) FromDB(bs []byte) error {
|
||||
return jsonUnmarshalHandleDoubleEncode(bs, &cfg)
|
||||
return JSONUnmarshalHandleDoubleEncode(bs, &cfg)
|
||||
}
|
||||
|
||||
// ToDB exports a ExternalWikiConfig to a serialized format.
|
||||
@ -62,7 +62,7 @@ type ExternalTrackerConfig struct {
|
||||
|
||||
// FromDB fills up a ExternalTrackerConfig from serialized format.
|
||||
func (cfg *ExternalTrackerConfig) FromDB(bs []byte) error {
|
||||
return jsonUnmarshalHandleDoubleEncode(bs, &cfg)
|
||||
return JSONUnmarshalHandleDoubleEncode(bs, &cfg)
|
||||
}
|
||||
|
||||
// ToDB exports a ExternalTrackerConfig to a serialized format.
|
||||
@ -80,7 +80,7 @@ type IssuesConfig struct {
|
||||
|
||||
// FromDB fills up a IssuesConfig from serialized format.
|
||||
func (cfg *IssuesConfig) FromDB(bs []byte) error {
|
||||
return jsonUnmarshalHandleDoubleEncode(bs, &cfg)
|
||||
return JSONUnmarshalHandleDoubleEncode(bs, &cfg)
|
||||
}
|
||||
|
||||
// ToDB exports a IssuesConfig to a serialized format.
|
||||
@ -104,7 +104,7 @@ type PullRequestsConfig struct {
|
||||
|
||||
// FromDB fills up a PullRequestsConfig from serialized format.
|
||||
func (cfg *PullRequestsConfig) FromDB(bs []byte) error {
|
||||
return jsonUnmarshalHandleDoubleEncode(bs, &cfg)
|
||||
return JSONUnmarshalHandleDoubleEncode(bs, &cfg)
|
||||
}
|
||||
|
||||
// ToDB exports a PullRequestsConfig to a serialized format.
|
||||
|
1142
models/ssh_key.go
1142
models/ssh_key.go
File diff suppressed because it is too large
Load Diff
219
models/ssh_key_authorized_keys.go
Normal file
219
models/ssh_key_authorized_keys.go
Normal file
@ -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
|
||||
}
|
142
models/ssh_key_authorized_principals.go
Normal file
142
models/ssh_key_authorized_principals.go
Normal file
@ -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
|
||||
}
|
299
models/ssh_key_deploy.go
Normal file
299
models/ssh_key_deploy.go
Normal file
@ -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)
|
||||
}
|
97
models/ssh_key_fingerprint.go
Normal file
97
models/ssh_key_fingerprint.go
Normal file
@ -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
|
||||
}
|
309
models/ssh_key_parse.go
Normal file
309
models/ssh_key_parse.go
Normal file
File diff suppressed because it is too large
Load Diff
125
models/ssh_key_principals.go
Normal file
125
models/ssh_key_principals.go
Normal file
@ -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)
|
||||
}
|
16
models/store.go
Normal file
16
models/store.go
Normal file
@ -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
|
||||
}
|
341
models/user.go
341
models/user.go
File diff suppressed because it is too large
Load Diff
@ -453,8 +453,8 @@ ssh-dss AAAAB3NzaC1kc3MAAACBAOChCC7lf6Uo9n7BmZ6M8St19PZf4Tn59NriyboW2x/DZuYAz3ib
|
||||
|
||||
for i, kase := range testCases {
|
||||
s.ID = int64(i) + 20
|
||||
addLdapSSHPublicKeys(user, s, []string{kase.keyString})
|
||||
keys, err := ListPublicLdapSSHKeys(user.ID, s.ID)
|
||||
AddPublicKeysBySource(user, s, []string{kase.keyString})
|
||||
keys, err := ListPublicKeysBySource(user.ID, s.ID)
|
||||
assert.NoError(t, err)
|
||||
if err != nil {
|
||||
continue
|
||||
|
@ -218,7 +218,7 @@ func (ctx *APIContext) CheckForOTP() {
|
||||
}
|
||||
|
||||
// APIAuth converts auth.Auth as a middleware
|
||||
func APIAuth(authMethod auth.Auth) func(*APIContext) {
|
||||
func APIAuth(authMethod auth.Method) func(*APIContext) {
|
||||
return func(ctx *APIContext) {
|
||||
// Get user from session if logged in.
|
||||
ctx.User = authMethod.Verify(ctx.Req, ctx.Resp, ctx, ctx.Session)
|
||||
|
@ -627,7 +627,7 @@ func getCsrfOpts() CsrfOptions {
|
||||
}
|
||||
|
||||
// Auth converts auth.Auth as a middleware
|
||||
func Auth(authMethod auth.Auth) func(*Context) {
|
||||
func Auth(authMethod auth.Method) func(*Context) {
|
||||
return func(ctx *Context) {
|
||||
ctx.User = authMethod.Verify(ctx.Req, ctx.Resp, ctx, ctx.Session)
|
||||
if ctx.User != nil {
|
||||
|
@ -12,6 +12,7 @@ import (
|
||||
"code.gitea.io/gitea/modules/migrations"
|
||||
repository_service "code.gitea.io/gitea/modules/repository"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/services/auth"
|
||||
mirror_service "code.gitea.io/gitea/services/mirror"
|
||||
)
|
||||
|
||||
@ -80,7 +81,7 @@ func registerSyncExternalUsers() {
|
||||
UpdateExisting: true,
|
||||
}, func(ctx context.Context, _ *models.User, config Config) error {
|
||||
realConfig := config.(*UpdateExistingConfig)
|
||||
return models.SyncExternalUsers(ctx, realConfig.UpdateExisting)
|
||||
return auth.SyncExternalUsers(ctx, realConfig.UpdateExisting)
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -35,6 +35,7 @@ import (
|
||||
web_routers "code.gitea.io/gitea/routers/web"
|
||||
"code.gitea.io/gitea/services/archiver"
|
||||
"code.gitea.io/gitea/services/auth"
|
||||
"code.gitea.io/gitea/services/auth/source/oauth2"
|
||||
"code.gitea.io/gitea/services/mailer"
|
||||
mirror_service "code.gitea.io/gitea/services/mirror"
|
||||
pull_service "code.gitea.io/gitea/services/pull"
|
||||
@ -100,7 +101,7 @@ func GlobalInit(ctx context.Context) {
|
||||
log.Fatal("ORM engine initialization failed: %v", err)
|
||||
}
|
||||
|
||||
if err := models.InitOAuth2(); err != nil {
|
||||
if err := oauth2.Init(); err != nil {
|
||||
log.Fatal("Failed to initialize OAuth2 support: %v", err)
|
||||
}
|
||||
|
||||
|
@ -11,8 +11,6 @@ import (
|
||||
"regexp"
|
||||
|
||||
"code.gitea.io/gitea/models"
|
||||
"code.gitea.io/gitea/modules/auth/ldap"
|
||||
"code.gitea.io/gitea/modules/auth/oauth2"
|
||||
"code.gitea.io/gitea/modules/auth/pam"
|
||||
"code.gitea.io/gitea/modules/base"
|
||||
"code.gitea.io/gitea/modules/context"
|
||||
@ -20,6 +18,11 @@ import (
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
"code.gitea.io/gitea/modules/web"
|
||||
"code.gitea.io/gitea/services/auth/source/ldap"
|
||||
"code.gitea.io/gitea/services/auth/source/oauth2"
|
||||
pamService "code.gitea.io/gitea/services/auth/source/pam"
|
||||
"code.gitea.io/gitea/services/auth/source/smtp"
|
||||
"code.gitea.io/gitea/services/auth/source/sspi"
|
||||
"code.gitea.io/gitea/services/forms"
|
||||
|
||||
"xorm.io/xorm/convert"
|
||||
@ -74,9 +77,9 @@ var (
|
||||
}()
|
||||
|
||||
securityProtocols = []dropdownItem{
|
||||
{models.SecurityProtocolNames[ldap.SecurityProtocolUnencrypted], ldap.SecurityProtocolUnencrypted},
|
||||
{models.SecurityProtocolNames[ldap.SecurityProtocolLDAPS], ldap.SecurityProtocolLDAPS},
|
||||
{models.SecurityProtocolNames[ldap.SecurityProtocolStartTLS], ldap.SecurityProtocolStartTLS},
|
||||
{ldap.SecurityProtocolNames[ldap.SecurityProtocolUnencrypted], ldap.SecurityProtocolUnencrypted},
|
||||
{ldap.SecurityProtocolNames[ldap.SecurityProtocolLDAPS], ldap.SecurityProtocolLDAPS},
|
||||
{ldap.SecurityProtocolNames[ldap.SecurityProtocolStartTLS], ldap.SecurityProtocolStartTLS},
|
||||
}
|
||||
)
|
||||
|
||||
@ -88,15 +91,15 @@ func NewAuthSource(ctx *context.Context) {
|
||||
|
||||
ctx.Data["type"] = models.LoginLDAP
|
||||
ctx.Data["CurrentTypeName"] = models.LoginNames[models.LoginLDAP]
|
||||
ctx.Data["CurrentSecurityProtocol"] = models.SecurityProtocolNames[ldap.SecurityProtocolUnencrypted]
|
||||
ctx.Data["CurrentSecurityProtocol"] = ldap.SecurityProtocolNames[ldap.SecurityProtocolUnencrypted]
|
||||
ctx.Data["smtp_auth"] = "PLAIN"
|
||||
ctx.Data["is_active"] = true
|
||||
ctx.Data["is_sync_enabled"] = true
|
||||
ctx.Data["AuthSources"] = authSources
|
||||
ctx.Data["SecurityProtocols"] = securityProtocols
|
||||
ctx.Data["SMTPAuths"] = models.SMTPAuths
|
||||
ctx.Data["OAuth2Providers"] = models.OAuth2Providers
|
||||
ctx.Data["OAuth2DefaultCustomURLMappings"] = models.OAuth2DefaultCustomURLMappings
|
||||
ctx.Data["SMTPAuths"] = smtp.Authenticators
|
||||
ctx.Data["OAuth2Providers"] = oauth2.Providers
|
||||
ctx.Data["OAuth2DefaultCustomURLMappings"] = oauth2.DefaultCustomURLMappings
|
||||
|
||||
ctx.Data["SSPIAutoCreateUsers"] = true
|
||||
ctx.Data["SSPIAutoActivateUsers"] = true
|
||||
@ -105,7 +108,7 @@ func NewAuthSource(ctx *context.Context) {
|
||||
ctx.Data["SSPIDefaultLanguage"] = ""
|
||||
|
||||
// only the first as default
|
||||
for key := range models.OAuth2Providers {
|
||||
for key := range oauth2.Providers {
|
||||
ctx.Data["oauth2_provider"] = key
|
||||
break
|
||||
}
|
||||
@ -113,13 +116,12 @@ func NewAuthSource(ctx *context.Context) {
|
||||
ctx.HTML(http.StatusOK, tplAuthNew)
|
||||
}
|
||||
|
||||
func parseLDAPConfig(form forms.AuthenticationForm) *models.LDAPConfig {
|
||||
func parseLDAPConfig(form forms.AuthenticationForm) *ldap.Source {
|
||||
var pageSize uint32
|
||||
if form.UsePagedSearch {
|
||||
pageSize = uint32(form.SearchPageSize)
|
||||
}
|
||||
return &models.LDAPConfig{
|
||||
Source: &ldap.Source{
|
||||
return &ldap.Source{
|
||||
Name: form.Name,
|
||||
Host: form.Host,
|
||||
Port: form.Port,
|
||||
@ -146,12 +148,11 @@ func parseLDAPConfig(form forms.AuthenticationForm) *models.LDAPConfig {
|
||||
RestrictedFilter: form.RestrictedFilter,
|
||||
AllowDeactivateAll: form.AllowDeactivateAll,
|
||||
Enabled: true,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func parseSMTPConfig(form forms.AuthenticationForm) *models.SMTPConfig {
|
||||
return &models.SMTPConfig{
|
||||
func parseSMTPConfig(form forms.AuthenticationForm) *smtp.Source {
|
||||
return &smtp.Source{
|
||||
Auth: form.SMTPAuth,
|
||||
Host: form.SMTPHost,
|
||||
Port: form.SMTPPort,
|
||||
@ -161,7 +162,7 @@ func parseSMTPConfig(form forms.AuthenticationForm) *models.SMTPConfig {
|
||||
}
|
||||
}
|
||||
|
||||
func parseOAuth2Config(form forms.AuthenticationForm) *models.OAuth2Config {
|
||||
func parseOAuth2Config(form forms.AuthenticationForm) *oauth2.Source {
|
||||
var customURLMapping *oauth2.CustomURLMapping
|
||||
if form.Oauth2UseCustomURL {
|
||||
customURLMapping = &oauth2.CustomURLMapping{
|
||||
@ -173,7 +174,7 @@ func parseOAuth2Config(form forms.AuthenticationForm) *models.OAuth2Config {
|
||||
} else {
|
||||
customURLMapping = nil
|
||||
}
|
||||
return &models.OAuth2Config{
|
||||
return &oauth2.Source{
|
||||
Provider: form.Oauth2Provider,
|
||||
ClientID: form.Oauth2Key,
|
||||
ClientSecret: form.Oauth2Secret,
|
||||
@ -183,7 +184,7 @@ func parseOAuth2Config(form forms.AuthenticationForm) *models.OAuth2Config {
|
||||
}
|
||||
}
|
||||
|
||||
func parseSSPIConfig(ctx *context.Context, form forms.AuthenticationForm) (*models.SSPIConfig, error) {
|
||||
func parseSSPIConfig(ctx *context.Context, form forms.AuthenticationForm) (*sspi.Source, error) {
|
||||
if util.IsEmptyString(form.SSPISeparatorReplacement) {
|
||||
ctx.Data["Err_SSPISeparatorReplacement"] = true
|
||||
return nil, errors.New(ctx.Tr("form.SSPISeparatorReplacement") + ctx.Tr("form.require_error"))
|
||||
@ -198,7 +199,7 @@ func parseSSPIConfig(ctx *context.Context, form forms.AuthenticationForm) (*mode
|
||||
return nil, errors.New(ctx.Tr("form.lang_select_error"))
|
||||
}
|
||||
|
||||
return &models.SSPIConfig{
|
||||
return &sspi.Source{
|
||||
AutoCreateUsers: form.SSPIAutoCreateUsers,
|
||||
AutoActivateUsers: form.SSPIAutoActivateUsers,
|
||||
StripDomainNames: form.SSPIStripDomainNames,
|
||||
@ -215,12 +216,12 @@ func NewAuthSourcePost(ctx *context.Context) {
|
||||
ctx.Data["PageIsAdminAuthentications"] = true
|
||||
|
||||
ctx.Data["CurrentTypeName"] = models.LoginNames[models.LoginType(form.Type)]
|
||||
ctx.Data["CurrentSecurityProtocol"] = models.SecurityProtocolNames[ldap.SecurityProtocol(form.SecurityProtocol)]
|
||||
ctx.Data["CurrentSecurityProtocol"] = ldap.SecurityProtocolNames[ldap.SecurityProtocol(form.SecurityProtocol)]
|
||||
ctx.Data["AuthSources"] = authSources
|
||||
ctx.Data["SecurityProtocols"] = securityProtocols
|
||||
ctx.Data["SMTPAuths"] = models.SMTPAuths
|
||||
ctx.Data["OAuth2Providers"] = models.OAuth2Providers
|
||||
ctx.Data["OAuth2DefaultCustomURLMappings"] = models.OAuth2DefaultCustomURLMappings
|
||||
ctx.Data["SMTPAuths"] = smtp.Authenticators
|
||||
ctx.Data["OAuth2Providers"] = oauth2.Providers
|
||||
ctx.Data["OAuth2DefaultCustomURLMappings"] = oauth2.DefaultCustomURLMappings
|
||||
|
||||
ctx.Data["SSPIAutoCreateUsers"] = true
|
||||
ctx.Data["SSPIAutoActivateUsers"] = true
|
||||
@ -238,7 +239,7 @@ func NewAuthSourcePost(ctx *context.Context) {
|
||||
config = parseSMTPConfig(form)
|
||||
hasTLS = true
|
||||
case models.LoginPAM:
|
||||
config = &models.PAMConfig{
|
||||
config = &pamService.Source{
|
||||
ServiceName: form.PAMServiceName,
|
||||
EmailDomain: form.PAMEmailDomain,
|
||||
}
|
||||
@ -271,7 +272,7 @@ func NewAuthSourcePost(ctx *context.Context) {
|
||||
if err := models.CreateLoginSource(&models.LoginSource{
|
||||
Type: models.LoginType(form.Type),
|
||||
Name: form.Name,
|
||||
IsActived: form.IsActive,
|
||||
IsActive: form.IsActive,
|
||||
IsSyncEnabled: form.IsSyncEnabled,
|
||||
Cfg: config,
|
||||
}); err != nil {
|
||||
@ -297,9 +298,9 @@ func EditAuthSource(ctx *context.Context) {
|
||||
ctx.Data["PageIsAdminAuthentications"] = true
|
||||
|
||||
ctx.Data["SecurityProtocols"] = securityProtocols
|
||||
ctx.Data["SMTPAuths"] = models.SMTPAuths
|
||||
ctx.Data["OAuth2Providers"] = models.OAuth2Providers
|
||||
ctx.Data["OAuth2DefaultCustomURLMappings"] = models.OAuth2DefaultCustomURLMappings
|
||||
ctx.Data["SMTPAuths"] = smtp.Authenticators
|
||||
ctx.Data["OAuth2Providers"] = oauth2.Providers
|
||||
ctx.Data["OAuth2DefaultCustomURLMappings"] = oauth2.DefaultCustomURLMappings
|
||||
|
||||
source, err := models.GetLoginSourceByID(ctx.ParamsInt64(":authid"))
|
||||
if err != nil {
|
||||
@ -310,7 +311,7 @@ func EditAuthSource(ctx *context.Context) {
|
||||
ctx.Data["HasTLS"] = source.HasTLS()
|
||||
|
||||
if source.IsOAuth2() {
|
||||
ctx.Data["CurrentOAuth2Provider"] = models.OAuth2Providers[source.OAuth2().Provider]
|
||||
ctx.Data["CurrentOAuth2Provider"] = oauth2.Providers[source.Cfg.(*oauth2.Source).Provider]
|
||||
}
|
||||
ctx.HTML(http.StatusOK, tplAuthEdit)
|
||||
}
|
||||
@ -322,9 +323,9 @@ func EditAuthSourcePost(ctx *context.Context) {
|
||||
ctx.Data["PageIsAdmin"] = true
|
||||
ctx.Data["PageIsAdminAuthentications"] = true
|
||||
|
||||
ctx.Data["SMTPAuths"] = models.SMTPAuths
|
||||
ctx.Data["OAuth2Providers"] = models.OAuth2Providers
|
||||
ctx.Data["OAuth2DefaultCustomURLMappings"] = models.OAuth2DefaultCustomURLMappings
|
||||
ctx.Data["SMTPAuths"] = smtp.Authenticators
|
||||
ctx.Data["OAuth2Providers"] = oauth2.Providers
|
||||
ctx.Data["OAuth2DefaultCustomURLMappings"] = oauth2.DefaultCustomURLMappings
|
||||
|
||||
source, err := models.GetLoginSourceByID(ctx.ParamsInt64(":authid"))
|
||||
if err != nil {
|
||||
@ -346,7 +347,7 @@ func EditAuthSourcePost(ctx *context.Context) {
|
||||
case models.LoginSMTP:
|
||||
config = parseSMTPConfig(form)
|
||||
case models.LoginPAM:
|
||||
config = &models.PAMConfig{
|
||||
config = &pamService.Source{
|
||||
ServiceName: form.PAMServiceName,
|
||||
EmailDomain: form.PAMEmailDomain,
|
||||
}
|
||||
@ -364,7 +365,7 @@ func EditAuthSourcePost(ctx *context.Context) {
|
||||
}
|
||||
|
||||
source.Name = form.Name
|
||||
source.IsActived = form.IsActive
|
||||
source.IsActive = form.IsActive
|
||||
source.IsSyncEnabled = form.IsSyncEnabled
|
||||
source.Cfg = config
|
||||
if err := models.UpdateSource(source); err != nil {
|
||||
|
@ -14,7 +14,6 @@ import (
|
||||
"strings"
|
||||
|
||||
"code.gitea.io/gitea/models"
|
||||
"code.gitea.io/gitea/modules/auth/oauth2"
|
||||
"code.gitea.io/gitea/modules/base"
|
||||
"code.gitea.io/gitea/modules/context"
|
||||
"code.gitea.io/gitea/modules/eventsource"
|
||||
@ -27,6 +26,8 @@ import (
|
||||
"code.gitea.io/gitea/modules/web"
|
||||
"code.gitea.io/gitea/modules/web/middleware"
|
||||
"code.gitea.io/gitea/routers/utils"
|
||||
"code.gitea.io/gitea/services/auth"
|
||||
"code.gitea.io/gitea/services/auth/source/oauth2"
|
||||
"code.gitea.io/gitea/services/externalaccount"
|
||||
"code.gitea.io/gitea/services/forms"
|
||||
"code.gitea.io/gitea/services/mailer"
|
||||
@ -135,7 +136,7 @@ func SignIn(ctx *context.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
orderedOAuth2Names, oauth2Providers, err := models.GetActiveOAuth2Providers()
|
||||
orderedOAuth2Names, oauth2Providers, err := oauth2.GetActiveOAuth2Providers()
|
||||
if err != nil {
|
||||
ctx.ServerError("UserSignIn", err)
|
||||
return
|
||||
@ -155,7 +156,7 @@ func SignIn(ctx *context.Context) {
|
||||
func SignInPost(ctx *context.Context) {
|
||||
ctx.Data["Title"] = ctx.Tr("sign_in")
|
||||
|
||||
orderedOAuth2Names, oauth2Providers, err := models.GetActiveOAuth2Providers()
|
||||
orderedOAuth2Names, oauth2Providers, err := oauth2.GetActiveOAuth2Providers()
|
||||
if err != nil {
|
||||
ctx.ServerError("UserSignIn", err)
|
||||
return
|
||||
@ -174,7 +175,7 @@ func SignInPost(ctx *context.Context) {
|
||||
}
|
||||
|
||||
form := web.GetForm(ctx).(*forms.SignInForm)
|
||||
u, err := models.UserSignIn(form.UserName, form.Password)
|
||||
u, err := auth.UserSignIn(form.UserName, form.Password)
|
||||
if err != nil {
|
||||
if models.IsErrUserNotExist(err) {
|
||||
ctx.RenderWithErr(ctx.Tr("form.username_password_incorrect"), tplSignIn, &form)
|
||||
@ -577,13 +578,13 @@ func SignInOAuth(ctx *context.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
if err = oauth2.Auth(loginSource.Name, ctx.Req, ctx.Resp); err != nil {
|
||||
if err = loginSource.Cfg.(*oauth2.Source).Callout(ctx.Req, ctx.Resp); err != nil {
|
||||
if strings.Contains(err.Error(), "no provider for ") {
|
||||
if err = models.ResetOAuth2(); err != nil {
|
||||
if err = oauth2.ResetOAuth2(); err != nil {
|
||||
ctx.ServerError("SignIn", err)
|
||||
return
|
||||
}
|
||||
if err = oauth2.Auth(loginSource.Name, ctx.Req, ctx.Resp); err != nil {
|
||||
if err = loginSource.Cfg.(*oauth2.Source).Callout(ctx.Req, ctx.Resp); err != nil {
|
||||
ctx.ServerError("SignIn", err)
|
||||
}
|
||||
return
|
||||
@ -631,7 +632,7 @@ func SignInOAuthCallback(ctx *context.Context) {
|
||||
}
|
||||
if len(missingFields) > 0 {
|
||||
log.Error("OAuth2 Provider %s returned empty or missing fields: %s", loginSource.Name, missingFields)
|
||||
if loginSource.IsOAuth2() && loginSource.OAuth2().Provider == "openidConnect" {
|
||||
if loginSource.IsOAuth2() && loginSource.Cfg.(*oauth2.Source).Provider == "openidConnect" {
|
||||
log.Error("You may need to change the 'OPENID_CONNECT_SCOPES' setting to request all required fields")
|
||||
}
|
||||
err = fmt.Errorf("OAuth2 Provider %s returned empty or missing fields: %s", loginSource.Name, missingFields)
|
||||
@ -772,8 +773,7 @@ func handleOAuth2SignIn(ctx *context.Context, u *models.User, gothUser goth.User
|
||||
// OAuth2UserLoginCallback attempts to handle the callback from the OAuth2 provider and if successful
|
||||
// login the user
|
||||
func oAuth2UserLoginCallback(loginSource *models.LoginSource, request *http.Request, response http.ResponseWriter) (*models.User, goth.User, error) {
|
||||
gothUser, err := oauth2.ProviderCallback(loginSource.Name, request, response)
|
||||
|
||||
gothUser, err := loginSource.Cfg.(*oauth2.Source).Callback(request, response)
|
||||
if err != nil {
|
||||
if err.Error() == "securecookie: the value is too long" {
|
||||
log.Error("OAuth2 Provider %s returned too long a token. Current max: %d. Either increase the [OAuth2] MAX_TOKEN_LENGTH or reduce the information returned from the OAuth2 provider", loginSource.Name, setting.OAuth2.MaxTokenLength)
|
||||
@ -901,7 +901,7 @@ func LinkAccountPostSignIn(ctx *context.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
u, err := models.UserSignIn(signInForm.UserName, signInForm.Password)
|
||||
u, err := auth.UserSignIn(signInForm.UserName, signInForm.Password)
|
||||
if err != nil {
|
||||
if models.IsErrUserNotExist(err) {
|
||||
ctx.Data["user_exists"] = true
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user