mirror of
				https://codeberg.org/forgejo/forgejo.git
				synced 2025-10-25 03:22:36 +00:00 
			
		
		
		
	Fix #31730 This PR rewrote the function `PublicKeysAreExternallyManaged` with a simple test. The new function removed the loop to make it more readable. (cherry picked from commit b491b2104f83ee8fc4956c099c427b339291b3be)
		
			
				
	
	
		
			427 lines
		
	
	
	
		
			12 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			427 lines
		
	
	
	
		
			12 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
| // Copyright 2014 The Gogs Authors. All rights reserved.
 | |
| // Copyright 2019 The Gitea Authors. All rights reserved.
 | |
| // SPDX-License-Identifier: MIT
 | |
| 
 | |
| package asymkey
 | |
| 
 | |
| import (
 | |
| 	"context"
 | |
| 	"fmt"
 | |
| 	"strings"
 | |
| 	"time"
 | |
| 
 | |
| 	"code.gitea.io/gitea/models/auth"
 | |
| 	"code.gitea.io/gitea/models/db"
 | |
| 	"code.gitea.io/gitea/models/perm"
 | |
| 	user_model "code.gitea.io/gitea/models/user"
 | |
| 	"code.gitea.io/gitea/modules/log"
 | |
| 	"code.gitea.io/gitea/modules/timeutil"
 | |
| 	"code.gitea.io/gitea/modules/util"
 | |
| 
 | |
| 	"golang.org/x/crypto/ssh"
 | |
| 	"xorm.io/builder"
 | |
| )
 | |
| 
 | |
| // KeyType specifies the key type
 | |
| type KeyType int
 | |
| 
 | |
| const (
 | |
| 	// KeyTypeUser specifies the user key
 | |
| 	KeyTypeUser = iota + 1
 | |
| 	// KeyTypeDeploy specifies the deploy key
 | |
| 	KeyTypeDeploy
 | |
| 	// KeyTypePrincipal specifies the authorized principal key
 | |
| 	KeyTypePrincipal
 | |
| )
 | |
| 
 | |
| // PublicKey represents a user or deploy SSH public key.
 | |
| type PublicKey struct {
 | |
| 	ID            int64           `xorm:"pk autoincr"`
 | |
| 	OwnerID       int64           `xorm:"INDEX NOT NULL"`
 | |
| 	Name          string          `xorm:"NOT NULL"`
 | |
| 	Fingerprint   string          `xorm:"INDEX NOT NULL"`
 | |
| 	Content       string          `xorm:"MEDIUMTEXT NOT NULL"`
 | |
| 	Mode          perm.AccessMode `xorm:"NOT NULL DEFAULT 2"`
 | |
| 	Type          KeyType         `xorm:"NOT NULL DEFAULT 1"`
 | |
| 	LoginSourceID int64           `xorm:"NOT NULL DEFAULT 0"`
 | |
| 
 | |
| 	CreatedUnix       timeutil.TimeStamp `xorm:"created"`
 | |
| 	UpdatedUnix       timeutil.TimeStamp `xorm:"updated"`
 | |
| 	HasRecentActivity bool               `xorm:"-"`
 | |
| 	HasUsed           bool               `xorm:"-"`
 | |
| 	Verified          bool               `xorm:"NOT NULL DEFAULT false"`
 | |
| }
 | |
| 
 | |
| func init() {
 | |
| 	db.RegisterModel(new(PublicKey))
 | |
| }
 | |
| 
 | |
| // AfterLoad is invoked from XORM after setting the values of all fields of this object.
 | |
| func (key *PublicKey) AfterLoad() {
 | |
| 	key.HasUsed = key.UpdatedUnix > key.CreatedUnix
 | |
| 	key.HasRecentActivity = key.UpdatedUnix.AddDuration(7*24*time.Hour) > timeutil.TimeStampNow()
 | |
| }
 | |
| 
 | |
| // OmitEmail returns content of public key without email address.
 | |
| func (key *PublicKey) OmitEmail() string {
 | |
| 	return strings.Join(strings.Split(key.Content, " ")[:2], " ")
 | |
| }
 | |
| 
 | |
| // AuthorizedString returns formatted public key string for authorized_keys file.
 | |
| //
 | |
| // TODO: Consider dropping this function
 | |
| func (key *PublicKey) AuthorizedString() string {
 | |
| 	return AuthorizedStringForKey(key)
 | |
| }
 | |
