diff --git a/models/migrations/migrations.go b/models/migrations/migrations.go index f85f672f4c..43cfa6e4d8 100644 --- a/models/migrations/migrations.go +++ b/models/migrations/migrations.go @@ -386,7 +386,6 @@ func prepareMigrationTasks() []*migration { // Gitea 1.24.0 ends at migration ID number 320 (database version 321) newMigration(321, "Use LONGTEXT for some columns and fix review_state.updated_files column", v1_25.UseLongTextInSomeColumnsAndFixBugs), - newMigration(322, "Add Mirror SSH keypair table", v1_25.AddUserSSHKeypairTable), } return preparedMigrations } diff --git a/models/migrations/v1_25/v322.go b/models/migrations/v1_25/v322.go deleted file mode 100644 index 64138ac6c8..0000000000 --- a/models/migrations/v1_25/v322.go +++ /dev/null @@ -1,24 +0,0 @@ -// Copyright 2025 The Gitea Authors. All rights reserved. -// SPDX-License-Identifier: MIT - -package v1_25 - -import ( - "code.gitea.io/gitea/modules/timeutil" - - "xorm.io/xorm" -) - -func AddUserSSHKeypairTable(x *xorm.Engine) error { - type UserSSHKeypair struct { - ID int64 `xorm:"pk autoincr"` - OwnerID int64 `xorm:"INDEX NOT NULL"` - PrivateKeyEncrypted string `xorm:"TEXT NOT NULL"` - PublicKey string `xorm:"TEXT NOT NULL"` - Fingerprint string `xorm:"VARCHAR(255) UNIQUE NOT NULL"` - CreatedUnix timeutil.TimeStamp `xorm:"created"` - UpdatedUnix timeutil.TimeStamp `xorm:"updated"` - } - - return x.Sync(new(UserSSHKeypair)) -} diff --git a/models/repo/mirror_ssh_keypair.go b/models/repo/mirror_ssh_keypair.go index 7db1ffdc29..fd72a4a8da 100644 --- a/models/repo/mirror_ssh_keypair.go +++ b/models/repo/mirror_ssh_keypair.go @@ -16,7 +16,6 @@ import ( user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/modules/secret" "code.gitea.io/gitea/modules/setting" - "code.gitea.io/gitea/modules/timeutil" "code.gitea.io/gitea/modules/util" "golang.org/x/crypto/ssh" @@ -24,30 +23,44 @@ import ( // UserSSHKeypair represents an SSH keypair for repository mirroring type UserSSHKeypair struct { - ID int64 `xorm:"pk autoincr"` - OwnerID int64 `xorm:"INDEX NOT NULL"` - PrivateKeyEncrypted string `xorm:"TEXT NOT NULL"` - PublicKey string `xorm:"TEXT NOT NULL"` - Fingerprint string `xorm:"VARCHAR(255) UNIQUE NOT NULL"` - CreatedUnix timeutil.TimeStamp `xorm:"created"` - UpdatedUnix timeutil.TimeStamp `xorm:"updated"` + OwnerID int64 + PrivateKeyEncrypted string + PublicKey string + Fingerprint string } -func init() { - db.RegisterModel(new(UserSSHKeypair)) -} - -// GetUserSSHKeypairByOwner gets the most recent SSH keypair for the given owner +// GetUserSSHKeypairByOwner gets the SSH keypair for the given owner func GetUserSSHKeypairByOwner(ctx context.Context, ownerID int64) (*UserSSHKeypair, error) { - keypair := &UserSSHKeypair{} - has, err := db.GetEngine(ctx).Where("owner_id = ?", ownerID). - Desc("created_unix").Get(keypair) + settings, err := user_model.GetSettings(ctx, ownerID, []string{ + user_model.UserSSHMirrorPrivPem, + user_model.UserSSHMirrorPubPem, + user_model.UserSSHMirrorFingerprint, + }) if err != nil { return nil, err } - if !has { + if len(settings) == 0 { return nil, util.NewNotExistErrorf("SSH keypair does not exist for owner %d", ownerID) } + + keypair := &UserSSHKeypair{ + OwnerID: ownerID, + } + + if privSetting, exists := settings[user_model.UserSSHMirrorPrivPem]; exists { + keypair.PrivateKeyEncrypted = privSetting.SettingValue + } + if pubSetting, exists := settings[user_model.UserSSHMirrorPubPem]; exists { + keypair.PublicKey = pubSetting.SettingValue + } + if fpSetting, exists := settings[user_model.UserSSHMirrorFingerprint]; exists { + keypair.Fingerprint = fpSetting.SettingValue + } + + if keypair.PrivateKeyEncrypted == "" || keypair.PublicKey == "" || keypair.Fingerprint == "" { + return nil, util.NewNotExistErrorf("SSH keypair incomplete for owner %d", ownerID) + } + return keypair, nil } @@ -73,6 +86,22 @@ func CreateUserSSHKeypair(ctx context.Context, ownerID int64) (*UserSSHKeypair, return nil, fmt.Errorf("failed to encrypt private key: %w", err) } + err = db.WithTx(ctx, func(ctx context.Context) error { + if err := user_model.SetUserSetting(ctx, ownerID, user_model.UserSSHMirrorPrivPem, privateKeyEncrypted); err != nil { + return fmt.Errorf("failed to save private key: %w", err) + } + if err := user_model.SetUserSetting(ctx, ownerID, user_model.UserSSHMirrorPubPem, publicKeyStr); err != nil { + return fmt.Errorf("failed to save public key: %w", err) + } + if err := user_model.SetUserSetting(ctx, ownerID, user_model.UserSSHMirrorFingerprint, fingerprintStr); err != nil { + return fmt.Errorf("failed to save fingerprint: %w", err) + } + return nil + }) + if err != nil { + return nil, err + } + keypair := &UserSSHKeypair{ OwnerID: ownerID, PrivateKeyEncrypted: privateKeyEncrypted, @@ -80,7 +109,7 @@ func CreateUserSSHKeypair(ctx context.Context, ownerID int64) (*UserSSHKeypair, Fingerprint: fingerprintStr, } - return keypair, db.Insert(ctx, keypair) + return keypair, nil } // GetDecryptedPrivateKey returns the decrypted private key @@ -115,12 +144,29 @@ func (k *UserSSHKeypair) GetPublicKeyWithComment(ctx context.Context) (string, e // DeleteUserSSHKeypair deletes an SSH keypair func DeleteUserSSHKeypair(ctx context.Context, ownerID int64) error { - _, err := db.GetEngine(ctx).Where("owner_id = ?", ownerID).Delete(&UserSSHKeypair{}) - return err + return db.WithTx(ctx, func(ctx context.Context) error { + if err := user_model.DeleteUserSetting(ctx, ownerID, user_model.UserSSHMirrorPrivPem); err != nil { + return err + } + if err := user_model.DeleteUserSetting(ctx, ownerID, user_model.UserSSHMirrorPubPem); err != nil { + return err + } + return user_model.DeleteUserSetting(ctx, ownerID, user_model.UserSSHMirrorFingerprint) + }) } // RegenerateUserSSHKeypair regenerates an SSH keypair for the given owner func RegenerateUserSSHKeypair(ctx context.Context, ownerID int64) (*UserSSHKeypair, error) { - // TODO: This creates a new one old ones will be garbage collected later, as the user may accidentally regenerate - return CreateUserSSHKeypair(ctx, ownerID) + var keypair *UserSSHKeypair + err := db.WithTx(ctx, func(ctx context.Context) error { + _ = DeleteUserSSHKeypair(ctx, ownerID) + + newKeypair, err := CreateUserSSHKeypair(ctx, ownerID) + if err != nil { + return err + } + keypair = newKeypair + return nil + }) + return keypair, err } diff --git a/models/repo/mirror_ssh_keypair_test.go b/models/repo/mirror_ssh_keypair_test.go index 16471ff886..c0a0cbff86 100644 --- a/models/repo/mirror_ssh_keypair_test.go +++ b/models/repo/mirror_ssh_keypair_test.go @@ -29,8 +29,6 @@ func TestUserSSHKeypair(t *testing.T) { assert.NotEmpty(t, keypair.PublicKey) assert.NotEmpty(t, keypair.PrivateKeyEncrypted) assert.NotEmpty(t, keypair.Fingerprint) - assert.Positive(t, keypair.CreatedUnix) - assert.Positive(t, keypair.UpdatedUnix) // Verify the public key is in SSH format assert.Contains(t, keypair.PublicKey, "ssh-ed25519") @@ -54,7 +52,7 @@ func TestUserSSHKeypair(t *testing.T) { // Test retrieving the keypair retrieved, err := repo_model.GetUserSSHKeypairByOwner(db.DefaultContext, 3) require.NoError(t, err) - assert.Equal(t, created.ID, retrieved.ID) + assert.Equal(t, created.OwnerID, retrieved.OwnerID) assert.Equal(t, created.PublicKey, retrieved.PublicKey) assert.Equal(t, created.Fingerprint, retrieved.Fingerprint) @@ -128,7 +126,7 @@ func TestUserSSHKeypairConcurrency(t *testing.T) { // Test concurrent creation of keypairs to ensure no race conditions t.Run("ConcurrentCreation", func(t *testing.T) { - ctx := t.Context() + ctx := db.DefaultContext results := make(chan error, 10) // Start multiple goroutines creating keypairs for different owners diff --git a/models/user/setting_options.go b/models/user/setting_options.go index 7be5039329..e3ba24c0c1 100644 --- a/models/user/setting_options.go +++ b/models/user/setting_options.go @@ -26,4 +26,8 @@ const ( SettingEmailNotificationGiteaActionsAll = "all" SettingEmailNotificationGiteaActionsFailureOnly = "failure-only" // Default for actions email preference SettingEmailNotificationGiteaActionsDisabled = "disabled" + + UserSSHMirrorPrivPem = "ssh_mirror.priv_pem" + UserSSHMirrorPubPem = "ssh_mirror.pub_pem" + UserSSHMirrorFingerprint = "ssh_mirror.fingerprint" )