mirror of
https://github.com/go-gitea/gitea.git
synced 2026-10-19 09:33:54 +09:00
Backport https://github.com/go-gitea/gitea/pull/39839 by @silverwind Several handlers and model helpers skipped checks that their siblings already perform. - Only match principal keys when looking up SSH certificate principals - Return no attachments for an unsaved issue instead of every issue-less one - Reject issue and comment attachments when adding release attachments - Share one permission check for editing, deleting and reacting to issue content and for comment attachments between the web UI and the API, refusing them on locked issues for users who can't write - Reject issue edits, deletion and label changes in archived repositories via the API - Apply cross-origin protection to the repository topics endpoint - Return 404 for the repositories tab of individual users and respect profile visibility for the heatmap * Fixes: https://github.com/go-gitea/gitea/issues/39226 Co-authored-by: wxiaoguang <wxiaoguang@gmail.com> Co-authored-by: bircni <bircni@icloud.com>
453 lines
14 KiB
Go
453 lines
14 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"
|
|
|
|
"gitea.dev/models/auth"
|
|
"gitea.dev/models/db"
|
|
"gitea.dev/models/perm"
|
|
user_model "gitea.dev/models/user"
|
|
"gitea.dev/modules/log"
|
|
"gitea.dev/modules/setting"
|
|
"gitea.dev/modules/timeutil"
|
|
"gitea.dev/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"` // deploy-key doesn't have owner
|
|
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 {
|
|
fields := strings.Split(key.Content, " ") // format: ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQC... comment
|
|
if len(fields) < 2 {
|
|
setting.PanicInDevOrTesting("invalid public key %d content: %s", key.ID, key.Content)
|
|
return "" // not a valid public key, it shouldn't really happen, the value is managed internally
|
|
}
|
|
return strings.Join(fields[:2], " ")
|
|
}
|
|
|
|
func addPublicKey(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)
|
|
}
|
|
|
|
// FindOrAddDeployPublicKey returns the shared public key that deploy keys of the given content link to, adding it on first use.
|
|
func FindOrAddDeployPublicKey(ctx context.Context, content string) (*PublicKey, error) {
|
|
fingerprint, err := CalcFingerprint(content)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
pkey, exist, err := db.Get[PublicKey](ctx, builder.Eq{"fingerprint": fingerprint})
|
|
if err != nil {
|
|
return nil, err
|
|
} else if exist {
|
|
if pkey.Type != KeyTypeDeploy {
|
|
return nil, ErrKeyAlreadyExist{0, fingerprint, ""}
|
|
}
|
|
return pkey, nil
|
|
}
|
|
|
|
pkey = &PublicKey{
|
|
Mode: perm.AccessModeNone,
|
|
Type: KeyTypeDeploy,
|
|
Name: "(DeployKey)",
|
|
Content: content,
|
|
Fingerprint: fingerprint,
|
|
}
|
|
if err = addPublicKey(ctx, pkey); err != nil {
|
|
return nil, fmt.Errorf("addPublicKey: %w", err)
|
|
}
|
|
return pkey, nil
|
|
}
|
|
|
|
// AddPublicKey adds new public key to database and authorized_keys file.
|
|
func AddPublicKey(ctx context.Context, ownerID int64, name, content string, authSourceID int64, verified bool) (*PublicKey, error) {
|
|
log.Trace(content)
|
|
|
|
fingerprint, err := CalcFingerprint(content)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return db.WithTx2(ctx, func(ctx context.Context) (*PublicKey, error) {
|
|
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,
|
|
Verified: verified,
|
|
}
|
|
if err = addPublicKey(ctx, key); err != nil {
|
|
return nil, fmt.Errorf("addKey: %w", err)
|
|
}
|
|
|
|
return key, nil
|
|
})
|
|
}
|
|
|
|
// 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
|
|
}
|
|
|
|
func SearchPublicKeyForSSH(ctx context.Context, sshPubKey string) (*PublicKey, error) {
|
|
// this function is designed to only accept SSH public keys,
|
|
// because there might be different methods to calculate the fingerprint in the future (at the moment: "SHA256:...")
|
|
fingerprint, err := CalcFingerprint(sshPubKey)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
key := new(PublicKey)
|
|
has, err := db.GetEngine(ctx).
|
|
Where("fingerprint = ?", fingerprint).
|
|
Get(key)
|
|
if err != nil {
|
|
return nil, err
|
|
} else if !has {
|
|
return nil, ErrKeyNotExist{}
|
|
}
|
|
return key, nil
|
|
}
|
|
|
|
func SearchPrincipalKey(ctx context.Context, principalKey string) (*PublicKey, error) {
|
|
// FIXME: this function is wrong, and there is no index on the content column
|
|
// In the future, the existing principal keys should be migrated to use the fingerprint ("principal:{name}") instead of the content
|
|
key := new(PublicKey)
|
|
has, err := db.GetEngine(ctx).
|
|
Where("content = ? AND type = ?", principalKey, KeyTypePrincipal).
|
|
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) ToOrders() string {
|
|
return "id"
|
|
}
|
|
|
|
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) {
|
|
return db.WithTx2(ctx, func(ctx context.Context) (bool, error) {
|
|
// Delete keys marked for deletion
|
|
var sshKeysNeedUpdate bool
|
|
for _, sshKeyToDelete := range keys {
|
|
key, err := SearchPublicKeyForSSH(ctx, sshKeyToDelete)
|
|
if err != nil {
|
|
log.Error("SearchPublicKeyForSSH: %v", err)
|
|
continue
|
|
}
|
|
if _, err = db.DeleteByID[PublicKey](ctx, key.ID); err != nil {
|
|
log.Error("DeleteByID[PublicKey]: %v", err)
|
|
continue
|
|
}
|
|
sshKeysNeedUpdate = true
|
|
}
|
|
|
|
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, verified bool) 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, verified); 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 user's public keys. Returns true if there are changes.
|
|
func SynchronizePublicKeys(ctx context.Context, usr *user_model.User, s *auth.Source, sshPublicKeys []string, verified bool) 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 the current auth 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, verified) {
|
|
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
|
|
}
|