| 
 | |
| func addKey(ctx context.Context, key *PublicKey) (err error) {
 | |
| 	if len(key.Fingerprint) == 0 {
 | |
| 		key.Fingerprint, err = CalcFingerprint(key.Content)
 | |
| 		if err != nil {
 | |
| 			return err
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	// Save SSH key.
 | |
| 	if err = db.Insert(ctx, key); err != nil {
 | |
| 		return err
 | |
| 	}
 | |
| 
 | |
| 	return appendAuthorizedKeysToFile(key)
 | |
| }
 | |
| 
 | |
| // AddPublicKey adds new public key to database and authorized_keys file.
 | |
| func AddPublicKey(ctx context.Context, ownerID int64, name, content string, authSourceID int64) (*PublicKey, error) {
 | |
| 	log.Trace(content)
 | |
| 
 | |
| 	fingerprint, err := CalcFingerprint(content)
 | |
| 	if err != nil {
 | |
| 		return nil, err
 | |
| 	}
 | |
| 
 | |
| 	ctx, committer, err := db.TxContext(ctx)
 | |
| 	if err != nil {
 | |
| 		return nil, err
 | |
| 	}
 | |
| 	defer committer.Close()
 | |
| 
 | |
| 	if err := checkKeyFingerprint(ctx, fingerprint); err != nil {
 | |
| 		return nil, err
 | |
| 	}
 | |
| 
 | |
| 	// Key name of same user cannot be duplicated.
 | |
| 	has, err := db.GetEngine(ctx).
 | |
| 		Where("owner_id = ? AND name = ?", ownerID, name).
 | |
| 		Get(new(PublicKey))
 | |
| 	if err != nil {
 | |
| 		return nil, err
 | |
| 	} else if has {
 | |
| 		return nil, ErrKeyNameAlreadyUsed{ownerID, name}
 | |
| 	}
 | |
| 
 | |
| 	key := &PublicKey{
 | |
| 		OwnerID:       ownerID,
 | |
| 		Name:          name,
 | |
| 		Fingerprint:   fingerprint,
 | |
| 		Content:       content,
 | |
| 		Mode:          perm.AccessModeWrite,
 | |
| 		Type:          KeyTypeUser,
 | |
| 		LoginSourceID: authSourceID,
 | |
| 	}
 | |
| 	if err = addKey(ctx, key); err != nil {
 | |
| 		return nil, fmt.Errorf("addKey: %w", err)
 | |
| 	}
 | |
| 
 | |
| 	return key, committer.Commit()
 | |
| }
 | |
| 
 | |
| // GetPublicKeyByID returns public key by given ID.
 | |
| func GetPublicKeyByID(ctx context.Context, keyID int64) (*PublicKey, error) {
 | |
| 	key := new(PublicKey)
 | |
| 	has, err := db.GetEngine(ctx).
 | |
| 		ID(keyID).
 | |
| 		Get(key)
 | |
| 	if err != nil {
 | |
| 		return nil, err
 | |
| 	} else if !has {
 | |
| 		return nil, ErrKeyNotExist{keyID}
 | |
| 	}
 | |
| 	return key, nil
 | |
| }
 | |
| 
 | |
| // SearchPublicKeyByContent searches content as prefix (leak e-mail part)
 | |
| // and returns public key found.
 | |
| func SearchPublicKeyByContent(ctx context.Context, content string) (*PublicKey, error) {
 | |
| 	key := new(PublicKey)
 | |
| 	has, err := db.GetEngine(ctx).
 | |
| 		Where("content like ?", content+"%").
 | |
| 		Get(key)
 | |
| 	if err != nil {
 | |
| 		return nil, err
 | |
| 	} else if !has {
 | |
| 		return nil, ErrKeyNotExist{}
 | |
| 	}
 | |
| 	return key, nil
 | |
| }
 | |
| 
 | |
| // SearchPublicKeyByContentExact searches content
 | |
| // and returns public key found.
 | |
| func SearchPublicKeyByContentExact(ctx context.Context, content string) (*PublicKey, error) {
 | |
| 	key := new(PublicKey)
 | |
| 	has, err := db.GetEngine(ctx).
 | |
| 		Where("content = ?", content).
 | |
| 		Get(key)
 | |
| 	if err != nil {
 | |
| 		return nil, err
 | |
| 	} else if !has {
 | |
| 		return nil, ErrKeyNotExist{}
 | |
| 	}
 | |
| 	return key, nil
 | |
| }
 | |
| 
 | |
| type FindPublicKeyOptions struct {
 | |
| 	db.ListOptions
 | |
| 	OwnerID       int64
 | |
| 	Fingerprint   string
 | |
| 	KeyTypes      []KeyType
 | |
| 	NotKeytype    KeyType
 | |
| 	LoginSourceID int64
 | |
| }
 | |
| 
 | |
| func (opts FindPublicKeyOptions) ToConds() builder.Cond {
 | |
| 	cond := builder.NewCond()
 | |
| 	if opts.OwnerID > 0 {
 | |
| 		cond = cond.And(builder.Eq{"owner_id": opts.OwnerID})
 | |
| 	}
 | |
| 	if opts.Fingerprint != "" {
 | |
| 		cond = cond.And(builder.Eq{"fingerprint": opts.Fingerprint})
 | |
| 	}
 | |
| 	if len(opts.KeyTypes) > 0 {
 | |
| 		cond = cond.And(builder.In("`type`", opts.KeyTypes))
 | |
| 	}
 | |
| 	if opts.NotKeytype > 0 {
 | |
| 		cond = cond.And(builder.Neq{"`type`": opts.NotKeytype})
 | |
| 	}
 | |
| 	if opts.LoginSourceID > 0 {
 | |
| 		cond = cond.And(builder.Eq{"login_source_id": opts.LoginSourceID})
 | |
| 	}
 | |
| 	return cond
 | |
| }
 | |
| 
 | |
| // UpdatePublicKeyUpdated updates public key use time.
 | |
| func UpdatePublicKeyUpdated(ctx context.Context, id int64) error {
 | |
| 	// Check if key exists before update as affected rows count is unreliable
 | |
| 	//    and will return 0 affected rows if two updates are made at the same time
 | |
| 	if cnt, err := db.GetEngine(ctx).ID(id).Count(&PublicKey{}); err != nil {
 | |
| 		return err
 | |
| 	} else if cnt != 1 {
 | |
| 		return ErrKeyNotExist{id}
 | |
| 	}
 | |
| 
 | |
| 	_, err := db.GetEngine(ctx).ID(id).Cols("updated_unix").Update(&PublicKey{
 | |
| 		UpdatedUnix: timeutil.TimeStampNow(),
 | |
| 	})
 | |
| 	if err != nil {
 | |
| 		return err
 | |
| 	}
 | |
| 	return nil
 | |
| }
 | |
| 
 | |
| // PublicKeysAreExternallyManaged returns whether the provided KeyID represents an externally managed Key
 | |
| func PublicKeysAreExternallyManaged(ctx context.Context, keys []*PublicKey) ([]bool, error) {
 | |
| 	sourceCache := make(map[int64]*auth.Source, len(keys))
 | |
| 	externals := make([]bool, len(keys))
 | |
| 
 | |
| 	for i, key := range keys {
 | |
| 		if key.LoginSourceID == 0 {
 | |
| 			externals[i] = false
 | |
| 			continue
 | |
| 		}
 | |
| 
 | |
| 		source, ok := sourceCache[key.LoginSourceID]
 | |
| 		if !ok {
 | |
| 			var err error
 | |
| 			source, err = auth.GetSourceByID(ctx, key.LoginSourceID)
 | |
| 			if err != nil {
 | |
| 				if auth.IsErrSourceNotExist(err) {
 | |
| 					externals[i] = false
 | |
| 					sourceCache[key.LoginSourceID] = &auth.Source{
 | |
| 						ID: key.LoginSourceID,
 | |
| 					}
 | |
| 					continue
 | |
| 				}
 | |
| 				return nil, err
 | |
| 			}
 | |
| 		}
 | |
| 
 | |
| 		if sshKeyProvider, ok := source.Cfg.(auth.SSHKeyProvider); ok && sshKeyProvider.ProvidesSSHKeys() {
 | |
| 			// Disable setting SSH keys for this user
 | |
| 			externals[i] = true
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	return externals, nil
 | |
| }
 | |
| 
 | |
| // PublicKeyIsExternallyManaged returns whether the provided KeyID represents an externally managed Key
 | |
| func PublicKeyIsExternallyManaged(ctx context.Context, id int64) (bool, error) {
 | |
| 	key, err := GetPublicKeyByID(ctx, id)
 | |
| 	if err != nil {
 | |
| 		return false, err
 | |
| 	}
 | |
| 	if key.LoginSourceID == 0 {
 | |
| 		return false, nil
 | |
| 	}
 | |
| 	source, err := auth.GetSourceByID(ctx, key.LoginSourceID)
 | |
| 	if err != nil {
 | |
| 		if auth.IsErrSourceNotExist(err) {
 | |
| 			return false, nil
 | |
| 		}
 | |
| 		return false, err
 | |
| 	}
 | |
| 	if sshKeyProvider, ok := source.Cfg.(auth.SSHKeyProvider); ok && sshKeyProvider.ProvidesSSHKeys() {
 | |
| 		// Disable setting SSH keys for this user
 | |
| 		return true, nil
 | |
| 	}
 | |
| 	return false, nil
 | |
| }
 | |
| 
 | |
| // deleteKeysMarkedForDeletion returns true if ssh keys needs update
 | |
| func deleteKeysMarkedForDeletion(ctx context.Context, keys []string) (bool, error) {
 | |
| 	// Start session
 | |
| 	ctx, committer, err := db.TxContext(ctx)
 | |
| 	if err != nil {
 | |
| 		return false, err
 | |
| 	}
 | |
| 	defer committer.Close()
 | |
| 
 | |
| 	// Delete keys marked for deletion
 | |
| 	var sshKeysNeedUpdate bool
 | |
| 	for _, KeyToDelete := range keys {
 | |
| 		key, err := SearchPublicKeyByContent(ctx, KeyToDelete)
 | |
| 		if err != nil {
 | |
| 			log.Error("SearchPublicKeyByContent: %v", err)
 | |
| 			continue
 | |
| 		}
 | |
| 		if _, err = db.DeleteByID[PublicKey](ctx, key.ID); err != nil {
 | |
| 			log.Error("DeleteByID[PublicKey]: %v", err)
 | |
| 			continue
 | |
| 		}
 | |
| 		sshKeysNeedUpdate = true
 | |
| 	}
 | |
| 
 | |
| 	if err := committer.Commit(); err != nil {
 | |
| 		return false, err
 | |
| 	}
 | |
| 
 | |
| 	return sshKeysNeedUpdate, nil
 | |
| }
 | |
| 
 | |
| // AddPublicKeysBySource add a users public keys. Returns true if there are changes.
 | |
| func AddPublicKeysBySource(ctx context.Context, usr *user_model.User, s *auth.Source, sshPublicKeys []string) bool {
 | |
| 	var sshKeysNeedUpdate bool
 | |
| 	for _, sshKey := range sshPublicKeys {
 | |
| 		var err error
 | |
| 		found := false
 | |
| 		keys := []byte(sshKey)
 | |
| 	loop:
 | |
| 		for len(keys) > 0 && err == nil {
 | |
| 			var out ssh.PublicKey
 | |
| 			// We ignore options as they are not relevant to Gitea
 | |
| 			out, _, _, keys, err = ssh.ParseAuthorizedKey(keys)
 | |
| 			if err != nil {
 | |
| 				break loop
 | |
| 			}
 | |
| 			found = true
 | |
| 			marshalled := string(ssh.MarshalAuthorizedKey(out))
 | |
| 			marshalled = marshalled[:len(marshalled)-1]
 | |
| 			sshKeyName := fmt.Sprintf("%s-%s", s.Name, ssh.FingerprintSHA256(out))
 | |
| 
 | |
| 			if _, err := AddPublicKey(ctx, usr.ID, sshKeyName, marshalled, s.ID); err != nil {
 | |
| 				if IsErrKeyAlreadyExist(err) {
 | |
| 					log.Trace("AddPublicKeysBySource[%s]: Public SSH Key %s already exists for user", sshKeyName, usr.Name)
 | |
| 				} else {
 | |
| 					log.Error("AddPublicKeysBySource[%s]: Error adding Public SSH Key for user %s: %v", sshKeyName, usr.Name, err)
 | |
| 				}
 | |
| 			} else {
 | |
| 				log.Trace("AddPublicKeysBySource[%s]: Added Public SSH Key for user %s", sshKeyName, usr.Name)
 | |
| 				sshKeysNeedUpdate = true
 | |
| 			}
 | |
| 		}
 | |
| 		if !found && err != nil {
 | |
| 			log.Warn("AddPublicKeysBySource[%s]: Skipping invalid Public SSH Key for user %s: %v", s.Name, usr.Name, sshKey)
 | |
| 		}
 | |
| 	}
 | |
| 	return sshKeysNeedUpdate
 | |
| }
 | |
| 
 | |
| // SynchronizePublicKeys updates a users public keys. Returns true if there are changes.
 | |
| func SynchronizePublicKeys(ctx context.Context, usr *user_model.User, s *auth.Source, sshPublicKeys []string) bool {
 | |
| 	var sshKeysNeedUpdate bool
 | |
| 
 | |
| 	log.Trace("synchronizePublicKeys[%s]: Handling Public SSH Key synchronization for user %s", s.Name, usr.Name)
 | |
| 
 | |
| 	// Get Public Keys from DB with current LDAP source
 | |
| 	var giteaKeys []string
 | |
| 	keys, err := db.Find[PublicKey](ctx, FindPublicKeyOptions{
 | |
| 		OwnerID:       usr.ID,
 | |
| 		LoginSourceID: s.ID,
 | |
| 	})
 | |
| 	if err != nil {
 | |
| 		log.Error("synchronizePublicKeys[%s]: Error listing Public SSH Keys for user %s: %v", s.Name, usr.Name, err)
 | |
| 	}
 | |
| 
 | |
| 	for _, v := range keys {
 | |
| 		giteaKeys = append(giteaKeys, v.OmitEmail())
 | |
| 	}
 | |
| 
 | |
| 	// Process the provided keys to remove duplicates and name part
 | |
| 	var providedKeys []string
 | |
| 	for _, v := range sshPublicKeys {
 | |
| 		sshKeySplit := strings.Split(v, " ")
 | |
| 		if len(sshKeySplit) > 1 {
 | |
| 			key := strings.Join(sshKeySplit[:2], " ")
 | |
| 			if !util.SliceContainsString(providedKeys, key) {
 | |
| 				providedKeys = append(providedKeys, key)
 | |
| 			}
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	// Check if Public Key sync is needed
 | |
| 	if util.SliceSortedEqual(giteaKeys, providedKeys) {
 | |
| 		log.Trace("synchronizePublicKeys[%s]: Public Keys are already in sync for %s (Source:%v/DB:%v)", s.Name, usr.Name, len(providedKeys), len(giteaKeys))
 | |
| 		return false
 | |
| 	}
 | |
| 	log.Trace("synchronizePublicKeys[%s]: Public Key needs update for user %s (Source:%v/DB:%v)", s.Name, usr.Name, len(providedKeys), len(giteaKeys))
 | |
| 
 | |
| 	// Add new Public SSH Keys that doesn't already exist in DB
 | |
| 	var newKeys []string
 | |
| 	for _, key := range providedKeys {
 | |
| 		if !util.SliceContainsString(giteaKeys, key) {
 | |
| 			newKeys = append(newKeys, key)
 | |
| 		}
 | |
| 	}
 | |
| 	if AddPublicKeysBySource(ctx, usr, s, newKeys) {
 | |
| 		sshKeysNeedUpdate = true
 | |
| 	}
 | |
| 
 | |
| 	// Mark keys from DB that no longer exist in the source for deletion
 | |
| 	var giteaKeysToDelete []string
 | |
| 	for _, giteaKey := range giteaKeys {
 | |
| 		if !util.SliceContainsString(providedKeys, giteaKey) {
 | |
| 			log.Trace("synchronizePublicKeys[%s]: Marking Public SSH Key for deletion for user %s: %v", s.Name, usr.Name, giteaKey)
 | |
| 			giteaKeysToDelete = append(giteaKeysToDelete, giteaKey)
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	// Delete keys from DB that no longer exist in the source
 | |
| 	needUpd, err := deleteKeysMarkedForDeletion(ctx, giteaKeysToDelete)
 | |
| 	if err != nil {
 | |
| 		log.Error("synchronizePublicKeys[%s]: Error deleting Public Keys marked for deletion for user %s: %v", s.Name, usr.Name, err)
 | |
| 	}
 | |
| 	if needUpd {
 | |
| 		sshKeysNeedUpdate = true
 | |
| 	}
 | |
| 
 | |
| 	return sshKeysNeedUpdate
 | |
| }
 |