diff --git a/cmd/server/openapi/docs.go b/cmd/server/openapi/docs.go index 0c0dacad7..9fe9d2fa8 100644 --- a/cmd/server/openapi/docs.go +++ b/cmd/server/openapi/docs.go @@ -4476,6 +4476,19 @@ const docTemplate = `{ "name": "login", "in": "path", "required": true + }, + { + "type": "string", + "description": "specify forge (else default will be used)", + "name": "forge_id", + "in": "query", + "required": true + }, + { + "type": "string", + "description": "specify user id at forge (else fallback to login)", + "name": "forge_remote_id", + "in": "query" } ], "responses": { @@ -4511,6 +4524,19 @@ const docTemplate = `{ "name": "login", "in": "path", "required": true + }, + { + "type": "string", + "description": "specify forge (else default will be used)", + "name": "forge_id", + "in": "query", + "required": true + }, + { + "type": "string", + "description": "specify user id at forge (else fallback to login)", + "name": "forge_remote_id", + "in": "query" } ], "responses": { @@ -5552,6 +5578,9 @@ const docTemplate = `{ "forge_id": { "type": "integer" }, + "forge_remote_id": { + "type": "string" + }, "id": { "description": "the id for this user.\n\nrequired: true", "type": "integer" diff --git a/server/api/login.go b/server/api/login.go index 614d9fcde..060d53638 100644 --- a/server/api/login.go +++ b/server/api/login.go @@ -160,13 +160,29 @@ func HandleAuth(c *gin.Context) { } } + var user *model.User + // get the user from the database - user, err := _store.GetUserRemoteID(userFromForge.ForgeRemoteID, userFromForge.Login) + user, err = _store.GetUserByRemoteID(forgeID, userFromForge.ForgeRemoteID) if err != nil && !errors.Is(err, types.RecordNotExist) { log.Error().Err(err).Msgf("cannot get user %s", userFromForge.Login) c.Redirect(http.StatusSeeOther, server.Config.Server.RootPath+"/login?error=internal_error") return } + // update user login (in case forge supports renaming) + if user != nil { + user.Login = userFromForge.Login + } + + // re-try with login name + if user == nil || errors.Is(err, types.RecordNotExist) { + user, err = _store.GetUserByLogin(forgeID, userFromForge.Login) + if err != nil && !errors.Is(err, types.RecordNotExist) { + log.Error().Err(err).Msgf("cannot get user %s", userFromForge.Login) + c.Redirect(http.StatusSeeOther, server.Config.Server.RootPath+"/login?error=internal_error") + return + } + } if user == nil || errors.Is(err, types.RecordNotExist) { // if self-registration is disabled we should return a not authorized error diff --git a/server/api/login_test.go b/server/api/login_test.go index e410cd37c..f2dba3221 100644 --- a/server/api/login_test.go +++ b/server/api/login_test.go @@ -158,7 +158,8 @@ func TestHandleAuth(t *testing.T) { _manager.On("ForgeByID", int64(1)).Return(_forge, nil) _forge.On("Login", mock.Anything, mock.Anything).Return(user, "", nil) - _store.On("GetUserRemoteID", user.ForgeRemoteID, user.Login).Return(nil, types.RecordNotExist) + _store.On("GetUserByRemoteID", user.ForgeID, user.ForgeRemoteID).Return(nil, types.RecordNotExist) + _store.On("GetUserByLogin", user.ForgeID, user.Login).Return(nil, types.RecordNotExist) _store.On("CreateUser", mock.Anything).Return(nil) _store.On("OrgFindByName", user.Login, user.ForgeID).Return(nil, nil) _store.On("OrgCreate", mock.Anything).Return(nil) @@ -192,7 +193,7 @@ func TestHandleAuth(t *testing.T) { _manager.On("ForgeByID", int64(1)).Return(_forge, nil) _forge.On("Login", mock.Anything, mock.Anything).Return(user, "", nil) - _store.On("GetUserRemoteID", user.ForgeRemoteID, user.Login).Return(user, nil) + _store.On("GetUserByRemoteID", user.ForgeID, user.ForgeRemoteID).Return(user, nil) _store.On("OrgGet", org.ID).Return(org, nil) _store.On("UpdateUser", mock.Anything).Return(nil) _forge.On("Repos", mock.Anything, mock.Anything, mock.Anything).Return(nil, nil) @@ -224,7 +225,8 @@ func TestHandleAuth(t *testing.T) { _manager.On("ForgeByID", int64(1)).Return(_forge, nil) _forge.On("Login", mock.Anything, mock.Anything).Return(user, "", nil) - _store.On("GetUserRemoteID", user.ForgeRemoteID, user.Login).Return(nil, types.RecordNotExist) + _store.On("GetUserByRemoteID", user.ForgeID, user.ForgeRemoteID).Return(nil, types.RecordNotExist) + _store.On("GetUserByLogin", user.ForgeID, user.Login).Return(nil, types.RecordNotExist) api.HandleAuth(c) @@ -285,7 +287,7 @@ func TestHandleAuth(t *testing.T) { _manager.On("ForgeByID", int64(1)).Return(_forge, nil) _forge.On("Login", mock.Anything, mock.Anything).Return(user, "", nil) - _store.On("GetUserRemoteID", user.ForgeRemoteID, user.Login).Return(user, nil) + _store.On("GetUserByRemoteID", user.ForgeID, user.ForgeRemoteID).Return(user, nil) _store.On("OrgFindByName", user.Login, user.ForgeID).Return(nil, types.RecordNotExist) _store.On("OrgCreate", mock.Anything).Return(nil) _store.On("UpdateUser", mock.Anything).Return(nil) @@ -319,7 +321,7 @@ func TestHandleAuth(t *testing.T) { _manager.On("ForgeByID", int64(1)).Return(_forge, nil) _forge.On("Login", mock.Anything, mock.Anything).Return(user, "", nil) - _store.On("GetUserRemoteID", user.ForgeRemoteID, user.Login).Return(user, nil) + _store.On("GetUserByRemoteID", user.ForgeID, user.ForgeRemoteID).Return(user, nil) _store.On("OrgFindByName", user.Login, user.ForgeID).Return(org, nil) _store.On("OrgUpdate", mock.Anything).Return(nil) _store.On("UpdateUser", mock.Anything).Return(nil) @@ -353,7 +355,7 @@ func TestHandleAuth(t *testing.T) { _manager.On("ForgeByID", int64(1)).Return(_forge, nil) _forge.On("Login", mock.Anything, mock.Anything).Return(user, "", nil) - _store.On("GetUserRemoteID", user.ForgeRemoteID, user.Login).Return(user, nil) + _store.On("GetUserByRemoteID", user.ForgeID, user.ForgeRemoteID).Return(user, nil) _store.On("OrgGet", user.OrgID).Return(org, nil) _store.On("OrgUpdate", mock.Anything).Return(nil) _store.On("UpdateUser", mock.Anything).Return(nil) diff --git a/server/api/users.go b/server/api/users.go index 1ccf488c5..414cae651 100644 --- a/server/api/users.go +++ b/server/api/users.go @@ -16,7 +16,10 @@ package api import ( "encoding/base32" + "errors" + "fmt" "net/http" + "strconv" "github.com/gin-gonic/gin" "github.com/google/tink/go/subtle/random" @@ -24,8 +27,11 @@ import ( "go.woodpecker-ci.org/woodpecker/v3/server/model" "go.woodpecker-ci.org/woodpecker/v3/server/router/middleware/session" "go.woodpecker-ci.org/woodpecker/v3/server/store" + "go.woodpecker-ci.org/woodpecker/v3/server/store/types" ) +const defaultForgeID = 1 + // GetUsers // // @Summary List users @@ -56,8 +62,23 @@ func GetUsers(c *gin.Context) { // @Tags Users // @Param Authorization header string true "Insert your personal access token" default(Bearer ) // @Param login path string true "the user's login name" +// @Param forge_id query string true "specify forge (else default will be used)" +// @Param forge_remote_id query string false "specify user id at forge (else fallback to login)" func GetUser(c *gin.Context) { - user, err := store.FromContext(c).GetUserLogin(c.Param("login")) + forgeID, err := strconv.ParseInt(c.DefaultQuery("forge_id", fmt.Sprint(defaultForgeID)), 10, 64) + if err != nil { + c.AbortWithStatus(http.StatusBadRequest) + return + } + forgeRemoteID := model.ForgeRemoteID(c.Query("forge_remote_id")) + + var user *model.User + + if forgeRemoteID.IsValid() { + user, err = store.FromContext(c).GetUserByRemoteID(forgeID, forgeRemoteID) + } else { + user, err = store.FromContext(c).GetUserByLogin(forgeID, c.Param("login")) + } if err != nil { handleDBError(c, err) return @@ -87,14 +108,26 @@ func PatchUser(c *gin.Context) { return } - user, err := _store.GetUserLogin(c.Param("login")) - if err != nil { + if in.ForgeID < defaultForgeID { + in.ForgeID = defaultForgeID + } + + user, err := store.FromContext(c).GetUserByRemoteID(in.ForgeID, in.ForgeRemoteID) + if err != nil && !errors.Is(err, types.RecordNotExist) { handleDBError(c, err) return } - // TODO: allow to change login (currently used as primary key) + if user == nil { + user, err = _store.GetUserByLogin(in.ForgeID, c.Param("login")) + if err != nil { + handleDBError(c, err) + return + } + } + // TODO: disallow to change login, email, avatar if the user is using oauth + user.Login = in.Login user.Email = in.Email user.Avatar = in.Avatar user.Admin = in.Admin @@ -132,7 +165,7 @@ func PostUser(c *gin.Context) { Hash: base32.StdEncoding.EncodeToString( random.GetRandomBytes(32), ), - ForgeID: 1, // TODO: replace with forge id when multiple forges are supported + ForgeID: in.ForgeID, ForgeRemoteID: model.ForgeRemoteID("0"), // TODO: search for the user in the forge and get the remote id } if err = user.Validate(); err != nil { @@ -156,10 +189,25 @@ func PostUser(c *gin.Context) { // @Tags Users // @Param Authorization header string true "Insert your personal access token" default(Bearer ) // @Param login path string true "the user's login name" +// @Param forge_id query string true "specify forge (else default will be used)" +// @Param forge_remote_id query string false "specify user id at forge (else fallback to login)" func DeleteUser(c *gin.Context) { _store := store.FromContext(c) - user, err := _store.GetUserLogin(c.Param("login")) + forgeID, err := strconv.ParseInt(c.DefaultQuery("forge_id", fmt.Sprint(defaultForgeID)), 10, 64) + if err != nil { + c.AbortWithStatus(http.StatusBadRequest) + return + } + forgeRemoteID := model.ForgeRemoteID(c.Query("forge_remote_id")) + + var user *model.User + + if forgeRemoteID.IsValid() { + user, err = store.FromContext(c).GetUserByRemoteID(forgeID, forgeRemoteID) + } else { + user, err = store.FromContext(c).GetUserByLogin(forgeID, c.Param("login")) + } if err != nil { handleDBError(c, err) return diff --git a/server/model/repo.go b/server/model/repo.go index 387d0170c..f2e009f60 100644 --- a/server/model/repo.go +++ b/server/model/repo.go @@ -45,9 +45,9 @@ func (mode ApprovalMode) Valid() bool { type Repo struct { ID int64 `json:"id,omitempty" xorm:"pk autoincr 'id'"` UserID int64 `json:"-" xorm:"INDEX 'user_id'"` - ForgeID int64 `json:"forge_id,omitempty" xorm:"forge_id"` + ForgeID int64 `json:"forge_id,omitempty" xorm:"UNIQUE(forge) forge_id"` // ForgeRemoteID is the unique identifier for the repository on the forge. - ForgeRemoteID ForgeRemoteID `json:"forge_remote_id" xorm:"forge_remote_id"` + ForgeRemoteID ForgeRemoteID `json:"forge_remote_id" xorm:"UNIQUE(forge) forge_remote_id"` OrgID int64 `json:"org_id" xorm:"INDEX 'org_id'"` Owner string `json:"owner" xorm:"UNIQUE(name) 'owner'"` Name string `json:"name" xorm:"UNIQUE(name) 'name'"` diff --git a/server/model/user.go b/server/model/user.go index 6129ceb12..7a7cbf366 100644 --- a/server/model/user.go +++ b/server/model/user.go @@ -34,9 +34,9 @@ type User struct { // required: true ID int64 `json:"id" xorm:"pk autoincr 'id'"` - ForgeID int64 `json:"forge_id,omitempty" xorm:"forge_id"` + ForgeID int64 `json:"forge_id,omitempty" xorm:"forge_id UNIQUE(forge)"` - ForgeRemoteID ForgeRemoteID `json:"-" xorm:"forge_remote_id"` + ForgeRemoteID ForgeRemoteID `json:"forge_remote_id" xorm:"forge_remote_id UNIQUE(forge)"` // Login is the username for this user. // diff --git a/server/store/datastore/org_test.go b/server/store/datastore/org_test.go index 2f5f3ad60..5f028b285 100644 --- a/server/store/datastore/org_test.go +++ b/server/store/datastore/org_test.go @@ -61,9 +61,9 @@ func TestOrgCRUD(t *testing.T) { someUser := &model.Org{Name: "some_other_u", IsUser: true} assert.NoError(t, store.OrgCreate(someUser)) assert.NoError(t, store.OrgCreate(&model.Org{Name: "some_other_org"})) - assert.NoError(t, store.CreateRepo(&model.Repo{UserID: 1, Owner: "some_other_u", Name: "abc", FullName: "some_other_u/abc", OrgID: someUser.ID})) - assert.NoError(t, store.CreateRepo(&model.Repo{UserID: 1, Owner: "some_other_u", Name: "xyz", FullName: "some_other_u/xyz", OrgID: someUser.ID})) - assert.NoError(t, store.CreateRepo(&model.Repo{UserID: 1, Owner: "renamedorg", Name: "567", FullName: "renamedorg/567", OrgID: orgOne.ID})) + assert.NoError(t, store.CreateRepo(&model.Repo{ForgeRemoteID: "a", UserID: 1, Owner: "some_other_u", Name: "abc", FullName: "some_other_u/abc", OrgID: someUser.ID})) + assert.NoError(t, store.CreateRepo(&model.Repo{ForgeRemoteID: "b", UserID: 1, Owner: "some_other_u", Name: "xyz", FullName: "some_other_u/xyz", OrgID: someUser.ID})) + assert.NoError(t, store.CreateRepo(&model.Repo{ForgeRemoteID: "c", UserID: 1, Owner: "renamedorg", Name: "567", FullName: "renamedorg/567", OrgID: orgOne.ID})) assert.Error(t, store.OrgCreate(&model.Org{Name: ""}), "expect to fail if name is empty") // get all repos for a specific org diff --git a/server/store/datastore/pipeline_test.go b/server/store/datastore/pipeline_test.go index fed95149b..44d42fcf6 100644 --- a/server/store/datastore/pipeline_test.go +++ b/server/store/datastore/pipeline_test.go @@ -206,8 +206,8 @@ func TestPipelineIncrement(t *testing.T) { store, closer := newTestStore(t, new(model.Pipeline), new(model.Repo)) defer closer() - assert.NoError(t, store.CreateRepo(&model.Repo{ID: 1, Owner: "1", Name: "1", FullName: "1/1"})) - assert.NoError(t, store.CreateRepo(&model.Repo{ID: 2, Owner: "2", Name: "2", FullName: "2/2"})) + assert.NoError(t, store.CreateRepo(&model.Repo{ID: 1, Owner: "1", Name: "1", FullName: "1/1", ForgeRemoteID: "1"})) + assert.NoError(t, store.CreateRepo(&model.Repo{ID: 2, Owner: "2", Name: "2", FullName: "2/2", ForgeRemoteID: "2"})) pipelineA := &model.Pipeline{RepoID: 1} if !assert.NoError(t, store.CreatePipeline(pipelineA)) { diff --git a/server/store/datastore/repo_test.go b/server/store/datastore/repo_test.go index 5f256ad62..ff6b55a52 100644 --- a/server/store/datastore/repo_test.go +++ b/server/store/datastore/repo_test.go @@ -254,22 +254,25 @@ func TestRepoCount(t *testing.T) { defer closer() repo1 := &model.Repo{ - Owner: "bradrydzewski", - Name: "test", - FullName: "bradrydzewski/test", - IsActive: true, + ForgeRemoteID: "A", + Owner: "bradrydzewski", + Name: "test", + FullName: "bradrydzewski/test", + IsActive: true, } repo2 := &model.Repo{ - Owner: "test", - Name: "test", - FullName: "test/test", - IsActive: true, + ForgeRemoteID: "B", + Owner: "test", + Name: "test", + FullName: "test/test", + IsActive: true, } repo3 := &model.Repo{ - Owner: "test", - Name: "test-ui", - FullName: "test/test-ui", - IsActive: false, + ForgeRemoteID: "C", + Owner: "test", + Name: "test-ui", + FullName: "test/test-ui", + IsActive: false, } assert.NoError(t, store.CreateRepo(repo1)) assert.NoError(t, store.CreateRepo(repo2)) @@ -297,10 +300,12 @@ func TestRepoCrud(t *testing.T) { defer closer() repo := model.Repo{ - UserID: 1, - FullName: "bradrydzewski/test", - Owner: "bradrydzewski", - Name: "test", + ForgeID: 1, + ForgeRemoteID: "bradrydzewskitest", + UserID: 1, + FullName: "bradrydzewski/test", + Owner: "bradrydzewski", + Name: "test", } assert.NoError(t, store.CreateRepo(&repo)) pipeline := model.Pipeline{ @@ -313,10 +318,12 @@ func TestRepoCrud(t *testing.T) { // create unrelated repoUnrelated := model.Repo{ - UserID: 2, - FullName: "x/x", - Owner: "x", - Name: "x", + ForgeRemoteID: "xx", + ForgeID: 1, + UserID: 2, + FullName: "x/x", + Owner: "x", + Name: "x", } assert.NoError(t, store.CreateRepo(&repoUnrelated)) pipelineUnrelated := model.Pipeline{ @@ -350,6 +357,7 @@ func TestRepoRedirection(t *testing.T) { repo := model.Repo{ UserID: 1, + ForgeID: 1, ForgeRemoteID: "1", FullName: "bradrydzewski/test", Owner: "bradrydzewski", @@ -378,10 +386,11 @@ func TestRepoRedirection(t *testing.T) { // test getting repo without forge ID (use name fallback) repo = model.Repo{ - UserID: 1, - FullName: "bradrydzewski/test-no-forge-id", - Owner: "bradrydzewski", - Name: "test-no-forge-id", + UserID: 1, + ForgeRemoteID: "bradrydzewski/test-no-forge-id", + FullName: "bradrydzewski/test-no-forge-id", + Owner: "bradrydzewski", + Name: "test-no-forge-id", } assert.NoError(t, store.CreateRepo(&repo)) diff --git a/server/store/datastore/user.go b/server/store/datastore/user.go index 522ab1177..9cb89c2a4 100644 --- a/server/store/datastore/user.go +++ b/server/store/datastore/user.go @@ -18,8 +18,6 @@ import ( "errors" "fmt" - "xorm.io/xorm" - "go.woodpecker-ci.org/woodpecker/v3/server/model" "go.woodpecker-ci.org/woodpecker/v3/server/store/types" ) @@ -29,23 +27,16 @@ func (s storage) GetUser(id int64) (*model.User, error) { return user, wrapGet(s.engine.ID(id).Get(user)) } -func (s storage) GetUserRemoteID(remoteID model.ForgeRemoteID, login string) (*model.User, error) { +func (s storage) GetUserByRemoteID(forgeID int64, userRemoteID model.ForgeRemoteID) (*model.User, error) { sess := s.engine.NewSession() user := new(model.User) - err := wrapGet(sess.Where("forge_remote_id = ?", remoteID).Get(user)) - if err != nil { - return s.getUserLogin(sess, login) - } - return user, err + return user, wrapGet(sess.Where("forge_id = ? AND forge_remote_id = ?", forgeID, userRemoteID).Get(user)) } -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) { +func (s storage) GetUserByLogin(forgeID int64, login string) (*model.User, error) { + sess := s.engine.NewSession() user := new(model.User) - return user, wrapGet(sess.Where("login=?", login).Get(user)) + return user, wrapGet(sess.Where("forge_id = ? AND login=?", forgeID, login).Get(user)) } func (s storage) GetUserList(p *model.ListOptions) ([]*model.User, error) { diff --git a/server/store/datastore/user_test.go b/server/store/datastore/user_test.go index 12c9aca29..19a528f27 100644 --- a/server/store/datastore/user_test.go +++ b/server/store/datastore/user_test.go @@ -32,11 +32,12 @@ func TestUsers(t *testing.T) { assert.Zero(t, count) user := model.User{ - Login: "joe", - AccessToken: "f0b461ca586c27872b43a0685cbc2847", - RefreshToken: "976f22a5eef7caacb7e678d6c52f49b1", - Email: "foo@bar.com", - Avatar: "b9015b0857e16ac4d94a0ffd9a0b79c8", + Login: "joe", + ForgeRemoteID: "joe", + AccessToken: "f0b461ca586c27872b43a0685cbc2847", + RefreshToken: "976f22a5eef7caacb7e678d6c52f49b1", + Email: "foo@bar.com", + Avatar: "b9015b0857e16ac4d94a0ffd9a0b79c8", } err = store.CreateUser(&user) assert.NoError(t, err) @@ -54,25 +55,27 @@ func TestUsers(t *testing.T) { assert.Equal(t, user.Email, getUser.Email) assert.Equal(t, user.Avatar, getUser.Avatar) - getUser, err = store.GetUserLogin(user.Login) + getUser, err = store.GetUserByLogin(user.ForgeID, user.Login) assert.NoError(t, err) assert.Equal(t, user.ID, getUser.ID) assert.Equal(t, user.Login, getUser.Login) // check unique login user2 := model.User{ - Login: "Joe", - Email: "foo2@bar.com", - AccessToken: "ab20g0ddaf012c744e136da16aa21ad9", + Login: "Joe", + ForgeRemoteID: "joe", + Email: "foo2@bar.com", + AccessToken: "ab20g0ddaf012c744e136da16aa21ad9", } err2 = store.CreateUser(&user2) assert.Error(t, err2) user2 = model.User{ - Login: "jane", - Email: "foo@bar.com", - AccessToken: "ab20g0ddaf012c744e136da16aa21ad9", - Hash: "A", + Login: "jane", + ForgeRemoteID: "jane", + Email: "foo@bar.com", + AccessToken: "ab20g0ddaf012c744e136da16aa21ad9", + Hash: "A", } assert.NoError(t, store.CreateUser(&user2)) users, err := store.GetUserList(&model.ListOptions{Page: 1, PerPage: 50}) @@ -112,9 +115,10 @@ func TestCreateUserWithExistingOrg(t *testing.T) { // Create a new user with the same name as the existing organization newUser := &model.User{ - Login: "existingOrg", - Hash: "A", - ForgeID: 1, + Login: "existingOrg", + ForgeRemoteID: "A", + Hash: "A", + ForgeID: 1, } err = store.CreateUser(newUser) assert.NoError(t, err) @@ -124,9 +128,10 @@ func TestCreateUserWithExistingOrg(t *testing.T) { assert.Equal(t, "existingOrg", updatedOrg.Name) newUser2 := &model.User{ - Login: "new-user", - ForgeID: 1, - Hash: "B", + Login: "new-user", + ForgeRemoteID: "B", + ForgeID: 1, + Hash: "B", } err = store.CreateUser(newUser2) assert.NoError(t, err) diff --git a/server/store/mocks/mock_Store.go b/server/store/mocks/mock_Store.go index 568e4ca83..6cff923dd 100644 --- a/server/store/mocks/mock_Store.go +++ b/server/store/mocks/mock_Store.go @@ -2712,6 +2712,142 @@ func (_c *MockStore_GetUser_Call) RunAndReturn(run func(n int64) (*model.User, e return _c } +// GetUserByLogin provides a mock function for the type MockStore +func (_mock *MockStore) GetUserByLogin(n int64, s string) (*model.User, error) { + ret := _mock.Called(n, s) + + if len(ret) == 0 { + panic("no return value specified for GetUserByLogin") + } + + var r0 *model.User + var r1 error + if returnFunc, ok := ret.Get(0).(func(int64, string) (*model.User, error)); ok { + return returnFunc(n, s) + } + if returnFunc, ok := ret.Get(0).(func(int64, string) *model.User); ok { + r0 = returnFunc(n, s) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*model.User) + } + } + if returnFunc, ok := ret.Get(1).(func(int64, string) error); ok { + r1 = returnFunc(n, s) + } else { + r1 = ret.Error(1) + } + return r0, r1 +} + +// MockStore_GetUserByLogin_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetUserByLogin' +type MockStore_GetUserByLogin_Call struct { + *mock.Call +} + +// GetUserByLogin is a helper method to define mock.On call +// - n int64 +// - s string +func (_e *MockStore_Expecter) GetUserByLogin(n interface{}, s interface{}) *MockStore_GetUserByLogin_Call { + return &MockStore_GetUserByLogin_Call{Call: _e.mock.On("GetUserByLogin", n, s)} +} + +func (_c *MockStore_GetUserByLogin_Call) Run(run func(n int64, s string)) *MockStore_GetUserByLogin_Call { + _c.Call.Run(func(args mock.Arguments) { + var arg0 int64 + if args[0] != nil { + arg0 = args[0].(int64) + } + var arg1 string + if args[1] != nil { + arg1 = args[1].(string) + } + run( + arg0, + arg1, + ) + }) + return _c +} + +func (_c *MockStore_GetUserByLogin_Call) Return(user *model.User, err error) *MockStore_GetUserByLogin_Call { + _c.Call.Return(user, err) + return _c +} + +func (_c *MockStore_GetUserByLogin_Call) RunAndReturn(run func(n int64, s string) (*model.User, error)) *MockStore_GetUserByLogin_Call { + _c.Call.Return(run) + return _c +} + +// GetUserByRemoteID provides a mock function for the type MockStore +func (_mock *MockStore) GetUserByRemoteID(n int64, forgeRemoteID model.ForgeRemoteID) (*model.User, error) { + ret := _mock.Called(n, forgeRemoteID) + + if len(ret) == 0 { + panic("no return value specified for GetUserByRemoteID") + } + + var r0 *model.User + var r1 error + if returnFunc, ok := ret.Get(0).(func(int64, model.ForgeRemoteID) (*model.User, error)); ok { + return returnFunc(n, forgeRemoteID) + } + if returnFunc, ok := ret.Get(0).(func(int64, model.ForgeRemoteID) *model.User); ok { + r0 = returnFunc(n, forgeRemoteID) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*model.User) + } + } + if returnFunc, ok := ret.Get(1).(func(int64, model.ForgeRemoteID) error); ok { + r1 = returnFunc(n, forgeRemoteID) + } else { + r1 = ret.Error(1) + } + return r0, r1 +} + +// MockStore_GetUserByRemoteID_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetUserByRemoteID' +type MockStore_GetUserByRemoteID_Call struct { + *mock.Call +} + +// GetUserByRemoteID is a helper method to define mock.On call +// - n int64 +// - forgeRemoteID model.ForgeRemoteID +func (_e *MockStore_Expecter) GetUserByRemoteID(n interface{}, forgeRemoteID interface{}) *MockStore_GetUserByRemoteID_Call { + return &MockStore_GetUserByRemoteID_Call{Call: _e.mock.On("GetUserByRemoteID", n, forgeRemoteID)} +} + +func (_c *MockStore_GetUserByRemoteID_Call) Run(run func(n int64, forgeRemoteID model.ForgeRemoteID)) *MockStore_GetUserByRemoteID_Call { + _c.Call.Run(func(args mock.Arguments) { + var arg0 int64 + if args[0] != nil { + arg0 = args[0].(int64) + } + var arg1 model.ForgeRemoteID + if args[1] != nil { + arg1 = args[1].(model.ForgeRemoteID) + } + run( + arg0, + arg1, + ) + }) + return _c +} + +func (_c *MockStore_GetUserByRemoteID_Call) Return(user *model.User, err error) *MockStore_GetUserByRemoteID_Call { + _c.Call.Return(user, err) + return _c +} + +func (_c *MockStore_GetUserByRemoteID_Call) RunAndReturn(run func(n int64, forgeRemoteID model.ForgeRemoteID) (*model.User, error)) *MockStore_GetUserByRemoteID_Call { + _c.Call.Return(run) + return _c +} + // GetUserCount provides a mock function for the type MockStore func (_mock *MockStore) GetUserCount() (int64, error) { ret := _mock.Called() @@ -2827,136 +2963,6 @@ func (_c *MockStore_GetUserList_Call) RunAndReturn(run func(p *model.ListOptions return _c } -// GetUserLogin provides a mock function for the type MockStore -func (_mock *MockStore) GetUserLogin(s string) (*model.User, error) { - ret := _mock.Called(s) - - if len(ret) == 0 { - panic("no return value specified for GetUserLogin") - } - - var r0 *model.User - var r1 error - if returnFunc, ok := ret.Get(0).(func(string) (*model.User, error)); ok { - return returnFunc(s) - } - if returnFunc, ok := ret.Get(0).(func(string) *model.User); ok { - r0 = returnFunc(s) - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).(*model.User) - } - } - if returnFunc, ok := ret.Get(1).(func(string) error); ok { - r1 = returnFunc(s) - } else { - r1 = ret.Error(1) - } - return r0, r1 -} - -// MockStore_GetUserLogin_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetUserLogin' -type MockStore_GetUserLogin_Call struct { - *mock.Call -} - -// GetUserLogin is a helper method to define mock.On call -// - s string -func (_e *MockStore_Expecter) GetUserLogin(s interface{}) *MockStore_GetUserLogin_Call { - return &MockStore_GetUserLogin_Call{Call: _e.mock.On("GetUserLogin", s)} -} - -func (_c *MockStore_GetUserLogin_Call) Run(run func(s string)) *MockStore_GetUserLogin_Call { - _c.Call.Run(func(args mock.Arguments) { - var arg0 string - if args[0] != nil { - arg0 = args[0].(string) - } - run( - arg0, - ) - }) - return _c -} - -func (_c *MockStore_GetUserLogin_Call) Return(user *model.User, err error) *MockStore_GetUserLogin_Call { - _c.Call.Return(user, err) - return _c -} - -func (_c *MockStore_GetUserLogin_Call) RunAndReturn(run func(s string) (*model.User, error)) *MockStore_GetUserLogin_Call { - _c.Call.Return(run) - return _c -} - -// GetUserRemoteID provides a mock function for the type MockStore -func (_mock *MockStore) GetUserRemoteID(forgeRemoteID model.ForgeRemoteID, s string) (*model.User, error) { - ret := _mock.Called(forgeRemoteID, s) - - if len(ret) == 0 { - panic("no return value specified for GetUserRemoteID") - } - - var r0 *model.User - var r1 error - if returnFunc, ok := ret.Get(0).(func(model.ForgeRemoteID, string) (*model.User, error)); ok { - return returnFunc(forgeRemoteID, s) - } - if returnFunc, ok := ret.Get(0).(func(model.ForgeRemoteID, string) *model.User); ok { - r0 = returnFunc(forgeRemoteID, s) - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).(*model.User) - } - } - if returnFunc, ok := ret.Get(1).(func(model.ForgeRemoteID, string) error); ok { - r1 = returnFunc(forgeRemoteID, s) - } else { - r1 = ret.Error(1) - } - return r0, r1 -} - -// MockStore_GetUserRemoteID_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetUserRemoteID' -type MockStore_GetUserRemoteID_Call struct { - *mock.Call -} - -// GetUserRemoteID is a helper method to define mock.On call -// - forgeRemoteID model.ForgeRemoteID -// - s string -func (_e *MockStore_Expecter) GetUserRemoteID(forgeRemoteID interface{}, s interface{}) *MockStore_GetUserRemoteID_Call { - return &MockStore_GetUserRemoteID_Call{Call: _e.mock.On("GetUserRemoteID", forgeRemoteID, s)} -} - -func (_c *MockStore_GetUserRemoteID_Call) Run(run func(forgeRemoteID model.ForgeRemoteID, s string)) *MockStore_GetUserRemoteID_Call { - _c.Call.Run(func(args mock.Arguments) { - var arg0 model.ForgeRemoteID - if args[0] != nil { - arg0 = args[0].(model.ForgeRemoteID) - } - var arg1 string - if args[1] != nil { - arg1 = args[1].(string) - } - run( - arg0, - arg1, - ) - }) - return _c -} - -func (_c *MockStore_GetUserRemoteID_Call) Return(user *model.User, err error) *MockStore_GetUserRemoteID_Call { - _c.Call.Return(user, err) - return _c -} - -func (_c *MockStore_GetUserRemoteID_Call) RunAndReturn(run func(forgeRemoteID model.ForgeRemoteID, s string) (*model.User, error)) *MockStore_GetUserRemoteID_Call { - _c.Call.Return(run) - return _c -} - // GlobalRegistryFind provides a mock function for the type MockStore func (_mock *MockStore) GlobalRegistryFind(s string) (*model.Registry, error) { ret := _mock.Called(s) diff --git a/server/store/store.go b/server/store/store.go index 8215296e6..a922b51d9 100644 --- a/server/store/store.go +++ b/server/store/store.go @@ -26,10 +26,10 @@ 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) + // GetUserByRemoteID gets a user by remote ID. + GetUserByRemoteID(int64, model.ForgeRemoteID) (*model.User, error) + // GetUserByLogin gets a user by its login name. + GetUserByLogin(int64, string) (*model.User, error) // GetUserList gets a list of all users in the system. GetUserList(p *model.ListOptions) ([]*model.User, error) // GetUserCount gets a count of all users in the system. diff --git a/web/src/lib/api/index.ts b/web/src/lib/api/index.ts index 588b054b7..55455b394 100644 --- a/web/src/lib/api/index.ts +++ b/web/src/lib/api/index.ts @@ -20,6 +20,8 @@ import type { User, } from './types'; +const DEFAULT_FORGE_ID = 1; + interface RepoListOptions { all?: boolean; } @@ -386,8 +388,9 @@ export default class WoodpeckerClient extends ApiClient { return this._get(`/api/users?${query}`) as Promise; } - async getUser(username: string): Promise { - return this._get(`/api/users/${username}`) as Promise; + async getUser(username: string, forgeID?: number): Promise { + const forge = forgeID ?? DEFAULT_FORGE_ID; + return this._get(`/api/users/${username}?forge_id=${forge}`) as Promise; } async createUser(user: Partial): Promise { @@ -399,7 +402,7 @@ export default class WoodpeckerClient extends ApiClient { } async deleteUser(user: User): Promise { - return this._delete(`/api/users/${user.login}`); + return this._delete(`/api/users/${user.login}?forge_id=${user.forge_id}`); } async resetToken(): Promise { diff --git a/web/src/lib/api/types/user.ts b/web/src/lib/api/types/user.ts index 0e17aca48..6d6589b88 100644 --- a/web/src/lib/api/types/user.ts +++ b/web/src/lib/api/types/user.ts @@ -3,6 +3,12 @@ export interface User { id: number; // The unique identifier for the account. + forge_id: number; + // The unique identifier of the forge the account belongs to. + + forge_remote_id: string; + // The unique identifier of user at the remote forge. + login: string; // The login name for the account. diff --git a/woodpecker-go/woodpecker/const.go b/woodpecker-go/woodpecker/const.go index eb7e7d46c..811bfa5e0 100644 --- a/woodpecker-go/woodpecker/const.go +++ b/woodpecker-go/woodpecker/const.go @@ -60,3 +60,5 @@ const ( StepTypeCommands StepType = "commands" StepTypeCache StepType = "cache" ) + +const defaultForgeID = 1 diff --git a/woodpecker-go/woodpecker/interface.go b/woodpecker-go/woodpecker/interface.go index abbfe647a..a5d4f5ecc 100644 --- a/woodpecker-go/woodpecker/interface.go +++ b/woodpecker-go/woodpecker/interface.go @@ -30,7 +30,8 @@ type Client interface { Self() (*User, error) // User returns a user by login. - User(string) (*User, error) + // It is recommended to specify forgeID (default is 1). + User(login string, forgeID ...int64) (*User, error) // UserList returns a list of all registered users. UserList(opt UserListOptions) ([]*User, error) @@ -42,7 +43,8 @@ type Client interface { UserPatch(*User) (*User, error) // UserDel deletes a user account. - UserDel(string) error + // It is recommended to specify forgeID (default is 1). + UserDel(login string, forgeID ...int64) error // Repo returns a repository by name. Repo(repoID int64) (*Repo, error) diff --git a/woodpecker-go/woodpecker/mocks/mock_Client.go b/woodpecker-go/woodpecker/mocks/mock_Client.go index e119679bf..60df56d73 100644 --- a/woodpecker-go/woodpecker/mocks/mock_Client.go +++ b/woodpecker-go/woodpecker/mocks/mock_Client.go @@ -4653,8 +4653,14 @@ func (_c *MockClient_StepLogsPurge_Call) RunAndReturn(run func(repoID int64, pip } // User provides a mock function for the type MockClient -func (_mock *MockClient) User(s string) (*woodpecker.User, error) { - ret := _mock.Called(s) +func (_mock *MockClient) User(login string, forgeID ...int64) (*woodpecker.User, error) { + var tmpRet mock.Arguments + if len(forgeID) > 0 { + tmpRet = _mock.Called(login, forgeID) + } else { + tmpRet = _mock.Called(login) + } + ret := tmpRet if len(ret) == 0 { panic("no return value specified for User") @@ -4662,18 +4668,18 @@ func (_mock *MockClient) User(s string) (*woodpecker.User, error) { var r0 *woodpecker.User var r1 error - if returnFunc, ok := ret.Get(0).(func(string) (*woodpecker.User, error)); ok { - return returnFunc(s) + if returnFunc, ok := ret.Get(0).(func(string, ...int64) (*woodpecker.User, error)); ok { + return returnFunc(login, forgeID...) } - if returnFunc, ok := ret.Get(0).(func(string) *woodpecker.User); ok { - r0 = returnFunc(s) + if returnFunc, ok := ret.Get(0).(func(string, ...int64) *woodpecker.User); ok { + r0 = returnFunc(login, forgeID...) } else { if ret.Get(0) != nil { r0 = ret.Get(0).(*woodpecker.User) } } - if returnFunc, ok := ret.Get(1).(func(string) error); ok { - r1 = returnFunc(s) + if returnFunc, ok := ret.Get(1).(func(string, ...int64) error); ok { + r1 = returnFunc(login, forgeID...) } else { r1 = ret.Error(1) } @@ -4686,19 +4692,28 @@ type MockClient_User_Call struct { } // User is a helper method to define mock.On call -// - s string -func (_e *MockClient_Expecter) User(s interface{}) *MockClient_User_Call { - return &MockClient_User_Call{Call: _e.mock.On("User", s)} +// - login string +// - forgeID ...int64 +func (_e *MockClient_Expecter) User(login interface{}, forgeID ...interface{}) *MockClient_User_Call { + return &MockClient_User_Call{Call: _e.mock.On("User", + append([]interface{}{login}, forgeID...)...)} } -func (_c *MockClient_User_Call) Run(run func(s string)) *MockClient_User_Call { +func (_c *MockClient_User_Call) Run(run func(login string, forgeID ...int64)) *MockClient_User_Call { _c.Call.Run(func(args mock.Arguments) { var arg0 string if args[0] != nil { arg0 = args[0].(string) } + var arg1 []int64 + var variadicArgs []int64 + if len(args) > 1 { + variadicArgs = args[1].([]int64) + } + arg1 = variadicArgs run( arg0, + arg1..., ) }) return _c @@ -4709,22 +4724,28 @@ func (_c *MockClient_User_Call) Return(user *woodpecker.User, err error) *MockCl return _c } -func (_c *MockClient_User_Call) RunAndReturn(run func(s string) (*woodpecker.User, error)) *MockClient_User_Call { +func (_c *MockClient_User_Call) RunAndReturn(run func(login string, forgeID ...int64) (*woodpecker.User, error)) *MockClient_User_Call { _c.Call.Return(run) return _c } // UserDel provides a mock function for the type MockClient -func (_mock *MockClient) UserDel(s string) error { - ret := _mock.Called(s) +func (_mock *MockClient) UserDel(login string, forgeID ...int64) error { + var tmpRet mock.Arguments + if len(forgeID) > 0 { + tmpRet = _mock.Called(login, forgeID) + } else { + tmpRet = _mock.Called(login) + } + ret := tmpRet if len(ret) == 0 { panic("no return value specified for UserDel") } var r0 error - if returnFunc, ok := ret.Get(0).(func(string) error); ok { - r0 = returnFunc(s) + if returnFunc, ok := ret.Get(0).(func(string, ...int64) error); ok { + r0 = returnFunc(login, forgeID...) } else { r0 = ret.Error(0) } @@ -4737,19 +4758,28 @@ type MockClient_UserDel_Call struct { } // UserDel is a helper method to define mock.On call -// - s string -func (_e *MockClient_Expecter) UserDel(s interface{}) *MockClient_UserDel_Call { - return &MockClient_UserDel_Call{Call: _e.mock.On("UserDel", s)} +// - login string +// - forgeID ...int64 +func (_e *MockClient_Expecter) UserDel(login interface{}, forgeID ...interface{}) *MockClient_UserDel_Call { + return &MockClient_UserDel_Call{Call: _e.mock.On("UserDel", + append([]interface{}{login}, forgeID...)...)} } -func (_c *MockClient_UserDel_Call) Run(run func(s string)) *MockClient_UserDel_Call { +func (_c *MockClient_UserDel_Call) Run(run func(login string, forgeID ...int64)) *MockClient_UserDel_Call { _c.Call.Run(func(args mock.Arguments) { var arg0 string if args[0] != nil { arg0 = args[0].(string) } + var arg1 []int64 + var variadicArgs []int64 + if len(args) > 1 { + variadicArgs = args[1].([]int64) + } + arg1 = variadicArgs run( arg0, + arg1..., ) }) return _c @@ -4760,7 +4790,7 @@ func (_c *MockClient_UserDel_Call) Return(err error) *MockClient_UserDel_Call { return _c } -func (_c *MockClient_UserDel_Call) RunAndReturn(run func(s string) error) *MockClient_UserDel_Call { +func (_c *MockClient_UserDel_Call) RunAndReturn(run func(login string, forgeID ...int64) error) *MockClient_UserDel_Call { _c.Call.Return(run) return _c } diff --git a/woodpecker-go/woodpecker/types.go b/woodpecker-go/woodpecker/types.go index 7b071ba6c..98fa6aa97 100644 --- a/woodpecker-go/woodpecker/types.go +++ b/woodpecker-go/woodpecker/types.go @@ -38,12 +38,14 @@ func (mode ApprovalMode) Valid() bool { type ( // User represents a user account. User struct { - ID int64 `json:"id"` - Login string `json:"login"` - Email string `json:"email"` - Avatar string `json:"avatar_url"` - Active bool `json:"active"` - Admin bool `json:"admin"` + ID int64 `json:"id"` + ForgeID int64 `json:"forge_id"` + ForgeRemoteID string `json:"forge_remote_id"` + Login string `json:"login"` + Email string `json:"email"` + Avatar string `json:"avatar_url"` + Active bool `json:"active"` + Admin bool `json:"admin"` } TrustedConfiguration struct { diff --git a/woodpecker-go/woodpecker/user.go b/woodpecker-go/woodpecker/user.go index 144280aef..dc3d4aed5 100644 --- a/woodpecker-go/woodpecker/user.go +++ b/woodpecker-go/woodpecker/user.go @@ -9,7 +9,7 @@ const ( pathSelf = "%s/api/user" pathRepos = "%s/api/user/repos" pathUsers = "%s/api/users" - pathUser = "%s/api/users/%s" + pathUser = "%s/api/users/%s?forge_id=%d" ) type RepoListOptions struct { @@ -38,10 +38,13 @@ func (c *client) Self() (*User, error) { } // User returns a user by login. -func (c *client) User(login string) (*User, error) { +// It is recommended to specify forgeID (default is 1). +func (c *client) User(login string, forgeID ...int64) (*User, error) { out := new(User) - uri := fmt.Sprintf(pathUser, c.addr, login) - err := c.get(uri, out) + if len(forgeID) == 0 { + forgeID = []int64{defaultForgeID} + } + err := c.get(fmt.Sprintf(pathUser, c.addr, login, forgeID[0]), out) return out, err } @@ -64,17 +67,22 @@ func (c *client) UserPost(in *User) (*User, error) { // UserPatch updates a user account. func (c *client) UserPatch(in *User) (*User, error) { + if in.ForgeID < defaultForgeID { + in.ForgeID = defaultForgeID + } out := new(User) - uri := fmt.Sprintf(pathUser, c.addr, in.Login) + uri := fmt.Sprintf(pathUser, c.addr, in.Login, in.ForgeID) err := c.patch(uri, in, out) return out, err } // UserDel deletes a user account. -func (c *client) UserDel(login string) error { - uri := fmt.Sprintf(pathUser, c.addr, login) - err := c.delete(uri) - return err +// It is recommended to specify forgeID (default is 1). +func (c *client) UserDel(login string, forgeID ...int64) error { + if len(forgeID) == 0 { + forgeID = []int64{defaultForgeID} + } + return c.delete(fmt.Sprintf(pathUser, c.addr, login, forgeID[0])) } // RepoList returns a list of all repositories to which