diff --git a/models/perm/access_mode.go b/models/perm/access_mode.go index 0364191e2e..6baeb5531a 100644 --- a/models/perm/access_mode.go +++ b/models/perm/access_mode.go @@ -60,3 +60,6 @@ func ParseAccessMode(permission string, allowed ...AccessMode) AccessMode { } return util.Iif(slices.Contains(allowed, m), m, AccessModeNone) } + +// ErrInvalidAccessMode is returned when an invalid access mode is used +var ErrInvalidAccessMode = util.NewInvalidArgumentErrorf("Invalid access mode") diff --git a/modules/repository/collaborator.go b/modules/repository/collaborator.go deleted file mode 100644 index f71c58fbdf..0000000000 --- a/modules/repository/collaborator.go +++ /dev/null @@ -1,48 +0,0 @@ -// Copyright 2022 The Gitea Authors. All rights reserved. -// SPDX-License-Identifier: MIT - -package repository - -import ( - "context" - - "code.gitea.io/gitea/models/db" - "code.gitea.io/gitea/models/perm" - access_model "code.gitea.io/gitea/models/perm/access" - repo_model "code.gitea.io/gitea/models/repo" - user_model "code.gitea.io/gitea/models/user" - - "xorm.io/builder" -) - -func AddCollaborator(ctx context.Context, repo *repo_model.Repository, u *user_model.User) error { - if err := repo.LoadOwner(ctx); err != nil { - return err - } - - if user_model.IsUserBlockedBy(ctx, u, repo.OwnerID) || user_model.IsUserBlockedBy(ctx, repo.Owner, u.ID) { - return user_model.ErrBlockedUser - } - - return db.WithTx(ctx, func(ctx context.Context) error { - has, err := db.Exist[repo_model.Collaboration](ctx, builder.Eq{ - "repo_id": repo.ID, - "user_id": u.ID, - }) - if err != nil { - return err - } else if has { - return nil - } - - if err = db.Insert(ctx, &repo_model.Collaboration{ - RepoID: repo.ID, - UserID: u.ID, - Mode: perm.AccessModeWrite, - }); err != nil { - return err - } - - return access_model.RecalculateUserAccess(ctx, repo, u.ID) - }) -} diff --git a/modules/repository/collaborator_test.go b/modules/repository/collaborator_test.go deleted file mode 100644 index 622f6abce4..0000000000 --- a/modules/repository/collaborator_test.go +++ /dev/null @@ -1,280 +0,0 @@ -// Copyright 2019 The Gitea Authors. All rights reserved. -// SPDX-License-Identifier: MIT - -package repository - -import ( - "testing" - - "code.gitea.io/gitea/models/db" - "code.gitea.io/gitea/models/organization" - perm_model "code.gitea.io/gitea/models/perm" - access_model "code.gitea.io/gitea/models/perm/access" - repo_model "code.gitea.io/gitea/models/repo" - "code.gitea.io/gitea/models/unit" - "code.gitea.io/gitea/models/unittest" - user_model "code.gitea.io/gitea/models/user" - - "github.com/stretchr/testify/assert" -) - -func TestRepository_AddCollaborator(t *testing.T) { - assert.NoError(t, unittest.PrepareTestDatabase()) - - testSuccess := func(repoID, userID int64) { - repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: repoID}) - assert.NoError(t, repo.LoadOwner(db.DefaultContext)) - user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: userID}) - assert.NoError(t, AddCollaborator(db.DefaultContext, repo, user)) - unittest.CheckConsistencyFor(t, &repo_model.Repository{ID: repoID}, &user_model.User{ID: userID}) - } - testSuccess(1, 4) - testSuccess(1, 4) - testSuccess(3, 4) -} - -func TestRepoPermissionPublicNonOrgRepo(t *testing.T) { - assert.NoError(t, unittest.PrepareTestDatabase()) - - // public non-organization repo - repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 4}) - assert.NoError(t, repo.LoadUnits(db.DefaultContext)) - - // plain user - user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}) - perm, err := access_model.GetUserRepoPermission(db.DefaultContext, repo, user) - assert.NoError(t, err) - for _, unit := range repo.Units { - assert.True(t, perm.CanRead(unit.Type)) - assert.False(t, perm.CanWrite(unit.Type)) - } - - // change to collaborator - assert.NoError(t, AddCollaborator(db.DefaultContext, repo, user)) - perm, err = access_model.GetUserRepoPermission(db.DefaultContext, repo, user) - assert.NoError(t, err) - for _, unit := range repo.Units { - assert.True(t, perm.CanRead(unit.Type)) - assert.True(t, perm.CanWrite(unit.Type)) - } - - // collaborator - collaborator := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 4}) - perm, err = access_model.GetUserRepoPermission(db.DefaultContext, repo, collaborator) - assert.NoError(t, err) - for _, unit := range repo.Units { - assert.True(t, perm.CanRead(unit.Type)) - assert.True(t, perm.CanWrite(unit.Type)) - } - - // owner - owner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 5}) - perm, err = access_model.GetUserRepoPermission(db.DefaultContext, repo, owner) - assert.NoError(t, err) - for _, unit := range repo.Units { - assert.True(t, perm.CanRead(unit.Type)) - assert.True(t, perm.CanWrite(unit.Type)) - } - - // admin - admin := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1}) - perm, err = access_model.GetUserRepoPermission(db.DefaultContext, repo, admin) - assert.NoError(t, err) - for _, unit := range repo.Units { - assert.True(t, perm.CanRead(unit.Type)) - assert.True(t, perm.CanWrite(unit.Type)) - } -} - -func TestRepoPermissionPrivateNonOrgRepo(t *testing.T) { - assert.NoError(t, unittest.PrepareTestDatabase()) - - // private non-organization repo - repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 2}) - assert.NoError(t, repo.LoadUnits(db.DefaultContext)) - - // plain user - user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 4}) - perm, err := access_model.GetUserRepoPermission(db.DefaultContext, repo, user) - assert.NoError(t, err) - for _, unit := range repo.Units { - assert.False(t, perm.CanRead(unit.Type)) - assert.False(t, perm.CanWrite(unit.Type)) - } - - // change to collaborator to default write access - assert.NoError(t, AddCollaborator(db.DefaultContext, repo, user)) - perm, err = access_model.GetUserRepoPermission(db.DefaultContext, repo, user) - assert.NoError(t, err) - for _, unit := range repo.Units { - assert.True(t, perm.CanRead(unit.Type)) - assert.True(t, perm.CanWrite(unit.Type)) - } - - assert.NoError(t, repo_model.ChangeCollaborationAccessMode(db.DefaultContext, repo, user.ID, perm_model.AccessModeRead)) - perm, err = access_model.GetUserRepoPermission(db.DefaultContext, repo, user) - assert.NoError(t, err) - for _, unit := range repo.Units { - assert.True(t, perm.CanRead(unit.Type)) - assert.False(t, perm.CanWrite(unit.Type)) - } - - // owner - owner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}) - perm, err = access_model.GetUserRepoPermission(db.DefaultContext, repo, owner) - assert.NoError(t, err) - for _, unit := range repo.Units { - assert.True(t, perm.CanRead(unit.Type)) - assert.True(t, perm.CanWrite(unit.Type)) - } - - // admin - admin := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1}) - perm, err = access_model.GetUserRepoPermission(db.DefaultContext, repo, admin) - assert.NoError(t, err) - for _, unit := range repo.Units { - assert.True(t, perm.CanRead(unit.Type)) - assert.True(t, perm.CanWrite(unit.Type)) - } -} - -func TestRepoPermissionPublicOrgRepo(t *testing.T) { - assert.NoError(t, unittest.PrepareTestDatabase()) - - // public organization repo - repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 32}) - assert.NoError(t, repo.LoadUnits(db.DefaultContext)) - - // plain user - user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 5}) - perm, err := access_model.GetUserRepoPermission(db.DefaultContext, repo, user) - assert.NoError(t, err) - for _, unit := range repo.Units { - assert.True(t, perm.CanRead(unit.Type)) - assert.False(t, perm.CanWrite(unit.Type)) - } - - // change to collaborator to default write access - assert.NoError(t, AddCollaborator(db.DefaultContext, repo, user)) - perm, err = access_model.GetUserRepoPermission(db.DefaultContext, repo, user) - assert.NoError(t, err) - for _, unit := range repo.Units { - assert.True(t, perm.CanRead(unit.Type)) - assert.True(t, perm.CanWrite(unit.Type)) - } - - assert.NoError(t, repo_model.ChangeCollaborationAccessMode(db.DefaultContext, repo, user.ID, perm_model.AccessModeRead)) - perm, err = access_model.GetUserRepoPermission(db.DefaultContext, repo, user) - assert.NoError(t, err) - for _, unit := range repo.Units { - assert.True(t, perm.CanRead(unit.Type)) - assert.False(t, perm.CanWrite(unit.Type)) - } - - // org member team owner - owner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}) - perm, err = access_model.GetUserRepoPermission(db.DefaultContext, repo, owner) - assert.NoError(t, err) - for _, unit := range repo.Units { - assert.True(t, perm.CanRead(unit.Type)) - assert.True(t, perm.CanWrite(unit.Type)) - } - - // org member team tester - member := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 15}) - perm, err = access_model.GetUserRepoPermission(db.DefaultContext, repo, member) - assert.NoError(t, err) - for _, unit := range repo.Units { - assert.True(t, perm.CanRead(unit.Type)) - } - assert.True(t, perm.CanWrite(unit.TypeIssues)) - assert.False(t, perm.CanWrite(unit.TypeCode)) - - // admin - admin := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1}) - perm, err = access_model.GetUserRepoPermission(db.DefaultContext, repo, admin) - assert.NoError(t, err) - for _, unit := range repo.Units { - assert.True(t, perm.CanRead(unit.Type)) - assert.True(t, perm.CanWrite(unit.Type)) - } -} - -func TestRepoPermissionPrivateOrgRepo(t *testing.T) { - assert.NoError(t, unittest.PrepareTestDatabase()) - - // private organization repo - repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 24}) - assert.NoError(t, repo.LoadUnits(db.DefaultContext)) - - // plain user - user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 5}) - perm, err := access_model.GetUserRepoPermission(db.DefaultContext, repo, user) - assert.NoError(t, err) - for _, unit := range repo.Units { - assert.False(t, perm.CanRead(unit.Type)) - assert.False(t, perm.CanWrite(unit.Type)) - } - - // change to collaborator to default write access - assert.NoError(t, AddCollaborator(db.DefaultContext, repo, user)) - perm, err = access_model.GetUserRepoPermission(db.DefaultContext, repo, user) - assert.NoError(t, err) - for _, unit := range repo.Units { - assert.True(t, perm.CanRead(unit.Type)) - assert.True(t, perm.CanWrite(unit.Type)) - } - - assert.NoError(t, repo_model.ChangeCollaborationAccessMode(db.DefaultContext, repo, user.ID, perm_model.AccessModeRead)) - perm, err = access_model.GetUserRepoPermission(db.DefaultContext, repo, user) - assert.NoError(t, err) - for _, unit := range repo.Units { - assert.True(t, perm.CanRead(unit.Type)) - assert.False(t, perm.CanWrite(unit.Type)) - } - - // org member team owner - owner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 15}) - perm, err = access_model.GetUserRepoPermission(db.DefaultContext, repo, owner) - assert.NoError(t, err) - for _, unit := range repo.Units { - assert.True(t, perm.CanRead(unit.Type)) - assert.True(t, perm.CanWrite(unit.Type)) - } - - // update team information and then check permission - team := unittest.AssertExistsAndLoadBean(t, &organization.Team{ID: 5}) - err = organization.UpdateTeamUnits(db.DefaultContext, team, nil) - assert.NoError(t, err) - perm, err = access_model.GetUserRepoPermission(db.DefaultContext, repo, owner) - assert.NoError(t, err) - for _, unit := range repo.Units { - assert.True(t, perm.CanRead(unit.Type)) - assert.True(t, perm.CanWrite(unit.Type)) - } - - // org member team tester - tester := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}) - perm, err = access_model.GetUserRepoPermission(db.DefaultContext, repo, tester) - assert.NoError(t, err) - assert.True(t, perm.CanWrite(unit.TypeIssues)) - assert.False(t, perm.CanWrite(unit.TypeCode)) - assert.False(t, perm.CanRead(unit.TypeCode)) - - // org member team reviewer - reviewer := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 20}) - perm, err = access_model.GetUserRepoPermission(db.DefaultContext, repo, reviewer) - assert.NoError(t, err) - assert.False(t, perm.CanRead(unit.TypeIssues)) - assert.False(t, perm.CanWrite(unit.TypeCode)) - assert.True(t, perm.CanRead(unit.TypeCode)) - - // admin - admin := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1}) - perm, err = access_model.GetUserRepoPermission(db.DefaultContext, repo, admin) - assert.NoError(t, err) - for _, unit := range repo.Units { - assert.True(t, perm.CanRead(unit.Type)) - assert.True(t, perm.CanWrite(unit.Type)) - } -} diff --git a/modules/repository/create.go b/modules/repository/create.go index 4f18b9b3fa..b4f7033bd7 100644 --- a/modules/repository/create.go +++ b/modules/repository/create.go @@ -11,160 +11,17 @@ import ( "path/filepath" "strings" - "code.gitea.io/gitea/models" activities_model "code.gitea.io/gitea/models/activities" "code.gitea.io/gitea/models/db" git_model "code.gitea.io/gitea/models/git" - "code.gitea.io/gitea/models/organization" - "code.gitea.io/gitea/models/perm" access_model "code.gitea.io/gitea/models/perm/access" repo_model "code.gitea.io/gitea/models/repo" - "code.gitea.io/gitea/models/unit" - user_model "code.gitea.io/gitea/models/user" - "code.gitea.io/gitea/models/webhook" issue_indexer "code.gitea.io/gitea/modules/indexer/issues" "code.gitea.io/gitea/modules/log" - "code.gitea.io/gitea/modules/setting" api "code.gitea.io/gitea/modules/structs" "code.gitea.io/gitea/modules/util" ) -// CreateRepositoryByExample creates a repository for the user/organization. -func CreateRepositoryByExample(ctx context.Context, doer, u *user_model.User, repo *repo_model.Repository, overwriteOrAdopt, isFork bool) (err error) { - if err = repo_model.IsUsableRepoName(repo.Name); err != nil { - return err - } - - has, err := repo_model.IsRepositoryModelExist(ctx, u, repo.Name) - if err != nil { - return fmt.Errorf("IsRepositoryExist: %w", err) - } else if has { - return repo_model.ErrRepoAlreadyExist{ - Uname: u.Name, - Name: repo.Name, - } - } - - repoPath := repo_model.RepoPath(u.Name, repo.Name) - isExist, err := util.IsExist(repoPath) - if err != nil { - log.Error("Unable to check if %s exists. Error: %v", repoPath, err) - return err - } - if !overwriteOrAdopt && isExist { - log.Error("Files already exist in %s and we are not going to adopt or delete.", repoPath) - return repo_model.ErrRepoFilesAlreadyExist{ - Uname: u.Name, - Name: repo.Name, - } - } - - if err = db.Insert(ctx, repo); err != nil { - return err - } - if err = repo_model.DeleteRedirect(ctx, u.ID, repo.Name); err != nil { - return err - } - - // insert units for repo - defaultUnits := unit.DefaultRepoUnits - if isFork { - defaultUnits = unit.DefaultForkRepoUnits - } - units := make([]repo_model.RepoUnit, 0, len(defaultUnits)) - for _, tp := range defaultUnits { - if tp == unit.TypeIssues { - units = append(units, repo_model.RepoUnit{ - RepoID: repo.ID, - Type: tp, - Config: &repo_model.IssuesConfig{ - EnableTimetracker: setting.Service.DefaultEnableTimetracking, - AllowOnlyContributorsToTrackTime: setting.Service.DefaultAllowOnlyContributorsToTrackTime, - EnableDependencies: setting.Service.DefaultEnableDependencies, - }, - }) - } else if tp == unit.TypePullRequests { - units = append(units, repo_model.RepoUnit{ - RepoID: repo.ID, - Type: tp, - Config: &repo_model.PullRequestsConfig{ - AllowMerge: true, AllowRebase: true, AllowRebaseMerge: true, AllowSquash: true, AllowFastForwardOnly: true, - DefaultMergeStyle: repo_model.MergeStyle(setting.Repository.PullRequest.DefaultMergeStyle), - AllowRebaseUpdate: true, - }, - }) - } else if tp == unit.TypeProjects { - units = append(units, repo_model.RepoUnit{ - RepoID: repo.ID, - Type: tp, - Config: &repo_model.ProjectsConfig{ProjectsMode: repo_model.ProjectsModeAll}, - }) - } else { - units = append(units, repo_model.RepoUnit{ - RepoID: repo.ID, - Type: tp, - }) - } - } - - if err = db.Insert(ctx, units); err != nil { - return err - } - - // Remember visibility preference. - u.LastRepoVisibility = repo.IsPrivate - if err = user_model.UpdateUserCols(ctx, u, "last_repo_visibility"); err != nil { - return fmt.Errorf("UpdateUserCols: %w", err) - } - - if err = user_model.IncrUserRepoNum(ctx, u.ID); err != nil { - return fmt.Errorf("IncrUserRepoNum: %w", err) - } - u.NumRepos++ - - // Give access to all members in teams with access to all repositories. - if u.IsOrganization() { - teams, err := organization.FindOrgTeams(ctx, u.ID) - if err != nil { - return fmt.Errorf("FindOrgTeams: %w", err) - } - for _, t := range teams { - if t.IncludesAllRepositories { - if err := models.AddRepository(ctx, t, repo); err != nil { - return fmt.Errorf("AddRepository: %w", err) - } - } - } - - if isAdmin, err := access_model.IsUserRepoAdmin(ctx, repo, doer); err != nil { - return fmt.Errorf("IsUserRepoAdmin: %w", err) - } else if !isAdmin { - // Make creator repo admin if it wasn't assigned automatically - if err = AddCollaborator(ctx, repo, doer); err != nil { - return fmt.Errorf("AddCollaborator: %w", err) - } - if err = repo_model.ChangeCollaborationAccessMode(ctx, repo, doer.ID, perm.AccessModeAdmin); err != nil { - return fmt.Errorf("ChangeCollaborationAccessModeCtx: %w", err) - } - } - } else if err = access_model.RecalculateAccesses(ctx, repo); err != nil { - // Organization automatically called this in AddRepository method. - return fmt.Errorf("RecalculateAccesses: %w", err) - } - - if setting.Service.AutoWatchNewRepos { - if err = repo_model.WatchRepo(ctx, doer, repo, true); err != nil { - return fmt.Errorf("WatchRepo: %w", err) - } - } - - if err = webhook.CopyDefaultWebhooksToRepo(ctx, repo.ID); err != nil { - return fmt.Errorf("CopyDefaultWebhooksToRepo: %w", err) - } - - return nil -} - const notRegularFileMode = os.ModeSymlink | os.ModeNamedPipe | os.ModeSocket | os.ModeDevice | os.ModeCharDevice | os.ModeIrregular // getDirectorySize returns the disk consumption for a given path diff --git a/modules/repository/main_test.go b/modules/repository/main_test.go index f81dfcdafb..799e8c17c3 100644 --- a/modules/repository/main_test.go +++ b/modules/repository/main_test.go @@ -8,6 +8,7 @@ import ( "code.gitea.io/gitea/models/unittest" + _ "code.gitea.io/gitea/models" _ "code.gitea.io/gitea/models/actions" ) diff --git a/routers/api/v1/api.go b/routers/api/v1/api.go index bfc601c835..23f466873b 100644 --- a/routers/api/v1/api.go +++ b/routers/api/v1/api.go @@ -1172,7 +1172,7 @@ func Routes() *web.Router { m.Get("", reqAnyRepoReader(), repo.ListCollaborators) m.Group("/{collaborator}", func() { m.Combo("").Get(reqAnyRepoReader(), repo.IsCollaborator). - Put(reqAdmin(), bind(api.AddCollaboratorOption{}), repo.AddCollaborator). + Put(reqAdmin(), bind(api.AddCollaboratorOption{}), repo.AddOrUpdateCollaborator). Delete(reqAdmin(), repo.DeleteCollaborator) m.Get("/permission", repo.GetRepoPermissions) }) diff --git a/routers/api/v1/repo/collaborators.go b/routers/api/v1/repo/collaborators.go index 39c9ba527d..ea9d8b0f37 100644 --- a/routers/api/v1/repo/collaborators.go +++ b/routers/api/v1/repo/collaborators.go @@ -12,7 +12,6 @@ import ( access_model "code.gitea.io/gitea/models/perm/access" repo_model "code.gitea.io/gitea/models/repo" user_model "code.gitea.io/gitea/models/user" - repo_module "code.gitea.io/gitea/modules/repository" api "code.gitea.io/gitea/modules/structs" "code.gitea.io/gitea/modules/web" "code.gitea.io/gitea/routers/api/v1/utils" @@ -123,11 +122,11 @@ func IsCollaborator(ctx *context.APIContext) { } } -// AddCollaborator add a collaborator to a repository -func AddCollaborator(ctx *context.APIContext) { +// AddOrUpdateCollaborator add or update a collaborator to a repository +func AddOrUpdateCollaborator(ctx *context.APIContext) { // swagger:operation PUT /repos/{owner}/{repo}/collaborators/{collaborator} repository repoAddCollaborator // --- - // summary: Add a collaborator to a repository + // summary: Add or Update a collaborator to a repository // produces: // - application/json // parameters: @@ -177,20 +176,18 @@ func AddCollaborator(ctx *context.APIContext) { return } - if err := repo_module.AddCollaborator(ctx, ctx.Repo.Repository, collaborator); err != nil { - if errors.Is(err, user_model.ErrBlockedUser) { - ctx.Error(http.StatusForbidden, "AddCollaborator", err) - } else { - ctx.Error(http.StatusInternalServerError, "AddCollaborator", err) - } - return + p := perm.AccessModeWrite + if form.Permission != nil { + p = perm.ParseAccessMode(*form.Permission) } - if form.Permission != nil { - if err := repo_model.ChangeCollaborationAccessMode(ctx, ctx.Repo.Repository, collaborator.ID, perm.ParseAccessMode(*form.Permission)); err != nil { - ctx.Error(http.StatusInternalServerError, "ChangeCollaborationAccessMode", err) - return + if err := repo_service.AddOrUpdateCollaborator(ctx, ctx.Repo.Repository, collaborator, p); err != nil { + if errors.Is(err, user_model.ErrBlockedUser) { + ctx.Error(http.StatusForbidden, "AddOrUpdateCollaborator", err) + } else { + ctx.Error(http.StatusInternalServerError, "AddOrUpdateCollaborator", err) } + return } ctx.Status(http.StatusNoContent) diff --git a/routers/web/repo/setting/collaboration.go b/routers/web/repo/setting/collaboration.go index 31f9f76d0f..18ecff8250 100644 --- a/routers/web/repo/setting/collaboration.go +++ b/routers/web/repo/setting/collaboration.go @@ -14,7 +14,6 @@ import ( unit_model "code.gitea.io/gitea/models/unit" user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/modules/log" - repo_module "code.gitea.io/gitea/modules/repository" "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/services/context" "code.gitea.io/gitea/services/mailer" @@ -100,12 +99,12 @@ func CollaborationPost(ctx *context.Context) { } } - if err = repo_module.AddCollaborator(ctx, ctx.Repo.Repository, u); err != nil { + if err = repo_service.AddOrUpdateCollaborator(ctx, ctx.Repo.Repository, u, perm.AccessModeWrite); err != nil { if errors.Is(err, user_model.ErrBlockedUser) { ctx.Flash.Error(ctx.Tr("repo.settings.add_collaborator.blocked_user")) ctx.Redirect(ctx.Repo.RepoLink + "/settings/collaboration") } else { - ctx.ServerError("AddCollaborator", err) + ctx.ServerError("AddOrUpdateCollaborator", err) } return } diff --git a/services/feed/action_test.go b/services/feed/action_test.go index e1b071d8f6..60cf7fbb49 100644 --- a/services/feed/action_test.go +++ b/services/feed/action_test.go @@ -13,6 +13,7 @@ import ( "code.gitea.io/gitea/models/unittest" user_model "code.gitea.io/gitea/models/user" + _ "code.gitea.io/gitea/models" _ "code.gitea.io/gitea/models/actions" "github.com/stretchr/testify/assert" diff --git a/services/issue/main_test.go b/services/issue/main_test.go index 5dac54183b..819c5d98c3 100644 --- a/services/issue/main_test.go +++ b/services/issue/main_test.go @@ -8,6 +8,7 @@ import ( "code.gitea.io/gitea/models/unittest" + _ "code.gitea.io/gitea/models" _ "code.gitea.io/gitea/models/actions" ) diff --git a/services/mailer/main_test.go b/services/mailer/main_test.go index f803c736ca..5591bea02b 100644 --- a/services/mailer/main_test.go +++ b/services/mailer/main_test.go @@ -8,6 +8,7 @@ import ( "code.gitea.io/gitea/models/unittest" + _ "code.gitea.io/gitea/models" _ "code.gitea.io/gitea/models/actions" ) diff --git a/services/repository/adopt.go b/services/repository/adopt.go index 3d6fe71a09..615f4d482c 100644 --- a/services/repository/adopt.go +++ b/services/repository/adopt.go @@ -66,7 +66,7 @@ func AdoptRepository(ctx context.Context, doer, u *user_model.User, opts CreateR } } - if err := repo_module.CreateRepositoryByExample(ctx, doer, u, repo, true, false); err != nil { + if err := CreateRepositoryByExample(ctx, doer, u, repo, true, false); err != nil { return err } diff --git a/services/repository/collaboration.go b/services/repository/collaboration.go index 4a43ae2a28..abe0489fc5 100644 --- a/services/repository/collaboration.go +++ b/services/repository/collaboration.go @@ -9,11 +9,60 @@ import ( "code.gitea.io/gitea/models" "code.gitea.io/gitea/models/db" + "code.gitea.io/gitea/models/perm" access_model "code.gitea.io/gitea/models/perm/access" repo_model "code.gitea.io/gitea/models/repo" user_model "code.gitea.io/gitea/models/user" + + "xorm.io/builder" ) +func AddOrUpdateCollaborator(ctx context.Context, repo *repo_model.Repository, u *user_model.User, mode perm.AccessMode) error { + // only allow valid access modes, read, write and admin + if mode < perm.AccessModeRead || mode > perm.AccessModeAdmin { + return perm.ErrInvalidAccessMode + } + + if err := repo.LoadOwner(ctx); err != nil { + return err + } + + if user_model.IsUserBlockedBy(ctx, u, repo.OwnerID) || user_model.IsUserBlockedBy(ctx, repo.Owner, u.ID) { + return user_model.ErrBlockedUser + } + + return db.WithTx(ctx, func(ctx context.Context) error { + collaboration, has, err := db.Get[repo_model.Collaboration](ctx, builder.Eq{ + "repo_id": repo.ID, + "user_id": u.ID, + }) + if err != nil { + return err + } else if has { + if collaboration.Mode == mode { + return nil + } + if _, err = db.GetEngine(ctx). + Where("repo_id=?", repo.ID). + And("user_id=?", u.ID). + Cols("mode"). + Update(&repo_model.Collaboration{ + Mode: mode, + }); err != nil { + return err + } + } else if err = db.Insert(ctx, &repo_model.Collaboration{ + RepoID: repo.ID, + UserID: u.ID, + Mode: mode, + }); err != nil { + return err + } + + return access_model.RecalculateUserAccess(ctx, repo, u.ID) + }) +} + // DeleteCollaboration removes collaboration relation between the user and repository. func DeleteCollaboration(ctx context.Context, repo *repo_model.Repository, collaborator *user_model.User) (err error) { collaboration := &repo_model.Collaboration{ diff --git a/services/repository/collaboration_test.go b/services/repository/collaboration_test.go index a2eb06b81a..2b9a5d0b8b 100644 --- a/services/repository/collaboration_test.go +++ b/services/repository/collaboration_test.go @@ -7,6 +7,7 @@ import ( "testing" "code.gitea.io/gitea/models/db" + "code.gitea.io/gitea/models/perm" repo_model "code.gitea.io/gitea/models/repo" "code.gitea.io/gitea/models/unittest" user_model "code.gitea.io/gitea/models/user" @@ -14,6 +15,21 @@ import ( "github.com/stretchr/testify/assert" ) +func TestRepository_AddCollaborator(t *testing.T) { + assert.NoError(t, unittest.PrepareTestDatabase()) + + testSuccess := func(repoID, userID int64) { + repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: repoID}) + assert.NoError(t, repo.LoadOwner(db.DefaultContext)) + user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: userID}) + assert.NoError(t, AddOrUpdateCollaborator(db.DefaultContext, repo, user, perm.AccessModeWrite)) + unittest.CheckConsistencyFor(t, &repo_model.Repository{ID: repoID}, &user_model.User{ID: userID}) + } + testSuccess(1, 4) + testSuccess(1, 4) + testSuccess(3, 4) +} + func TestRepository_DeleteCollaboration(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) diff --git a/services/repository/create.go b/services/repository/create.go index 282b2d3e58..261ac7fccc 100644 --- a/services/repository/create.go +++ b/services/repository/create.go @@ -12,9 +12,15 @@ import ( "strings" "time" + "code.gitea.io/gitea/models" "code.gitea.io/gitea/models/db" + "code.gitea.io/gitea/models/organization" + "code.gitea.io/gitea/models/perm" + access_model "code.gitea.io/gitea/models/perm/access" repo_model "code.gitea.io/gitea/models/repo" + "code.gitea.io/gitea/models/unit" user_model "code.gitea.io/gitea/models/user" + "code.gitea.io/gitea/models/webhook" "code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/gitrepo" "code.gitea.io/gitea/modules/log" @@ -243,7 +249,7 @@ func CreateRepositoryDirectly(ctx context.Context, doer, u *user_model.User, opt var rollbackRepo *repo_model.Repository if err := db.WithTx(ctx, func(ctx context.Context) error { - if err := repo_module.CreateRepositoryByExample(ctx, doer, u, repo, false, false); err != nil { + if err := CreateRepositoryByExample(ctx, doer, u, repo, false, false); err != nil { return err } @@ -335,3 +341,136 @@ func CreateRepositoryDirectly(ctx context.Context, doer, u *user_model.User, opt return repo, nil } + +// CreateRepositoryByExample creates a repository for the user/organization. +func CreateRepositoryByExample(ctx context.Context, doer, u *user_model.User, repo *repo_model.Repository, overwriteOrAdopt, isFork bool) (err error) { + if err = repo_model.IsUsableRepoName(repo.Name); err != nil { + return err + } + + has, err := repo_model.IsRepositoryModelExist(ctx, u, repo.Name) + if err != nil { + return fmt.Errorf("IsRepositoryExist: %w", err) + } else if has { + return repo_model.ErrRepoAlreadyExist{ + Uname: u.Name, + Name: repo.Name, + } + } + + repoPath := repo_model.RepoPath(u.Name, repo.Name) + isExist, err := util.IsExist(repoPath) + if err != nil { + log.Error("Unable to check if %s exists. Error: %v", repoPath, err) + return err + } + if !overwriteOrAdopt && isExist { + log.Error("Files already exist in %s and we are not going to adopt or delete.", repoPath) + return repo_model.ErrRepoFilesAlreadyExist{ + Uname: u.Name, + Name: repo.Name, + } + } + + if err = db.Insert(ctx, repo); err != nil { + return err + } + if err = repo_model.DeleteRedirect(ctx, u.ID, repo.Name); err != nil { + return err + } + + // insert units for repo + defaultUnits := unit.DefaultRepoUnits + if isFork { + defaultUnits = unit.DefaultForkRepoUnits + } + units := make([]repo_model.RepoUnit, 0, len(defaultUnits)) + for _, tp := range defaultUnits { + if tp == unit.TypeIssues { + units = append(units, repo_model.RepoUnit{ + RepoID: repo.ID, + Type: tp, + Config: &repo_model.IssuesConfig{ + EnableTimetracker: setting.Service.DefaultEnableTimetracking, + AllowOnlyContributorsToTrackTime: setting.Service.DefaultAllowOnlyContributorsToTrackTime, + EnableDependencies: setting.Service.DefaultEnableDependencies, + }, + }) + } else if tp == unit.TypePullRequests { + units = append(units, repo_model.RepoUnit{ + RepoID: repo.ID, + Type: tp, + Config: &repo_model.PullRequestsConfig{ + AllowMerge: true, AllowRebase: true, AllowRebaseMerge: true, AllowSquash: true, AllowFastForwardOnly: true, + DefaultMergeStyle: repo_model.MergeStyle(setting.Repository.PullRequest.DefaultMergeStyle), + AllowRebaseUpdate: true, + }, + }) + } else if tp == unit.TypeProjects { + units = append(units, repo_model.RepoUnit{ + RepoID: repo.ID, + Type: tp, + Config: &repo_model.ProjectsConfig{ProjectsMode: repo_model.ProjectsModeAll}, + }) + } else { + units = append(units, repo_model.RepoUnit{ + RepoID: repo.ID, + Type: tp, + }) + } + } + + if err = db.Insert(ctx, units); err != nil { + return err + } + + // Remember visibility preference. + u.LastRepoVisibility = repo.IsPrivate + if err = user_model.UpdateUserCols(ctx, u, "last_repo_visibility"); err != nil { + return fmt.Errorf("UpdateUserCols: %w", err) + } + + if err = user_model.IncrUserRepoNum(ctx, u.ID); err != nil { + return fmt.Errorf("IncrUserRepoNum: %w", err) + } + u.NumRepos++ + + // Give access to all members in teams with access to all repositories. + if u.IsOrganization() { + teams, err := organization.FindOrgTeams(ctx, u.ID) + if err != nil { + return fmt.Errorf("FindOrgTeams: %w", err) + } + for _, t := range teams { + if t.IncludesAllRepositories { + if err := models.AddRepository(ctx, t, repo); err != nil { + return fmt.Errorf("AddRepository: %w", err) + } + } + } + + if isAdmin, err := access_model.IsUserRepoAdmin(ctx, repo, doer); err != nil { + return fmt.Errorf("IsUserRepoAdmin: %w", err) + } else if !isAdmin { + // Make creator repo admin if it wasn't assigned automatically + if err = AddOrUpdateCollaborator(ctx, repo, doer, perm.AccessModeAdmin); err != nil { + return fmt.Errorf("AddCollaborator: %w", err) + } + } + } else if err = access_model.RecalculateAccesses(ctx, repo); err != nil { + // Organization automatically called this in AddRepository method. + return fmt.Errorf("RecalculateAccesses: %w", err) + } + + if setting.Service.AutoWatchNewRepos { + if err = repo_model.WatchRepo(ctx, doer, repo, true); err != nil { + return fmt.Errorf("WatchRepo: %w", err) + } + } + + if err = webhook.CopyDefaultWebhooksToRepo(ctx, repo.ID); err != nil { + return fmt.Errorf("CopyDefaultWebhooksToRepo: %w", err) + } + + return nil +} diff --git a/services/repository/fork.go b/services/repository/fork.go index e114555679..5b24015a03 100644 --- a/services/repository/fork.go +++ b/services/repository/fork.go @@ -134,7 +134,7 @@ func ForkRepository(ctx context.Context, doer, owner *user_model.User, opts Fork }() err = db.WithTx(ctx, func(txCtx context.Context) error { - if err = repo_module.CreateRepositoryByExample(txCtx, doer, owner, repo, false, true); err != nil { + if err = CreateRepositoryByExample(txCtx, doer, owner, repo, false, true); err != nil { return err } diff --git a/services/repository/generate.go b/services/repository/generate.go index 2b95bbcd4d..f2280de8b2 100644 --- a/services/repository/generate.go +++ b/services/repository/generate.go @@ -343,7 +343,7 @@ func generateRepository(ctx context.Context, doer, owner *user_model.User, templ ObjectFormatName: templateRepo.ObjectFormatName, } - if err = repo_module.CreateRepositoryByExample(ctx, doer, owner, generateRepo, false, false); err != nil { + if err = CreateRepositoryByExample(ctx, doer, owner, generateRepo, false, false); err != nil { return nil, err } diff --git a/services/repository/transfer.go b/services/repository/transfer.go index 7ad6b46fa4..301d895337 100644 --- a/services/repository/transfer.go +++ b/services/repository/transfer.go @@ -20,7 +20,6 @@ import ( user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/modules/globallock" "code.gitea.io/gitea/modules/log" - repo_module "code.gitea.io/gitea/modules/repository" "code.gitea.io/gitea/modules/util" notify_service "code.gitea.io/gitea/services/notify" ) @@ -419,10 +418,7 @@ func StartRepositoryTransfer(ctx context.Context, doer, newOwner *user_model.Use return err } if !hasAccess { - if err := repo_module.AddCollaborator(ctx, repo, newOwner); err != nil { - return err - } - if err := repo_model.ChangeCollaborationAccessMode(ctx, repo, newOwner.ID, perm.AccessModeRead); err != nil { + if err := AddOrUpdateCollaborator(ctx, repo, newOwner, perm.AccessModeRead); err != nil { return err } } diff --git a/templates/swagger/v1_json.tmpl b/templates/swagger/v1_json.tmpl index 01b12460ac..b9fb1910a3 100644 --- a/templates/swagger/v1_json.tmpl +++ b/templates/swagger/v1_json.tmpl @@ -5095,7 +5095,7 @@ "tags": [ "repository" ], - "summary": "Add a collaborator to a repository", + "summary": "Add or Update a collaborator to a repository", "operationId": "repoAddCollaborator", "parameters": [ {