1
1
mirror of https://github.com/go-gitea/gitea synced 2025-07-22 18:28:37 +00:00

Split migrations folder (#21549)

There are too many files in `models/migrations` folder so that I split
them into sub folders.
This commit is contained in:
Lunny Xiao
2022-11-02 16:54:36 +08:00
committed by GitHub
parent 4827f42f56
commit e72acd5e5b
190 changed files with 1711 additions and 1481 deletions

View File

@@ -0,0 +1,15 @@
// Copyright 2021 The Gitea Authors. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
package v1_16 //nolint
import (
"testing"
"code.gitea.io/gitea/models/migrations/base"
)
func TestMain(m *testing.M) {
base.MainTest(m)
}

View File

@@ -0,0 +1,112 @@
// Copyright 2021 The Gitea Authors. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
package v1_16 //nolint
import (
"encoding/binary"
"fmt"
"code.gitea.io/gitea/models/migrations/base"
"code.gitea.io/gitea/modules/json"
"xorm.io/xorm"
)
func UnwrapLDAPSourceCfg(x *xorm.Engine) error {
jsonUnmarshalHandleDoubleEncode := func(bs []byte, v interface{}) 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]interface{}
}
// change lower_email as unique
if err := x.Sync2(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]interface{}{},
}
err := jsonUnmarshalHandleDoubleEncode([]byte(source.Cfg), &wrapped)
if err != nil {
return fmt.Errorf("failed to unmarshal %s: %w", source.Cfg, err)
}
if wrapped.Source != nil && 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()
}

View File

@@ -0,0 +1,83 @@
// Copyright 2021 The Gitea Authors. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
package v1_16 //nolint
import (
"testing"
"code.gitea.io/gitea/models/migrations/base"
"code.gitea.io/gitea/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 := base.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]interface{}{}
expected := map[string]interface{}{}
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.EqualValues(t, expected, converted, "UnwrapLDAPSourceCfg failed for %d", source.ID)
assert.EqualValues(t, source.ID%2 == 0, source.IsActive, "UnwrapLDAPSourceCfg failed for %d", source.ID)
}
}
}

View File

@@ -0,0 +1,24 @@
// Copyright 2021 The Gitea Authors. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
package v1_16 //nolint
import (
"fmt"
"xorm.io/xorm"
)
func AddAgitFlowPullRequest(x *xorm.Engine) error {
type PullRequestFlow int
type PullRequest struct {
Flow PullRequestFlow `xorm:"NOT NULL DEFAULT 0"`
}
if err := x.Sync2(new(PullRequest)); err != nil {
return fmt.Errorf("sync2: %w", err)
}
return nil
}

View File

@@ -0,0 +1,29 @@
// Copyright 2021 The Gitea Authors. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
package v1_16 //nolint
import (
"code.gitea.io/gitea/modules/setting"
"xorm.io/xorm"
)
func AlterIssueAndCommentTextFieldsToLongText(x *xorm.Engine) error {
sess := x.NewSession()
defer sess.Close()
if err := sess.Begin(); err != nil {
return err
}
if setting.Database.UseMySQL {
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()
}

View File

@@ -0,0 +1,20 @@
// Copyright 2021 The Gitea Authors. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
package v1_16 //nolint
import (
"code.gitea.io/gitea/models/migrations/base"
"xorm.io/xorm"
)
func RecreateIssueResourceIndexTable(x *xorm.Engine) error {
type IssueIndex struct {
GroupID int64 `xorm:"pk"`
MaxIndex int64 `xorm:"index"`
}
return base.RecreateTables(new(IssueIndex))(x)
}

View File

@@ -0,0 +1,33 @@
// Copyright 2021 The Gitea Authors. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
package v1_16 //nolint
import (
"xorm.io/xorm"
)
func AddRepoIDForAttachment(x *xorm.Engine) 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.Sync2(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
}

View File

@@ -0,0 +1,73 @@
// Copyright 2021 The Gitea Authors. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
package v1_16 //nolint
import (
"testing"
"code.gitea.io/gitea/models/migrations/base"
"github.com/stretchr/testify/assert"
)
func Test_AddRepoIDForAttachment(t *testing.T) {
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"`
}
type Issue struct {
ID int64
RepoID int64
}
type Release struct {
ID int64
RepoID int64
}
// Prepare and load the testing database
x, deferrable := base.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
}
var issueAttachments []*Attachment
err := x.Where("issue_id > 0").Find(&issueAttachments)
assert.NoError(t, err)
for _, attach := range issueAttachments {
assert.Greater(t, attach.RepoID, 0)
assert.Greater(t, attach.IssueID, 0)
var issue Issue
has, err := x.ID(attach.IssueID).Get(&issue)
assert.NoError(t, err)
assert.True(t, has)
assert.EqualValues(t, attach.RepoID, issue.RepoID)
}
var releaseAttachments []*Attachment
err = x.Where("release_id > 0").Find(&releaseAttachments)
assert.NoError(t, err)
for _, attach := range releaseAttachments {
assert.Greater(t, attach.RepoID, 0)
assert.Greater(t, attach.IssueID, 0)
var release Release
has, err := x.ID(attach.ReleaseID).Get(&release)
assert.NoError(t, err)
assert.True(t, has)
assert.EqualValues(t, attach.RepoID, release.RepoID)
}
}

