Skip to content

Commit bc05ddc

Browse files
AndrewBezold6543zeripathlunny
authored
Redirect on changed user and org name (#11649)
* Add redirect for user * Add redirect for orgs * Add user redirect test * Appease linter * Add comment to DeleteUserRedirect function * Fix locale changes * Fix GetUserByParams * Fix orgAssignment * Remove debug logging * Add redirect prompt * Dont Export DeleteUserRedirect & only use it within a session * Unexport newUserRedirect * cleanup * Fix & Dedub API code * Format Template * Add Migration & rm dublicat * Refactor: unexport newRepoRedirect() & rm dedub del exec * if this fails we'll need to re-rename the user directory Co-authored-by: 6543 <[email protected]> Co-authored-by: zeripath <[email protected]> Co-authored-by: Lunny Xiao <[email protected]>
1 parent 4f608ad commit bc05ddc

File tree

25 files changed

+325
-64
lines changed

25 files changed

+325
-64
lines changed

models/error.go

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,21 @@ func (err ErrUserNotExist) Error() string {
146146
return fmt.Sprintf("user does not exist [uid: %d, name: %s, keyid: %d]", err.UID, err.Name, err.KeyID)
147147
}
148148

149+
// ErrUserRedirectNotExist represents a "UserRedirectNotExist" kind of error.
150+
type ErrUserRedirectNotExist struct {
151+
Name string
152+
}
153+
154+
// IsErrUserRedirectNotExist check if an error is an ErrUserRedirectNotExist.
155+
func IsErrUserRedirectNotExist(err error) bool {
156+
_, ok := err.(ErrUserRedirectNotExist)
157+
return ok
158+
}
159+
160+
func (err ErrUserRedirectNotExist) Error() string {
161+
return fmt.Sprintf("user redirect does not exist [name: %s]", err.Name)
162+
}
163+
149164
// ErrUserProhibitLogin represents a "ErrUserProhibitLogin" kind of error.
150165
type ErrUserProhibitLogin struct {
151166
UID int64

models/fixtures/user_redirect.yml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
-
2+
id: 1
3+
lower_name: olduser1
4+
redirect_user_id: 1

models/migrations/migrations.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -279,6 +279,8 @@ var migrations = []Migration{
279279
NewMigration("Convert hook task type from char(16) to varchar(16) and trim the column", convertHookTaskTypeToVarcharAndTrim),
280280
// v166 -> v167
281281
NewMigration("Where Password is Valid with Empty String delete it", recalculateUserEmptyPWD),
282+
// v167 -> v168
283+
NewMigration("Add user redirect", addUserRedirect),
282284
}
283285

284286
// GetCurrentDBVersion returns the current db version

models/migrations/v167.go

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
// Copyright 2021 The Gitea Authors. All rights reserved.
2+
// Use of this source code is governed by a MIT-style
3+
// license that can be found in the LICENSE file.
4+
5+
package migrations
6+
7+
import (
8+
"fmt"
9+
10+
"xorm.io/xorm"
11+
)
12+
13+
func addUserRedirect(x *xorm.Engine) (err error) {
14+
type UserRedirect struct {
15+
ID int64 `xorm:"pk autoincr"`
16+
LowerName string `xorm:"UNIQUE(s) INDEX NOT NULL"`
17+
RedirectUserID int64
18+
}
19+
20+
if err := x.Sync2(new(UserRedirect)); err != nil {
21+
return fmt.Errorf("Sync2: %v", err)
22+
}
23+
return nil
24+
}

models/models.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,7 @@ func init() {
128128
new(Task),
129129
new(LanguageStat),
130130
new(EmailHash),
131+
new(UserRedirect),
131132
new(Project),
132133
new(ProjectBoard),
133134
new(ProjectIssue),

models/org.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -171,6 +171,10 @@ func CreateOrganization(org, owner *User) (err error) {
171171
return err
172172
}
173173

174+
if err = deleteUserRedirect(sess, org.Name); err != nil {
175+
return err
176+
}
177+
174178
if _, err = sess.Insert(org); err != nil {
175179
return fmt.Errorf("insert organization: %v", err)
176180
}

models/repo.go

Lines changed: 3 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1312,8 +1312,8 @@ func TransferOwnership(doer *User, newOwnerName string, repo *Repository) error
13121312
return fmt.Errorf("delete repo redirect: %v", err)
13131313
}
13141314

1315-
if err := NewRepoRedirect(DBContext{sess}, oldOwner.ID, repo.ID, repo.Name, repo.Name); err != nil {
1316-
return fmt.Errorf("NewRepoRedirect: %v", err)
1315+
if err := newRepoRedirect(sess, oldOwner.ID, repo.ID, repo.Name, repo.Name); err != nil {
1316+
return fmt.Errorf("newRepoRedirect: %v", err)
13171317
}
13181318

13191319
return sess.Commit()
@@ -1361,12 +1361,7 @@ func ChangeRepositoryName(doer *User, repo *Repository, newRepoName string) (err
13611361
return fmt.Errorf("sess.Begin: %v", err)
13621362
}
13631363

1364-
// If there was previously a redirect at this location, remove it.
1365-
if err = deleteRepoRedirect(sess, repo.OwnerID, newRepoName); err != nil {
1366-
return fmt.Errorf("delete repo redirect: %v", err)
1367-
}
1368-
1369-
if err := NewRepoRedirect(DBContext{sess}, repo.Owner.ID, repo.ID, oldRepoName, newRepoName); err != nil {
1364+
if err := newRepoRedirect(sess, repo.Owner.ID, repo.ID, oldRepoName, newRepoName); err != nil {
13701365
return err
13711366
}
13721367

models/repo_redirect.go

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -28,16 +28,16 @@ func LookupRepoRedirect(ownerID int64, repoName string) (int64, error) {
2828
return redirect.RedirectRepoID, nil
2929
}
3030

31-
// NewRepoRedirect create a new repo redirect
32-
func NewRepoRedirect(ctx DBContext, ownerID, repoID int64, oldRepoName, newRepoName string) error {
31+
// newRepoRedirect create a new repo redirect
32+
func newRepoRedirect(e Engine, ownerID, repoID int64, oldRepoName, newRepoName string) error {
3333
oldRepoName = strings.ToLower(oldRepoName)
3434
newRepoName = strings.ToLower(newRepoName)
3535

36-
if err := deleteRepoRedirect(ctx.e, ownerID, newRepoName); err != nil {
36+
if err := deleteRepoRedirect(e, ownerID, newRepoName); err != nil {
3737
return err
3838
}
3939

40-
if _, err := ctx.e.Insert(&RepoRedirect{
40+
if _, err := e.Insert(&RepoRedirect{
4141
OwnerID: ownerID,
4242
LowerName: oldRepoName,
4343
RedirectRepoID: repoID,

models/repo_redirect_test.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ func TestNewRepoRedirect(t *testing.T) {
2626
assert.NoError(t, PrepareTestDatabase())
2727

2828
repo := AssertExistsAndLoadBean(t, &Repository{ID: 1}).(*Repository)
29-
assert.NoError(t, NewRepoRedirect(DefaultDBContext(), repo.OwnerID, repo.ID, repo.Name, "newreponame"))
29+
assert.NoError(t, newRepoRedirect(x, repo.OwnerID, repo.ID, repo.Name, "newreponame"))
3030

3131
AssertExistsAndLoadBean(t, &RepoRedirect{
3232
OwnerID: repo.OwnerID,
@@ -45,7 +45,7 @@ func TestNewRepoRedirect2(t *testing.T) {
4545
assert.NoError(t, PrepareTestDatabase())
4646

4747
repo := AssertExistsAndLoadBean(t, &Repository{ID: 1}).(*Repository)
48-
assert.NoError(t, NewRepoRedirect(DefaultDBContext(), repo.OwnerID, repo.ID, repo.Name, "oldrepo1"))
48+
assert.NoError(t, newRepoRedirect(x, repo.OwnerID, repo.ID, repo.Name, "oldrepo1"))
4949

5050
AssertExistsAndLoadBean(t, &RepoRedirect{
5151
OwnerID: repo.OwnerID,
@@ -64,7 +64,7 @@ func TestNewRepoRedirect3(t *testing.T) {
6464
assert.NoError(t, PrepareTestDatabase())
6565

6666
repo := AssertExistsAndLoadBean(t, &Repository{ID: 2}).(*Repository)
67-
assert.NoError(t, NewRepoRedirect(DefaultDBContext(), repo.OwnerID, repo.ID, repo.Name, "newreponame"))
67+
assert.NoError(t, newRepoRedirect(x, repo.OwnerID, repo.ID, repo.Name, "newreponame"))
6868

6969
AssertExistsAndLoadBean(t, &RepoRedirect{
7070
OwnerID: repo.OwnerID,

models/user.go

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -863,6 +863,10 @@ func CreateUser(u *User) (err error) {
863863
return ErrUserAlreadyExist{u.Name}
864864
}
865865

866+
if err = deleteUserRedirect(sess, u.Name); err != nil {
867+
return err
868+
}
869+
866870
u.Email = strings.ToLower(u.Email)
867871
isExist, err = sess.
868872
Where("email=?", u.Email).
@@ -973,6 +977,7 @@ func VerifyActiveEmailCode(code, email string) *EmailAddress {
973977

974978
// ChangeUserName changes all corresponding setting from old user name to new one.
975979
func ChangeUserName(u *User, newUserName string) (err error) {
980+
oldUserName := u.Name
976981
if err = IsUsableUsername(newUserName); err != nil {
977982
return err
978983
}
@@ -990,16 +995,28 @@ func ChangeUserName(u *User, newUserName string) (err error) {
990995
return ErrUserAlreadyExist{newUserName}
991996
}
992997

993-
if _, err = sess.Exec("UPDATE `repository` SET owner_name=? WHERE owner_name=?", newUserName, u.Name); err != nil {
998+
if _, err = sess.Exec("UPDATE `repository` SET owner_name=? WHERE owner_name=?", newUserName, oldUserName); err != nil {
994999
return fmt.Errorf("Change repo owner name: %v", err)
9951000
}
9961001

9971002
// Do not fail if directory does not exist
998-
if err = os.Rename(UserPath(u.Name), UserPath(newUserName)); err != nil && !os.IsNotExist(err) {
1003+
if err = os.Rename(UserPath(oldUserName), UserPath(newUserName)); err != nil && !os.IsNotExist(err) {
9991004
return fmt.Errorf("Rename user directory: %v", err)
10001005
}
10011006

1002-
return sess.Commit()
1007+
if err = newUserRedirect(sess, u.ID, oldUserName, newUserName); err != nil {
1008+
return err
1009+
}
1010+
1011+
if err = sess.Commit(); err != nil {
1012+
if err2 := os.Rename(UserPath(newUserName), UserPath(oldUserName)); err2 != nil && !os.IsNotExist(err2) {
1013+
log.Critical("Unable to rollback directory change during failed username change from: %s to: %s. DB Error: %v. Filesystem Error: %v", oldUserName, newUserName, err, err2)
1014+
return fmt.Errorf("failed to rollback directory change during failed username change from: %s to: %s. DB Error: %w. Filesystem Error: %v", oldUserName, newUserName, err, err2)
1015+
}
1016+
return err
1017+
}
1018+
1019+
return nil
10031020
}
10041021

10051022
// checkDupEmail checks whether there are the same email with the user

models/user_redirect.go

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
// Copyright 2020 The Gitea Authors. All rights reserved.
2+
// Use of this source code is governed by a MIT-style
3+
// license that can be found in the LICENSE file.
4+
5+
package models
6+
7+
import "strings"
8+
9+
// UserRedirect represents that a user name should be redirected to another
10+
type UserRedirect struct {
11+
ID int64 `xorm:"pk autoincr"`
12+
LowerName string `xorm:"UNIQUE(s) INDEX NOT NULL"`
13+
RedirectUserID int64 // userID to redirect to
14+
}
15+
16+
// LookupUserRedirect look up userID if a user has a redirect name
17+
func LookupUserRedirect(userName string) (int64, error) {
18+
userName = strings.ToLower(userName)
19+
redirect := &UserRedirect{LowerName: userName}
20+
if has, err := x.Get(redirect); err != nil {
21+
return 0, err
22+
} else if !has {
23+
return 0, ErrUserRedirectNotExist{Name: userName}
24+
}
25+
return redirect.RedirectUserID, nil
26+
}
27+
28+
// newUserRedirect create a new user redirect
29+
func newUserRedirect(e Engine, ID int64, oldUserName, newUserName string) error {
30+
oldUserName = strings.ToLower(oldUserName)
31+
newUserName = strings.ToLower(newUserName)
32+
33+
if err := deleteUserRedirect(e, newUserName); err != nil {
34+
return err
35+
}
36+
37+
if _, err := e.Insert(&UserRedirect{
38+
LowerName: oldUserName,
39+
RedirectUserID: ID,
40+
}); err != nil {
41+
return err
42+
}
43+
return nil
44+
}
45+
46+
// deleteUserRedirect delete any redirect from the specified user name to
47+
// anything else
48+
func deleteUserRedirect(e Engine, userName string) error {
49+
userName = strings.ToLower(userName)
50+
_, err := e.Delete(&UserRedirect{LowerName: userName})
51+
return err
52+
}

models/user_redirect_test.go

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
// Copyright 2020 The Gitea Authors. All rights reserved.
2+
// Use of this source code is governed by a MIT-style
3+
// license that can be found in the LICENSE file.
4+
5+
package models
6+
7+
import (
8+
"testing"
9+
10+
"github.com/stretchr/testify/assert"
11+
)
12+
13+
func TestLookupUserRedirect(t *testing.T) {
14+
assert.NoError(t, PrepareTestDatabase())
15+
16+
userID, err := LookupUserRedirect("olduser1")
17+
assert.NoError(t, err)
18+
assert.EqualValues(t, 1, userID)
19+
20+
_, err = LookupUserRedirect("doesnotexist")
21+
assert.True(t, IsErrUserRedirectNotExist(err))
22+
}
23+
24+
func TestNewUserRedirect(t *testing.T) {
25+
// redirect to a completely new name
26+
assert.NoError(t, PrepareTestDatabase())
27+
28+
user := AssertExistsAndLoadBean(t, &User{ID: 1}).(*User)
29+
assert.NoError(t, newUserRedirect(x, user.ID, user.Name, "newusername"))
30+
31+
AssertExistsAndLoadBean(t, &UserRedirect{
32+
LowerName: user.LowerName,
33+
RedirectUserID: user.ID,
34+
})
35+
AssertExistsAndLoadBean(t, &UserRedirect{
36+
LowerName: "olduser1",
37+
RedirectUserID: user.ID,
38+
})
39+
}
40+
41+
func TestNewUserRedirect2(t *testing.T) {
42+
// redirect to previously used name
43+
assert.NoError(t, PrepareTestDatabase())
44+
45+
user := AssertExistsAndLoadBean(t, &User{ID: 1}).(*User)
46+
assert.NoError(t, newUserRedirect(x, user.ID, user.Name, "olduser1"))
47+
48+
AssertExistsAndLoadBean(t, &UserRedirect{
49+
LowerName: user.LowerName,
50+
RedirectUserID: user.ID,
51+
})
52+
AssertNotExistsBean(t, &UserRedirect{
53+
LowerName: "olduser1",
54+
RedirectUserID: user.ID,
55+
})
56+
}
57+
58+
func TestNewUserRedirect3(t *testing.T) {
59+
// redirect for a previously-unredirected user
60+
assert.NoError(t, PrepareTestDatabase())
61+
62+
user := AssertExistsAndLoadBean(t, &User{ID: 2}).(*User)
63+
assert.NoError(t, newUserRedirect(x, user.ID, user.Name, "newusername"))
64+
65+
AssertExistsAndLoadBean(t, &UserRedirect{
66+
LowerName: user.LowerName,
67+
RedirectUserID: user.ID,
68+
})
69+
}

modules/context/context.go

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,26 @@ func (ctx *Context) IsUserRepoReaderAny() bool {
9090
return ctx.Repo.HasAccess()
9191
}
9292

93+
// RedirectToUser redirect to a differently-named user
94+
func RedirectToUser(ctx *Context, userName string, redirectUserID int64) {
95+
user, err := models.GetUserByID(redirectUserID)
96+
if err != nil {
97+
ctx.ServerError("GetUserByID", err)
98+
return
99+
}
100+
101+
redirectPath := strings.Replace(
102+
ctx.Req.URL.Path,
103+
userName,
104+
user.Name,
105+
1,
106+
)
107+
if ctx.Req.URL.RawQuery != "" {
108+
redirectPath += "?" + ctx.Req.URL.RawQuery
109+
}
110+
ctx.Redirect(path.Join(setting.AppSubURL, redirectPath))
111+
}
112+
93113
// HasAPIError returns true if error occurs in form validation.
94114
func (ctx *Context) HasAPIError() bool {
95115
hasErr, ok := ctx.Data["HasError"]

modules/context/org.go

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,14 @@ func HandleOrgAssignment(ctx *Context, args ...bool) {
5454
ctx.Org.Organization, err = models.GetUserByName(orgName)
5555
if err != nil {
5656
if models.IsErrUserNotExist(err) {
57-
ctx.NotFound("GetUserByName", err)
57+
redirectUserID, err := models.LookupUserRedirect(orgName)
58+
if err == nil {
59+
RedirectToUser(ctx, orgName, redirectUserID)
60+
} else if models.IsErrUserRedirectNotExist(err) {
61+
ctx.NotFound("GetUserByName", err)
62+
} else {
63+
ctx.ServerError("LookupUserRedirect", err)
64+
}
5865
} else {
5966
ctx.ServerError("GetUserByName", err)
6067
}

modules/context/repo.go

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -411,11 +411,18 @@ func RepoAssignment() macaron.Handler {
411411
owner, err = models.GetUserByName(userName)
412412
if err != nil {
413413
if models.IsErrUserNotExist(err) {
414-
if ctx.Query("go-get") == "1" {
415-
EarlyResponseForGoGetMeta(ctx)
416-
return
414+
redirectUserID, err := models.LookupUserRedirect(userName)
415+
if err == nil {
416+
RedirectToUser(ctx, userName, redirectUserID)
417+
} else if models.IsErrUserRedirectNotExist(err) {
418+
if ctx.Query("go-get") == "1" {
419+
EarlyResponseForGoGetMeta(ctx)
420+
return
421+
}
422+
ctx.NotFound("GetUserByName", nil)
423+
} else {
424+
ctx.ServerError("LookupUserRedirect", err)
417425
}
418-
ctx.NotFound("GetUserByName", nil)
419426
} else {
420427
ctx.ServerError("GetUserByName", err)
421428
}

0 commit comments

Comments
 (0)