From 98c8760cd1ff83b8f48086fb2260b0459fe27d2b Mon Sep 17 00:00:00 2001 From: KN4CK3R Date: Mon, 26 Apr 2021 17:13:00 +0200 Subject: [PATCH 01/35] Added tag protection in hook. --- cmd/hook.go | 6 +- models/migrations/migrations.go | 2 + models/migrations/v180.go | 38 ++++ models/models.go | 1 + models/tags.go | 95 ++++++++ routers/private/hook.go | 370 ++++++++++++++++++-------------- 6 files changed, 347 insertions(+), 165 deletions(-) create mode 100644 models/migrations/v180.go create mode 100644 models/tags.go diff --git a/cmd/hook.go b/cmd/hook.go index def3b636eb674..45b7d7ceb4e13 100644 --- a/cmd/hook.go +++ b/cmd/hook.go @@ -222,7 +222,7 @@ Gitea or set your environment appropriately.`, "") lastline++ // If the ref is a branch, check if it's protected - if strings.HasPrefix(refFullName, git.BranchPrefix) { + if strings.HasPrefix(refFullName, git.BranchPrefix) || strings.HasPrefix(refFullName, git.TagPrefix) { oldCommitIDs[count] = oldCommitID newCommitIDs[count] = newCommitID refFullNames[count] = refFullName @@ -230,7 +230,7 @@ Gitea or set your environment appropriately.`, "") fmt.Fprintf(out, "*") if count >= hookBatchSize { - fmt.Fprintf(out, " Checking %d branches\n", count) + fmt.Fprintf(out, " Checking %d references\n", count) hookOptions.OldCommitIDs = oldCommitIDs hookOptions.NewCommitIDs = newCommitIDs @@ -261,7 +261,7 @@ Gitea or set your environment appropriately.`, "") hookOptions.NewCommitIDs = newCommitIDs[:count] hookOptions.RefFullNames = refFullNames[:count] - fmt.Fprintf(out, " Checking %d branches\n", count) + fmt.Fprintf(out, " Checking %d references\n", count) statusCode, msg := private.HookPreReceive(username, reponame, hookOptions) switch statusCode { diff --git a/models/migrations/migrations.go b/models/migrations/migrations.go index c54c383fb810d..185e4aeccb211 100644 --- a/models/migrations/migrations.go +++ b/models/migrations/migrations.go @@ -309,6 +309,8 @@ var migrations = []Migration{ NewMigration("Add LFS columns to Mirror", addLFSMirrorColumns), // v179 -> v180 NewMigration("Convert avatar url to text", convertAvatarURLToText), + // v180 -> v181 + NewMigration("Create protected tag table", createProtectedTagTable), } // GetCurrentDBVersion returns the current db version diff --git a/models/migrations/v180.go b/models/migrations/v180.go new file mode 100644 index 0000000000000..616e28214c765 --- /dev/null +++ b/models/migrations/v180.go @@ -0,0 +1,38 @@ +// Copyright 2021 The Gitea Authors. All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. + +package migrations + +import ( + "fmt" + + "code.gitea.io/gitea/modules/timeutil" + + "xorm.io/xorm" +) + +func createProtectedTagTable(x *xorm.Engine) error { + type ProtectedTag struct { + ID int64 `xorm:"pk autoincr"` + RepoID int64 `xorm:"UNIQUE(s)"` + NamePattern string `xorm:"UNIQUE(s)"` + WhitelistUserIDs []int64 `xorm:"JSON TEXT"` + WhitelistTeamIDs []int64 `xorm:"JSON TEXT"` + + CreatedUnix timeutil.TimeStamp `xorm:"created"` + UpdatedUnix timeutil.TimeStamp `xorm:"updated"` + } + + sess := x.NewSession() + defer sess.Close() + if err := sess.Begin(); err != nil { + return err + } + + if err := sess.Sync2(new(ProtectedTag)); err != nil { + return fmt.Errorf("Sync2: %v", err) + } + + return sess.Commit() +} diff --git a/models/models.go b/models/models.go index 73e65d828bdf1..b12302248e4d7 100644 --- a/models/models.go +++ b/models/models.go @@ -134,6 +134,7 @@ func init() { new(ProjectIssue), new(Session), new(RepoTransfer), + new(ProtectedTag), ) gonicNames := []string{"SSL", "UID"} diff --git a/models/tags.go b/models/tags.go new file mode 100644 index 0000000000000..d2c98af08e0ff --- /dev/null +++ b/models/tags.go @@ -0,0 +1,95 @@ +// Copyright 2021 The Gitea Authors. All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. + +package models + +import ( + "fmt" + "strings" + + "code.gitea.io/gitea/modules/base" + "code.gitea.io/gitea/modules/log" + "code.gitea.io/gitea/modules/timeutil" + + "github.com/gobwas/glob" +) + +// ProtectedTag struct +type ProtectedTag struct { + ID int64 `xorm:"pk autoincr"` + RepoID int64 `xorm:"UNIQUE(s)"` + NamePattern string `xorm:"UNIQUE(s)"` + NameGlob glob.Glob `xorm:"-"` + WhitelistUserIDs []int64 `xorm:"JSON TEXT"` + WhitelistTeamIDs []int64 `xorm:"JSON TEXT"` + + CreatedUnix timeutil.TimeStamp `xorm:"created"` + UpdatedUnix timeutil.TimeStamp `xorm:"updated"` +} + +// BeforeInsert will be invoked by XORM before inserting a record +func (pt *ProtectedTag) BeforeInsert() { + pt.CreatedUnix = timeutil.TimeStampNow() + pt.UpdatedUnix = timeutil.TimeStampNow() +} + +// BeforeUpdate is invoked from XORM before updating this object. +func (pt *ProtectedTag) BeforeUpdate() { + pt.UpdatedUnix = timeutil.TimeStampNow() +} + +// InsertProtectedTag inserts a protected tag to database +func InsertProtectedTag(pt *ProtectedTag) error { + _, err := x.Insert(pt) + return err +} + +// UpdateProtectedTag updates the protected tag +func UpdateProtectedTag(pt *ProtectedTag) error { + _, err := x.ID(pt.ID).AllCols().Update(pt) + return err +} + +// DeleteProtectedTag deletes a protected tag by ID +func DeleteProtectedTag(pt *ProtectedTag) error { + _, err := x.Delete(&ProtectedTag{ID: pt.ID}) + return err +} + +// EnsureCompiledPattern returns if the branch is protected +func (pt *ProtectedTag) EnsureCompiledPattern() error { + if pt.NameGlob != nil { + return nil + } + + expr := strings.TrimSpace(pt.NamePattern) + + var err error + pt.NameGlob, err = glob.Compile(expr) + return err +} + +// IsUserAllowed returns true if the user is allowed to modify the tag +func (pt *ProtectedTag) IsUserAllowed(userID int64) bool { + if base.Int64sContains(pt.WhitelistUserIDs, userID) { + return true + } + + if len(pt.WhitelistTeamIDs) == 0 { + return false + } + + in, err := IsUserInTeams(userID, pt.WhitelistTeamIDs) + if err != nil { + log.Error("IsUserInTeams: %v", err) + return false + } + return in +} + +// GetProtectedTags gets all protected tags +func (repo *Repository) GetProtectedTags() ([]*ProtectedTag, error) { + tags := make([]*ProtectedTag, 0) + return tags, x.Find(&tags, &ProtectedTag{RepoID: repo.ID}) +} diff --git a/routers/private/hook.go b/routers/private/hook.go index 83c3f21b8f422..5597b80347377 100644 --- a/routers/private/hook.go +++ b/routers/private/hook.go @@ -155,215 +155,261 @@ func HookPreReceive(ctx *gitea_context.PrivateContext) { private.GitQuarantinePath+"="+opts.GitQuarantinePath) } + protectedTags, err := repo.GetProtectedTags() + if err != nil { + log.Error("Unable to get protected tags for %-v Error: %v", repo, err) + ctx.JSON(http.StatusInternalServerError, private.HookPostReceiveResult{ + Err: err.Error(), + }) + return + } + // Iterate across the provided old commit IDs for i := range opts.OldCommitIDs { oldCommitID := opts.OldCommitIDs[i] newCommitID := opts.NewCommitIDs[i] refFullName := opts.RefFullNames[i] - branchName := strings.TrimPrefix(refFullName, git.BranchPrefix) - if branchName == repo.DefaultBranch && newCommitID == git.EmptySHA { - log.Warn("Forbidden: Branch: %s is the default branch in %-v and cannot be deleted", branchName, repo) - ctx.JSON(http.StatusForbidden, map[string]interface{}{ - "err": fmt.Sprintf("branch %s is the default branch and cannot be deleted", branchName), - }) - return - } - - protectBranch, err := models.GetProtectedBranchBy(repo.ID, branchName) - if err != nil { - log.Error("Unable to get protected branch: %s in %-v Error: %v", branchName, repo, err) - ctx.JSON(http.StatusInternalServerError, map[string]interface{}{ - "err": err.Error(), - }) - return - } - - // Allow pushes to non-protected branches - if protectBranch == nil || !protectBranch.IsProtected() { - continue - } - - // This ref is a protected branch. - // - // First of all we need to enforce absolutely: - // - // 1. Detect and prevent deletion of the branch - if newCommitID == git.EmptySHA { - log.Warn("Forbidden: Branch: %s in %-v is protected from deletion", branchName, repo) - ctx.JSON(http.StatusForbidden, map[string]interface{}{ - "err": fmt.Sprintf("branch %s is protected from deletion", branchName), - }) - return - } + if strings.HasPrefix(refFullName, git.BranchPrefix) { + branchName := strings.TrimPrefix(refFullName, git.BranchPrefix) + if branchName == repo.DefaultBranch && newCommitID == git.EmptySHA { + log.Warn("Forbidden: Branch: %s is the default branch in %-v and cannot be deleted", branchName, repo) + ctx.JSON(http.StatusForbidden, map[string]interface{}{ + "err": fmt.Sprintf("branch %s is the default branch and cannot be deleted", branchName), + }) + return + } - // 2. Disallow force pushes to protected branches - if git.EmptySHA != oldCommitID { - output, err := git.NewCommand("rev-list", "--max-count=1", oldCommitID, "^"+newCommitID).RunInDirWithEnv(repo.RepoPath(), env) + protectBranch, err := models.GetProtectedBranchBy(repo.ID, branchName) if err != nil { - log.Error("Unable to detect force push between: %s and %s in %-v Error: %v", oldCommitID, newCommitID, repo, err) + log.Error("Unable to get protected branch: %s in %-v Error: %v", branchName, repo, err) ctx.JSON(http.StatusInternalServerError, map[string]interface{}{ - "err": fmt.Sprintf("Fail to detect force push: %v", err), + "err": err.Error(), }) return - } else if len(output) > 0 { - log.Warn("Forbidden: Branch: %s in %-v is protected from force push", branchName, repo) + } + + // Allow pushes to non-protected branches + if protectBranch == nil || !protectBranch.IsProtected() { + continue + } + + // This ref is a protected branch. + // + // First of all we need to enforce absolutely: + // + // 1. Detect and prevent deletion of the branch + if newCommitID == git.EmptySHA { + log.Warn("Forbidden: Branch: %s in %-v is protected from deletion", branchName, repo) ctx.JSON(http.StatusForbidden, map[string]interface{}{ - "err": fmt.Sprintf("branch %s is protected from force push", branchName), + "err": fmt.Sprintf("branch %s is protected from deletion", branchName), }) return - } - } - // 3. Enforce require signed commits - if protectBranch.RequireSignedCommits { - err := verifyCommits(oldCommitID, newCommitID, gitRepo, env) - if err != nil { - if !isErrUnverifiedCommit(err) { - log.Error("Unable to check commits from %s to %s in %-v: %v", oldCommitID, newCommitID, repo, err) + // 2. Disallow force pushes to protected branches + if git.EmptySHA != oldCommitID { + output, err := git.NewCommand("rev-list", "--max-count=1", oldCommitID, "^"+newCommitID).RunInDirWithEnv(repo.RepoPath(), env) + if err != nil { + log.Error("Unable to detect force push between: %s and %s in %-v Error: %v", oldCommitID, newCommitID, repo, err) ctx.JSON(http.StatusInternalServerError, map[string]interface{}{ - "err": fmt.Sprintf("Unable to check commits from %s to %s: %v", oldCommitID, newCommitID, err), + "err": fmt.Sprintf("Fail to detect force push: %v", err), }) return + } else if len(output) > 0 { + log.Warn("Forbidden: Branch: %s in %-v is protected from force push", branchName, repo) + ctx.JSON(http.StatusForbidden, map[string]interface{}{ + "err": fmt.Sprintf("branch %s is protected from force push", branchName), + }) + return + } - unverifiedCommit := err.(*errUnverifiedCommit).sha - log.Warn("Forbidden: Branch: %s in %-v is protected from unverified commit %s", branchName, repo, unverifiedCommit) - ctx.JSON(http.StatusForbidden, map[string]interface{}{ - "err": fmt.Sprintf("branch %s is protected from unverified commit %s", branchName, unverifiedCommit), - }) - return } - } - - // Now there are several tests which can be overridden: - // - // 4. Check protected file patterns - this is overridable from the UI - changedProtectedfiles := false - protectedFilePath := "" - globs := protectBranch.GetProtectedFilePatterns() - if len(globs) > 0 { - _, err := pull_service.CheckFileProtection(oldCommitID, newCommitID, globs, 1, env, gitRepo) - if err != nil { - if !models.IsErrFilePathProtected(err) { - log.Error("Unable to check file protection for commits from %s to %s in %-v: %v", oldCommitID, newCommitID, repo, err) - ctx.JSON(http.StatusInternalServerError, map[string]interface{}{ - "err": fmt.Sprintf("Unable to check file protection for commits from %s to %s: %v", oldCommitID, newCommitID, err), + // 3. Enforce require signed commits + if protectBranch.RequireSignedCommits { + err := verifyCommits(oldCommitID, newCommitID, gitRepo, env) + if err != nil { + if !isErrUnverifiedCommit(err) { + log.Error("Unable to check commits from %s to %s in %-v: %v", oldCommitID, newCommitID, repo, err) + ctx.JSON(http.StatusInternalServerError, map[string]interface{}{ + "err": fmt.Sprintf("Unable to check commits from %s to %s: %v", oldCommitID, newCommitID, err), + }) + return + } + unverifiedCommit := err.(*errUnverifiedCommit).sha + log.Warn("Forbidden: Branch: %s in %-v is protected from unverified commit %s", branchName, repo, unverifiedCommit) + ctx.JSON(http.StatusForbidden, map[string]interface{}{ + "err": fmt.Sprintf("branch %s is protected from unverified commit %s", branchName, unverifiedCommit), }) return } + } + + // Now there are several tests which can be overridden: + // + // 4. Check protected file patterns - this is overridable from the UI + changedProtectedfiles := false + protectedFilePath := "" - changedProtectedfiles = true - protectedFilePath = err.(models.ErrFilePathProtected).Path + globs := protectBranch.GetProtectedFilePatterns() + if len(globs) > 0 { + _, err := pull_service.CheckFileProtection(oldCommitID, newCommitID, globs, 1, env, gitRepo) + if err != nil { + if !models.IsErrFilePathProtected(err) { + log.Error("Unable to check file protection for commits from %s to %s in %-v: %v", oldCommitID, newCommitID, repo, err) + ctx.JSON(http.StatusInternalServerError, map[string]interface{}{ + "err": fmt.Sprintf("Unable to check file protection for commits from %s to %s: %v", oldCommitID, newCommitID, err), + }) + return + } + + changedProtectedfiles = true + protectedFilePath = err.(models.ErrFilePathProtected).Path + } } - } - // 5. Check if the doer is allowed to push - canPush := false - if opts.IsDeployKey { - canPush = !changedProtectedfiles && protectBranch.CanPush && (!protectBranch.EnableWhitelist || protectBranch.WhitelistDeployKeys) - } else { - canPush = !changedProtectedfiles && protectBranch.CanUserPush(opts.UserID) - } + // 5. Check if the doer is allowed to push + canPush := false + if opts.IsDeployKey { + canPush = !changedProtectedfiles && protectBranch.CanPush && (!protectBranch.EnableWhitelist || protectBranch.WhitelistDeployKeys) + } else { + canPush = !changedProtectedfiles && protectBranch.CanUserPush(opts.UserID) + } - // 6. If we're not allowed to push directly - if !canPush { - // Is this is a merge from the UI/API? - if opts.ProtectedBranchID == 0 { - // 6a. If we're not merging from the UI/API then there are two ways we got here: - // - // We are changing a protected file and we're not allowed to do that - if changedProtectedfiles { - log.Warn("Forbidden: Branch: %s in %-v is protected from changing file %s", branchName, repo, protectedFilePath) + // 6. If we're not allowed to push directly + if !canPush { + // Is this is a merge from the UI/API? + if opts.ProtectedBranchID == 0 { + // 6a. If we're not merging from the UI/API then there are two ways we got here: + // + // We are changing a protected file and we're not allowed to do that + if changedProtectedfiles { + log.Warn("Forbidden: Branch: %s in %-v is protected from changing file %s", branchName, repo, protectedFilePath) + ctx.JSON(http.StatusForbidden, map[string]interface{}{ + "err": fmt.Sprintf("branch %s is protected from changing file %s", branchName, protectedFilePath), + }) + return + } + + // Or we're simply not able to push to this protected branch + log.Warn("Forbidden: User %d is not allowed to push to protected branch: %s in %-v", opts.UserID, branchName, repo) ctx.JSON(http.StatusForbidden, map[string]interface{}{ - "err": fmt.Sprintf("branch %s is protected from changing file %s", branchName, protectedFilePath), + "err": fmt.Sprintf("Not allowed to push to protected branch %s", branchName), }) return } + // 6b. Merge (from UI or API) - // Or we're simply not able to push to this protected branch - log.Warn("Forbidden: User %d is not allowed to push to protected branch: %s in %-v", opts.UserID, branchName, repo) - ctx.JSON(http.StatusForbidden, map[string]interface{}{ - "err": fmt.Sprintf("Not allowed to push to protected branch %s", branchName), - }) - return - } - // 6b. Merge (from UI or API) + // Get the PR, user and permissions for the user in the repository + pr, err := models.GetPullRequestByID(opts.ProtectedBranchID) + if err != nil { + log.Error("Unable to get PullRequest %d Error: %v", opts.ProtectedBranchID, err) + ctx.JSON(http.StatusInternalServerError, map[string]interface{}{ + "err": fmt.Sprintf("Unable to get PullRequest %d Error: %v", opts.ProtectedBranchID, err), + }) + return + } + user, err := models.GetUserByID(opts.UserID) + if err != nil { + log.Error("Unable to get User id %d Error: %v", opts.UserID, err) + ctx.JSON(http.StatusInternalServerError, map[string]interface{}{ + "err": fmt.Sprintf("Unable to get User id %d Error: %v", opts.UserID, err), + }) + return + } + perm, err := models.GetUserRepoPermission(repo, user) + if err != nil { + log.Error("Unable to get Repo permission of repo %s/%s of User %s", repo.OwnerName, repo.Name, user.Name, err) + ctx.JSON(http.StatusInternalServerError, map[string]interface{}{ + "err": fmt.Sprintf("Unable to get Repo permission of repo %s/%s of User %s: %v", repo.OwnerName, repo.Name, user.Name, err), + }) + return + } - // Get the PR, user and permissions for the user in the repository - pr, err := models.GetPullRequestByID(opts.ProtectedBranchID) - if err != nil { - log.Error("Unable to get PullRequest %d Error: %v", opts.ProtectedBranchID, err) - ctx.JSON(http.StatusInternalServerError, map[string]interface{}{ - "err": fmt.Sprintf("Unable to get PullRequest %d Error: %v", opts.ProtectedBranchID, err), - }) - return - } - user, err := models.GetUserByID(opts.UserID) - if err != nil { - log.Error("Unable to get User id %d Error: %v", opts.UserID, err) - ctx.JSON(http.StatusInternalServerError, map[string]interface{}{ - "err": fmt.Sprintf("Unable to get User id %d Error: %v", opts.UserID, err), - }) - return - } - perm, err := models.GetUserRepoPermission(repo, user) - if err != nil { - log.Error("Unable to get Repo permission of repo %s/%s of User %s", repo.OwnerName, repo.Name, user.Name, err) - ctx.JSON(http.StatusInternalServerError, map[string]interface{}{ - "err": fmt.Sprintf("Unable to get Repo permission of repo %s/%s of User %s: %v", repo.OwnerName, repo.Name, user.Name, err), - }) - return - } + // Now check if the user is allowed to merge PRs for this repository + allowedMerge, err := pull_service.IsUserAllowedToMerge(pr, perm, user) + if err != nil { + log.Error("Error calculating if allowed to merge: %v", err) + ctx.JSON(http.StatusInternalServerError, map[string]interface{}{ + "err": fmt.Sprintf("Error calculating if allowed to merge: %v", err), + }) + return + } - // Now check if the user is allowed to merge PRs for this repository - allowedMerge, err := pull_service.IsUserAllowedToMerge(pr, perm, user) - if err != nil { - log.Error("Error calculating if allowed to merge: %v", err) - ctx.JSON(http.StatusInternalServerError, map[string]interface{}{ - "err": fmt.Sprintf("Error calculating if allowed to merge: %v", err), - }) - return - } + if !allowedMerge { + log.Warn("Forbidden: User %d is not allowed to push to protected branch: %s in %-v and is not allowed to merge pr #%d", opts.UserID, branchName, repo, pr.Index) + ctx.JSON(http.StatusForbidden, map[string]interface{}{ + "err": fmt.Sprintf("Not allowed to push to protected branch %s", branchName), + }) + return + } - if !allowedMerge { - log.Warn("Forbidden: User %d is not allowed to push to protected branch: %s in %-v and is not allowed to merge pr #%d", opts.UserID, branchName, repo, pr.Index) - ctx.JSON(http.StatusForbidden, map[string]interface{}{ - "err": fmt.Sprintf("Not allowed to push to protected branch %s", branchName), - }) - return - } + // If we're an admin for the repository we can ignore status checks, reviews and override protected files + if perm.IsAdmin() { + continue + } - // If we're an admin for the repository we can ignore status checks, reviews and override protected files - if perm.IsAdmin() { - continue - } + // Now if we're not an admin - we can't overwrite protected files so fail now + if changedProtectedfiles { + log.Warn("Forbidden: Branch: %s in %-v is protected from changing file %s", branchName, repo, protectedFilePath) + ctx.JSON(http.StatusForbidden, map[string]interface{}{ + "err": fmt.Sprintf("branch %s is protected from changing file %s", branchName, protectedFilePath), + }) + return + } - // Now if we're not an admin - we can't overwrite protected files so fail now - if changedProtectedfiles { - log.Warn("Forbidden: Branch: %s in %-v is protected from changing file %s", branchName, repo, protectedFilePath) - ctx.JSON(http.StatusForbidden, map[string]interface{}{ - "err": fmt.Sprintf("branch %s is protected from changing file %s", branchName, protectedFilePath), - }) - return + // Check all status checks and reviews are ok + if err := pull_service.CheckPRReadyToMerge(pr, true); err != nil { + if models.IsErrNotAllowedToMerge(err) { + log.Warn("Forbidden: User %d is not allowed push to protected branch %s in %-v and pr #%d is not ready to be merged: %s", opts.UserID, branchName, repo, pr.Index, err.Error()) + ctx.JSON(http.StatusForbidden, map[string]interface{}{ + "err": fmt.Sprintf("Not allowed to push to protected branch %s and pr #%d is not ready to be merged: %s", branchName, opts.ProtectedBranchID, err.Error()), + }) + return + } + log.Error("Unable to check if mergable: protected branch %s in %-v and pr #%d. Error: %v", opts.UserID, branchName, repo, pr.Index, err) + ctx.JSON(http.StatusInternalServerError, map[string]interface{}{ + "err": fmt.Sprintf("Unable to get status of pull request %d. Error: %v", opts.ProtectedBranchID, err), + }) + return + } } + } else if strings.HasPrefix(refFullName, git.TagPrefix) { + tagName := strings.TrimPrefix(refFullName, git.TagPrefix) - // Check all status checks and reviews are ok - if err := pull_service.CheckPRReadyToMerge(pr, true); err != nil { - if models.IsErrNotAllowedToMerge(err) { - log.Warn("Forbidden: User %d is not allowed push to protected branch %s in %-v and pr #%d is not ready to be merged: %s", opts.UserID, branchName, repo, pr.Index, err.Error()) - ctx.JSON(http.StatusForbidden, map[string]interface{}{ - "err": fmt.Sprintf("Not allowed to push to protected branch %s and pr #%d is not ready to be merged: %s", branchName, opts.ProtectedBranchID, err.Error()), + isAllowed := true + for _, tag := range protectedTags { + if err := tag.EnsureCompiledPattern(); err != nil { + log.Error("Error compiling pattern: %v", err) + ctx.JSON(http.StatusInternalServerError, private.HookPostReceiveResult{ + Err: err.Error(), }) return } - log.Error("Unable to check if mergable: protected branch %s in %-v and pr #%d. Error: %v", opts.UserID, branchName, repo, pr.Index, err) - ctx.JSON(http.StatusInternalServerError, map[string]interface{}{ - "err": fmt.Sprintf("Unable to get status of pull request %d. Error: %v", opts.ProtectedBranchID, err), + + if !tag.NameGlob.Match(tagName) { + continue + } + + isAllowed = tag.IsUserAllowed(opts.UserID) + if isAllowed { + break + } + } + + if !isAllowed { + log.Warn("Forbidden: Tag %s in %-v is protected", tagName, repo) + ctx.JSON(http.StatusForbidden, private.HookPostReceiveResult{ + Err: fmt.Sprintf("tag %s is protected", tagName), }) return } + } else { + log.Error("Unexpected ref: %s", refFullName) + ctx.JSON(http.StatusInternalServerError, private.HookPostReceiveResult{ + Err: fmt.Sprintf("Unexpected ref: %s", refFullName), + }) } } From 4718c2a0c234db6f6893ea7d49567b6dee08b978 Mon Sep 17 00:00:00 2001 From: KN4CK3R Date: Mon, 26 Apr 2021 17:23:00 +0200 Subject: [PATCH 02/35] Prevent UI tag creation if protected. --- models/error.go | 6 +++++- models/tags.go | 1 - options/locale/locale_en-US.ini | 1 + routers/repo/release.go | 10 ++++++++++ services/mirror/mirror_test.go | 1 + services/release/release.go | 30 ++++++++++++++++++++++++++++++ services/release/release_test.go | 10 ++++++++++ 7 files changed, 57 insertions(+), 2 deletions(-) diff --git a/models/error.go b/models/error.go index 48cba57a8135c..a53e4f80d2eb0 100644 --- a/models/error.go +++ b/models/error.go @@ -957,7 +957,8 @@ func (err ErrReleaseNotExist) Error() string { // ErrInvalidTagName represents a "InvalidTagName" kind of error. type ErrInvalidTagName struct { - TagName string + TagName string + Protected bool } // IsErrInvalidTagName checks if an error is a ErrInvalidTagName. @@ -967,6 +968,9 @@ func IsErrInvalidTagName(err error) bool { } func (err ErrInvalidTagName) Error() string { + if err.Protected { + return fmt.Sprintf("release tag name is protected [tag_name: %s]", err.TagName) + } return fmt.Sprintf("release tag name is not valid [tag_name: %s]", err.TagName) } diff --git a/models/tags.go b/models/tags.go index d2c98af08e0ff..81315c8cc8363 100644 --- a/models/tags.go +++ b/models/tags.go @@ -5,7 +5,6 @@ package models import ( - "fmt" "strings" "code.gitea.io/gitea/modules/base" diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini index 1a8d253749cf5..314adfdb5b6aa 100644 --- a/options/locale/locale_en-US.ini +++ b/options/locale/locale_en-US.ini @@ -1939,6 +1939,7 @@ release.deletion_tag_desc = Will delete this tag from repository. Repository con release.deletion_tag_success = The tag has been deleted. release.tag_name_already_exist = A release with this tag name already exists. release.tag_name_invalid = The tag name is not valid. +release.tag_name_protected = The tag name is protected. release.tag_already_exist = This tag name already exists. release.downloads = Downloads release.download_count = Downloads: %s diff --git a/routers/repo/release.go b/routers/repo/release.go index abce3e9ac1a28..0daf62dba0d6b 100644 --- a/routers/repo/release.go +++ b/routers/repo/release.go @@ -298,6 +298,15 @@ func NewReleasePost(ctx *context.Context) { ctx.Flash.Error(ctx.Tr("repo.branch.tag_collision", e.TagName)) ctx.Redirect(ctx.Repo.RepoLink + "/src/" + ctx.Repo.BranchNameSubURL()) return + } else if models.IsErrInvalidTagName(err) { + e := err.(models.ErrInvalidTagName) + if e.Protected { + ctx.Flash.Error(ctx.Tr("repo.release.tag_name_protected")) + } else { + ctx.Flash.Error(ctx.Tr("repo.release.tag_name_invalid")) + } + ctx.Redirect(ctx.Repo.RepoLink + "/src/" + ctx.Repo.BranchNameSubURL()) + return } ctx.ServerError("releaseservice.CreateNewTag", err) @@ -311,6 +320,7 @@ func NewReleasePost(ctx *context.Context) { rel = &models.Release{ RepoID: ctx.Repo.Repository.ID, + Repo: ctx.Repo.Repository, PublisherID: ctx.User.ID, Title: form.Title, TagName: form.TagName, diff --git a/services/mirror/mirror_test.go b/services/mirror/mirror_test.go index 20492c784bdb8..cb93578c0ec5c 100644 --- a/services/mirror/mirror_test.go +++ b/services/mirror/mirror_test.go @@ -63,6 +63,7 @@ func TestRelease_MirrorDelete(t *testing.T) { assert.NoError(t, release_service.CreateRelease(gitRepo, &models.Release{ RepoID: repo.ID, + Repo: repo, PublisherID: user.ID, TagName: "v0.2", Target: "master", diff --git a/services/release/release.go b/services/release/release.go index 9d201edf6d28c..e6ffe077f2b23 100644 --- a/services/release/release.go +++ b/services/release/release.go @@ -23,6 +23,35 @@ func createTag(gitRepo *git.Repository, rel *models.Release, msg string) (bool, // Only actual create when publish. if !rel.IsDraft { if !gitRepo.IsTagExist(rel.TagName) { + if rel.Repo != nil { + protectedTags, err := rel.Repo.GetProtectedTags() + if err != nil { + return false, fmt.Errorf("GetProtectedTags: %v", err) + } + isAllowed := true + for _, tag := range protectedTags { + if err := tag.EnsureCompiledPattern(); err != nil { + return false, fmt.Errorf("EnsureCompiledPattern: %v", err) + } + + if !tag.NameGlob.Match(rel.TagName) { + continue + } + + isAllowed = tag.IsUserAllowed(rel.PublisherID) + if isAllowed { + break + } + } + + if !isAllowed { + return false, models.ErrInvalidTagName{ + TagName: rel.TagName, + Protected: true, + } + } + } + commit, err := gitRepo.GetCommit(rel.Target) if err != nil { return false, fmt.Errorf("GetCommit: %v", err) @@ -137,6 +166,7 @@ func CreateNewTag(doer *models.User, repo *models.Repository, commit, tagName, m rel := &models.Release{ RepoID: repo.ID, + Repo: repo, PublisherID: doer.ID, TagName: tagName, Target: commit, diff --git a/services/release/release_test.go b/services/release/release_test.go index 102e3d7e0c0e1..5fb9e4b88d41f 100644 --- a/services/release/release_test.go +++ b/services/release/release_test.go @@ -33,6 +33,7 @@ func TestRelease_Create(t *testing.T) { assert.NoError(t, CreateRelease(gitRepo, &models.Release{ RepoID: repo.ID, + Repo: repo, PublisherID: user.ID, TagName: "v0.1", Target: "master", @@ -45,6 +46,7 @@ func TestRelease_Create(t *testing.T) { assert.NoError(t, CreateRelease(gitRepo, &models.Release{ RepoID: repo.ID, + Repo: repo, PublisherID: user.ID, TagName: "v0.1.1", Target: "65f1bf27bc3bf70f64657658635e66094edbcb4d", @@ -57,6 +59,7 @@ func TestRelease_Create(t *testing.T) { assert.NoError(t, CreateRelease(gitRepo, &models.Release{ RepoID: repo.ID, + Repo: repo, PublisherID: user.ID, TagName: "v0.1.2", Target: "65f1bf2", @@ -69,6 +72,7 @@ func TestRelease_Create(t *testing.T) { assert.NoError(t, CreateRelease(gitRepo, &models.Release{ RepoID: repo.ID, + Repo: repo, PublisherID: user.ID, TagName: "v0.1.3", Target: "65f1bf2", @@ -81,6 +85,7 @@ func TestRelease_Create(t *testing.T) { assert.NoError(t, CreateRelease(gitRepo, &models.Release{ RepoID: repo.ID, + Repo: repo, PublisherID: user.ID, TagName: "v0.1.4", Target: "65f1bf2", @@ -99,6 +104,7 @@ func TestRelease_Create(t *testing.T) { var release = models.Release{ RepoID: repo.ID, + Repo: repo, PublisherID: user.ID, TagName: "v0.1.5", Target: "65f1bf2", @@ -125,6 +131,7 @@ func TestRelease_Update(t *testing.T) { // Test a changed release assert.NoError(t, CreateRelease(gitRepo, &models.Release{ RepoID: repo.ID, + Repo: repo, PublisherID: user.ID, TagName: "v1.1.1", Target: "master", @@ -147,6 +154,7 @@ func TestRelease_Update(t *testing.T) { // Test a changed draft assert.NoError(t, CreateRelease(gitRepo, &models.Release{ RepoID: repo.ID, + Repo: repo, PublisherID: user.ID, TagName: "v1.2.1", Target: "65f1bf2", @@ -169,6 +177,7 @@ func TestRelease_Update(t *testing.T) { // Test a changed pre-release assert.NoError(t, CreateRelease(gitRepo, &models.Release{ RepoID: repo.ID, + Repo: repo, PublisherID: user.ID, TagName: "v1.3.1", Target: "65f1bf2", @@ -192,6 +201,7 @@ func TestRelease_Update(t *testing.T) { // Test create release release = &models.Release{ RepoID: repo.ID, + Repo: repo, PublisherID: user.ID, TagName: "v1.1.2", Target: "master", From 911fec6e0a57d25ad31b26c5ae744c4313422681 Mon Sep 17 00:00:00 2001 From: KN4CK3R Date: Mon, 26 Apr 2021 18:41:00 +0200 Subject: [PATCH 03/35] Added settings page. --- options/locale/locale_en-US.ini | 11 +++ routers/repo/setting.go | 1 + routers/repo/tag.go | 131 ++++++++++++++++++++++++++++ routers/routes/web.go | 6 ++ services/forms/repo_tag_form.go | 27 ++++++ templates/repo/settings/nav.tmpl | 1 + templates/repo/settings/navbar.tmpl | 3 + templates/repo/settings/tags.tmpl | 122 ++++++++++++++++++++++++++ 8 files changed, 302 insertions(+) create mode 100644 routers/repo/tag.go create mode 100644 services/forms/repo_tag_form.go create mode 100644 templates/repo/settings/tags.tmpl diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini index 314adfdb5b6aa..946ec2682a165 100644 --- a/options/locale/locale_en-US.ini +++ b/options/locale/locale_en-US.ini @@ -1814,6 +1814,16 @@ settings.choose_branch = Choose a branch… settings.no_protected_branch = There are no protected branches. settings.edit_protected_branch = Edit settings.protected_branch_required_approvals_min = Required approvals cannot be negative. +settings.tags = Tags +settings.tags.protection = Tag Protection +settings.tags.protection.pattern = Tag Pattern +settings.tags.protection.allowed = Allowed +settings.tags.protection.allowed.users = Allowed users +settings.tags.protection.allowed.teams = Allowed teams +settings.tags.protection.allowed.noone = No One +settings.tags.protection.create = Protect Tag +settings.tags.protection.none = There are no protected tags. +settings.tags.protection.pattern.description = Wildcards such as v* or *-release are supported. See github.com/gobwas/glob documentation for pattern syntax. settings.bot_token = Bot Token settings.chat_id = Chat ID settings.matrix.homeserver_url = Homeserver URL @@ -1827,6 +1837,7 @@ settings.archive.success = The repo was successfully archived. settings.archive.error = An error occurred while trying to archive the repo. See the log for more details. settings.archive.error_ismirror = You cannot archive a mirrored repo. settings.archive.branchsettings_unavailable = Branch settings are not available if the repo is archived. +settings.archive.tagsettings_unavailable = Tag settings are not available if the repo is archived. settings.unarchive.button = Un-Archive Repo settings.unarchive.header = Un-Archive This Repo settings.unarchive.text = Un-Archiving the repo will restore its ability to receive commits and pushes, as well as new issues and pull-requests. diff --git a/routers/repo/setting.go b/routers/repo/setting.go index 533adcbdf6ba5..c054d9151ac2e 100644 --- a/routers/repo/setting.go +++ b/routers/repo/setting.go @@ -37,6 +37,7 @@ const ( tplSettingsOptions base.TplName = "repo/settings/options" tplCollaboration base.TplName = "repo/settings/collaboration" tplBranches base.TplName = "repo/settings/branches" + tplTags base.TplName = "repo/settings/tags" tplGithooks base.TplName = "repo/settings/githooks" tplGithookEdit base.TplName = "repo/settings/githook_edit" tplDeployKeys base.TplName = "repo/settings/deploy_keys" diff --git a/routers/repo/tag.go b/routers/repo/tag.go new file mode 100644 index 0000000000000..8b9ac58c8722b --- /dev/null +++ b/routers/repo/tag.go @@ -0,0 +1,131 @@ +// Copyright 2021 The Gitea Authors. All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. + +package repo + +import ( + "fmt" + "net/http" + "strconv" + "strings" + + "code.gitea.io/gitea/models" + "code.gitea.io/gitea/modules/base" + "code.gitea.io/gitea/modules/context" + "code.gitea.io/gitea/modules/setting" + "code.gitea.io/gitea/modules/web" + "code.gitea.io/gitea/services/forms" +) + +// Tags render the page to protect tags +func Tags(ctx *context.Context) { + if setTagsContext(ctx) != nil { + return + } + + ctx.HTML(http.StatusOK, tplTags) +} + +// TagPost response for protect for a branch of a repository +func TagPost(ctx *context.Context) { + if setTagsContext(ctx) != nil { + return + } + + repo := ctx.Repo.Repository + + switch ctx.Query("action") { + case "create_protected_tag": + web.Bind(forms.ProtectTagForm{})(ctx.Resp, ctx.Req) + if ctx.HasError() { + ctx.HTML(http.StatusOK, tplTags) + return + } + + form := web.GetForm(ctx).(*forms.ProtectTagForm) + + pt := &models.ProtectedTag{ + RepoID: repo.ID, + NamePattern: form.NamePattern, + } + + if strings.TrimSpace(form.WhitelistUsers) != "" { + pt.WhitelistUserIDs, _ = base.StringsToInt64s(strings.Split(form.WhitelistUsers, ",")) + } + if strings.TrimSpace(form.WhitelistTeams) != "" { + pt.WhitelistTeamIDs, _ = base.StringsToInt64s(strings.Split(form.WhitelistTeams, ",")) + } + + if err := models.InsertProtectedTag(pt); err != nil { + ctx.ServerError("InsertProtectedTag", err) + return + } + + ctx.Flash.Success(ctx.Tr("repo.settings.update_settings_success")) + ctx.Redirect(setting.AppSubURL + ctx.Req.URL.Path) + case "remove_protected_tag": + pt, err := selectProtectedTagByContext(ctx, repo) + if err != nil { + ctx.NotFound("", nil) + return + } + + if err := models.DeleteProtectedTag(pt); err != nil { + ctx.ServerError("DeleteProtectedTag", err) + return + } + + ctx.Flash.Success(ctx.Tr("repo.settings.update_settings_success")) + ctx.Redirect(setting.AppSubURL + ctx.Req.URL.Path) + default: + ctx.NotFound("", nil) + } +} + +func setTagsContext(ctx *context.Context) error { + ctx.Data["Title"] = ctx.Tr("repo.settings") + ctx.Data["PageIsSettingsTags"] = true + + protectedTags, err := ctx.Repo.Repository.GetProtectedTags() + if err != nil { + ctx.ServerError("GetProtectedTags", err) + return err + } + ctx.Data["ProtectedTags"] = protectedTags + + users, err := ctx.Repo.Repository.GetReaders() + if err != nil { + ctx.ServerError("Repo.Repository.GetReaders", err) + return err + } + ctx.Data["Users"] = users + + if ctx.Repo.Owner.IsOrganization() { + teams, err := ctx.Repo.Owner.TeamsWithAccessToRepo(ctx.Repo.Repository.ID, models.AccessModeRead) + if err != nil { + ctx.ServerError("Repo.Owner.TeamsWithAccessToRepo", err) + return err + } + ctx.Data["Teams"] = teams + } + + return nil +} + +func selectProtectedTagByContext(ctx *context.Context, repo *models.Repository) (*models.ProtectedTag, error) { + pts, err := repo.GetProtectedTags() + if err != nil { + return nil, err + } + + id, _ := strconv.ParseInt(ctx.Query("id"), 10, 64) + + for _, pt := range pts { + if pt.ID == id { + return pt, nil + } + } + + return nil, fmt.Errorf("ProtectedTag[%v] not associated to repository %v", id, repo) +} diff --git a/routers/routes/web.go b/routers/routes/web.go index cedab78434b51..76438c2145bf7 100644 --- a/routers/routes/web.go +++ b/routers/routes/web.go @@ -710,12 +710,18 @@ func RegisterRoutes(m *web.Route) { m.Post("/delete", repo.DeleteTeam) }) }) + m.Group("/branches", func() { m.Combo("").Get(repo.ProtectedBranch).Post(repo.ProtectedBranchPost) m.Combo("/*").Get(repo.SettingsProtectedBranch). Post(bindIgnErr(forms.ProtectBranchForm{}), context.RepoMustNotBeArchived(), repo.SettingsProtectedBranchPost) }, repo.MustBeNotEmpty) + m.Group("/tags", func() { + m.Combo("").Get(repo.Tags). + Post(context.RepoMustNotBeArchived(), repo.TagPost) + }) + m.Group("/hooks/git", func() { m.Get("", repo.GitHooks) m.Combo("/{name}").Get(repo.GitHooksEdit). diff --git a/services/forms/repo_tag_form.go b/services/forms/repo_tag_form.go new file mode 100644 index 0000000000000..03f4301451d81 --- /dev/null +++ b/services/forms/repo_tag_form.go @@ -0,0 +1,27 @@ +// Copyright 2021 The Gitea Authors. All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. + +package forms + +import ( + "net/http" + + "code.gitea.io/gitea/modules/context" + "code.gitea.io/gitea/modules/web/middleware" + + "gitea.com/go-chi/binding" +) + +// ProtectTagForm form for changing protected tag settings +type ProtectTagForm struct { + NamePattern string `binding:"Required;GlobPattern"` + WhitelistUsers string + WhitelistTeams string +} + +// Validate validates the fields +func (f *ProtectTagForm) Validate(req *http.Request, errs binding.Errors) binding.Errors { + ctx := context.GetContext(req) + return middleware.Validate(errs, ctx.Data, f, ctx.Locale) +} diff --git a/templates/repo/settings/nav.tmpl b/templates/repo/settings/nav.tmpl index 4b89ece34918d..31672cb5ead0f 100644 --- a/templates/repo/settings/nav.tmpl +++ b/templates/repo/settings/nav.tmpl @@ -5,6 +5,7 @@
  • {{.i18n.Tr "repo.settings.options"}}
  • {{.i18n.Tr "repo.settings.collaboration"}}
  • {{.i18n.Tr "repo.settings.branches"}}
  • +
  • {{.i18n.Tr "repo.settings.tags"}}
  • {{if not DisableWebhooks}}
  • {{.i18n.Tr "repo.settings.hooks"}}
  • {{end}} diff --git a/templates/repo/settings/navbar.tmpl b/templates/repo/settings/navbar.tmpl index 501c3c4630a40..d8cdf218719bd 100644 --- a/templates/repo/settings/navbar.tmpl +++ b/templates/repo/settings/navbar.tmpl @@ -11,6 +11,9 @@ {{.i18n.Tr "repo.settings.branches"}} {{end}} + + {{.i18n.Tr "repo.settings.tags"}} + {{if not DisableWebhooks}} {{.i18n.Tr "repo.settings.hooks"}} diff --git a/templates/repo/settings/tags.tmpl b/templates/repo/settings/tags.tmpl new file mode 100644 index 0000000000000..4530a8850a5f3 --- /dev/null +++ b/templates/repo/settings/tags.tmpl @@ -0,0 +1,122 @@ +{{template "base/head" .}} +
    + {{template "repo/header" .}} + {{template "repo/settings/navbar" .}} +
    + {{template "base/alert" .}} + {{if .Repository.IsArchived}} +
    + {{.i18n.Tr "repo.settings.archive.tagsettings_unavailable"}} +
    + {{else}} +

    + {{.i18n.Tr "repo.settings.tags.protection"}} +

    + +
    +
    +
    +
    +
    + {{.CsrfTokenHtml}} + +
    + + +
    +
    + + +
    + {{if .Owner.IsOrganization}} +
    + + +
    + {{end}} +
    + +
    +
    +
    +
    + +
    + + + + + + + + {{range .ProtectedTags}} + + + + + + {{else}} + + {{end}} + +
    {{.i18n.Tr "repo.settings.tags.protection.pattern"}}{{.i18n.Tr "repo.settings.tags.protection.allowed"}}
    {{.NamePattern}} + {{if or .WhitelistUserIDs (and $.Owner.IsOrganization .WhitelistTeamIDs)}} + {{$userIDs := .WhitelistUserIDs}} + {{range $.Users}} + {{if contain $userIDs .ID }} + {{avatar . 26}} {{.GetDisplayName}} + {{end}} + {{end}} + {{if $.Owner.IsOrganization}} + {{$teamIDs := .WhitelistTeamIDs}} + {{range $.Teams}} + {{if contain $teamIDs .ID }} + {{.Name}} + {{end}} + {{end}} + {{end}} + {{else}} + {{$.i18n.Tr "repo.settings.tags.protection.allowed.noone"}} + {{end}} + +
    + {{$.CsrfTokenHtml}} + + + +
    +
    {{.i18n.Tr "repo.settings.tags.protection.none"}}
    +
    +
    +
    + {{end}} +
    +
    +{{template "base/footer" .}} From e20ddacdf327137f8bd9966c5c9cbe31cdc01dee Mon Sep 17 00:00:00 2001 From: KN4CK3R Date: Mon, 26 Apr 2021 20:10:11 +0000 Subject: [PATCH 04/35] Added tests. --- integrations/repo_tag_test.go | 162 ++++++++++++++++++++++++++++++++++ models/tags.go | 23 +++++ routers/private/hook.go | 27 ++---- services/release/release.go | 18 +--- 4 files changed, 195 insertions(+), 35 deletions(-) create mode 100644 integrations/repo_tag_test.go diff --git a/integrations/repo_tag_test.go b/integrations/repo_tag_test.go new file mode 100644 index 0000000000000..27ea719552673 --- /dev/null +++ b/integrations/repo_tag_test.go @@ -0,0 +1,162 @@ +// Copyright 2021 The Gitea Authors. All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. + +package integrations + +import ( + "io/ioutil" + "net/url" + "testing" + + "code.gitea.io/gitea/models" + "code.gitea.io/gitea/modules/git" + "code.gitea.io/gitea/modules/util" + "code.gitea.io/gitea/services/release" + + "github.com/stretchr/testify/assert" +) + +func TestIsUserAllowedToControlTag(t *testing.T) { + protectedTags := []*models.ProtectedTag{ + { + NamePattern: "*gitea", + WhitelistUserIDs: []int64{1}, + }, + { + NamePattern: "v-*", + WhitelistUserIDs: []int64{2}, + }, + { + NamePattern: "release", + }, + } + + cases := []struct { + name string + userid int64 + allowed bool + }{ + { + name: "test", + userid: 1, + allowed: true, + }, + { + name: "test", + userid: 3, + allowed: true, + }, + { + name: "gitea", + userid: 1, + allowed: true, + }, + { + name: "gitea", + userid: 3, + allowed: false, + }, + { + name: "test-gitea", + userid: 1, + allowed: true, + }, + { + name: "test-gitea", + userid: 3, + allowed: false, + }, + { + name: "gitea-test", + userid: 1, + allowed: true, + }, + { + name: "gitea-test", + userid: 3, + allowed: true, + }, + { + name: "v-1", + userid: 1, + allowed: false, + }, + { + name: "v-1", + userid: 2, + allowed: true, + }, + { + name: "release", + userid: 1, + allowed: false, + }, + } + + for n, c := range cases { + isAllowed, err := models.IsUserAllowedToControlTag(protectedTags, c.name, c.userid) + assert.NoError(t, err) + assert.Equal(t, c.allowed, isAllowed, "case %d: error should match", n) + } +} + +func TestCreateNewTagProtected(t *testing.T) { + defer prepareTestEnv(t)() + + repo := models.AssertExistsAndLoadBean(t, &models.Repository{ID: 1}).(*models.Repository) + owner := models.AssertExistsAndLoadBean(t, &models.User{ID: repo.OwnerID}).(*models.User) + + t.Run("API", func(t *testing.T) { + defer PrintCurrentTest(t)() + + err := release.CreateNewTag(owner, repo, "master", "v-1", "first tag") + assert.NoError(t, err) + + err = models.InsertProtectedTag(&models.ProtectedTag{ + RepoID: repo.ID, + NamePattern: "v-*", + }) + assert.NoError(t, err) + err = models.InsertProtectedTag(&models.ProtectedTag{ + RepoID: repo.ID, + NamePattern: "v-1.1", + WhitelistUserIDs: []int64{repo.OwnerID}, + }) + assert.NoError(t, err) + + err = release.CreateNewTag(owner, repo, "master", "v-2", "second tag") + assert.Error(t, err) + assert.True(t, models.IsErrInvalidTagName(err)) + e := err.(models.ErrInvalidTagName) + assert.True(t, e.Protected) + + err = release.CreateNewTag(owner, repo, "master", "v-1.1", "third tag") + assert.NoError(t, err) + }) + + t.Run("Git", func(t *testing.T) { + defer PrintCurrentTest(t)() + + onGiteaRun(t, func(t *testing.T, u *url.URL) { + username := "user2" + httpContext := NewAPITestContext(t, username, "repo1") + + dstPath, err := ioutil.TempDir("", httpContext.Reponame) + assert.NoError(t, err) + defer util.RemoveAll(dstPath) + + u.Path = httpContext.GitPath() + u.User = url.UserPassword(username, userPassword) + + doGitClone(dstPath, u)(t) + + _, err = git.NewCommand("tag", "v-2").RunInDir(dstPath) + assert.NoError(t, err) + + _, err = git.NewCommand("push", "--tags").RunInDir(dstPath) + assert.Error(t, err) + assert.Contains(t, err.Error(), "Tag v-2 is protected") + }) + }) +} diff --git a/models/tags.go b/models/tags.go index 81315c8cc8363..fbbbc6b9ee961 100644 --- a/models/tags.go +++ b/models/tags.go @@ -92,3 +92,26 @@ func (repo *Repository) GetProtectedTags() ([]*ProtectedTag, error) { tags := make([]*ProtectedTag, 0) return tags, x.Find(&tags, &ProtectedTag{RepoID: repo.ID}) } + +// IsUserAllowedToControlTag checks if a user can control the specific tag. +// It returns true if the tag name is not protected or the user is allowed to control it. +func IsUserAllowedToControlTag(tags []*ProtectedTag, tagName string, userID int64) (bool, error) { + isAllowed := true + for _, tag := range tags { + if err := tag.EnsureCompiledPattern(); err != nil { + log.Error("EnsureCompiledPattern failed: %v", err) + return false, err + } + + if !tag.NameGlob.Match(tagName) { + continue + } + + isAllowed = tag.IsUserAllowed(userID) + if isAllowed { + break + } + } + + return isAllowed, nil +} diff --git a/routers/private/hook.go b/routers/private/hook.go index 5597b80347377..f08d53ddb40b9 100644 --- a/routers/private/hook.go +++ b/routers/private/hook.go @@ -378,30 +378,17 @@ func HookPreReceive(ctx *gitea_context.PrivateContext) { } else if strings.HasPrefix(refFullName, git.TagPrefix) { tagName := strings.TrimPrefix(refFullName, git.TagPrefix) - isAllowed := true - for _, tag := range protectedTags { - if err := tag.EnsureCompiledPattern(); err != nil { - log.Error("Error compiling pattern: %v", err) - ctx.JSON(http.StatusInternalServerError, private.HookPostReceiveResult{ - Err: err.Error(), - }) - return - } - - if !tag.NameGlob.Match(tagName) { - continue - } - - isAllowed = tag.IsUserAllowed(opts.UserID) - if isAllowed { - break - } + isAllowed, err := models.IsUserAllowedToControlTag(protectedTags, tagName, opts.UserID) + if err != nil { + ctx.JSON(http.StatusInternalServerError, private.HookPostReceiveResult{ + Err: err.Error(), + }) + return } - if !isAllowed { log.Warn("Forbidden: Tag %s in %-v is protected", tagName, repo) ctx.JSON(http.StatusForbidden, private.HookPostReceiveResult{ - Err: fmt.Sprintf("tag %s is protected", tagName), + Err: fmt.Sprintf("Tag %s is protected", tagName), }) return } diff --git a/services/release/release.go b/services/release/release.go index e6ffe077f2b23..8d3d35518b63b 100644 --- a/services/release/release.go +++ b/services/release/release.go @@ -28,22 +28,10 @@ func createTag(gitRepo *git.Repository, rel *models.Release, msg string) (bool, if err != nil { return false, fmt.Errorf("GetProtectedTags: %v", err) } - isAllowed := true - for _, tag := range protectedTags { - if err := tag.EnsureCompiledPattern(); err != nil { - return false, fmt.Errorf("EnsureCompiledPattern: %v", err) - } - - if !tag.NameGlob.Match(rel.TagName) { - continue - } - - isAllowed = tag.IsUserAllowed(rel.PublisherID) - if isAllowed { - break - } + isAllowed, err := models.IsUserAllowedToControlTag(protectedTags, rel.TagName, rel.PublisherID) + if err != nil { + return false, err } - if !isAllowed { return false, models.ErrInvalidTagName{ TagName: rel.TagName, From 751c13b0f047230dc96d1e1a7323e2f2ebbc281b Mon Sep 17 00:00:00 2001 From: KN4CK3R Date: Tue, 27 Apr 2021 07:31:00 +0200 Subject: [PATCH 05/35] Added suggestions. --- cmd/hook.go | 2 +- integrations/repo_tag_test.go | 2 -- models/migrations/v180.go | 14 +----------- models/repo.go | 2 ++ models/tags.go | 21 +++++++++--------- routers/repo/release.go | 5 ++++- services/mirror/mirror_test.go | 1 + services/release/release.go | 38 ++++++++++++++++---------------- services/release/release_test.go | 16 ++++++++++++++ 9 files changed, 55 insertions(+), 46 deletions(-) diff --git a/cmd/hook.go b/cmd/hook.go index 45b7d7ceb4e13..2f264dc5b9648 100644 --- a/cmd/hook.go +++ b/cmd/hook.go @@ -221,7 +221,7 @@ Gitea or set your environment appropriately.`, "") total++ lastline++ - // If the ref is a branch, check if it's protected + // If the ref is a branch or tag, check if it's protected if strings.HasPrefix(refFullName, git.BranchPrefix) || strings.HasPrefix(refFullName, git.TagPrefix) { oldCommitIDs[count] = oldCommitID newCommitIDs[count] = newCommitID diff --git a/integrations/repo_tag_test.go b/integrations/repo_tag_test.go index 27ea719552673..f2b93b4991d68 100644 --- a/integrations/repo_tag_test.go +++ b/integrations/repo_tag_test.go @@ -136,8 +136,6 @@ func TestCreateNewTagProtected(t *testing.T) { }) t.Run("Git", func(t *testing.T) { - defer PrintCurrentTest(t)() - onGiteaRun(t, func(t *testing.T, u *url.URL) { username := "user2" httpContext := NewAPITestContext(t, username, "repo1") diff --git a/models/migrations/v180.go b/models/migrations/v180.go index 616e28214c765..417fd00181d0a 100644 --- a/models/migrations/v180.go +++ b/models/migrations/v180.go @@ -5,8 +5,6 @@ package migrations import ( - "fmt" - "code.gitea.io/gitea/modules/timeutil" "xorm.io/xorm" @@ -24,15 +22,5 @@ func createProtectedTagTable(x *xorm.Engine) error { UpdatedUnix timeutil.TimeStamp `xorm:"updated"` } - sess := x.NewSession() - defer sess.Close() - if err := sess.Begin(); err != nil { - return err - } - - if err := sess.Sync2(new(ProtectedTag)); err != nil { - return fmt.Errorf("Sync2: %v", err) - } - - return sess.Commit() + return x.Sync2(new(ProtectedTag)) } diff --git a/models/repo.go b/models/repo.go index fc673cace8b57..02d09a863c7ba 100644 --- a/models/repo.go +++ b/models/repo.go @@ -1472,6 +1472,8 @@ func DeleteRepository(doer *User, uid, repoID int64) error { &LanguageStat{RepoID: repoID}, &Comment{RefRepoID: repoID}, &Task{RepoID: repoID}, + &ProtectedBranch{RepoID: repoID}, + &ProtectedTag{RepoID: repoID}, ); err != nil { return fmt.Errorf("deleteBeans: %v", err) } diff --git a/models/tags.go b/models/tags.go index fbbbc6b9ee961..beb7c0e229bcb 100644 --- a/models/tags.go +++ b/models/tags.go @@ -8,7 +8,6 @@ import ( "strings" "code.gitea.io/gitea/modules/base" - "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/timeutil" "github.com/gobwas/glob" @@ -70,21 +69,20 @@ func (pt *ProtectedTag) EnsureCompiledPattern() error { } // IsUserAllowed returns true if the user is allowed to modify the tag -func (pt *ProtectedTag) IsUserAllowed(userID int64) bool { +func (pt *ProtectedTag) IsUserAllowed(userID int64) (bool, error) { if base.Int64sContains(pt.WhitelistUserIDs, userID) { - return true + return true, nil } if len(pt.WhitelistTeamIDs) == 0 { - return false + return false, nil } in, err := IsUserInTeams(userID, pt.WhitelistTeamIDs) if err != nil { - log.Error("IsUserInTeams: %v", err) - return false + return false, err } - return in + return in, nil } // GetProtectedTags gets all protected tags @@ -98,8 +96,8 @@ func (repo *Repository) GetProtectedTags() ([]*ProtectedTag, error) { func IsUserAllowedToControlTag(tags []*ProtectedTag, tagName string, userID int64) (bool, error) { isAllowed := true for _, tag := range tags { - if err := tag.EnsureCompiledPattern(); err != nil { - log.Error("EnsureCompiledPattern failed: %v", err) + err := tag.EnsureCompiledPattern() + if err != nil { return false, err } @@ -107,7 +105,10 @@ func IsUserAllowedToControlTag(tags []*ProtectedTag, tagName string, userID int6 continue } - isAllowed = tag.IsUserAllowed(userID) + isAllowed, err = tag.IsUserAllowed(userID) + if err != nil { + return false, err + } if isAllowed { break } diff --git a/routers/repo/release.go b/routers/repo/release.go index 0daf62dba0d6b..c838f345a3116 100644 --- a/routers/repo/release.go +++ b/routers/repo/release.go @@ -298,7 +298,9 @@ func NewReleasePost(ctx *context.Context) { ctx.Flash.Error(ctx.Tr("repo.branch.tag_collision", e.TagName)) ctx.Redirect(ctx.Repo.RepoLink + "/src/" + ctx.Repo.BranchNameSubURL()) return - } else if models.IsErrInvalidTagName(err) { + } + + if models.IsErrInvalidTagName(err) { e := err.(models.ErrInvalidTagName) if e.Protected { ctx.Flash.Error(ctx.Tr("repo.release.tag_name_protected")) @@ -322,6 +324,7 @@ func NewReleasePost(ctx *context.Context) { RepoID: ctx.Repo.Repository.ID, Repo: ctx.Repo.Repository, PublisherID: ctx.User.ID, + Publisher: ctx.User, Title: form.Title, TagName: form.TagName, Target: form.Target, diff --git a/services/mirror/mirror_test.go b/services/mirror/mirror_test.go index cb93578c0ec5c..aad9a694f053f 100644 --- a/services/mirror/mirror_test.go +++ b/services/mirror/mirror_test.go @@ -65,6 +65,7 @@ func TestRelease_MirrorDelete(t *testing.T) { RepoID: repo.ID, Repo: repo, PublisherID: user.ID, + Publisher: user, TagName: "v0.2", Target: "master", Title: "v0.2 is released", diff --git a/services/release/release.go b/services/release/release.go index 8d3d35518b63b..ddbfd6a7cd43c 100644 --- a/services/release/release.go +++ b/services/release/release.go @@ -23,20 +23,23 @@ func createTag(gitRepo *git.Repository, rel *models.Release, msg string) (bool, // Only actual create when publish. if !rel.IsDraft { if !gitRepo.IsTagExist(rel.TagName) { - if rel.Repo != nil { - protectedTags, err := rel.Repo.GetProtectedTags() - if err != nil { - return false, fmt.Errorf("GetProtectedTags: %v", err) - } - isAllowed, err := models.IsUserAllowedToControlTag(protectedTags, rel.TagName, rel.PublisherID) - if err != nil { - return false, err - } - if !isAllowed { - return false, models.ErrInvalidTagName{ - TagName: rel.TagName, - Protected: true, - } + if err := rel.LoadAttributes(); err != nil { + log.Error("LoadAttributes: %v", err) + return false, err + } + + protectedTags, err := rel.Repo.GetProtectedTags() + if err != nil { + return false, fmt.Errorf("GetProtectedTags: %v", err) + } + isAllowed, err := models.IsUserAllowedToControlTag(protectedTags, rel.TagName, rel.PublisherID) + if err != nil { + return false, err + } + if !isAllowed { + return false, models.ErrInvalidTagName{ + TagName: rel.TagName, + Protected: true, } } @@ -66,11 +69,7 @@ func createTag(gitRepo *git.Repository, rel *models.Release, msg string) (bool, } created = true rel.LowerTagName = strings.ToLower(rel.TagName) - // Prepare Notify - if err := rel.LoadAttributes(); err != nil { - log.Error("LoadAttributes: %v", err) - return false, err - } + notification.NotifyPushCommits( rel.Publisher, rel.Repo, &repository.PushUpdateOptions{ @@ -156,6 +155,7 @@ func CreateNewTag(doer *models.User, repo *models.Repository, commit, tagName, m RepoID: repo.ID, Repo: repo, PublisherID: doer.ID, + Publisher: doer, TagName: tagName, Target: commit, IsDraft: false, diff --git a/services/release/release_test.go b/services/release/release_test.go index 5fb9e4b88d41f..56b8e05000395 100644 --- a/services/release/release_test.go +++ b/services/release/release_test.go @@ -35,6 +35,7 @@ func TestRelease_Create(t *testing.T) { RepoID: repo.ID, Repo: repo, PublisherID: user.ID, + Publisher: user, TagName: "v0.1", Target: "master", Title: "v0.1 is released", @@ -48,6 +49,7 @@ func TestRelease_Create(t *testing.T) { RepoID: repo.ID, Repo: repo, PublisherID: user.ID, + Publisher: user, TagName: "v0.1.1", Target: "65f1bf27bc3bf70f64657658635e66094edbcb4d", Title: "v0.1.1 is released", @@ -61,6 +63,7 @@ func TestRelease_Create(t *testing.T) { RepoID: repo.ID, Repo: repo, PublisherID: user.ID, + Publisher: user, TagName: "v0.1.2", Target: "65f1bf2", Title: "v0.1.2 is released", @@ -74,6 +77,7 @@ func TestRelease_Create(t *testing.T) { RepoID: repo.ID, Repo: repo, PublisherID: user.ID, + Publisher: user, TagName: "v0.1.3", Target: "65f1bf2", Title: "v0.1.3 is released", @@ -87,6 +91,7 @@ func TestRelease_Create(t *testing.T) { RepoID: repo.ID, Repo: repo, PublisherID: user.ID, + Publisher: user, TagName: "v0.1.4", Target: "65f1bf2", Title: "v0.1.4 is released", @@ -106,6 +111,7 @@ func TestRelease_Create(t *testing.T) { RepoID: repo.ID, Repo: repo, PublisherID: user.ID, + Publisher: user, TagName: "v0.1.5", Target: "65f1bf2", Title: "v0.1.5 is released", @@ -133,6 +139,7 @@ func TestRelease_Update(t *testing.T) { RepoID: repo.ID, Repo: repo, PublisherID: user.ID, + Publisher: user, TagName: "v1.1.1", Target: "master", Title: "v1.1.1 is released", @@ -156,6 +163,7 @@ func TestRelease_Update(t *testing.T) { RepoID: repo.ID, Repo: repo, PublisherID: user.ID, + Publisher: user, TagName: "v1.2.1", Target: "65f1bf2", Title: "v1.2.1 is draft", @@ -179,6 +187,7 @@ func TestRelease_Update(t *testing.T) { RepoID: repo.ID, Repo: repo, PublisherID: user.ID, + Publisher: user, TagName: "v1.3.1", Target: "65f1bf2", Title: "v1.3.1 is pre-released", @@ -203,6 +212,7 @@ func TestRelease_Update(t *testing.T) { RepoID: repo.ID, Repo: repo, PublisherID: user.ID, + Publisher: user, TagName: "v1.1.2", Target: "master", Title: "v1.1.2 is released", @@ -268,7 +278,9 @@ func TestRelease_createTag(t *testing.T) { // Test a changed release release := &models.Release{ RepoID: repo.ID, + Repo: repo, PublisherID: user.ID, + Publisher: user, TagName: "v2.1.1", Target: "master", Title: "v2.1.1 is released", @@ -290,7 +302,9 @@ func TestRelease_createTag(t *testing.T) { // Test a changed draft release = &models.Release{ RepoID: repo.ID, + Repo: repo, PublisherID: user.ID, + Publisher: user, TagName: "v2.2.1", Target: "65f1bf2", Title: "v2.2.1 is draft", @@ -311,7 +325,9 @@ func TestRelease_createTag(t *testing.T) { // Test a changed pre-release release = &models.Release{ RepoID: repo.ID, + Repo: repo, PublisherID: user.ID, + Publisher: user, TagName: "v2.3.1", Target: "65f1bf2", Title: "v2.3.1 is pre-released", From 932a67702d7fc22def7c5efbb4bac005875032dc Mon Sep 17 00:00:00 2001 From: KN4CK3R Date: Tue, 27 Apr 2021 07:32:00 +0200 Subject: [PATCH 06/35] Renamed file. --- models/{tags.go => protected_tag.go} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename models/{tags.go => protected_tag.go} (100%) diff --git a/models/tags.go b/models/protected_tag.go similarity index 100% rename from models/tags.go rename to models/protected_tag.go From 922cab127ab285e3bed0fccb58f8b1ff951ee31e Mon Sep 17 00:00:00 2001 From: KN4CK3R Date: Tue, 27 Apr 2021 07:41:00 +0200 Subject: [PATCH 07/35] Added suggestions. --- routers/repo/tag.go | 84 +++++++++++++++---------------- routers/routes/web.go | 4 +- templates/repo/settings/tags.tmpl | 4 +- 3 files changed, 43 insertions(+), 49 deletions(-) diff --git a/routers/repo/tag.go b/routers/repo/tag.go index 8b9ac58c8722b..1e8f630e9bd30 100644 --- a/routers/repo/tag.go +++ b/routers/repo/tag.go @@ -27,60 +27,56 @@ func Tags(ctx *context.Context) { ctx.HTML(http.StatusOK, tplTags) } -// TagPost response for protect for a branch of a repository +// TagPost handles creation of a protect tag func TagPost(ctx *context.Context) { if setTagsContext(ctx) != nil { return } - repo := ctx.Repo.Repository - - switch ctx.Query("action") { - case "create_protected_tag": - web.Bind(forms.ProtectTagForm{})(ctx.Resp, ctx.Req) - if ctx.HasError() { - ctx.HTML(http.StatusOK, tplTags) - return - } - - form := web.GetForm(ctx).(*forms.ProtectTagForm) + if ctx.HasError() { + ctx.HTML(http.StatusOK, tplTags) + return + } - pt := &models.ProtectedTag{ - RepoID: repo.ID, - NamePattern: form.NamePattern, - } + repo := ctx.Repo.Repository + form := web.GetForm(ctx).(*forms.ProtectTagForm) - if strings.TrimSpace(form.WhitelistUsers) != "" { - pt.WhitelistUserIDs, _ = base.StringsToInt64s(strings.Split(form.WhitelistUsers, ",")) - } - if strings.TrimSpace(form.WhitelistTeams) != "" { - pt.WhitelistTeamIDs, _ = base.StringsToInt64s(strings.Split(form.WhitelistTeams, ",")) - } + pt := &models.ProtectedTag{ + RepoID: repo.ID, + NamePattern: form.NamePattern, + } - if err := models.InsertProtectedTag(pt); err != nil { - ctx.ServerError("InsertProtectedTag", err) - return - } + if strings.TrimSpace(form.WhitelistUsers) != "" { + pt.WhitelistUserIDs, _ = base.StringsToInt64s(strings.Split(form.WhitelistUsers, ",")) + } + if strings.TrimSpace(form.WhitelistTeams) != "" { + pt.WhitelistTeamIDs, _ = base.StringsToInt64s(strings.Split(form.WhitelistTeams, ",")) + } - ctx.Flash.Success(ctx.Tr("repo.settings.update_settings_success")) - ctx.Redirect(setting.AppSubURL + ctx.Req.URL.Path) - case "remove_protected_tag": - pt, err := selectProtectedTagByContext(ctx, repo) - if err != nil { - ctx.NotFound("", nil) - return - } + if err := models.InsertProtectedTag(pt); err != nil { + ctx.ServerError("InsertProtectedTag", err) + return + } - if err := models.DeleteProtectedTag(pt); err != nil { - ctx.ServerError("DeleteProtectedTag", err) - return - } + ctx.Flash.Success(ctx.Tr("repo.settings.update_settings_success")) + ctx.Redirect(setting.AppSubURL + ctx.Req.URL.Path) +} - ctx.Flash.Success(ctx.Tr("repo.settings.update_settings_success")) - ctx.Redirect(setting.AppSubURL + ctx.Req.URL.Path) - default: +// TagDelete handles deletion of a protected tag +func TagDelete(ctx *context.Context) { + pt, err := selectProtectedTagByContext(ctx) + if err != nil { ctx.NotFound("", nil) + return + } + + if err := models.DeleteProtectedTag(pt); err != nil { + ctx.ServerError("DeleteProtectedTag", err) + return } + + ctx.Flash.Success(ctx.Tr("repo.settings.update_settings_success")) + ctx.Redirect(ctx.Repo.Repository.Link() + "/settings/tags") } func setTagsContext(ctx *context.Context) error { @@ -113,8 +109,8 @@ func setTagsContext(ctx *context.Context) error { return nil } -func selectProtectedTagByContext(ctx *context.Context, repo *models.Repository) (*models.ProtectedTag, error) { - pts, err := repo.GetProtectedTags() +func selectProtectedTagByContext(ctx *context.Context) (*models.ProtectedTag, error) { + pts, err := ctx.Repo.Repository.GetProtectedTags() if err != nil { return nil, err } @@ -127,5 +123,5 @@ func selectProtectedTagByContext(ctx *context.Context, repo *models.Repository) } } - return nil, fmt.Errorf("ProtectedTag[%v] not associated to repository %v", id, repo) + return nil, fmt.Errorf("ProtectedTag[%v] not associated to repository %v", id, ctx.Repo.Repository) } diff --git a/routers/routes/web.go b/routers/routes/web.go index 76438c2145bf7..cdc9b819033cf 100644 --- a/routers/routes/web.go +++ b/routers/routes/web.go @@ -718,8 +718,8 @@ func RegisterRoutes(m *web.Route) { }, repo.MustBeNotEmpty) m.Group("/tags", func() { - m.Combo("").Get(repo.Tags). - Post(context.RepoMustNotBeArchived(), repo.TagPost) + m.Combo("").Get(repo.Tags).Post(bindIgnErr(forms.ProtectTagForm{}), context.RepoMustNotBeArchived(), repo.TagPost) + m.Post("/delete", context.RepoMustNotBeArchived(), repo.TagDelete) }) m.Group("/hooks/git", func() { diff --git a/templates/repo/settings/tags.tmpl b/templates/repo/settings/tags.tmpl index 4530a8850a5f3..4215a1b251fa7 100644 --- a/templates/repo/settings/tags.tmpl +++ b/templates/repo/settings/tags.tmpl @@ -19,7 +19,6 @@
    {{.CsrfTokenHtml}} -
    {{end}}
    - +
    @@ -99,7 +105,8 @@ {{end}} -
    + {{$.i18n.Tr "edit"}} + {{$.CsrfTokenHtml}} From e5a68046e7b7948116c682e5abfbc9958bfc94b6 Mon Sep 17 00:00:00 2001 From: KN4CK3R Date: Sat, 1 May 2021 10:30:52 +0000 Subject: [PATCH 17/35] lint --- models/protected_tag.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/models/protected_tag.go b/models/protected_tag.go index e9c25b5da2c8e..db436f08f4b1d 100644 --- a/models/protected_tag.go +++ b/models/protected_tag.go @@ -15,7 +15,7 @@ import ( // ProtectedTag struct type ProtectedTag struct { - ID int64 `xorm:"pk autoincr"` + ID int64 `xorm:"pk autoincr"` RepoID int64 NamePattern string NameGlob glob.Glob `xorm:"-"` From f3a4d02fbeb280b658f13bb3a4810018e0456a63 Mon Sep 17 00:00:00 2001 From: KN4CK3R Date: Tue, 11 May 2021 14:24:00 +0000 Subject: [PATCH 18/35] Removed unique key from migration. --- models/migrations/v180.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/models/migrations/v180.go b/models/migrations/v180.go index 417fd00181d0a..fcbf8beac0832 100644 --- a/models/migrations/v180.go +++ b/models/migrations/v180.go @@ -12,9 +12,9 @@ import ( func createProtectedTagTable(x *xorm.Engine) error { type ProtectedTag struct { - ID int64 `xorm:"pk autoincr"` - RepoID int64 `xorm:"UNIQUE(s)"` - NamePattern string `xorm:"UNIQUE(s)"` + ID int64 `xorm:"pk autoincr"` + RepoID int64 + NamePattern string WhitelistUserIDs []int64 `xorm:"JSON TEXT"` WhitelistTeamIDs []int64 `xorm:"JSON TEXT"` From 7080e285ad5bb6616842b38eb559e14c0a2f2474 Mon Sep 17 00:00:00 2001 From: KN4CK3R Date: Tue, 11 May 2021 14:25:45 +0000 Subject: [PATCH 19/35] Apply suggestion. --- models/protected_tag.go | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/models/protected_tag.go b/models/protected_tag.go index db436f08f4b1d..19039ba69b320 100644 --- a/models/protected_tag.go +++ b/models/protected_tag.go @@ -50,10 +50,8 @@ func (pt *ProtectedTag) EnsureCompiledPattern() error { return nil } - expr := strings.TrimSpace(pt.NamePattern) - var err error - pt.NameGlob, err = glob.Compile(expr) + pt.NameGlob, err = glob.Compile(strings.TrimSpace(pt.NamePattern)) return err } From 970a19b0cd80df866640cf0428cda793b995d493 Mon Sep 17 00:00:00 2001 From: KN4CK3R Date: Wed, 12 May 2021 07:14:00 +0200 Subject: [PATCH 20/35] Fixed comment. --- models/protected_tag.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/models/protected_tag.go b/models/protected_tag.go index 19039ba69b320..40bf514fc9f86 100644 --- a/models/protected_tag.go +++ b/models/protected_tag.go @@ -44,7 +44,7 @@ func DeleteProtectedTag(pt *ProtectedTag) error { return err } -// EnsureCompiledPattern returns if the branch is protected +// EnsureCompiledPattern ensures the glob pattern is compiled func (pt *ProtectedTag) EnsureCompiledPattern() error { if pt.NameGlob != nil { return nil @@ -72,7 +72,7 @@ func (pt *ProtectedTag) IsUserAllowed(userID int64) (bool, error) { return in, nil } -// GetProtectedTags gets all protected tags +// GetProtectedTags gets all protected tags of the repository func (repo *Repository) GetProtectedTags() ([]*ProtectedTag, error) { tags := make([]*ProtectedTag, 0) return tags, x.Find(&tags, &ProtectedTag{RepoID: repo.ID}) From 746692539dda238fdb1294b146fb305c7084e9c6 Mon Sep 17 00:00:00 2001 From: KN4CK3R Date: Thu, 13 May 2021 19:48:14 +0000 Subject: [PATCH 21/35] Get tag by id. --- models/protected_tag.go | 13 +++++++++++++ routers/repo/tag.go | 16 +++++++--------- 2 files changed, 20 insertions(+), 9 deletions(-) diff --git a/models/protected_tag.go b/models/protected_tag.go index 40bf514fc9f86..c08c7ff908c34 100644 --- a/models/protected_tag.go +++ b/models/protected_tag.go @@ -78,6 +78,19 @@ func (repo *Repository) GetProtectedTags() ([]*ProtectedTag, error) { return tags, x.Find(&tags, &ProtectedTag{RepoID: repo.ID}) } +// GetProtectedTagByID gets the protected tag with the specific id +func GetProtectedTagByID(id int64) (*ProtectedTag, error) { + tag := &ProtectedTag{ID: id} + has, err := x.Get(tag) + if err != nil { + return nil, err + } + if !has { + return nil, nil + } + return tag, nil +} + // IsUserAllowedToControlTag checks if a user can control the specific tag. // It returns true if the tag name is not protected or the user is allowed to control it. func IsUserAllowedToControlTag(tags []*ProtectedTag, tagName string, userID int64) (bool, error) { diff --git a/routers/repo/tag.go b/routers/repo/tag.go index 33ee0fac87abe..8286493df06f6 100644 --- a/routers/repo/tag.go +++ b/routers/repo/tag.go @@ -164,20 +164,18 @@ func setTagsContext(ctx *context.Context) error { } func selectProtectedTagByContext(ctx *context.Context) (*models.ProtectedTag, error) { - pts, err := ctx.Repo.Repository.GetProtectedTags() - if err != nil { - return nil, err - } - id := ctx.QueryInt64("id") if id == 0 { id = ctx.ParamsInt64(":id") } - for _, pt := range pts { - if pt.ID == id { - return pt, nil - } + tag, err := models.GetProtectedTagByID(id) + if tag == nil || err != nil { + return nil, err + } + + if tag.RepoID == ctx.Repo.Repository.ID { + return tag, nil } return nil, fmt.Errorf("ProtectedTag[%v] not associated to repository %v", id, ctx.Repo.Repository) From a5ecd2c0b81129d142647d242607209b66c3867b Mon Sep 17 00:00:00 2001 From: KN4CK3R Date: Thu, 13 May 2021 20:15:45 +0000 Subject: [PATCH 22/35] Added docs page. --- .../doc/advanced/protected-tags.en-us.md | 46 +++++++++++++++++++ 1 file changed, 46 insertions(+) create mode 100644 docs/content/doc/advanced/protected-tags.en-us.md diff --git a/docs/content/doc/advanced/protected-tags.en-us.md b/docs/content/doc/advanced/protected-tags.en-us.md new file mode 100644 index 0000000000000..a08760b9d0bad --- /dev/null +++ b/docs/content/doc/advanced/protected-tags.en-us.md @@ -0,0 +1,46 @@ +--- +date: "2019-09-06T01:35:00-03:00" +title: "Protected tags" +slug: "protected-tags" +weight: 45 +toc: false +draft: false +menu: + sidebar: + parent: "advanced" + name: "Protected tags" + weight: 45 + identifier: "protected-tags" +--- + +# Protected tags + +Protected tags allow control over who has permission to create or update git tags. Each rule allows you to match either an individual tag name, or use wildcards to control multiple tags at once. + +**Table of Contents** + +{{< toc >}} + +## Setting up protected tags + +To protect a tag, you need to follow these steps: + +1. Go to the repository’s **Settings** > **Tags** page. +2. Type the name of specific tag or use a pattern to match multiple tags at once. +3. Choose the allowed users and/or teams. If you leave these fields empty noone is allowed to create or modify this tag. +4. Select **Save** to save the configuration. + +## Wildcard protected tags + +You can specify a wildcard protected tag, which protects all tags matching the wildcard. For example: + +| Wildcard Protected Tag | Matching Tags | +| ---------------------- | --------------------------------------- | +| `v*` | `v`, `v-1`, `version2` | +| `v[0-9]` | `v0`, `v1` up to `v9` | +| `*-release` | `2.1-release`, `final-release` | +| `*gitea*` | `gitea`, `2.1-gitea`, `1_gitea-release` | +| `{v,rel}-*` | `v-`, `v-1`, `v-final`, `rel-`, `rel-x` | +| `*` | matches all possible tag names | + +See [github.com/gobwas/glob](https://pkg.go.dev/github.com/gobwas/glob#Compile) documentation for syntax. From d814f026c75069d401c28d93e895097f19d1cbea Mon Sep 17 00:00:00 2001 From: KN4CK3R Date: Fri, 14 May 2021 19:29:36 +0000 Subject: [PATCH 23/35] Changed date. --- docs/content/doc/advanced/protected-tags.en-us.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/content/doc/advanced/protected-tags.en-us.md b/docs/content/doc/advanced/protected-tags.en-us.md index a08760b9d0bad..27019d13e68af 100644 --- a/docs/content/doc/advanced/protected-tags.en-us.md +++ b/docs/content/doc/advanced/protected-tags.en-us.md @@ -1,5 +1,5 @@ --- -date: "2019-09-06T01:35:00-03:00" +date: "2021-05-14T00:00:00-00:00" title: "Protected tags" slug: "protected-tags" weight: 45 From 7cbbe4ec2ead426d05cb15a78f32088eaf389e7c Mon Sep 17 00:00:00 2001 From: KN4CK3R Date: Tue, 18 May 2021 14:48:59 +0000 Subject: [PATCH 24/35] Respond with 404 to not found tags. --- routers/repo/tag.go | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/routers/repo/tag.go b/routers/repo/tag.go index 8286493df06f6..8bfef75d055f3 100644 --- a/routers/repo/tag.go +++ b/routers/repo/tag.go @@ -69,9 +69,8 @@ func EditProtectedTag(ctx *context.Context) { ctx.Data["PageIsEditProtectedTag"] = true - pt, err := selectProtectedTagByContext(ctx) - if err != nil { - ctx.NotFound("", err) + pt := selectProtectedTagByContext(ctx) + if pt == nil { return } @@ -95,9 +94,8 @@ func EditProtectedTagPost(ctx *context.Context) { return } - pt, err := selectProtectedTagByContext(ctx) - if err != nil { - ctx.NotFound("", err) + pt := selectProtectedTagByContext(ctx) + if pt == nil { return } @@ -118,9 +116,8 @@ func EditProtectedTagPost(ctx *context.Context) { // DeleteProtectedTagPost handles deletion of a protected tag func DeleteProtectedTagPost(ctx *context.Context) { - pt, err := selectProtectedTagByContext(ctx) - if err != nil { - ctx.NotFound("", err) + pt := selectProtectedTagByContext(ctx) + if pt == nil { return } @@ -163,20 +160,23 @@ func setTagsContext(ctx *context.Context) error { return nil } -func selectProtectedTagByContext(ctx *context.Context) (*models.ProtectedTag, error) { +func selectProtectedTagByContext(ctx *context.Context) *models.ProtectedTag { id := ctx.QueryInt64("id") if id == 0 { id = ctx.ParamsInt64(":id") } tag, err := models.GetProtectedTagByID(id) - if tag == nil || err != nil { - return nil, err + if err != nil { + ctx.ServerError("GetProtectedTagByID", err) + return nil } - if tag.RepoID == ctx.Repo.Repository.ID { - return tag, nil + if tag != nil && tag.RepoID == ctx.Repo.Repository.ID { + return tag } - return nil, fmt.Errorf("ProtectedTag[%v] not associated to repository %v", id, ctx.Repo.Repository) + ctx.NotFound("", fmt.Errorf("ProtectedTag[%v] not associated to repository %v", id, ctx.Repo.Repository)) + + return nil } From deebd92f656c5a95f04606a528a51daea3eed4eb Mon Sep 17 00:00:00 2001 From: KN4CK3R Date: Tue, 18 May 2021 14:57:31 +0000 Subject: [PATCH 25/35] Handle id = 0. --- models/protected_tag.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/models/protected_tag.go b/models/protected_tag.go index c08c7ff908c34..a409b5cc1d331 100644 --- a/models/protected_tag.go +++ b/models/protected_tag.go @@ -80,8 +80,8 @@ func (repo *Repository) GetProtectedTags() ([]*ProtectedTag, error) { // GetProtectedTagByID gets the protected tag with the specific id func GetProtectedTagByID(id int64) (*ProtectedTag, error) { - tag := &ProtectedTag{ID: id} - has, err := x.Get(tag) + tag := new(ProtectedTag) + has, err := e.ID(id).Get(tag) if err != nil { return nil, err } From 4b947208b3dec3d2538c0b460107712acda843ed Mon Sep 17 00:00:00 2001 From: KN4CK3R Date: Tue, 18 May 2021 15:56:16 +0000 Subject: [PATCH 26/35] Lint --- models/protected_tag.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/models/protected_tag.go b/models/protected_tag.go index a409b5cc1d331..9533436a85cdb 100644 --- a/models/protected_tag.go +++ b/models/protected_tag.go @@ -81,7 +81,7 @@ func (repo *Repository) GetProtectedTags() ([]*ProtectedTag, error) { // GetProtectedTagByID gets the protected tag with the specific id func GetProtectedTagByID(id int64) (*ProtectedTag, error) { tag := new(ProtectedTag) - has, err := e.ID(id).Get(tag) + has, err := x.ID(id).Get(tag) if err != nil { return nil, err } From ddf3ece1444ee9a199704fea982ea4ce5d88ca1e Mon Sep 17 00:00:00 2001 From: KN4CK3R Date: Tue, 25 May 2021 19:42:04 +0000 Subject: [PATCH 27/35] Replaced glob with regex pattern. --- .../doc/advanced/protected-tags.en-us.md | 32 +++++----- models/protected_tag.go | 15 +++-- models/protected_tag_test.go | 4 +- modules/validation/binding.go | 22 +++++++ modules/validation/binding_test.go | 7 ++- modules/validation/regex_pattern_test.go | 60 +++++++++++++++++++ modules/web/middleware/binding.go | 2 + options/locale/locale_en-US.ini | 3 +- services/forms/repo_tag_form.go | 2 +- templates/repo/settings/tags.tmpl | 4 +- 10 files changed, 119 insertions(+), 32 deletions(-) create mode 100644 modules/validation/regex_pattern_test.go diff --git a/docs/content/doc/advanced/protected-tags.en-us.md b/docs/content/doc/advanced/protected-tags.en-us.md index 27019d13e68af..9c9af30943c4b 100644 --- a/docs/content/doc/advanced/protected-tags.en-us.md +++ b/docs/content/doc/advanced/protected-tags.en-us.md @@ -15,7 +15,7 @@ menu: # Protected tags -Protected tags allow control over who has permission to create or update git tags. Each rule allows you to match either an individual tag name, or use wildcards to control multiple tags at once. +Protected tags allow control over who has permission to create or update git tags. Each rule allows you to match either an individual tag name, or use an appropriate pattern to control multiple tags at once. **Table of Contents** @@ -26,21 +26,23 @@ Protected tags allow control over who has permission to create or update git tag To protect a tag, you need to follow these steps: 1. Go to the repository’s **Settings** > **Tags** page. -2. Type the name of specific tag or use a pattern to match multiple tags at once. -3. Choose the allowed users and/or teams. If you leave these fields empty noone is allowed to create or modify this tag. -4. Select **Save** to save the configuration. +1. Type a regular expression pattern to match a name. +1. Choose the allowed users and/or teams. If you leave these fields empty noone is allowed to create or modify this tag. +1. Select **Save** to save the configuration. -## Wildcard protected tags +## Pattern protected tags -You can specify a wildcard protected tag, which protects all tags matching the wildcard. For example: +The pattern uses regular expressions to match a tag name. Examples: -| Wildcard Protected Tag | Matching Tags | +| Pattern Protected Tag | Possible Matching Tags | | ---------------------- | --------------------------------------- | -| `v*` | `v`, `v-1`, `version2` | -| `v[0-9]` | `v0`, `v1` up to `v9` | -| `*-release` | `2.1-release`, `final-release` | -| `*gitea*` | `gitea`, `2.1-gitea`, `1_gitea-release` | -| `{v,rel}-*` | `v-`, `v-1`, `v-final`, `rel-`, `rel-x` | -| `*` | matches all possible tag names | - -See [github.com/gobwas/glob](https://pkg.go.dev/github.com/gobwas/glob#Compile) documentation for syntax. +| `\Av` | `v`, `v-1`, `version2` | +| `\Av[0-9]\z` | `v0`, `v1` up to `v9` | +| `\Av\d+\.\d+\.\d+\z` | `v1.0.17`, `v2.1.0` | +| `\Av\d+(\.\d+){0,2}\z` | `v1`, `v2.1`, `v1.2.34` | +| `-release\z` | `2.1-release`, `final-release` | +| `gitea` | `gitea`, `2.1-gitea`, `1_gitea-release` | +| `\Agitea\z` | only `gitea` | +| `^gitea$` | only `gitea` | +| `\A(v\|rel)-` | `v-`, `v-1`, `v-final`, `rel-`, `rel-x` | +| `.+` | matches all possible tag names | diff --git a/models/protected_tag.go b/models/protected_tag.go index 9533436a85cdb..b677b16589550 100644 --- a/models/protected_tag.go +++ b/models/protected_tag.go @@ -5,12 +5,11 @@ package models import ( + "regexp" "strings" "code.gitea.io/gitea/modules/base" "code.gitea.io/gitea/modules/timeutil" - - "github.com/gobwas/glob" ) // ProtectedTag struct @@ -18,9 +17,9 @@ type ProtectedTag struct { ID int64 `xorm:"pk autoincr"` RepoID int64 NamePattern string - NameGlob glob.Glob `xorm:"-"` - WhitelistUserIDs []int64 `xorm:"JSON TEXT"` - WhitelistTeamIDs []int64 `xorm:"JSON TEXT"` + CompiledPattern *regexp.Regexp `xorm:"-"` + WhitelistUserIDs []int64 `xorm:"JSON TEXT"` + WhitelistTeamIDs []int64 `xorm:"JSON TEXT"` CreatedUnix timeutil.TimeStamp `xorm:"created"` UpdatedUnix timeutil.TimeStamp `xorm:"updated"` @@ -46,12 +45,12 @@ func DeleteProtectedTag(pt *ProtectedTag) error { // EnsureCompiledPattern ensures the glob pattern is compiled func (pt *ProtectedTag) EnsureCompiledPattern() error { - if pt.NameGlob != nil { + if pt.CompiledPattern != nil { return nil } var err error - pt.NameGlob, err = glob.Compile(strings.TrimSpace(pt.NamePattern)) + pt.CompiledPattern, err = regexp.Compile(strings.TrimSpace(pt.NamePattern)) return err } @@ -101,7 +100,7 @@ func IsUserAllowedToControlTag(tags []*ProtectedTag, tagName string, userID int6 return false, err } - if !tag.NameGlob.Match(tagName) { + if !tag.CompiledPattern.MatchString(tagName) { continue } diff --git a/models/protected_tag_test.go b/models/protected_tag_test.go index 0d54f7eef7f74..ccd24d0963034 100644 --- a/models/protected_tag_test.go +++ b/models/protected_tag_test.go @@ -56,11 +56,11 @@ func TestIsUserAllowed(t *testing.T) { func TestIsUserAllowedToControlTag(t *testing.T) { protectedTags := []*ProtectedTag{ { - NamePattern: "*gitea", + NamePattern: `gitea\z`, WhitelistUserIDs: []int64{1}, }, { - NamePattern: "v-*", + NamePattern: `\Av-`, WhitelistUserIDs: []int64{2}, }, { diff --git a/modules/validation/binding.go b/modules/validation/binding.go index 5cfd994d2daeb..340c99e4307e4 100644 --- a/modules/validation/binding.go +++ b/modules/validation/binding.go @@ -19,6 +19,9 @@ const ( // ErrGlobPattern is returned when glob pattern is invalid ErrGlobPattern = "GlobPattern" + + // ErrRegexPattern is returned when a regex pattern is invalid + ErrRegexPattern = "RegexPattern" ) var ( @@ -53,6 +56,7 @@ func AddBindingRules() { addGitRefNameBindingRule() addValidURLBindingRule() addGlobPatternRule() + addRegexPatternRule() } func addGitRefNameBindingRule() { @@ -117,6 +121,24 @@ func addGlobPatternRule() { }) } +func addRegexPatternRule() { + binding.AddRule(&binding.Rule{ + IsMatch: func(rule string) bool { + return rule == "RegexPattern" + }, + IsValid: func(errs binding.Errors, name string, val interface{}) (bool, binding.Errors) { + str := fmt.Sprintf("%v", val) + + if _, err := regexp.Compile(str); err != nil { + errs.Add([]string{name}, ErrRegexPattern, err.Error()) + return false, errs + } + + return true, errs + }, + }) +} + func portOnly(hostport string) string { colon := strings.IndexByte(hostport, ':') if colon == -1 { diff --git a/modules/validation/binding_test.go b/modules/validation/binding_test.go index e0daba89e502a..d555119079e5c 100644 --- a/modules/validation/binding_test.go +++ b/modules/validation/binding_test.go @@ -26,9 +26,10 @@ type ( } TestForm struct { - BranchName string `form:"BranchName" binding:"GitRefName"` - URL string `form:"ValidUrl" binding:"ValidUrl"` - GlobPattern string `form:"GlobPattern" binding:"GlobPattern"` + BranchName string `form:"BranchName" binding:"GitRefName"` + URL string `form:"ValidUrl" binding:"ValidUrl"` + GlobPattern string `form:"GlobPattern" binding:"GlobPattern"` + RegexPattern string `form:"GlobPattern" binding:"RegexPattern"` } ) diff --git a/modules/validation/regex_pattern_test.go b/modules/validation/regex_pattern_test.go new file mode 100644 index 0000000000000..afe1bcf425dfa --- /dev/null +++ b/modules/validation/regex_pattern_test.go @@ -0,0 +1,60 @@ +// Copyright 2021 The Gitea Authors. All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. + +package validation + +import ( + "regexp" + "testing" + + "gitea.com/go-chi/binding" +) + +func getRegexPatternErrorString(pattern string) string { + if _, err := regexp.Compile(pattern); err != nil { + return err.Error() + } + return "" +} + +var regexValidationTestCases = []validationTestCase{ + { + description: "Empty regex pattern", + data: TestForm{ + RegexPattern: "", + }, + expectedErrors: binding.Errors{}, + }, + { + description: "Valid regex", + data: TestForm{ + RegexPattern: `(\d{1,3})+`, + }, + expectedErrors: binding.Errors{}, + }, + + { + description: "Invalid regex", + data: TestForm{ + RegexPattern: "[a-", + }, + expectedErrors: binding.Errors{ + binding.Error{ + FieldNames: []string{"RegexPattern"}, + Classification: ErrRegexPattern, + Message: getRegexPatternErrorString("[a-"), + }, + }, + }, +} + +func Test_RegexPatternValidation(t *testing.T) { + AddBindingRules() + + for _, testCase := range regexValidationTestCases { + t.Run(testCase.description, func(t *testing.T) { + performValidationTest(t, testCase) + }) + } +} diff --git a/modules/web/middleware/binding.go b/modules/web/middleware/binding.go index cd418c9792b7c..cbdb29b812942 100644 --- a/modules/web/middleware/binding.go +++ b/modules/web/middleware/binding.go @@ -135,6 +135,8 @@ func Validate(errs binding.Errors, data map[string]interface{}, f Form, l transl data["ErrorMsg"] = trName + l.Tr("form.include_error", GetInclude(field)) case validation.ErrGlobPattern: data["ErrorMsg"] = trName + l.Tr("form.glob_pattern_error", errs[0].Message) + case validation.ErrRegexPattern: + data["ErrorMsg"] = trName + l.Tr("form.regex_pattern_error", errs[0].Message) default: data["ErrorMsg"] = l.Tr("form.unknown_error") + " " + errs[0].Classification } diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini index 669af63eb8e2c..76faafa8c9067 100644 --- a/options/locale/locale_en-US.ini +++ b/options/locale/locale_en-US.ini @@ -368,6 +368,7 @@ email_error = ` is not a valid email address.` url_error = ` is not a valid URL.` include_error = ` must contain substring '%s'.` glob_pattern_error = ` glob pattern is invalid: %s.` +regex_pattern_error = ` regex pattern is invalid: %s.` unknown_error = Unknown error: captcha_incorrect = The CAPTCHA code is incorrect. password_not_match = The passwords do not match. @@ -1828,7 +1829,7 @@ settings.tags.protection.allowed.teams = Allowed teams settings.tags.protection.allowed.noone = No One settings.tags.protection.create = Protect Tag settings.tags.protection.none = There are no protected tags. -settings.tags.protection.pattern.description = Wildcards such as v* or *-release are supported. See github.com/gobwas/glob documentation for pattern syntax. +settings.tags.protection.pattern.description = Use regular expression pattern like \Av- or -release\z to match multiple tags. Read more in the protected tags guide. settings.bot_token = Bot Token settings.chat_id = Chat ID settings.matrix.homeserver_url = Homeserver URL diff --git a/services/forms/repo_tag_form.go b/services/forms/repo_tag_form.go index 03f4301451d81..b2908109ce686 100644 --- a/services/forms/repo_tag_form.go +++ b/services/forms/repo_tag_form.go @@ -15,7 +15,7 @@ import ( // ProtectTagForm form for changing protected tag settings type ProtectTagForm struct { - NamePattern string `binding:"Required;GlobPattern"` + NamePattern string `binding:"Required;RegexPattern"` WhitelistUsers string WhitelistTeams string } diff --git a/templates/repo/settings/tags.tmpl b/templates/repo/settings/tags.tmpl index 34e7d55ffcd43..d5cb5a47f8186 100644 --- a/templates/repo/settings/tags.tmpl +++ b/templates/repo/settings/tags.tmpl @@ -83,7 +83,7 @@ {{range .ProtectedTags}} - {{.NamePattern}} +
    {{.NamePattern}}
    {{if or .WhitelistUserIDs (and $.Owner.IsOrganization .WhitelistTeamIDs)}} {{$userIDs := .WhitelistUserIDs}} @@ -106,7 +106,7 @@ {{$.i18n.Tr "edit"}} - + {{$.CsrfTokenHtml}} From 3063811dd713a6dff1d8d2d0f51a1c406b285eb7 Mon Sep 17 00:00:00 2001 From: KN4CK3R Date: Wed, 9 Jun 2021 14:55:44 +0000 Subject: [PATCH 28/35] Added support for glob and regex pattern. --- models/protected_tag.go | 22 ++++++++++--- modules/validation/binding.go | 57 ++++++++++++++++++++++----------- routers/web/repo/tag.go | 4 +-- services/forms/repo_tag_form.go | 2 +- 4 files changed, 60 insertions(+), 25 deletions(-) diff --git a/models/protected_tag.go b/models/protected_tag.go index b677b16589550..ee99e64672c38 100644 --- a/models/protected_tag.go +++ b/models/protected_tag.go @@ -10,6 +10,8 @@ import ( "code.gitea.io/gitea/modules/base" "code.gitea.io/gitea/modules/timeutil" + + "github.com/gobwas/glob" ) // ProtectedTag struct @@ -17,7 +19,8 @@ type ProtectedTag struct { ID int64 `xorm:"pk autoincr"` RepoID int64 NamePattern string - CompiledPattern *regexp.Regexp `xorm:"-"` + RegexPattern *regexp.Regexp `xorm:"-"` + GlobPattern glob.Glob `xorm:"-"` WhitelistUserIDs []int64 `xorm:"JSON TEXT"` WhitelistTeamIDs []int64 `xorm:"JSON TEXT"` @@ -45,12 +48,16 @@ func DeleteProtectedTag(pt *ProtectedTag) error { // EnsureCompiledPattern ensures the glob pattern is compiled func (pt *ProtectedTag) EnsureCompiledPattern() error { - if pt.CompiledPattern != nil { + if pt.RegexPattern != nil || pt.GlobPattern != nil { return nil } var err error - pt.CompiledPattern, err = regexp.Compile(strings.TrimSpace(pt.NamePattern)) + if len(pt.NamePattern) >= 2 && strings.HasPrefix(pt.NamePattern, "/") && strings.HasSuffix(pt.NamePattern, "/") { + pt.RegexPattern, err = regexp.Compile(pt.NamePattern[1:len(pt.NamePattern)-1]) + } else { + pt.GlobPattern, err = glob.Compile(pt.NamePattern) + } return err } @@ -100,7 +107,7 @@ func IsUserAllowedToControlTag(tags []*ProtectedTag, tagName string, userID int6 return false, err } - if !tag.CompiledPattern.MatchString(tagName) { + if !tag.matchString(tagName) { continue } @@ -115,3 +122,10 @@ func IsUserAllowedToControlTag(tags []*ProtectedTag, tagName string, userID int6 return isAllowed, nil } + +func (pt *ProtectedTag) matchString(name string) bool { + if pt.RegexPattern != nil { + return pt.RegexPattern.MatchString(name) + } + return pt.GlobPattern.Match(name) +} diff --git a/modules/validation/binding.go b/modules/validation/binding.go index 340c99e4307e4..51bbd81b397f1 100644 --- a/modules/validation/binding.go +++ b/modules/validation/binding.go @@ -57,6 +57,7 @@ func AddBindingRules() { addValidURLBindingRule() addGlobPatternRule() addRegexPatternRule() + addGlobOrRegexPatternRule() } func addGitRefNameBindingRule() { @@ -106,19 +107,21 @@ func addGlobPatternRule() { IsMatch: func(rule string) bool { return rule == "GlobPattern" }, - IsValid: func(errs binding.Errors, name string, val interface{}) (bool, binding.Errors) { - str := fmt.Sprintf("%v", val) + IsValid: globPatternValidator, + }) +} - if len(str) != 0 { - if _, err := glob.Compile(str); err != nil { - errs.Add([]string{name}, ErrGlobPattern, err.Error()) - return false, errs - } - } +func globPatternValidator(errs binding.Errors, name string, val interface{}) (bool, binding.Errors) { + str := fmt.Sprintf("%v", val) - return true, errs - }, - }) + if len(str) != 0 { + if _, err := glob.Compile(str); err != nil { + errs.Add([]string{name}, ErrGlobPattern, err.Error()) + return false, errs + } + } + + return true, errs } func addRegexPatternRule() { @@ -126,15 +129,33 @@ func addRegexPatternRule() { IsMatch: func(rule string) bool { return rule == "RegexPattern" }, - IsValid: func(errs binding.Errors, name string, val interface{}) (bool, binding.Errors) { - str := fmt.Sprintf("%v", val) + IsValid: regexPatternValidator, + }) +} - if _, err := regexp.Compile(str); err != nil { - errs.Add([]string{name}, ErrRegexPattern, err.Error()) - return false, errs - } +func regexPatternValidator(errs binding.Errors, name string, val interface{}) (bool, binding.Errors) { + str := fmt.Sprintf("%v", val) - return true, errs + if _, err := regexp.Compile(str); err != nil { + errs.Add([]string{name}, ErrRegexPattern, err.Error()) + return false, errs + } + + return true, errs +} + +func addGlobOrRegexPatternRule() { + binding.AddRule(&binding.Rule{ + IsMatch: func(rule string) bool { + return rule == "GlobOrRegexPattern" + }, + IsValid: func(errs binding.Errors, name string, val interface{}) (bool, binding.Errors) { + str := strings.TrimSpace(fmt.Sprintf("%v", val)) + + if len(str) >= 2 && strings.HasPrefix(str, "/") && strings.HasSuffix(str, "/") { + return regexPatternValidator(errs, name, str[1:len(str)-1]) + } + return globPatternValidator(errs, name, val) }, }) } diff --git a/routers/web/repo/tag.go b/routers/web/repo/tag.go index 8bfef75d055f3..7d3021613e51c 100644 --- a/routers/web/repo/tag.go +++ b/routers/web/repo/tag.go @@ -42,7 +42,7 @@ func NewProtectedTagPost(ctx *context.Context) { pt := &models.ProtectedTag{ RepoID: repo.ID, - NamePattern: form.NamePattern, + NamePattern: strings.TrimSpace(form.NamePattern), } if strings.TrimSpace(form.WhitelistUsers) != "" { @@ -101,7 +101,7 @@ func EditProtectedTagPost(ctx *context.Context) { form := web.GetForm(ctx).(*forms.ProtectTagForm) - pt.NamePattern = form.NamePattern + pt.NamePattern = strings.TrimSpace(form.NamePattern) pt.WhitelistUserIDs, _ = base.StringsToInt64s(strings.Split(form.WhitelistUsers, ",")) pt.WhitelistTeamIDs, _ = base.StringsToInt64s(strings.Split(form.WhitelistTeams, ",")) diff --git a/services/forms/repo_tag_form.go b/services/forms/repo_tag_form.go index b2908109ce686..a877d6ad7ff34 100644 --- a/services/forms/repo_tag_form.go +++ b/services/forms/repo_tag_form.go @@ -15,7 +15,7 @@ import ( // ProtectTagForm form for changing protected tag settings type ProtectTagForm struct { - NamePattern string `binding:"Required;RegexPattern"` + NamePattern string `binding:"Required;GlobOrRegexPattern"` WhitelistUsers string WhitelistTeams string } From b707196cd2535f49da28bd7623f05cbc34fb867f Mon Sep 17 00:00:00 2001 From: KN4CK3R Date: Wed, 9 Jun 2021 15:10:41 +0000 Subject: [PATCH 29/35] Updated documentation. --- .../doc/advanced/protected-tags.en-us.md | 39 ++++++++++++------- options/locale/locale_en-US.ini | 2 +- 2 files changed, 25 insertions(+), 16 deletions(-) diff --git a/docs/content/doc/advanced/protected-tags.en-us.md b/docs/content/doc/advanced/protected-tags.en-us.md index 9c9af30943c4b..36e6e169753c8 100644 --- a/docs/content/doc/advanced/protected-tags.en-us.md +++ b/docs/content/doc/advanced/protected-tags.en-us.md @@ -26,23 +26,32 @@ Protected tags allow control over who has permission to create or update git tag To protect a tag, you need to follow these steps: 1. Go to the repository’s **Settings** > **Tags** page. -1. Type a regular expression pattern to match a name. +1. Type a pattern to match a name. You can use a single name, a [glob pattern](https://pkg.go.dev/github.com/gobwas/glob#Compile) or a regular expression. 1. Choose the allowed users and/or teams. If you leave these fields empty noone is allowed to create or modify this tag. 1. Select **Save** to save the configuration. ## Pattern protected tags -The pattern uses regular expressions to match a tag name. Examples: - -| Pattern Protected Tag | Possible Matching Tags | -| ---------------------- | --------------------------------------- | -| `\Av` | `v`, `v-1`, `version2` | -| `\Av[0-9]\z` | `v0`, `v1` up to `v9` | -| `\Av\d+\.\d+\.\d+\z` | `v1.0.17`, `v2.1.0` | -| `\Av\d+(\.\d+){0,2}\z` | `v1`, `v2.1`, `v1.2.34` | -| `-release\z` | `2.1-release`, `final-release` | -| `gitea` | `gitea`, `2.1-gitea`, `1_gitea-release` | -| `\Agitea\z` | only `gitea` | -| `^gitea$` | only `gitea` | -| `\A(v\|rel)-` | `v-`, `v-1`, `v-final`, `rel-`, `rel-x` | -| `.+` | matches all possible tag names | +The pattern uses [glob](https://pkg.go.dev/github.com/gobwas/glob#Compile) or regular expressions to match a tag name. For regular expressions you need to enclose the pattern in slashes. + +Examples: + +| Type | Pattern Protected Tag | Possible Matching Tags | +| ----- | ------------------------ | --------------------------------------- | +| Glob | `v*` | `v`, `v-1`, `version2` | +| Glob | `v[0-9]` | `v0`, `v1` up to `v9` | +| Glob | `*-release` | `2.1-release`, `final-release` | +| Glob | `gitea` | only `gitea` | +| Glob | `*gitea*` | `gitea`, `2.1-gitea`, `1_gitea-release` | +| Glob | `{v,rel}-*` | `v-`, `v-1`, `v-final`, `rel-`, `rel-x` | +| Glob | `*` | matches all possible tag names | +| Regex | `/\Av/` | `v`, `v-1`, `version2` | +| Regex | `/\Av[0-9]\z/` | `v0`, `v1` up to `v9` | +| Regex | `/\Av\d+\.\d+\.\d+\z/` | `v1.0.17`, `v2.1.0` | +| Regex | `/\Av\d+(\.\d+){0,2}\z/` | `v1`, `v2.1`, `v1.2.34` | +| Regex | `/-release\z/` | `2.1-release`, `final-release` | +| Regex | `/gitea/` | `gitea`, `2.1-gitea`, `1_gitea-release` | +| Regex | `/\Agitea\z/` | only `gitea` | +| Regex | `/^gitea$/` | only `gitea` | +| Regex | `/\A(v\|rel)-/` | `v-`, `v-1`, `v-final`, `rel-`, `rel-x` | +| Regex | `/.+/` | matches all possible tag names | diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini index 71aa3a1dfc008..0e030185b840e 100644 --- a/options/locale/locale_en-US.ini +++ b/options/locale/locale_en-US.ini @@ -1836,7 +1836,7 @@ settings.tags.protection.allowed.teams = Allowed teams settings.tags.protection.allowed.noone = No One settings.tags.protection.create = Protect Tag settings.tags.protection.none = There are no protected tags. -settings.tags.protection.pattern.description = Use regular expression pattern like \Av- or -release\z to match multiple tags. Read more in the protected tags guide. +settings.tags.protection.pattern.description = You can use a single name or a glob pattern or regular expression to match multiple tags. Read more in the protected tags guide. settings.bot_token = Bot Token settings.chat_id = Chat ID settings.matrix.homeserver_url = Homeserver URL From 6c1c35fb45f7382a93006ec801fbb00e0c2e68c1 Mon Sep 17 00:00:00 2001 From: KN4CK3R Date: Wed, 9 Jun 2021 18:14:00 +0200 Subject: [PATCH 30/35] Added suggestions. --- models/protected_tag.go | 2 +- modules/validation/binding.go | 2 +- templates/repo/settings/tags.tmpl | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/models/protected_tag.go b/models/protected_tag.go index ee99e64672c38..ecaae6d0c55ae 100644 --- a/models/protected_tag.go +++ b/models/protected_tag.go @@ -54,7 +54,7 @@ func (pt *ProtectedTag) EnsureCompiledPattern() error { var err error if len(pt.NamePattern) >= 2 && strings.HasPrefix(pt.NamePattern, "/") && strings.HasSuffix(pt.NamePattern, "/") { - pt.RegexPattern, err = regexp.Compile(pt.NamePattern[1:len(pt.NamePattern)-1]) + pt.RegexPattern, err = regexp.Compile(pt.NamePattern[1 : len(pt.NamePattern)-1]) } else { pt.GlobPattern, err = glob.Compile(pt.NamePattern) } diff --git a/modules/validation/binding.go b/modules/validation/binding.go index 51bbd81b397f1..4cef48daf32d6 100644 --- a/modules/validation/binding.go +++ b/modules/validation/binding.go @@ -151,7 +151,7 @@ func addGlobOrRegexPatternRule() { }, IsValid: func(errs binding.Errors, name string, val interface{}) (bool, binding.Errors) { str := strings.TrimSpace(fmt.Sprintf("%v", val)) - + if len(str) >= 2 && strings.HasPrefix(str, "/") && strings.HasSuffix(str, "/") { return regexPatternValidator(errs, name, str[1:len(str)-1]) } diff --git a/templates/repo/settings/tags.tmpl b/templates/repo/settings/tags.tmpl index d5cb5a47f8186..f8e592a03aa1c 100644 --- a/templates/repo/settings/tags.tmpl +++ b/templates/repo/settings/tags.tmpl @@ -23,7 +23,7 @@ @@ -106,7 +106,7 @@ {{$.i18n.Tr "edit"}} - + {{$.CsrfTokenHtml}} From 16a360e08c8e94dd1950c2bcbe63791f14233269 Mon Sep 17 00:00:00 2001 From: KN4CK3R Date: Wed, 9 Jun 2021 18:20:00 +0200 Subject: [PATCH 31/35] Fixed tests. --- models/protected_tag_test.go | 62 +++++++++++++++++++++++++----------- 1 file changed, 43 insertions(+), 19 deletions(-) diff --git a/models/protected_tag_test.go b/models/protected_tag_test.go index ccd24d0963034..82f5010990c86 100644 --- a/models/protected_tag_test.go +++ b/models/protected_tag_test.go @@ -54,20 +54,6 @@ func TestIsUserAllowed(t *testing.T) { } func TestIsUserAllowedToControlTag(t *testing.T) { - protectedTags := []*ProtectedTag{ - { - NamePattern: `gitea\z`, - WhitelistUserIDs: []int64{1}, - }, - { - NamePattern: `\Av-`, - WhitelistUserIDs: []int64{2}, - }, - { - NamePattern: "release", - }, - } - cases := []struct { name string userid int64 @@ -130,9 +116,47 @@ func TestIsUserAllowedToControlTag(t *testing.T) { }, } - for n, c := range cases { - isAllowed, err := IsUserAllowedToControlTag(protectedTags, c.name, c.userid) - assert.NoError(t, err) - assert.Equal(t, c.allowed, isAllowed, "case %d: error should match", n) - } + t.Run("Glob", func(t *testing.T) { + protectedTags := []*ProtectedTag{ + { + NamePattern: `*gitea`, + WhitelistUserIDs: []int64{1}, + }, + { + NamePattern: `v-*`, + WhitelistUserIDs: []int64{2}, + }, + { + NamePattern: "release", + }, + } + + for n, c := range cases { + isAllowed, err := IsUserAllowedToControlTag(protectedTags, c.name, c.userid) + assert.NoError(t, err) + assert.Equal(t, c.allowed, isAllowed, "case %d: error should match", n) + } + }) + + t.Run("Regex", func(t *testing.T) { + protectedTags := []*ProtectedTag{ + { + NamePattern: `/gitea\z/`, + WhitelistUserIDs: []int64{1}, + }, + { + NamePattern: `/\Av-/`, + WhitelistUserIDs: []int64{2}, + }, + { + NamePattern: "/release/", + }, + } + + for n, c := range cases { + isAllowed, err := IsUserAllowedToControlTag(protectedTags, c.name, c.userid) + assert.NoError(t, err) + assert.Equal(t, c.allowed, isAllowed, "case %d: error should match", n) + } + }) } From 46eb17e622577a59f4fa96aff32723ab05816f2d Mon Sep 17 00:00:00 2001 From: KN4CK3R Date: Wed, 16 Jun 2021 19:14:05 +0000 Subject: [PATCH 32/35] Changed white* to allow*. --- integrations/repo_tag_test.go | 2 +- models/migrations/v184.go | 4 ++-- models/protected_tag.go | 10 +++++----- models/protected_tag_test.go | 16 ++++++++-------- routers/web/repo/tag.go | 16 ++++++++-------- services/forms/repo_tag_form.go | 4 ++-- templates/repo/settings/tags.tmpl | 10 +++++----- 7 files changed, 31 insertions(+), 31 deletions(-) diff --git a/integrations/repo_tag_test.go b/integrations/repo_tag_test.go index e3c97bfd5d20d..eb3f2b47fb9e2 100644 --- a/integrations/repo_tag_test.go +++ b/integrations/repo_tag_test.go @@ -37,7 +37,7 @@ func TestCreateNewTagProtected(t *testing.T) { err = models.InsertProtectedTag(&models.ProtectedTag{ RepoID: repo.ID, NamePattern: "v-1.1", - WhitelistUserIDs: []int64{repo.OwnerID}, + AllowlistUserIDs: []int64{repo.OwnerID}, }) assert.NoError(t, err) diff --git a/models/migrations/v184.go b/models/migrations/v184.go index fcbf8beac0832..eb6ec7118cd7b 100644 --- a/models/migrations/v184.go +++ b/models/migrations/v184.go @@ -15,8 +15,8 @@ func createProtectedTagTable(x *xorm.Engine) error { ID int64 `xorm:"pk autoincr"` RepoID int64 NamePattern string - WhitelistUserIDs []int64 `xorm:"JSON TEXT"` - WhitelistTeamIDs []int64 `xorm:"JSON TEXT"` + AllowlistUserIDs []int64 `xorm:"JSON TEXT"` + AllowlistTeamIDs []int64 `xorm:"JSON TEXT"` CreatedUnix timeutil.TimeStamp `xorm:"created"` UpdatedUnix timeutil.TimeStamp `xorm:"updated"` diff --git a/models/protected_tag.go b/models/protected_tag.go index ecaae6d0c55ae..88f20dd29a865 100644 --- a/models/protected_tag.go +++ b/models/protected_tag.go @@ -21,8 +21,8 @@ type ProtectedTag struct { NamePattern string RegexPattern *regexp.Regexp `xorm:"-"` GlobPattern glob.Glob `xorm:"-"` - WhitelistUserIDs []int64 `xorm:"JSON TEXT"` - WhitelistTeamIDs []int64 `xorm:"JSON TEXT"` + AllowlistUserIDs []int64 `xorm:"JSON TEXT"` + AllowlistTeamIDs []int64 `xorm:"JSON TEXT"` CreatedUnix timeutil.TimeStamp `xorm:"created"` UpdatedUnix timeutil.TimeStamp `xorm:"updated"` @@ -63,15 +63,15 @@ func (pt *ProtectedTag) EnsureCompiledPattern() error { // IsUserAllowed returns true if the user is allowed to modify the tag func (pt *ProtectedTag) IsUserAllowed(userID int64) (bool, error) { - if base.Int64sContains(pt.WhitelistUserIDs, userID) { + if base.Int64sContains(pt.AllowlistUserIDs, userID) { return true, nil } - if len(pt.WhitelistTeamIDs) == 0 { + if len(pt.AllowlistTeamIDs) == 0 { return false, nil } - in, err := IsUserInTeams(userID, pt.WhitelistTeamIDs) + in, err := IsUserInTeams(userID, pt.AllowlistTeamIDs) if err != nil { return false, err } diff --git a/models/protected_tag_test.go b/models/protected_tag_test.go index 82f5010990c86..3dc895c69fe11 100644 --- a/models/protected_tag_test.go +++ b/models/protected_tag_test.go @@ -19,7 +19,7 @@ func TestIsUserAllowed(t *testing.T) { assert.False(t, allowed) pt = &ProtectedTag{ - WhitelistUserIDs: []int64{1}, + AllowlistUserIDs: []int64{1}, } allowed, err = pt.IsUserAllowed(1) assert.NoError(t, err) @@ -30,7 +30,7 @@ func TestIsUserAllowed(t *testing.T) { assert.False(t, allowed) pt = &ProtectedTag{ - WhitelistTeamIDs: []int64{1}, + AllowlistTeamIDs: []int64{1}, } allowed, err = pt.IsUserAllowed(1) assert.NoError(t, err) @@ -41,8 +41,8 @@ func TestIsUserAllowed(t *testing.T) { assert.True(t, allowed) pt = &ProtectedTag{ - WhitelistUserIDs: []int64{1}, - WhitelistTeamIDs: []int64{1}, + AllowlistUserIDs: []int64{1}, + AllowlistTeamIDs: []int64{1}, } allowed, err = pt.IsUserAllowed(1) assert.NoError(t, err) @@ -120,11 +120,11 @@ func TestIsUserAllowedToControlTag(t *testing.T) { protectedTags := []*ProtectedTag{ { NamePattern: `*gitea`, - WhitelistUserIDs: []int64{1}, + AllowlistUserIDs: []int64{1}, }, { NamePattern: `v-*`, - WhitelistUserIDs: []int64{2}, + AllowlistUserIDs: []int64{2}, }, { NamePattern: "release", @@ -142,11 +142,11 @@ func TestIsUserAllowedToControlTag(t *testing.T) { protectedTags := []*ProtectedTag{ { NamePattern: `/gitea\z/`, - WhitelistUserIDs: []int64{1}, + AllowlistUserIDs: []int64{1}, }, { NamePattern: `/\Av-/`, - WhitelistUserIDs: []int64{2}, + AllowlistUserIDs: []int64{2}, }, { NamePattern: "/release/", diff --git a/routers/web/repo/tag.go b/routers/web/repo/tag.go index 7d3021613e51c..7928591371b7a 100644 --- a/routers/web/repo/tag.go +++ b/routers/web/repo/tag.go @@ -45,11 +45,11 @@ func NewProtectedTagPost(ctx *context.Context) { NamePattern: strings.TrimSpace(form.NamePattern), } - if strings.TrimSpace(form.WhitelistUsers) != "" { - pt.WhitelistUserIDs, _ = base.StringsToInt64s(strings.Split(form.WhitelistUsers, ",")) + if strings.TrimSpace(form.AllowlistUsers) != "" { + pt.AllowlistUserIDs, _ = base.StringsToInt64s(strings.Split(form.AllowlistUsers, ",")) } - if strings.TrimSpace(form.WhitelistTeams) != "" { - pt.WhitelistTeamIDs, _ = base.StringsToInt64s(strings.Split(form.WhitelistTeams, ",")) + if strings.TrimSpace(form.AllowlistTeams) != "" { + pt.AllowlistTeamIDs, _ = base.StringsToInt64s(strings.Split(form.AllowlistTeams, ",")) } if err := models.InsertProtectedTag(pt); err != nil { @@ -75,8 +75,8 @@ func EditProtectedTag(ctx *context.Context) { } ctx.Data["name_pattern"] = pt.NamePattern - ctx.Data["whitelist_users"] = strings.Join(base.Int64sToStrings(pt.WhitelistUserIDs), ",") - ctx.Data["whitelist_teams"] = strings.Join(base.Int64sToStrings(pt.WhitelistTeamIDs), ",") + ctx.Data["allowlist_users"] = strings.Join(base.Int64sToStrings(pt.AllowlistUserIDs), ",") + ctx.Data["allowlist_teams"] = strings.Join(base.Int64sToStrings(pt.AllowlistTeamIDs), ",") ctx.HTML(http.StatusOK, tplTags) } @@ -102,8 +102,8 @@ func EditProtectedTagPost(ctx *context.Context) { form := web.GetForm(ctx).(*forms.ProtectTagForm) pt.NamePattern = strings.TrimSpace(form.NamePattern) - pt.WhitelistUserIDs, _ = base.StringsToInt64s(strings.Split(form.WhitelistUsers, ",")) - pt.WhitelistTeamIDs, _ = base.StringsToInt64s(strings.Split(form.WhitelistTeams, ",")) + pt.AllowlistUserIDs, _ = base.StringsToInt64s(strings.Split(form.AllowlistUsers, ",")) + pt.AllowlistTeamIDs, _ = base.StringsToInt64s(strings.Split(form.AllowlistTeams, ",")) if err := models.UpdateProtectedTag(pt); err != nil { ctx.ServerError("UpdateProtectedTag", err) diff --git a/services/forms/repo_tag_form.go b/services/forms/repo_tag_form.go index a877d6ad7ff34..337e7fe1ea646 100644 --- a/services/forms/repo_tag_form.go +++ b/services/forms/repo_tag_form.go @@ -16,8 +16,8 @@ import ( // ProtectTagForm form for changing protected tag settings type ProtectTagForm struct { NamePattern string `binding:"Required;GlobOrRegexPattern"` - WhitelistUsers string - WhitelistTeams string + AllowlistUsers string + AllowlistTeams string } // Validate validates the fields diff --git a/templates/repo/settings/tags.tmpl b/templates/repo/settings/tags.tmpl index f8e592a03aa1c..dde07e9e1ccbf 100644 --- a/templates/repo/settings/tags.tmpl +++ b/templates/repo/settings/tags.tmpl @@ -31,7 +31,7 @@