code.gitea.io/gitea@v1.22.3/models/asymkey/ssh_key.go (about)

     1  // Copyright 2014 The Gogs Authors. All rights reserved.
     2  // Copyright 2019 The Gitea Authors. All rights reserved.
     3  // SPDX-License-Identifier: MIT
     4  
     5  package asymkey
     6  
     7  import (
     8  	"context"
     9  	"fmt"
    10  	"strings"
    11  	"time"
    12  
    13  	"code.gitea.io/gitea/models/auth"
    14  	"code.gitea.io/gitea/models/db"
    15  	"code.gitea.io/gitea/models/perm"
    16  	user_model "code.gitea.io/gitea/models/user"
    17  	"code.gitea.io/gitea/modules/log"
    18  	"code.gitea.io/gitea/modules/timeutil"
    19  	"code.gitea.io/gitea/modules/util"
    20  
    21  	"golang.org/x/crypto/ssh"
    22  	"xorm.io/builder"
    23  )
    24  
    25  // KeyType specifies the key type
    26  type KeyType int
    27  
    28  const (
    29  	// KeyTypeUser specifies the user key
    30  	KeyTypeUser = iota + 1
    31  	// KeyTypeDeploy specifies the deploy key
    32  	KeyTypeDeploy
    33  	// KeyTypePrincipal specifies the authorized principal key
    34  	KeyTypePrincipal
    35  )
    36  
    37  // PublicKey represents a user or deploy SSH public key.
    38  type PublicKey struct {
    39  	ID            int64           `xorm:"pk autoincr"`
    40  	OwnerID       int64           `xorm:"INDEX NOT NULL"`
    41  	Name          string          `xorm:"NOT NULL"`
    42  	Fingerprint   string          `xorm:"INDEX NOT NULL"`
    43  	Content       string          `xorm:"MEDIUMTEXT NOT NULL"`
    44  	Mode          perm.AccessMode `xorm:"NOT NULL DEFAULT 2"`
    45  	Type          KeyType         `xorm:"NOT NULL DEFAULT 1"`
    46  	LoginSourceID int64           `xorm:"NOT NULL DEFAULT 0"`
    47  
    48  	CreatedUnix       timeutil.TimeStamp `xorm:"created"`
    49  	UpdatedUnix       timeutil.TimeStamp `xorm:"updated"`
    50  	HasRecentActivity bool               `xorm:"-"`
    51  	HasUsed           bool               `xorm:"-"`
    52  	Verified          bool               `xorm:"NOT NULL DEFAULT false"`
    53  }
    54  
    55  func init() {
    56  	db.RegisterModel(new(PublicKey))
    57  }
    58  
    59  // AfterLoad is invoked from XORM after setting the values of all fields of this object.
    60  func (key *PublicKey) AfterLoad() {
    61  	key.HasUsed = key.UpdatedUnix > key.CreatedUnix
    62  	key.HasRecentActivity = key.UpdatedUnix.AddDuration(7*24*time.Hour) > timeutil.TimeStampNow()
    63  }
    64  
    65  // OmitEmail returns content of public key without email address.
    66  func (key *PublicKey) OmitEmail() string {
    67  	return strings.Join(strings.Split(key.Content, " ")[:2], " ")
    68  }
    69  
    70  // AuthorizedString returns formatted public key string for authorized_keys file.
    71  //
    72  // TODO: Consider dropping this function
    73  func (key *PublicKey) AuthorizedString() string {
    74  	return AuthorizedStringForKey(key)
    75  }
    76  
    77  func addKey(ctx context.Context, key *PublicKey) (err error) {
    78  	if len(key.Fingerprint) == 0 {
    79  		key.Fingerprint, err = CalcFingerprint(key.Content)
    80  		if err != nil {
    81  			return err
    82  		}
    83  	}
    84  
    85  	// Save SSH key.
    86  	if err = db.Insert(ctx, key); err != nil {
    87  		return err
    88  	}
    89  
    90  	return appendAuthorizedKeysToFile(key)
    91  }
    92  
    93  // AddPublicKey adds new public key to database and authorized_keys file.
    94  func AddPublicKey(ctx context.Context, ownerID int64, name, content string, authSourceID int64) (*PublicKey, error) {
    95  	log.Trace(content)
    96  
    97  	fingerprint, err := CalcFingerprint(content)
    98  	if err != nil {
    99  		return nil, err
   100  	}
   101  
   102  	ctx, committer, err := db.TxContext(ctx)
   103  	if err != nil {
   104  		return nil, err
   105  	}
   106  	defer committer.Close()
   107  
   108  	if err := checkKeyFingerprint(ctx, fingerprint); err != nil {
   109  		return nil, err
   110  	}
   111  
   112  	// Key name of same user cannot be duplicated.
   113  	has, err := db.GetEngine(ctx).
   114  		Where("owner_id = ? AND name = ?", ownerID, name).
   115  		Get(new(PublicKey))
   116  	if err != nil {
   117  		return nil, err
   118  	} else if has {
   119  		return nil, ErrKeyNameAlreadyUsed{ownerID, name}
   120  	}
   121  
   122  	key := &PublicKey{
   123  		OwnerID:       ownerID,
   124  		Name:          name,
   125  		Fingerprint:   fingerprint,
   126  		Content:       content,
   127  		Mode:          perm.AccessModeWrite,
   128  		Type:          KeyTypeUser,
   129  		LoginSourceID: authSourceID,
   130  	}
   131  	if err = addKey(ctx, key); err != nil {
   132  		return nil, fmt.Errorf("addKey: %w", err)
   133  	}
   134  
   135  	return key, committer.Commit()
   136  }
   137  
   138  // GetPublicKeyByID returns public key by given ID.
   139  func GetPublicKeyByID(ctx context.Context, keyID int64) (*PublicKey, error) {
   140  	key := new(PublicKey)
   141  	has, err := db.GetEngine(ctx).
   142  		ID(keyID).
   143  		Get(key)
   144  	if err != nil {
   145  		return nil, err
   146  	} else if !has {
   147  		return nil, ErrKeyNotExist{keyID}
   148  	}
   149  	return key, nil
   150  }
   151  
   152  // SearchPublicKeyByContent searches content as prefix (leak e-mail part)
   153  // and returns public key found.
   154  func SearchPublicKeyByContent(ctx context.Context, content string) (*PublicKey, error) {
   155  	key := new(PublicKey)
   156  	has, err := db.GetEngine(ctx).
   157  		Where("content like ?", content+"%").
   158  		Get(key)
   159  	if err != nil {
   160  		return nil, err
   161  	} else if !has {
   162  		return nil, ErrKeyNotExist{}
   163  	}
   164  	return key, nil
   165  }
   166  
   167  // SearchPublicKeyByContentExact searches content
   168  // and returns public key found.
   169  func SearchPublicKeyByContentExact(ctx context.Context, content string) (*PublicKey, error) {
   170  	key := new(PublicKey)
   171  	has, err := db.GetEngine(ctx).
   172  		Where("content = ?", content).
   173  		Get(key)
   174  	if err != nil {
   175  		return nil, err
   176  	} else if !has {
   177  		return nil, ErrKeyNotExist{}
   178  	}
   179  	return key, nil
   180  }
   181  
   182  type FindPublicKeyOptions struct {
   183  	db.ListOptions
   184  	OwnerID       int64
   185  	Fingerprint   string
   186  	KeyTypes      []KeyType
   187  	NotKeytype    KeyType
   188  	LoginSourceID int64
   189  }
   190  
   191  func (opts FindPublicKeyOptions) ToConds() builder.Cond {
   192  	cond := builder.NewCond()
   193  	if opts.OwnerID > 0 {
   194  		cond = cond.And(builder.Eq{"owner_id": opts.OwnerID})
   195  	}
   196  	if opts.Fingerprint != "" {
   197  		cond = cond.And(builder.Eq{"fingerprint": opts.Fingerprint})
   198  	}
   199  	if len(opts.KeyTypes) > 0 {
   200  		cond = cond.And(builder.In("`type`", opts.KeyTypes))
   201  	}
   202  	if opts.NotKeytype > 0 {
   203  		cond = cond.And(builder.Neq{"`type`": opts.NotKeytype})
   204  	}
   205  	if opts.LoginSourceID > 0 {
   206  		cond = cond.And(builder.Eq{"login_source_id": opts.LoginSourceID})
   207  	}
   208  	return cond
   209  }
   210  
   211  // UpdatePublicKeyUpdated updates public key use time.
   212  func UpdatePublicKeyUpdated(ctx context.Context, id int64) error {
   213  	// Check if key exists before update as affected rows count is unreliable
   214  	//    and will return 0 affected rows if two updates are made at the same time
   215  	if cnt, err := db.GetEngine(ctx).ID(id).Count(&PublicKey{}); err != nil {
   216  		return err
   217  	} else if cnt != 1 {
   218  		return ErrKeyNotExist{id}
   219  	}
   220  
   221  	_, err := db.GetEngine(ctx).ID(id).Cols("updated_unix").Update(&PublicKey{
   222  		UpdatedUnix: timeutil.TimeStampNow(),
   223  	})
   224  	if err != nil {
   225  		return err
   226  	}
   227  	return nil
   228  }
   229  
   230  // PublicKeysAreExternallyManaged returns whether the provided KeyID represents an externally managed Key
   231  func PublicKeysAreExternallyManaged(ctx context.Context, keys []*PublicKey) ([]bool, error) {
   232  	sourceCache := make(map[int64]*auth.Source, len(keys))
   233  	externals := make([]bool, len(keys))
   234  
   235  	for i, key := range keys {
   236  		if key.LoginSourceID == 0 {
   237  			externals[i] = false
   238  			continue
   239  		}
   240  
   241  		source, ok := sourceCache[key.LoginSourceID]
   242  		if !ok {
   243  			var err error
   244  			source, err = auth.GetSourceByID(ctx, key.LoginSourceID)
   245  			if err != nil {
   246  				if auth.IsErrSourceNotExist(err) {
   247  					externals[i] = false
   248  					sourceCache[key.LoginSourceID] = &auth.Source{
   249  						ID: key.LoginSourceID,
   250  					}
   251  					continue
   252  				}
   253  				return nil, err
   254  			}
   255  		}
   256  
   257  		if sshKeyProvider, ok := source.Cfg.(auth.SSHKeyProvider); ok && sshKeyProvider.ProvidesSSHKeys() {
   258  			// Disable setting SSH keys for this user
   259  			externals[i] = true
   260  		}
   261  	}
   262  
   263  	return externals, nil
   264  }
   265  
   266  // PublicKeyIsExternallyManaged returns whether the provided KeyID represents an externally managed Key
   267  func PublicKeyIsExternallyManaged(ctx context.Context, id int64) (bool, error) {
   268  	key, err := GetPublicKeyByID(ctx, id)
   269  	if err != nil {
   270  		return false, err
   271  	}
   272  	if key.LoginSourceID == 0 {
   273  		return false, nil
   274  	}
   275  	source, err := auth.GetSourceByID(ctx, key.LoginSourceID)
   276  	if err != nil {
   277  		if auth.IsErrSourceNotExist(err) {
   278  			return false, nil
   279  		}
   280  		return false, err
   281  	}
   282  	if sshKeyProvider, ok := source.Cfg.(auth.SSHKeyProvider); ok && sshKeyProvider.ProvidesSSHKeys() {
   283  		// Disable setting SSH keys for this user
   284  		return true, nil
   285  	}
   286  	return false, nil
   287  }
   288  
   289  // deleteKeysMarkedForDeletion returns true if ssh keys needs update
   290  func deleteKeysMarkedForDeletion(ctx context.Context, keys []string) (bool, error) {
   291  	// Start session
   292  	ctx, committer, err := db.TxContext(ctx)
   293  	if err != nil {
   294  		return false, err
   295  	}
   296  	defer committer.Close()
   297  
   298  	// Delete keys marked for deletion
   299  	var sshKeysNeedUpdate bool
   300  	for _, KeyToDelete := range keys {
   301  		key, err := SearchPublicKeyByContent(ctx, KeyToDelete)
   302  		if err != nil {
   303  			log.Error("SearchPublicKeyByContent: %v", err)
   304  			continue
   305  		}
   306  		if _, err = db.DeleteByID[PublicKey](ctx, key.ID); err != nil {
   307  			log.Error("DeleteByID[PublicKey]: %v", err)
   308  			continue
   309  		}
   310  		sshKeysNeedUpdate = true
   311  	}
   312  
   313  	if err := committer.Commit(); err != nil {
   314  		return false, err
   315  	}
   316  
   317  	return sshKeysNeedUpdate, nil
   318  }
   319  
   320  // AddPublicKeysBySource add a users public keys. Returns true if there are changes.
   321  func AddPublicKeysBySource(ctx context.Context, usr *user_model.User, s *auth.Source, sshPublicKeys []string) bool {
   322  	var sshKeysNeedUpdate bool
   323  	for _, sshKey := range sshPublicKeys {
   324  		var err error
   325  		found := false
   326  		keys := []byte(sshKey)
   327  	loop:
   328  		for len(keys) > 0 && err == nil {
   329  			var out ssh.PublicKey
   330  			// We ignore options as they are not relevant to Gitea
   331  			out, _, _, keys, err = ssh.ParseAuthorizedKey(keys)
   332  			if err != nil {
   333  				break loop
   334  			}
   335  			found = true
   336  			marshalled := string(ssh.MarshalAuthorizedKey(out))
   337  			marshalled = marshalled[:len(marshalled)-1]
   338  			sshKeyName := fmt.Sprintf("%s-%s", s.Name, ssh.FingerprintSHA256(out))
   339  
   340  			if _, err := AddPublicKey(ctx, usr.ID, sshKeyName, marshalled, s.ID); err != nil {
   341  				if IsErrKeyAlreadyExist(err) {
   342  					log.Trace("AddPublicKeysBySource[%s]: Public SSH Key %s already exists for user", sshKeyName, usr.Name)
   343  				} else {
   344  					log.Error("AddPublicKeysBySource[%s]: Error adding Public SSH Key for user %s: %v", sshKeyName, usr.Name, err)
   345  				}
   346  			} else {
   347  				log.Trace("AddPublicKeysBySource[%s]: Added Public SSH Key for user %s", sshKeyName, usr.Name)
   348  				sshKeysNeedUpdate = true
   349  			}
   350  		}
   351  		if !found && err != nil {
   352  			log.Warn("AddPublicKeysBySource[%s]: Skipping invalid Public SSH Key for user %s: %v", s.Name, usr.Name, sshKey)
   353  		}
   354  	}
   355  	return sshKeysNeedUpdate
   356  }
   357  
   358  // SynchronizePublicKeys updates a users public keys. Returns true if there are changes.
   359  func SynchronizePublicKeys(ctx context.Context, usr *user_model.User, s *auth.Source, sshPublicKeys []string) bool {
   360  	var sshKeysNeedUpdate bool
   361  
   362  	log.Trace("synchronizePublicKeys[%s]: Handling Public SSH Key synchronization for user %s", s.Name, usr.Name)
   363  
   364  	// Get Public Keys from DB with current LDAP source
   365  	var giteaKeys []string
   366  	keys, err := db.Find[PublicKey](ctx, FindPublicKeyOptions{
   367  		OwnerID:       usr.ID,
   368  		LoginSourceID: s.ID,
   369  	})
   370  	if err != nil {
   371  		log.Error("synchronizePublicKeys[%s]: Error listing Public SSH Keys for user %s: %v", s.Name, usr.Name, err)
   372  	}
   373  
   374  	for _, v := range keys {
   375  		giteaKeys = append(giteaKeys, v.OmitEmail())
   376  	}
   377  
   378  	// Process the provided keys to remove duplicates and name part
   379  	var providedKeys []string
   380  	for _, v := range sshPublicKeys {
   381  		sshKeySplit := strings.Split(v, " ")
   382  		if len(sshKeySplit) > 1 {
   383  			key := strings.Join(sshKeySplit[:2], " ")
   384  			if !util.SliceContainsString(providedKeys, key) {
   385  				providedKeys = append(providedKeys, key)
   386  			}
   387  		}
   388  	}
   389  
   390  	// Check if Public Key sync is needed
   391  	if util.SliceSortedEqual(giteaKeys, providedKeys) {
   392  		log.Trace("synchronizePublicKeys[%s]: Public Keys are already in sync for %s (Source:%v/DB:%v)", s.Name, usr.Name, len(providedKeys), len(giteaKeys))
   393  		return false
   394  	}
   395  	log.Trace("synchronizePublicKeys[%s]: Public Key needs update for user %s (Source:%v/DB:%v)", s.Name, usr.Name, len(providedKeys), len(giteaKeys))
   396  
   397  	// Add new Public SSH Keys that doesn't already exist in DB
   398  	var newKeys []string
   399  	for _, key := range providedKeys {
   400  		if !util.SliceContainsString(giteaKeys, key) {
   401  			newKeys = append(newKeys, key)
   402  		}
   403  	}
   404  	if AddPublicKeysBySource(ctx, usr, s, newKeys) {
   405  		sshKeysNeedUpdate = true
   406  	}
   407  
   408  	// Mark keys from DB that no longer exist in the source for deletion
   409  	var giteaKeysToDelete []string
   410  	for _, giteaKey := range giteaKeys {
   411  		if !util.SliceContainsString(providedKeys, giteaKey) {
   412  			log.Trace("synchronizePublicKeys[%s]: Marking Public SSH Key for deletion for user %s: %v", s.Name, usr.Name, giteaKey)
   413  			giteaKeysToDelete = append(giteaKeysToDelete, giteaKey)
   414  		}
   415  	}
   416  
   417  	// Delete keys from DB that no longer exist in the source
   418  	needUpd, err := deleteKeysMarkedForDeletion(ctx, giteaKeysToDelete)
   419  	if err != nil {
   420  		log.Error("synchronizePublicKeys[%s]: Error deleting Public Keys marked for deletion for user %s: %v", s.Name, usr.Name, err)
   421  	}
   422  	if needUpd {
   423  		sshKeysNeedUpdate = true
   424  	}
   425  
   426  	return sshKeysNeedUpdate
   427  }