summaryrefslogtreecommitdiffstats
path: root/models/asymkey/ssh_key.go
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--models/asymkey/ssh_key.go427
1 files changed, 427 insertions, 0 deletions
diff --git a/models/asymkey/ssh_key.go b/models/asymkey/ssh_key.go
new file mode 100644
index 0000000..7a18732
--- /dev/null
+++ b/models/asymkey/ssh_key.go
@@ -0,0 +1,427 @@
+// 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
+}