View File

@@ -0,0 +1,22 @@
// Copyright 2021 The Gitea Authors. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
package v1_16 //nolint
import (
"fmt"
"xorm.io/xorm"
)
func AddBranchProtectionUnprotectedFilesColumn(x *xorm.Engine) error {
type ProtectedBranch struct {
UnprotectedFilePatterns string `xorm:"TEXT"`
}
if err := x.Sync2(new(ProtectedBranch)); err != nil {
return fmt.Errorf("Sync2: %w", err)
}
return nil
}

View File

@@ -0,0 +1,47 @@
// Copyright 2021 The Gitea Authors. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
package v1_16 //nolint
import (
"fmt"
"xorm.io/xorm"
)
func AddTableCommitStatusIndex(x *xorm.Engine) 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.Sync2(new(CommitStatusIndex)); err != nil {
return fmt.Errorf("Sync2: %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()
}

View File

@@ -0,0 +1,64 @@
// Copyright 2021 The Gitea Authors. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
package v1_16 //nolint
import (
"testing"
"code.gitea.io/gitea/models/migrations/base"
"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 := base.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)
}
}

View File

@@ -0,0 +1,22 @@
// Copyright 2021 The Gitea Authors. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
package v1_16 //nolint
import (
"fmt"
"xorm.io/xorm"
)
func AddColorColToProjectBoard(x *xorm.Engine) error {
type ProjectBoard struct {
Color string `xorm:"VARCHAR(7)"`
}
if err := x.Sync2(new(ProjectBoard)); err != nil {
return fmt.Errorf("Sync2: %w", err)
}
return nil
}

View File

@@ -0,0 +1,20 @@
// Copyright 2021 The Gitea Authors. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
package v1_16 //nolint
import (
"xorm.io/xorm"
)
func AddRenamedBranchTable(x *xorm.Engine) 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.Sync2(new(RenamedBranch))
}

View File

@@ -0,0 +1,33 @@
// Copyright 2021 The Gitea Authors. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
package v1_16 //nolint
import (
"fmt"
"code.gitea.io/gitea/modules/timeutil"
"xorm.io/xorm"
)
func AddTableIssueContentHistory(x *xorm.Engine) 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.Sync2(new(IssueContentHistory)); err != nil {
return fmt.Errorf("Sync2: %w", err)
}
return sess.Commit()
}

View File

@@ -0,0 +1,7 @@
// Copyright 2021 The Gitea Authors. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
package v1_16 //nolint
// 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.

View File

@@ -0,0 +1,23 @@
// Copyright 2021 The Gitea Authors. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
package v1_16 //nolint
import (
"fmt"
"xorm.io/xorm"
)
func AddTableAppState(x *xorm.Engine) error {
type AppState struct {
ID string `xorm:"pk varchar(200)"`
Revision int64
Content string `xorm:"LONGTEXT"`
}
if err := x.Sync2(new(AppState)); err != nil {
return fmt.Errorf("Sync2: %w", err)
}
return nil
}

View File

