diff options
Diffstat (limited to 'models/migrations/v1_16')
27 files changed, 1068 insertions, 0 deletions
diff --git a/models/migrations/v1_16/main_test.go b/models/migrations/v1_16/main_test.go new file mode 100644 index 00000000..817a0c13 --- /dev/null +++ b/models/migrations/v1_16/main_test.go @@ -0,0 +1,14 @@ +// Copyright 2021 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package v1_16 //nolint + +import ( + "testing" + + "code.gitea.io/gitea/models/migrations/base" +) + +func TestMain(m *testing.M) { + base.MainTest(m) +} diff --git a/models/migrations/v1_16/v189.go b/models/migrations/v1_16/v189.go new file mode 100644 index 00000000..afd93b0e --- /dev/null +++ b/models/migrations/v1_16/v189.go @@ -0,0 +1,111 @@ +// Copyright 2021 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +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 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 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() +} diff --git a/models/migrations/v1_16/v189_test.go b/models/migrations/v1_16/v189_test.go new file mode 100644 index 00000000..c4e45e1e --- /dev/null +++ b/models/migrations/v1_16/v189_test.go @@ -0,0 +1,83 @@ +// Copyright 2021 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package v1_16 //nolint + +import ( + "testing" + + "code.gitea.io/gitea/models/migrations/base" + "code.gitea.io/gitea/modules/json" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +// 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 { + require.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 { + require.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 { + require.NoError(t, err) + return + } + + if err := json.Unmarshal([]byte(source.Expected), &expected); err != nil { + require.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) + } + } +} diff --git a/models/migrations/v1_16/v190.go b/models/migrations/v1_16/v190.go new file mode 100644 index 00000000..59538028 --- /dev/null +++ b/models/migrations/v1_16/v190.go @@ -0,0 +1,23 @@ +// Copyright 2021 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +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.Sync(new(PullRequest)); err != nil { + return fmt.Errorf("sync2: %w", err) + } + return nil +} diff --git a/models/migrations/v1_16/v191.go b/models/migrations/v1_16/v191.go new file mode 100644 index 00000000..c618783c --- /dev/null +++ b/models/migrations/v1_16/v191.go @@ -0,0 +1,28 @@ +// Copyright 2021 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +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.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() +} diff --git a/models/migrations/v1_16/v192.go b/models/migrations/v1_16/v192.go new file mode 100644 index 00000000..2d5d158a --- /dev/null +++ b/models/migrations/v1_16/v192.go @@ -0,0 +1,19 @@ +// Copyright 2021 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +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) +} diff --git a/models/migrations/v1_16/v193.go b/models/migrations/v1_16/v193.go new file mode 100644 index 00000000..8d3ce7a5 --- /dev/null +++ b/models/migrations/v1_16/v193.go @@ -0,0 +1,32 @@ +// Copyright 2021 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +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.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 +} diff --git a/models/migrations/v1_16/v193_test.go b/models/migrations/v1_16/v193_test.go new file mode 100644 index 00000000..1cfa7fba --- /dev/null +++ b/models/migrations/v1_16/v193_test.go @@ -0,0 +1,81 @@ +// Copyright 2021 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package v1_16 //nolint + +import ( + "testing" + + "code.gitea.io/gitea/models/migrations/base" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +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 := 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 { + require.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) + require.NoError(t, err) + for _, attach := range issueAttachments { + assert.Greater(t, attach.RepoID, int64(0)) + assert.Greater(t, attach.IssueID, int64(0)) + var issue Issue + has, err := x.ID(attach.IssueID).Get(&issue) + require.NoError(t, err) + assert.True(t, has) + assert.EqualValues(t, attach.RepoID, issue.RepoID) + } + + var releaseAttachments []*NewAttachment + err = x.Table("attachment").Where("release_id > 0").Find(&releaseAttachments) + require.NoError(t, err) + for _, attach := range releaseAttachments { + assert.Greater(t, attach.RepoID, int64(0)) + assert.Greater(t, attach.ReleaseID, int64(0)) + var release Release + has, err := x.ID(attach.ReleaseID).Get(&release) + require.NoError(t, err) + assert.True(t, has) + assert.EqualValues(t, attach.RepoID, release.RepoID) + } +} diff --git a/models/migrations/v1_16/v194.go b/models/migrations/v1_16/v194.go new file mode 100644 index 00000000..6aa13c50 --- /dev/null +++ b/models/migrations/v1_16/v194.go @@ -0,0 +1,21 @@ +// Copyright 2021 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +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.Sync(new(ProtectedBranch)); err != nil { + return fmt.Errorf("Sync: %w", err) + } + return nil +} diff --git a/models/migrations/v1_16/v195.go b/models/migrations/v1_16/v195.go new file mode 100644 index 00000000..6d7e9414 --- /dev/null +++ b/models/migrations/v1_16/v195.go @@ -0,0 +1,46 @@ +// Copyright 2021 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +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.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() +} diff --git a/models/migrations/v1_16/v195_test.go b/models/migrations/v1_16/v195_test.go new file mode 100644 index 00000000..dbe27634 --- /dev/null +++ b/models/migrations/v1_16/v195_test.go @@ -0,0 +1,64 @@ +// Copyright 2021 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package v1_16 //nolint + +import ( + "testing" + + "code.gitea.io/gitea/models/migrations/base" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +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 { + require.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) + require.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) + require.NoError(t, err) + assert.True(t, has) + assert.EqualValues(t, maxIndex, idx.MaxIndex) + } + if len(indexes) < batchSize { + break + } + start += len(indexes) + } +} diff --git a/models/migrations/v1_16/v196.go b/models/migrations/v1_16/v196.go new file mode 100644 index 00000000..7cbafc61 --- /dev/null +++ b/models/migrations/v1_16/v196.go @@ -0,0 +1,21 @@ +// Copyright 2021 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +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.Sync(new(ProjectBoard)); err != nil { + return fmt.Errorf("Sync: %w", err) + } + return nil +} diff --git a/models/migrations/v1_16/v197.go b/models/migrations/v1_16/v197.go new file mode 100644 index 00000000..97888b28 --- /dev/null +++ b/models/migrations/v1_16/v197.go @@ -0,0 +1,19 @@ +// Copyright 2021 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +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.Sync(new(RenamedBranch)) +} diff --git a/models/migrations/v1_16/v198.go b/models/migrations/v1_16/v198.go new file mode 100644 index 00000000..115bb313 --- /dev/null +++ b/models/migrations/v1_16/v198.go @@ -0,0 +1,32 @@ +// Copyright 2021 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +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.Sync(new(IssueContentHistory)); err != nil { + return fmt.Errorf("Sync: %w", err) + } + return sess.Commit() +} diff --git a/models/migrations/v1_16/v199.go b/models/migrations/v1_16/v199.go new file mode 100644 index 00000000..6adcf890 --- /dev/null +++ b/models/migrations/v1_16/v199.go @@ -0,0 +1,6 @@ +// Copyright 2021 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +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. diff --git a/models/migrations/v1_16/v200.go b/models/migrations/v1_16/v200.go new file mode 100644 index 00000000..c08c20e5 --- /dev/null +++ b/models/migrations/v1_16/v200.go @@ -0,0 +1,22 @@ +// Copyright 2021 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +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.Sync(new(AppState)); err != nil { + return fmt.Errorf("Sync: %w", err) + } + return nil +} diff --git a/models/migrations/v1_16/v201.go b/models/migrations/v1_16/v201.go new file mode 100644 index 00000000..35e0c9f2 --- /dev/null +++ b/models/migrations/v1_16/v201.go @@ -0,0 +1,14 @@ +// Copyright 2021 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +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 +} diff --git a/models/migrations/v1_16/v202.go b/models/migrations/v1_16/v202.go new file mode 100644 index 00000000..6ba36152 --- /dev/null +++ b/models/migrations/v1_16/v202.go @@ -0,0 +1,23 @@ +// Copyright 2021 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +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.Sync(new(UserSetting)); err != nil { + return fmt.Errorf("sync2: %w", err) + } + return nil +} diff --git a/models/migrations/v1_16/v203.go b/models/migrations/v1_16/v203.go new file mode 100644 index 00000000..e8e6b524 --- /dev/null +++ b/models/migrations/v1_16/v203.go @@ -0,0 +1,17 @@ +// Copyright 2021 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +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.Sync(new(ProjectIssue)) +} diff --git a/models/migrations/v1_16/v204.go b/models/migrations/v1_16/v204.go new file mode 100644 index 00000000..ece03e13 --- /dev/null +++ b/models/migrations/v1_16/v204.go @@ -0,0 +1,14 @@ +// Copyright 2021 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +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.Sync(new(PublicKey)) +} diff --git a/models/migrations/v1_16/v205.go b/models/migrations/v1_16/v205.go new file mode 100644 index 00000000..d6c57708 --- /dev/null +++ b/models/migrations/v1_16/v205.go @@ -0,0 +1,42 @@ +// Copyright 2022 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +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, + }) +} diff --git a/models/migrations/v1_16/v206.go b/models/migrations/v1_16/v206.go new file mode 100644 index 00000000..581a7d76 --- /dev/null +++ b/models/migrations/v1_16/v206.go @@ -0,0 +1,28 @@ +// Copyright 2022 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +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.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 +} diff --git a/models/migrations/v1_16/v207.go b/models/migrations/v1_16/v207.go new file mode 100644 index 00000000..91208f06 --- /dev/null +++ b/models/migrations/v1_16/v207.go @@ -0,0 +1,14 @@ +// Copyright 2021 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +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 +} diff --git a/models/migrations/v1_16/v208.go b/models/migrations/v1_16/v208.go new file mode 100644 index 00000000..1a11ef09 --- /dev/null +++ b/models/migrations/v1_16/v208.go @@ -0,0 +1,13 @@ +// Copyright 2021 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package v1_16 //nolint + +import ( + "xorm.io/xorm" +) + +func UseBase32HexForCredIDInWebAuthnCredential(x *xorm.Engine) error { + // noop + return nil +} diff --git a/models/migrations/v1_16/v209.go b/models/migrations/v1_16/v209.go new file mode 100644 index 00000000..be3100e0 --- /dev/null +++ b/models/migrations/v1_16/v209.go @@ -0,0 +1,16 @@ +// Copyright 2022 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +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 +} diff --git a/models/migrations/v1_16/v210.go b/models/migrations/v1_16/v210.go new file mode 100644 index 00000000..db45b11a --- /dev/null +++ b/models/migrations/v1_16/v210.go @@ -0,0 +1,177 @@ +// Copyright 2022 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package v1_16 //nolint + +import ( + "crypto/ecdh" + "encoding/base32" + "errors" + "fmt" + "strings" + + "code.gitea.io/gitea/modules/timeutil" + + "xorm.io/xorm" + "xorm.io/xorm/schemas" +) + +func parseU2FRegistration(raw []byte) (pubKey *ecdh.PublicKey, keyHandle []byte, err error) { + if len(raw) < 69 { + return nil, nil, errors.New("data is too short") + } + if raw[0] != 0x05 { + return nil, nil, errors.New("invalid reserved byte") + } + raw = raw[1:] + + pubKey, err = ecdh.P256().NewPublicKey(raw[:65]) + if err != nil { + return nil, nil, err + } + raw = raw[65:] + + khLen := int(raw[0]) + if len(raw) < khLen { + return nil, nil, errors.New("invalid key handle") + } + raw = raw[1:] + keyHandle = raw[:khLen] + + return pubKey, keyHandle, nil +} + +// 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.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.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(®s) + 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) + } + for _, reg := range regs { + pubKey, keyHandle, err := parseU2FRegistration(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(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 +} diff --git a/models/migrations/v1_16/v210_test.go b/models/migrations/v1_16/v210_test.go new file mode 100644 index 00000000..11e6f8d9 --- /dev/null +++ b/models/migrations/v1_16/v210_test.go @@ -0,0 +1,88 @@ +// Copyright 2021 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package v1_16 //nolint + +import ( + "encoding/hex" + "testing" + + "code.gitea.io/gitea/models/migrations/base" + "code.gitea.io/gitea/modules/timeutil" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "xorm.io/xorm/schemas" +) + +func TestParseU2FRegistration(t *testing.T) { + // test vectors from https://github.com/tstranex/u2f/blob/d21a03e0b1d9fc1df59ff54e7a513655c1748b0c/register_test.go#L15 + + const testRegRespHex = "0504b174bc49c7ca254b70d2e5c207cee9cf174820ebd77ea3c65508c26da51b657c1cc6b952f8621697936482da0a6d3d3826a59095daf6cd7c03e2e60385d2f6d9402a552dfdb7477ed65fd84133f86196010b2215b57da75d315b7b9e8fe2e3925a6019551bab61d16591659cbaf00b4950f7abfe6660e2e006f76868b772d70c253082013c3081e4a003020102020a47901280001155957352300a06082a8648ce3d0403023017311530130603550403130c476e756262792050696c6f74301e170d3132303831343138323933325a170d3133303831343138323933325a3031312f302d0603550403132650696c6f74476e756262792d302e342e312d34373930313238303030313135353935373335323059301306072a8648ce3d020106082a8648ce3d030107034200048d617e65c9508e64bcc5673ac82a6799da3c1446682c258c463fffdf58dfd2fa3e6c378b53d795c4a4dffb4199edd7862f23abaf0203b4b8911ba0569994e101300a06082a8648ce3d0403020347003044022060cdb6061e9c22262d1aac1d96d8c70829b2366531dda268832cb836bcd30dfa0220631b1459f09e6330055722c8d89b7f48883b9089b88d60d1d9795902b30410df304502201471899bcc3987e62e8202c9b39c33c19033f7340352dba80fcab017db9230e402210082677d673d891933ade6f617e5dbde2e247e70423fd5ad7804a6d3d3961ef871" + + regResp, err := hex.DecodeString(testRegRespHex) + require.NoError(t, err) + pubKey, keyHandle, err := parseU2FRegistration(regResp) + require.NoError(t, err) + assert.Equal(t, "04b174bc49c7ca254b70d2e5c207cee9cf174820ebd77ea3c65508c26da51b657c1cc6b952f8621697936482da0a6d3d3826a59095daf6cd7c03e2e60385d2f6d9", hex.EncodeToString(pubKey.Bytes())) + assert.Equal(t, "2a552dfdb7477ed65fd84133f86196010b2215b57da75d315b7b9e8fe2e3925a6019551bab61d16591659cbaf00b4950f7abfe6660e2e006f76868b772d70c25", hex.EncodeToString(keyHandle)) +} + +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 { + require.NoError(t, err) + return + } + + expected := []ExpectedWebauthnCredential{} + err := x.Table("expected_webauthn_credential").Asc("id").Find(&expected) + require.NoError(t, err) + + got := []ExpectedWebauthnCredential{} + err = x.Table("webauthn_credential").Select("id, credential_id").Asc("id").Find(&got) + require.NoError(t, err) + + assert.EqualValues(t, expected, got) +} |