refactor: prepare to decouple the "model migration" package and "models" package (#38533)

Migrations should never use model structs directly, because the model
structs can be different in different releases. e.g. if one migration uses
"User" model, it works in the early releases, then one day, when the
User model changes, the migration breaks because it will use the
new (incorrect) User model, it should only use the old User model.

The same to "modules/structs".

---------

Signed-off-by: wxiaoguang <wxiaoguang@gmail.com>
Co-authored-by: delvh <dev.lh@web.de>
This commit is contained in:
wxiaoguang
2026-07-20 03:42:02 +00:00
committed by GitHub
co-authored by delvh
parent 775e3bdb34
commit fff32e9469
378 changed files with 708 additions and 721 deletions
+14
View File
@@ -0,0 +1,14 @@
// Copyright 2021 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package v1_16
import (
"testing"
"gitea.dev/modelmigration/migrationtest"
)
func TestMain(m *testing.M) {
migrationtest.MainTest(m)
}
+109
View File
@@ -0,0 +1,109 @@
// Copyright 2021 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package v1_16
import (
"encoding/binary"
"fmt"
"gitea.dev/modelmigration/base"
"gitea.dev/modules/json"
)
func UnwrapLDAPSourceCfg(x base.EngineMigration) error {
jsonUnmarshalHandleDoubleEncode := func(bs []byte, v any) error {
err := json.Unmarshal(bs, v)
if err != nil {
ok := true
rs := []byte{}
temp := make([]byte, 2)
for _, rn := range string(bs) {
if rn > 0xffff {
ok = false
break
}
binary.LittleEndian.PutUint16(temp, uint16(rn))
rs = append(rs, temp...)
}
if ok {
if rs[0] == 0xff && rs[1] == 0xfe {
rs = rs[2:]
}
err = json.Unmarshal(rs, v)
}
}
if err != nil && len(bs) > 2 && bs[0] == 0xff && bs[1] == 0xfe {
err = json.Unmarshal(bs[2:], v)
}
return err
}
// LoginSource represents an external way for authorizing users.
type LoginSource struct {
ID int64 `xorm:"pk autoincr"`
Type int
IsActived bool `xorm:"INDEX NOT NULL DEFAULT false"`
IsActive bool `xorm:"INDEX NOT NULL DEFAULT false"`
Cfg string `xorm:"TEXT"`
}
const ldapType = 2
const dldapType = 5
type WrappedSource struct {
Source map[string]any
}
// change lower_email as unique
if err := x.Sync(new(LoginSource)); err != nil {
return err
}
sess := x.NewSession()
defer sess.Close()
const batchSize = 100
for start := 0; ; start += batchSize {
sources := make([]*LoginSource, 0, batchSize)
if err := sess.Limit(batchSize, start).Where("`type` = ? OR `type` = ?", ldapType, dldapType).Find(&sources); err != nil {
return err
}
if len(sources) == 0 {
break
}
for _, source := range sources {
wrapped := &WrappedSource{
Source: map[string]any{},
}
err := jsonUnmarshalHandleDoubleEncode([]byte(source.Cfg), &wrapped)
if err != nil {
return fmt.Errorf("failed to unmarshal %s: %w", source.Cfg, err)
}
if len(wrapped.Source) > 0 {
bs, err := json.Marshal(wrapped.Source)
if err != nil {
return err
}
source.Cfg = string(bs)
if _, err := sess.ID(source.ID).Cols("cfg").Update(source); err != nil {
return err
}
}
}
}
if _, err := x.SetExpr("is_active", "is_actived").Update(&LoginSource{}); err != nil {
return fmt.Errorf("SetExpr Update failed: %w", err)
}
if err := sess.Begin(); err != nil {
return err
}
if err := base.DropTableColumns(sess, "login_source", "is_actived"); err != nil {
return err
}
return sess.Commit()
}
+82
View File
@@ -0,0 +1,82 @@
// Copyright 2021 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package v1_16
import (
"testing"
"gitea.dev/modelmigration/migrationtest"
"gitea.dev/modules/json"
"github.com/stretchr/testify/assert"
)
// LoginSource represents an external way for authorizing users.
type LoginSourceOriginalV189 struct {
ID int64 `xorm:"pk autoincr"`
Type int
IsActived bool `xorm:"INDEX NOT NULL DEFAULT false"`
Cfg string `xorm:"TEXT"`
Expected string `xorm:"TEXT"`
}
func (ls *LoginSourceOriginalV189) TableName() string {
return "login_source"
}
func Test_UnwrapLDAPSourceCfg(t *testing.T) {
// Prepare and load the testing database
x, deferable := migrationtest.PrepareTestEnv(t, 0, new(LoginSourceOriginalV189))
if x == nil || t.Failed() {
defer deferable()
return
}
defer deferable()
// LoginSource represents an external way for authorizing users.
type LoginSource struct {
ID int64 `xorm:"pk autoincr"`
Type int
IsActive bool `xorm:"INDEX NOT NULL DEFAULT false"`
Cfg string `xorm:"TEXT"`
Expected string `xorm:"TEXT"`
}
// Run the migration
if err := UnwrapLDAPSourceCfg(x); err != nil {
assert.NoError(t, err)
return
}
const batchSize = 100
for start := 0; ; start += batchSize {
sources := make([]*LoginSource, 0, batchSize)
if err := x.Table("login_source").Limit(batchSize, start).Find(&sources); err != nil {
assert.NoError(t, err)
return
}
if len(sources) == 0 {
break
}
for _, source := range sources {
converted := map[string]any{}
expected := map[string]any{}
if err := json.Unmarshal([]byte(source.Cfg), &converted); err != nil {
assert.NoError(t, err)
return
}
if err := json.Unmarshal([]byte(source.Expected), &expected); err != nil {
assert.NoError(t, err)
return
}
assert.Equal(t, expected, converted, "UnwrapLDAPSourceCfg failed for %d", source.ID)
assert.Equal(t, source.ID%2 == 0, source.IsActive, "UnwrapLDAPSourceCfg failed for %d", source.ID)
}
}
}
+23
View File
@@ -0,0 +1,23 @@
// Copyright 2021 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package v1_16
import (
"fmt"
"gitea.dev/modelmigration/base"
)
func AddAgitFlowPullRequest(x base.EngineMigration) error {
type PullRequestFlow int
type PullRequest struct {
Flow PullRequestFlow `xorm:"NOT NULL DEFAULT 0"`
}
if err := x.Sync(new(PullRequest)); err != nil {
return fmt.Errorf("sync2: %w", err)
}
return nil
}
+27
View File
@@ -0,0 +1,27 @@
// Copyright 2021 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package v1_16
import (
"gitea.dev/modelmigration/base"
"gitea.dev/modules/setting"
)
func AlterIssueAndCommentTextFieldsToLongText(x base.EngineMigration) error {
sess := x.NewSession()
defer sess.Close()
if err := sess.Begin(); err != nil {
return err
}
if setting.Database.Type.IsMySQL() {
if _, err := sess.Exec("ALTER TABLE `issue` CHANGE `content` `content` LONGTEXT"); err != nil {
return err
}
if _, err := sess.Exec("ALTER TABLE `comment` CHANGE `content` `content` LONGTEXT, CHANGE `patch` `patch` LONGTEXT"); err != nil {
return err
}
}
return sess.Commit()
}
+17
View File
@@ -0,0 +1,17 @@
// Copyright 2021 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package v1_16
import (
"gitea.dev/modelmigration/base"
)
func RecreateIssueResourceIndexTable(x base.EngineMigration) error {
type IssueIndex struct {
GroupID int64 `xorm:"pk"`
MaxIndex int64 `xorm:"index"`
}
return base.RecreateTables(new(IssueIndex))(x)
}
+30
View File
@@ -0,0 +1,30 @@
// Copyright 2021 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package v1_16
import "gitea.dev/modelmigration/base"
func AddRepoIDForAttachment(x base.EngineMigration) error {
type Attachment struct {
ID int64 `xorm:"pk autoincr"`
UUID string `xorm:"uuid UNIQUE"`
RepoID int64 `xorm:"INDEX"` // this should not be zero
IssueID int64 `xorm:"INDEX"` // maybe zero when creating
ReleaseID int64 `xorm:"INDEX"` // maybe zero when creating
UploaderID int64 `xorm:"INDEX DEFAULT 0"`
}
if err := x.Sync(new(Attachment)); err != nil {
return err
}
if _, err := x.Exec("UPDATE `attachment` set repo_id = (SELECT repo_id FROM `issue` WHERE `issue`.id = `attachment`.issue_id) WHERE `attachment`.issue_id > 0"); err != nil {
return err
}
if _, err := x.Exec("UPDATE `attachment` set repo_id = (SELECT repo_id FROM `release` WHERE `release`.id = `attachment`.release_id) WHERE `attachment`.release_id > 0"); err != nil {
return err
}
return nil
}
+80
View File
@@ -0,0 +1,80 @@
// Copyright 2021 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package v1_16
import (
"testing"
"gitea.dev/modelmigration/migrationtest"
"github.com/stretchr/testify/assert"
)
func Test_AddRepoIDForAttachment(t *testing.T) {
type Attachment struct {
ID int64 `xorm:"pk autoincr"`
UUID string `xorm:"uuid UNIQUE"`
IssueID int64 `xorm:"INDEX"` // maybe zero when creating
ReleaseID int64 `xorm:"INDEX"` // maybe zero when creating
UploaderID int64 `xorm:"INDEX DEFAULT 0"`
}
type Issue struct {
ID int64
RepoID int64
}
type Release struct {
ID int64
RepoID int64
}
// Prepare and load the testing database
x, deferrable := migrationtest.PrepareTestEnv(t, 0, new(Attachment), new(Issue), new(Release))
defer deferrable()
if x == nil || t.Failed() {
return
}
// Run the migration
if err := AddRepoIDForAttachment(x); err != nil {
assert.NoError(t, err)
return
}
type NewAttachment struct {
ID int64 `xorm:"pk autoincr"`
UUID string `xorm:"uuid UNIQUE"`
RepoID int64 `xorm:"INDEX"` // this should not be zero
IssueID int64 `xorm:"INDEX"` // maybe zero when creating
ReleaseID int64 `xorm:"INDEX"` // maybe zero when creating
UploaderID int64 `xorm:"INDEX DEFAULT 0"`
}
var issueAttachments []*NewAttachment
err := x.Table("attachment").Where("issue_id > 0").Find(&issueAttachments)
assert.NoError(t, err)
for _, attach := range issueAttachments {
assert.Positive(t, attach.RepoID)
assert.Positive(t, attach.IssueID)
var issue Issue
has, err := x.ID(attach.IssueID).Get(&issue)
assert.NoError(t, err)
assert.True(t, has)
assert.Equal(t, attach.RepoID, issue.RepoID)
}
var releaseAttachments []*NewAttachment
err = x.Table("attachment").Where("release_id > 0").Find(&releaseAttachments)
assert.NoError(t, err)
for _, attach := range releaseAttachments {
assert.Positive(t, attach.RepoID)
assert.Positive(t, attach.ReleaseID)
var release Release
has, err := x.ID(attach.ReleaseID).Get(&release)
assert.NoError(t, err)
assert.True(t, has)
assert.Equal(t, attach.RepoID, release.RepoID)
}
}
+21
View File
@@ -0,0 +1,21 @@
// Copyright 2021 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package v1_16
import (
"fmt"
"gitea.dev/modelmigration/base"
)
func AddBranchProtectionUnprotectedFilesColumn(x base.EngineMigration) error {
type ProtectedBranch struct {
UnprotectedFilePatterns string `xorm:"TEXT"`
}
if err := x.Sync(new(ProtectedBranch)); err != nil {
return fmt.Errorf("Sync: %w", err)
}
return nil
}
+46
View File
@@ -0,0 +1,46 @@
// Copyright 2021 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package v1_16
import (
"fmt"
"gitea.dev/modelmigration/base"
)
func AddTableCommitStatusIndex(x base.EngineMigration) error {
// CommitStatusIndex represents a table for commit status index
type CommitStatusIndex struct {
ID int64
RepoID int64 `xorm:"unique(repo_sha)"`
SHA string `xorm:"unique(repo_sha)"`
MaxIndex int64 `xorm:"index"`
}
if err := x.Sync(new(CommitStatusIndex)); err != nil {
return fmt.Errorf("Sync: %w", err)
}
sess := x.NewSession()
defer sess.Close()
if err := sess.Begin(); err != nil {
return err
}
// Remove data we're goint to rebuild
if _, err := sess.Table("commit_status_index").Where("1=1").Delete(&CommitStatusIndex{}); err != nil {
return err
}
// Create current data for all repositories with issues and PRs
if _, err := sess.Exec("INSERT INTO commit_status_index (repo_id, sha, max_index) " +
"SELECT max_data.repo_id, max_data.sha, max_data.max_index " +
"FROM ( SELECT commit_status.repo_id AS repo_id, commit_status.sha AS sha, max(commit_status.`index`) AS max_index " +
"FROM commit_status GROUP BY commit_status.repo_id, commit_status.sha) AS max_data"); err != nil {
return err
}
return sess.Commit()
}
+63
View File
@@ -0,0 +1,63 @@
// Copyright 2021 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package v1_16
import (
"testing"
"gitea.dev/modelmigration/migrationtest"
"github.com/stretchr/testify/assert"
)
func Test_AddTableCommitStatusIndex(t *testing.T) {
// Create the models used in the migration
type CommitStatus struct {
ID int64 `xorm:"pk autoincr"`
Index int64 `xorm:"INDEX UNIQUE(repo_sha_index)"`
RepoID int64 `xorm:"INDEX UNIQUE(repo_sha_index)"`
SHA string `xorm:"VARCHAR(64) NOT NULL INDEX UNIQUE(repo_sha_index)"`
}
// Prepare and load the testing database
x, deferable := migrationtest.PrepareTestEnv(t, 0, new(CommitStatus))
if x == nil || t.Failed() {
defer deferable()
return
}
defer deferable()
// Run the migration
if err := AddTableCommitStatusIndex(x); err != nil {
assert.NoError(t, err)
return
}
type CommitStatusIndex struct {
ID int64
RepoID int64 `xorm:"unique(repo_sha)"`
SHA string `xorm:"unique(repo_sha)"`
MaxIndex int64 `xorm:"index"`
}
start := 0
const batchSize = 1000
for {
indexes := make([]CommitStatusIndex, 0, batchSize)
err := x.Table("commit_status_index").Limit(batchSize, start).Find(&indexes)
assert.NoError(t, err)
for _, idx := range indexes {
var maxIndex int
has, err := x.SQL("SELECT max(`index`) FROM commit_status WHERE repo_id = ? AND sha = ?", idx.RepoID, idx.SHA).Get(&maxIndex)
assert.NoError(t, err)
assert.True(t, has)
assert.EqualValues(t, maxIndex, idx.MaxIndex)
}
if len(indexes) < batchSize {
break
}
start += len(indexes)
}
}
+21
View File
@@ -0,0 +1,21 @@
// Copyright 2021 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package v1_16
import (
"fmt"
"gitea.dev/modelmigration/base"
)
func AddColorColToProjectBoard(x base.EngineMigration) error {
type ProjectBoard struct {
Color string `xorm:"VARCHAR(7)"`
}
if err := x.Sync(new(ProjectBoard)); err != nil {
return fmt.Errorf("Sync: %w", err)
}
return nil
}
+17
View File
@@ -0,0 +1,17 @@
// Copyright 2021 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package v1_16
import "gitea.dev/modelmigration/base"
func AddRenamedBranchTable(x base.EngineMigration) error {
type RenamedBranch struct {
ID int64 `xorm:"pk autoincr"`
RepoID int64 `xorm:"INDEX NOT NULL"`
From string
To string
CreatedUnix int64 `xorm:"created"`
}
return x.Sync(new(RenamedBranch))
}
+31
View File
@@ -0,0 +1,31 @@
// Copyright 2021 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package v1_16
import (
"fmt"
"gitea.dev/modelmigration/base"
"gitea.dev/modules/timeutil"
)
func AddTableIssueContentHistory(x base.EngineMigration) error {
type IssueContentHistory struct {
ID int64 `xorm:"pk autoincr"`
PosterID int64
IssueID int64 `xorm:"INDEX"`
CommentID int64 `xorm:"INDEX"`
EditedUnix timeutil.TimeStamp `xorm:"INDEX"`
ContentText string `xorm:"LONGTEXT"`
IsFirstCreated bool
IsDeleted bool
}
sess := x.NewSession()
defer sess.Close()
if err := sess.Sync(new(IssueContentHistory)); err != nil {
return fmt.Errorf("Sync: %w", err)
}
return sess.Commit()
}
+6
View File
@@ -0,0 +1,6 @@
// Copyright 2021 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package v1_16
// We used to use a table `remote_version` to store information for updater, now we use `AppState`, so this migration task is a no-op now.
+22
View File
@@ -0,0 +1,22 @@
// Copyright 2021 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package v1_16
import (
"fmt"
"gitea.dev/modelmigration/base"
)
func AddTableAppState(x base.EngineMigration) error {
type AppState struct {
ID string `xorm:"pk varchar(200)"`
Revision int64
Content string `xorm:"LONGTEXT"`
}
if err := x.Sync(new(AppState)); err != nil {
return fmt.Errorf("Sync: %w", err)
}
return nil
}
+12
View File
@@ -0,0 +1,12 @@
// Copyright 2021 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package v1_16
import "gitea.dev/modelmigration/base"
func DropTableRemoteVersion(x base.EngineMigration) error {
// drop the orphaned table introduced in `v199`, now the update checker also uses AppState, do not need this table
_ = x.DropTables("remote_version")
return nil
}
+23
View File
@@ -0,0 +1,23 @@
// Copyright 2021 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package v1_16
import (
"fmt"
"gitea.dev/modelmigration/base"
)
func CreateUserSettingsTable(x base.EngineMigration) error {
type UserSetting struct {
ID int64 `xorm:"pk autoincr"`
UserID int64 `xorm:"index unique(key_userid)"` // to load all of someone's settings
SettingKey string `xorm:"varchar(255) index unique(key_userid)"` // ensure key is always lowercase
SettingValue string `xorm:"text"`
}
if err := x.Sync(new(UserSetting)); err != nil {
return fmt.Errorf("sync2: %w", err)
}
return nil
}
+15
View File
@@ -0,0 +1,15 @@
// Copyright 2021 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package v1_16
import "gitea.dev/modelmigration/base"
func AddProjectIssueSorting(x base.EngineMigration) error {
// ProjectIssue saves relation from issue to a project
type ProjectIssue struct {
Sorting int64 `xorm:"NOT NULL DEFAULT 0"`
}
return x.Sync(new(ProjectIssue))
}
+14
View File
@@ -0,0 +1,14 @@
// Copyright 2021 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package v1_16
import "gitea.dev/modelmigration/base"
func AddSSHKeyIsVerified(x base.EngineMigration) error {
type PublicKey struct {
Verified bool `xorm:"NOT NULL DEFAULT false"`
}
return x.Sync(new(PublicKey))
}
+41
View File
@@ -0,0 +1,41 @@
// Copyright 2022 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package v1_16
import (
"gitea.dev/modelmigration/base"
"xorm.io/xorm/schemas"
)
func MigrateUserPasswordSalt(x base.EngineMigration) error {
dbType := x.Dialect().URI().DBType
// For SQLITE, the max length doesn't matter.
if dbType == schemas.SQLITE {
return nil
}
if err := base.ModifyColumn(x, "user", &schemas.Column{
Name: "rands",
SQLType: schemas.SQLType{
Name: "VARCHAR",
},
Length: 32,
// MySQL will like us again.
Nullable: true,
DefaultIsEmpty: true,
}); err != nil {
return err
}
return base.ModifyColumn(x, "user", &schemas.Column{
Name: "salt",
SQLType: schemas.SQLType{
Name: "VARCHAR",
},
Length: 32,
Nullable: true,
DefaultIsEmpty: true,
})
}
+28
View File
@@ -0,0 +1,28 @@
// Copyright 2022 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package v1_16
import (
"fmt"
"gitea.dev/modelmigration/base"
)
func AddAuthorizeColForTeamUnit(x base.EngineMigration) error {
type TeamUnit struct {
ID int64 `xorm:"pk autoincr"`
OrgID int64 `xorm:"INDEX"`
TeamID int64 `xorm:"UNIQUE(s)"`
Type int `xorm:"UNIQUE(s)"`
AccessMode int
}
if err := x.Sync(new(TeamUnit)); err != nil {
return fmt.Errorf("sync2: %w", err)
}
// migrate old permission
_, err := x.Exec("UPDATE team_unit SET access_mode = (SELECT authorize FROM team WHERE team.id = team_unit.team_id)")
return err
}
+12
View File
@@ -0,0 +1,12 @@
// Copyright 2021 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package v1_16
import "gitea.dev/modelmigration/base"
func AddWebAuthnCred(x base.EngineMigration) error {
// NO-OP Don't migrate here - let v210 do this.
return nil
}
+11
View File
@@ -0,0 +1,11 @@
// Copyright 2021 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package v1_16
import "gitea.dev/modelmigration/base"
func UseBase32HexForCredIDInWebAuthnCredential(x base.EngineMigration) error {
// noop
return nil
}
+14
View File
@@ -0,0 +1,14 @@
// Copyright 2022 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package v1_16
import "gitea.dev/modelmigration/base"
func IncreaseCredentialIDTo410(x base.EngineMigration) error {
// no-op
// v208 was completely wrong
// So now we have to no-op again.
return nil
}
+181
View File
@@ -0,0 +1,181 @@
// Copyright 2022 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package v1_16
import (
"encoding/base32"
"fmt"
"strings"
"gitea.dev/modelmigration/base"
"gitea.dev/modules/timeutil"
"github.com/tstranex/u2f"
"xorm.io/xorm/schemas"
)
// v208 migration was completely broken
func RemigrateU2FCredentials(x base.EngineMigration) error {
// Create webauthnCredential table
type webauthnCredential struct {
ID int64 `xorm:"pk autoincr"`
Name string
LowerName string `xorm:"unique(s)"`
UserID int64 `xorm:"INDEX unique(s)"`
CredentialID string `xorm:"INDEX VARCHAR(410)"` // CredentalID in U2F is at most 255bytes / 5 * 8 = 408 - add a few extra characters for safety
PublicKey []byte
AttestationType string
AAGUID []byte
SignCount uint32 `xorm:"BIGINT"`
CloneWarning bool
CreatedUnix timeutil.TimeStamp `xorm:"INDEX created"`
UpdatedUnix timeutil.TimeStamp `xorm:"INDEX updated"`
}
if err := x.Sync(&webauthnCredential{}); err != nil {
return err
}
switch x.Dialect().URI().DBType {
case schemas.MYSQL:
_, err := x.Exec("ALTER TABLE webauthn_credential MODIFY COLUMN credential_id VARCHAR(410)")
if err != nil {
return err
}
case schemas.MSSQL:
// This column has an index on it. I could write all of the code to attempt to change the index OR
// I could just use recreate table.
sess := x.NewSession()
if err := sess.Begin(); err != nil {
_ = sess.Close()
return err
}
if err := base.RecreateTable(sess, new(webauthnCredential)); err != nil {
_ = sess.Close()
return err
}
if err := sess.Commit(); err != nil {
_ = sess.Close()
return err
}
if err := sess.Close(); err != nil {
return err
}
case schemas.POSTGRES:
_, err := x.Exec("ALTER TABLE webauthn_credential ALTER COLUMN credential_id TYPE VARCHAR(410)")
if err != nil {
return err
}
default:
// SQLite doesn't support ALTER COLUMN, and it already makes String _TEXT_ by default so no migration needed
// nor is there any need to re-migrate
}
exist, err := x.IsTableExist("u2f_registration")
if err != nil {
return err
}
if !exist {
return nil
}
// Now migrate the old u2f registrations to the new format
type u2fRegistration struct {
ID int64 `xorm:"pk autoincr"`
Name string
UserID int64 `xorm:"INDEX"`
Raw []byte
Counter uint32 `xorm:"BIGINT"`
CreatedUnix timeutil.TimeStamp `xorm:"INDEX created"`
UpdatedUnix timeutil.TimeStamp `xorm:"INDEX updated"`
}
var start int
regs := make([]*u2fRegistration, 0, 50)
for {
err := x.OrderBy("id").Limit(50, start).Find(&regs)
if err != nil {
return err
}
err = func() error {
sess := x.NewSession()
defer sess.Close()
if err := sess.Begin(); err != nil {
return fmt.Errorf("unable to allow start session. Error: %w", err)
}
if x.Dialect().URI().DBType == schemas.MSSQL {
if _, err := sess.Exec("SET IDENTITY_INSERT `webauthn_credential` ON"); err != nil {
return fmt.Errorf("unable to allow identity insert on webauthn_credential. Error: %w", err)
}
}
for _, reg := range regs {
parsed := new(u2f.Registration)
err = parsed.UnmarshalBinary(reg.Raw)
if err != nil {
continue
}
pubKey, err := parsed.PubKey.ECDH()
if err != nil {
continue
}
remigrated := &webauthnCredential{
ID: reg.ID,
Name: reg.Name,
LowerName: strings.ToLower(reg.Name),
UserID: reg.UserID,
CredentialID: base32.HexEncoding.EncodeToString(parsed.KeyHandle),
PublicKey: pubKey.Bytes(),
AttestationType: "fido-u2f",
AAGUID: []byte{},
SignCount: reg.Counter,
UpdatedUnix: reg.UpdatedUnix,
CreatedUnix: reg.CreatedUnix,
}
has, err := sess.ID(reg.ID).Get(new(webauthnCredential))
if err != nil {
return fmt.Errorf("unable to get webauthn_credential[%d]. Error: %w", reg.ID, err)
}
if !has {
has, err := sess.Where("`lower_name`=?", remigrated.LowerName).And("`user_id`=?", remigrated.UserID).Exist(new(webauthnCredential))
if err != nil {
return fmt.Errorf("unable to check webauthn_credential[lower_name: %s, user_id: %d]. Error: %w", remigrated.LowerName, remigrated.UserID, err)
}
if !has {
_, err = sess.Insert(remigrated)
if err != nil {
return fmt.Errorf("unable to (re)insert webauthn_credential[%d]. Error: %w", reg.ID, err)
}
continue
}
}
_, err = sess.ID(remigrated.ID).AllCols().Update(remigrated)
if err != nil {
return fmt.Errorf("unable to update webauthn_credential[%d]. Error: %w", reg.ID, err)
}
}
return sess.Commit()
}()
if err != nil {
return err
}
if len(regs) < 50 {
break
}
start += 50
regs = regs[:0]
}
if x.Dialect().URI().DBType == schemas.POSTGRES {
if _, err := x.Exec("SELECT setval('webauthn_credential_id_seq', COALESCE((SELECT MAX(id)+1 FROM `webauthn_credential`), 1), false)"); err != nil {
return err
}
}
return nil
}
+75
View File
@@ -0,0 +1,75 @@
// Copyright 2021 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package v1_16
import (
"testing"
"gitea.dev/modelmigration/migrationtest"
"gitea.dev/modules/timeutil"
"github.com/stretchr/testify/assert"
"xorm.io/xorm/schemas"
)
func Test_RemigrateU2FCredentials(t *testing.T) {
// Create webauthnCredential table
type WebauthnCredential struct {
ID int64 `xorm:"pk autoincr"`
Name string
LowerName string `xorm:"unique(s)"`
UserID int64 `xorm:"INDEX unique(s)"`
CredentialID string `xorm:"INDEX VARCHAR(410)"` // CredentalID in U2F is at most 255bytes / 5 * 8 = 408 - add a few extra characters for safety
PublicKey []byte
AttestationType string
SignCount uint32 `xorm:"BIGINT"`
CloneWarning bool
}
// Now migrate the old u2f registrations to the new format
type U2fRegistration struct {
ID int64 `xorm:"pk autoincr"`
Name string
UserID int64 `xorm:"INDEX"`
Raw []byte
Counter uint32 `xorm:"BIGINT"`
CreatedUnix timeutil.TimeStamp `xorm:"INDEX created"`
UpdatedUnix timeutil.TimeStamp `xorm:"INDEX updated"`
}
type ExpectedWebauthnCredential struct {
ID int64 `xorm:"pk autoincr"`
CredentialID string `xorm:"INDEX VARCHAR(410)"` // CredentalID in U2F is at most 255bytes / 5 * 8 = 408 - add a few extra characters for safety
}
// Prepare and load the testing database
x, deferable := migrationtest.PrepareTestEnv(t, 0, new(WebauthnCredential), new(U2fRegistration), new(ExpectedWebauthnCredential))
if x == nil || t.Failed() {
defer deferable()
return
}
defer deferable()
if x.Dialect().URI().DBType == schemas.SQLITE {
return
}
// Run the migration
if err := RemigrateU2FCredentials(x); err != nil {
assert.NoError(t, err)
return
}
expected := []ExpectedWebauthnCredential{}
if err := x.Table("expected_webauthn_credential").Asc("id").Find(&expected); !assert.NoError(t, err) {
return
}
got := []ExpectedWebauthnCredential{}
if err := x.Table("webauthn_credential").Select("id, credential_id").Asc("id").Find(&got); !assert.NoError(t, err) {
return
}
assert.Equal(t, expected, got)
}