@@ -0,0 +1,15 @@
// Copyright 2021 The Gitea Authors. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
package v1_16 //nolint
import (
"xorm.io/xorm"
)
func DropTableRemoteVersion(x *xorm.Engine) 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
}

View File

@@ -0,0 +1,24 @@
// Copyright 2021 The Gitea Authors. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
package v1_16 //nolint
import (
"fmt"
"xorm.io/xorm"
)
func CreateUserSettingsTable(x *xorm.Engine) 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.Sync2(new(UserSetting)); err != nil {
return fmt.Errorf("sync2: %w", err)
}
return nil
}

View File

@@ -0,0 +1,18 @@
// Copyright 2021 The Gitea Authors. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
package v1_16 //nolint
import (
"xorm.io/xorm"
)
func AddProjectIssueSorting(x *xorm.Engine) error {
// ProjectIssue saves relation from issue to a project
type ProjectIssue struct {
Sorting int64 `xorm:"NOT NULL DEFAULT 0"`
}
return x.Sync2(new(ProjectIssue))
}

View File

@@ -0,0 +1,15 @@
// Copyright 2021 The Gitea Authors. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
package v1_16 //nolint
import "xorm.io/xorm"
func AddSSHKeyIsVerified(x *xorm.Engine) error {
type PublicKey struct {
Verified bool `xorm:"NOT NULL DEFAULT false"`
}
return x.Sync2(new(PublicKey))
}

View File

@@ -0,0 +1,43 @@
// Copyright 2022 The Gitea Authors. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
package v1_16 //nolint
import (
"code.gitea.io/gitea/models/migrations/base"
"xorm.io/xorm"
"xorm.io/xorm/schemas"
)
func MigrateUserPasswordSalt(x *xorm.Engine) 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,
})
}

View File

@@ -0,0 +1,29 @@
// Copyright 2022 The Gitea Authors. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
package v1_16 //nolint
import (
"fmt"
"xorm.io/xorm"
)
func AddAuthorizeColForTeamUnit(x *xorm.Engine) 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.Sync2(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
}

View File

@@ -0,0 +1,15 @@
// Copyright 2021 The Gitea Authors. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
package v1_16 //nolint
import (
"xorm.io/xorm"
)
func AddWebAuthnCred(x *xorm.Engine) error {
// NO-OP Don't migrate here - let v210 do this.
return nil
}

View File

@@ -0,0 +1,14 @@
// Copyright 2021 The Gitea Authors. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
package v1_16 //nolint
import (
"xorm.io/xorm"
)
func UseBase32HexForCredIDInWebAuthnCredential(x *xorm.Engine) error {
// noop
return nil
}

View File

@@ -0,0 +1,17 @@
// Copyright 2022 The Gitea Authors. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
package v1_16 //nolint
import (
"xorm.io/xorm"
)
func IncreaseCredentialIDTo410(x *xorm.Engine) error {
// no-op
// v208 was completely wrong
// So now we have to no-op again.
return nil
}

View File

@@ -0,0 +1,185 @@
// Copyright 2022 The Gitea Authors. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
package v1_16 //nolint
import (
"crypto/elliptic"
"encoding/base32"
"fmt"
"strings"
"code.gitea.io/gitea/models/migrations/base"
"code.gitea.io/gitea/modules/timeutil"
"github.com/tstranex/u2f"
"xorm.io/xorm"
"xorm.io/xorm/schemas"
)
// v208 migration was completely broken
func RemigrateU2FCredentials(x *xorm.Engine) 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.Sync2(&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.ORACLE:
_, err := x.Exec("ALTER TABLE webauthn_credential MODIFY 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
}
remigrated := &webauthnCredential{
ID: reg.ID,
Name: reg.Name,
LowerName: strings.ToLower(reg.Name),
UserID: reg.UserID,
CredentialID: base32.HexEncoding.EncodeToString(parsed.KeyHandle),
PublicKey: elliptic.Marshal(elliptic.P256(), parsed.PubKey.X, parsed.PubKey.Y),
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
}

View File

@@ -0,0 +1,76 @@
// Copyright 2021 The Gitea Authors. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
package v1_16 //nolint
import (
"testing"
"code.gitea.io/gitea/models/migrations/base"
"code.gitea.io/gitea/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 := base.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.EqualValues(t, expected, got)
}