diff --git a/server/api/login.go b/server/api/login.go index f1f491a75..f4326cb06 100644 --- a/server/api/login.go +++ b/server/api/login.go @@ -65,7 +65,7 @@ func HandleAuth(c *gin.Context) { config := ToConfig(c) // get the user from the database - u, err := _store.GetUserLogin(tmpuser.Login) + u, err := _store.GetUserRemoteID(tmpuser.ForgeRemoteID, tmpuser.Login) if err != nil { if !errors.Is(err, types.RecordNotExist) { _ = c.AbortWithError(http.StatusInternalServerError, err) @@ -92,11 +92,12 @@ func HandleAuth(c *gin.Context) { // create the user account u = &model.User{ - Login: tmpuser.Login, - Token: tmpuser.Token, - Secret: tmpuser.Secret, - Email: tmpuser.Email, - Avatar: tmpuser.Avatar, + Login: tmpuser.Login, + ForgeRemoteID: tmpuser.ForgeRemoteID, + Token: tmpuser.Token, + Secret: tmpuser.Secret, + Email: tmpuser.Email, + Avatar: tmpuser.Avatar, Hash: base32.StdEncoding.EncodeToString( securecookie.GenerateRandomKey(32), ), @@ -115,6 +116,8 @@ func HandleAuth(c *gin.Context) { u.Secret = tmpuser.Secret u.Email = tmpuser.Email u.Avatar = tmpuser.Avatar + u.ForgeRemoteID = tmpuser.ForgeRemoteID + u.Login = tmpuser.Login u.Admin = u.Admin || config.IsAdmin(tmpuser) // if self-registration is enabled for whitelisted organizations we need to diff --git a/server/forge/bitbucket/convert.go b/server/forge/bitbucket/convert.go index 8449e75a3..58d396c4a 100644 --- a/server/forge/bitbucket/convert.go +++ b/server/forge/bitbucket/convert.go @@ -118,11 +118,12 @@ func cloneLink(repo *internal.Repo) string { // structure to the Woodpecker User structure. func convertUser(from *internal.Account, token *oauth2.Token) *model.User { return &model.User{ - Login: from.Login, - Token: token.AccessToken, - Secret: token.RefreshToken, - Expiry: token.Expiry.UTC().Unix(), - Avatar: from.Links.Avatar.Href, + Login: from.Login, + Token: token.AccessToken, + Secret: token.RefreshToken, + Expiry: token.Expiry.UTC().Unix(), + Avatar: from.Links.Avatar.Href, + ForgeRemoteID: model.ForgeRemoteID(fmt.Sprint(from.ID)), } } diff --git a/server/forge/bitbucket/internal/types.go b/server/forge/bitbucket/internal/types.go index 930683a9e..b3e6cc4fa 100644 --- a/server/forge/bitbucket/internal/types.go +++ b/server/forge/bitbucket/internal/types.go @@ -21,6 +21,7 @@ import ( ) type Account struct { + ID int64 `json:"id"` Login string `json:"username"` Name string `json:"display_name"` Type string `json:"type"` diff --git a/server/forge/bitbucketserver/convert.go b/server/forge/bitbucketserver/convert.go index 43b0ea4a1..57367bb47 100644 --- a/server/forge/bitbucketserver/convert.go +++ b/server/forge/bitbucketserver/convert.go @@ -121,10 +121,11 @@ func convertPushHook(hook *internal.PostHook, baseURL string) *model.Pipeline { // structure to the Woodpecker User structure. func convertUser(from *internal.User, token *oauth.AccessToken) *model.User { return &model.User{ - Login: from.Slug, - Token: token.Token, - Email: from.EmailAddress, - Avatar: avatarLink(from.EmailAddress), + Login: from.Slug, + Token: token.Token, + Email: from.EmailAddress, + Avatar: avatarLink(from.EmailAddress), + ForgeRemoteID: model.ForgeRemoteID(fmt.Sprint(from.ID)), } } diff --git a/server/forge/bitbucketserver/internal/client.go b/server/forge/bitbucketserver/internal/client.go index 3f3af3614..b49c8efd6 100644 --- a/server/forge/bitbucketserver/internal/client.go +++ b/server/forge/bitbucketserver/internal/client.go @@ -70,29 +70,27 @@ func NewClientWithToken(ctx context.Context, url string, consumer *oauth.Consume } func (c *Client) FindCurrentUser() (*User, error) { - CurrentUserIDResponse, err := c.doGet(fmt.Sprintf(currentUserID, c.base)) - if CurrentUserIDResponse != nil { - defer CurrentUserIDResponse.Body.Close() - } + currentUserIDResponse, err := c.doGet(fmt.Sprintf(currentUserID, c.base)) if err != nil { return nil, err } + defer currentUserIDResponse.Body.Close() - bits, err := io.ReadAll(CurrentUserIDResponse.Body) + bits, err := io.ReadAll(currentUserIDResponse.Body) if err != nil { return nil, err } login := string(bits) - CurrentUserResponse, err := c.doGet(fmt.Sprintf(pathUser, c.base, login)) - if CurrentUserResponse != nil { - defer CurrentUserResponse.Body.Close() + currentUserResponse, err := c.doGet(fmt.Sprintf(pathUser, c.base, login)) + if currentUserResponse != nil { + defer currentUserResponse.Body.Close() } if err != nil { return nil, err } - contents, err := io.ReadAll(CurrentUserResponse.Body) + contents, err := io.ReadAll(currentUserResponse.Body) if err != nil { return nil, err } diff --git a/server/forge/gitea/gitea.go b/server/forge/gitea/gitea.go index 16c108aba..110b23131 100644 --- a/server/forge/gitea/gitea.go +++ b/server/forge/gitea/gitea.go @@ -143,12 +143,13 @@ func (c *Gitea) Login(ctx context.Context, w http.ResponseWriter, req *http.Requ } return &model.User{ - Token: token.AccessToken, - Secret: token.RefreshToken, - Expiry: token.Expiry.UTC().Unix(), - Login: account.UserName, - Email: account.Email, - Avatar: expandAvatar(c.URL, account.AvatarURL), + Token: token.AccessToken, + Secret: token.RefreshToken, + Expiry: token.Expiry.UTC().Unix(), + Login: account.UserName, + Email: account.Email, + ForgeRemoteID: model.ForgeRemoteID(fmt.Sprint(account.ID)), + Avatar: expandAvatar(c.URL, account.AvatarURL), }, nil } diff --git a/server/forge/github/github.go b/server/forge/github/github.go index c2822eaf0..f73570b94 100644 --- a/server/forge/github/github.go +++ b/server/forge/github/github.go @@ -129,10 +129,11 @@ func (c *client) Login(ctx context.Context, res http.ResponseWriter, req *http.R } return &model.User{ - Login: *user.Login, - Email: *email.Email, - Token: token.AccessToken, - Avatar: *user.AvatarURL, + Login: user.GetLogin(), + Email: email.GetEmail(), + Token: token.AccessToken, + Avatar: user.GetAvatarURL(), + ForgeRemoteID: model.ForgeRemoteID(fmt.Sprint(user.GetID())), }, nil } diff --git a/server/forge/gitlab/gitlab.go b/server/forge/gitlab/gitlab.go index 093e9069a..f98daaf96 100644 --- a/server/forge/gitlab/gitlab.go +++ b/server/forge/gitlab/gitlab.go @@ -133,11 +133,12 @@ func (g *GitLab) Login(ctx context.Context, res http.ResponseWriter, req *http.R } user := &model.User{ - Login: login.Username, - Email: login.Email, - Avatar: login.AvatarURL, - Token: token.AccessToken, - Secret: token.RefreshToken, + Login: login.Username, + Email: login.Email, + Avatar: login.AvatarURL, + ForgeRemoteID: model.ForgeRemoteID(fmt.Sprint(login.ID)), + Token: token.AccessToken, + Secret: token.RefreshToken, } if !strings.HasPrefix(user.Avatar, "http") { user.Avatar = g.URL + "/" + login.AvatarURL diff --git a/server/forge/gogs/gogs.go b/server/forge/gogs/gogs.go index 7ed272db3..45ce79adf 100644 --- a/server/forge/gogs/gogs.go +++ b/server/forge/gogs/gogs.go @@ -18,6 +18,7 @@ package gogs import ( "context" "crypto/tls" + "fmt" "net" "net/http" "net/url" @@ -118,10 +119,11 @@ func (c *client) Login(_ context.Context, res http.ResponseWriter, req *http.Req } return &model.User{ - Token: accessToken, - Login: account.UserName, - Email: account.Email, - Avatar: expandAvatar(c.URL, account.AvatarUrl), + Token: accessToken, + Login: account.UserName, + Email: account.Email, + Avatar: expandAvatar(c.URL, account.AvatarUrl), + ForgeRemoteID: model.ForgeRemoteID(fmt.Sprint(account.ID)), }, nil } diff --git a/server/forge/mocks/forge.go b/server/forge/mocks/forge.go index c7c6b6809..58d82a2a4 100644 --- a/server/forge/mocks/forge.go +++ b/server/forge/mocks/forge.go @@ -1,4 +1,4 @@ -// Code generated by mockery v2.23.1. DO NOT EDIT. +// Code generated by mockery v2.26.1. DO NOT EDIT. package mocks diff --git a/server/model/user.go b/server/model/user.go index 0e5fa54d5..bf8861911 100644 --- a/server/model/user.go +++ b/server/model/user.go @@ -34,6 +34,8 @@ type User struct { // required: true ID int64 `json:"id" xorm:"pk autoincr 'user_id'"` + ForgeRemoteID ForgeRemoteID `json:"-" xorm:"forge_remote_id"` + // Login is the username for this user. // // required: true diff --git a/server/store/datastore/user.go b/server/store/datastore/user.go index 3f8e84154..d21599b4c 100644 --- a/server/store/datastore/user.go +++ b/server/store/datastore/user.go @@ -16,6 +16,7 @@ package datastore import ( "github.com/woodpecker-ci/woodpecker/server/model" + "xorm.io/xorm" ) func (s storage) GetUser(id int64) (*model.User, error) { @@ -23,9 +24,23 @@ func (s storage) GetUser(id int64) (*model.User, error) { return user, wrapGet(s.engine.ID(id).Get(user)) } -func (s storage) GetUserLogin(login string) (*model.User, error) { +func (s storage) GetUserRemoteID(remoteID model.ForgeRemoteID, login string) (*model.User, error) { + sess := s.engine.NewSession() user := new(model.User) - return user, wrapGet(s.engine.Where("user_login=?", login).Get(user)) + err := wrapGet(sess.Where("forge_remote_id = ?", remoteID).Get(user)) + if err != nil { + user, err = s.getUserLogin(sess, login) + } + return user, err +} + +func (s storage) GetUserLogin(login string) (*model.User, error) { + return s.getUserLogin(s.engine.NewSession(), login) +} + +func (s storage) getUserLogin(sess *xorm.Session, login string) (*model.User, error) { + user := new(model.User) + return user, wrapGet(sess.Where("user_login=?", login).Get(user)) } func (s storage) GetUserList(p *model.ListOptions) ([]*model.User, error) { diff --git a/server/store/mocks/store.go b/server/store/mocks/store.go index 4987aaf27..078014c32 100644 --- a/server/store/mocks/store.go +++ b/server/store/mocks/store.go @@ -1,4 +1,4 @@ -// Code generated by mockery v2.23.1. DO NOT EDIT. +// Code generated by mockery v2.26.1. DO NOT EDIT. package mocks @@ -1079,6 +1079,32 @@ func (_m *Store) GetUserLogin(_a0 string) (*model.User, error) { return r0, r1 } +// GetUserRemoteID provides a mock function with given fields: _a0, _a1 +func (_m *Store) GetUserRemoteID(_a0 model.ForgeRemoteID, _a1 string) (*model.User, error) { + ret := _m.Called(_a0, _a1) + + var r0 *model.User + var r1 error + if rf, ok := ret.Get(0).(func(model.ForgeRemoteID, string) (*model.User, error)); ok { + return rf(_a0, _a1) + } + if rf, ok := ret.Get(0).(func(model.ForgeRemoteID, string) *model.User); ok { + r0 = rf(_a0, _a1) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*model.User) + } + } + + if rf, ok := ret.Get(1).(func(model.ForgeRemoteID, string) error); ok { + r1 = rf(_a0, _a1) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + // GlobalSecretFind provides a mock function with given fields: _a0 func (_m *Store) GlobalSecretFind(_a0 string) (*model.Secret, error) { ret := _m.Called(_a0) diff --git a/server/store/store.go b/server/store/store.go index 4e79089fb..358da35f9 100644 --- a/server/store/store.go +++ b/server/store/store.go @@ -29,6 +29,8 @@ type Store interface { // Users // GetUser gets a user by unique ID. GetUser(int64) (*model.User, error) + // GetUserRemoteID gets a user by remote ID with fallback to login name. + GetUserRemoteID(model.ForgeRemoteID, string) (*model.User, error) // GetUserLogin gets a user by unique Login name. GetUserLogin(string) (*model.User, error) // GetUserList gets a list of all users in the system.