Add tag protection (#15629)
* Added tag protection in hook. * Prevent UI tag creation if protected. * Added settings page. * Added tests. * Added suggestions. * Moved tests. * Use individual errors. * Removed unneeded methods. * Switched delete selector. * Changed method names. * No reason to be unique. * Allow editing of protected tags. * Removed unique key from migration. * Added docs page. * Changed date. * Respond with 404 to not found tags. * Replaced glob with regex pattern. * Added support for glob and regex pattern. * Updated documentation. * Changed white* to allow*. * Fixed edit button link. * Added cancel button. Co-authored-by: zeripath <art27@cantab.net> Co-authored-by: Lunny Xiao <xiaolunwen@gmail.com>release/v1.15
parent
7a0ed9a046
commit
44b8b07631
|
@ -221,8 +221,8 @@ Gitea or set your environment appropriately.`, "")
|
|||
total++
|
||||
lastline++
|
||||
|
||||
// If the ref is a branch, check if it's protected
|
||||
if strings.HasPrefix(refFullName, git.BranchPrefix) {
|
||||
// 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
|
||||
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 {
|
||||
|
|
|
@ -0,0 +1,57 @@
|
|||
---
|
||||
date: "2021-05-14T00:00:00-00: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 an appropriate pattern 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.
|
||||
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 [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 |
|
|
@ -59,7 +59,9 @@ func TestMirrorPull(t *testing.T) {
|
|||
|
||||
assert.NoError(t, release_service.CreateRelease(gitRepo, &models.Release{
|
||||
RepoID: repo.ID,
|
||||
Repo: repo,
|
||||
PublisherID: user.ID,
|
||||
Publisher: user,
|
||||
TagName: "v0.2",
|
||||
Target: "master",
|
||||
Title: "v0.2 is released",
|
||||
|
|
|
@ -0,0 +1,74 @@
|
|||
// 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 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",
|
||||
AllowlistUserIDs: []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.IsErrProtectedTagName(err))
|
||||
|
||||
err = release.CreateNewTag(owner, repo, "master", "v-1.1", "third tag")
|
||||
assert.NoError(t, err)
|
||||
})
|
||||
|
||||
t.Run("Git", func(t *testing.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")
|
||||
})
|
||||
})
|
||||
}
|
|
@ -985,6 +985,21 @@ func (err ErrInvalidTagName) Error() string {
|
|||
return fmt.Sprintf("release tag name is not valid [tag_name: %s]", err.TagName)
|
||||
}
|
||||
|
||||
// ErrProtectedTagName represents a "ProtectedTagName" kind of error.
|
||||
type ErrProtectedTagName struct {
|
||||
TagName string
|
||||
}
|
||||
|
||||
// IsErrProtectedTagName checks if an error is a ErrProtectedTagName.
|
||||
func IsErrProtectedTagName(err error) bool {
|
||||
_, ok := err.(ErrProtectedTagName)
|
||||
return ok
|
||||
}
|
||||
|
||||
func (err ErrProtectedTagName) Error() string {
|
||||
return fmt.Sprintf("release tag name is protected [tag_name: %s]", err.TagName)
|
||||
}
|
||||
|
||||
// ErrRepoFileAlreadyExists represents a "RepoFileAlreadyExist" kind of error.
|
||||
type ErrRepoFileAlreadyExists struct {
|
||||
Path string
|
||||
|
|
|
@ -321,6 +321,8 @@ var migrations = []Migration{
|
|||
NewMigration("Rename Task errors to message", renameTaskErrorsToMessage),
|
||||
// v185 -> v186
|
||||
NewMigration("Add new table repo_archiver", addRepoArchiver),
|
||||
// v186 -> v187
|
||||
NewMigration("Create protected tag table", createProtectedTagTable),
|
||||
}
|
||||
|
||||
// GetCurrentDBVersion returns the current db version
|
||||
|
|
|
@ -0,0 +1,26 @@
|
|||
// 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 (
|
||||
"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
|
||||
NamePattern string
|
||||
AllowlistUserIDs []int64 `xorm:"JSON TEXT"`
|
||||
AllowlistTeamIDs []int64 `xorm:"JSON TEXT"`
|
||||
|
||||
CreatedUnix timeutil.TimeStamp `xorm:"created"`
|
||||
UpdatedUnix timeutil.TimeStamp `xorm:"updated"`
|
||||
}
|
||||
|
||||
return x.Sync2(new(ProtectedTag))
|
||||
}
|
|
@ -137,6 +137,7 @@ func init() {
|
|||
new(IssueIndex),
|
||||
new(PushMirror),
|
||||
new(RepoArchiver),
|
||||
new(ProtectedTag),
|
||||
)
|
||||
|
||||
gonicNames := []string{"SSL", "UID"}
|
||||
|
|
|
@ -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 models
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"code.gitea.io/gitea/modules/base"
|
||||
"code.gitea.io/gitea/modules/timeutil"
|
||||
|
||||
"github.com/gobwas/glob"
|
||||
)
|
||||
|
||||
// ProtectedTag struct
|
||||
type ProtectedTag struct {
|
||||
ID int64 `xorm:"pk autoincr"`
|
||||
RepoID int64
|
||||
NamePattern string
|
||||
RegexPattern *regexp.Regexp `xorm:"-"`
|
||||
GlobPattern glob.Glob `xorm:"-"`
|
||||
AllowlistUserIDs []int64 `xorm:"JSON TEXT"`
|
||||
AllowlistTeamIDs []int64 `xorm:"JSON TEXT"`
|
||||
|
||||
CreatedUnix timeutil.TimeStamp `xorm:"created"`
|
||||
UpdatedUnix timeutil.TimeStamp `xorm:"updated"`
|
||||
}
|
||||
|
||||
// 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.ID(pt.ID).Delete(&ProtectedTag{})
|
||||
return err
|
||||
}
|
||||
|
||||
// EnsureCompiledPattern ensures the glob pattern is compiled
|
||||
func (pt *ProtectedTag) EnsureCompiledPattern() error {
|
||||
if pt.RegexPattern != nil || pt.GlobPattern != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
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])
|
||||
} else {
|
||||
pt.GlobPattern, err = glob.Compile(pt.NamePattern)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// IsUserAllowed returns true if the user is allowed to modify the tag
|
||||
func (pt *ProtectedTag) IsUserAllowed(userID int64) (bool, error) {
|
||||
if base.Int64sContains(pt.AllowlistUserIDs, userID) {
|
||||
return true, nil
|
||||
}
|
||||
|
||||
if len(pt.AllowlistTeamIDs) == 0 {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
in, err := IsUserInTeams(userID, pt.AllowlistTeamIDs)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
return in, nil
|
||||
}
|
||||
|
||||
// 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})
|
||||
}
|
||||
|
||||
// GetProtectedTagByID gets the protected tag with the specific id
|
||||
func GetProtectedTagByID(id int64) (*ProtectedTag, error) {
|
||||
tag := new(ProtectedTag)
|
||||
has, err := x.ID(id).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) {
|
||||
isAllowed := true
|
||||
for _, tag := range tags {
|
||||
err := tag.EnsureCompiledPattern()
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
if !tag.matchString(tagName) {
|
||||
continue
|
||||
}
|
||||
|
||||
isAllowed, err = tag.IsUserAllowed(userID)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
if isAllowed {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return isAllowed, nil
|
||||
}
|
||||
|
||||
func (pt *ProtectedTag) matchString(name string) bool {
|
||||
if pt.RegexPattern != nil {
|
||||
return pt.RegexPattern.MatchString(name)
|
||||
}
|
||||
return pt.GlobPattern.Match(name)
|
||||
}
|
|
@ -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 models
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestIsUserAllowed(t *testing.T) {
|
||||
assert.NoError(t, PrepareTestDatabase())
|
||||
|
||||
pt := &ProtectedTag{}
|
||||
allowed, err := pt.IsUserAllowed(1)
|
||||
assert.NoError(t, err)
|
||||
assert.False(t, allowed)
|
||||
|
||||
pt = &ProtectedTag{
|
||||
AllowlistUserIDs: []int64{1},
|
||||
}
|
||||
allowed, err = pt.IsUserAllowed(1)
|
||||
assert.NoError(t, err)
|
||||
assert.True(t, allowed)
|
||||
|
||||
allowed, err = pt.IsUserAllowed(2)
|
||||
assert.NoError(t, err)
|
||||
assert.False(t, allowed)
|
||||
|
||||
pt = &ProtectedTag{
|
||||
AllowlistTeamIDs: []int64{1},
|
||||
}
|
||||
allowed, err = pt.IsUserAllowed(1)
|
||||
assert.NoError(t, err)
|
||||
assert.False(t, allowed)
|
||||
|
||||
allowed, err = pt.IsUserAllowed(2)
|
||||
assert.NoError(t, err)
|
||||
assert.True(t, allowed)
|
||||
|
||||
pt = &ProtectedTag{
|
||||
AllowlistUserIDs: []int64{1},
|
||||
AllowlistTeamIDs: []int64{1},
|
||||
}
|
||||
allowed, err = pt.IsUserAllowed(1)
|
||||
assert.NoError(t, err)
|
||||
assert.True(t, allowed)
|
||||
|
||||
allowed, err = pt.IsUserAllowed(2)
|
||||
assert.NoError(t, err)
|
||||
assert.True(t, allowed)
|
||||
}
|
||||
|
||||
func TestIsUserAllowedToControlTag(t *testing.T) {
|
||||
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,
|
||||
},
|
||||
}
|
||||
|
||||
t.Run("Glob", func(t *testing.T) {
|
||||
protectedTags := []*ProtectedTag{
|
||||
{
|
||||
NamePattern: `*gitea`,
|
||||
AllowlistUserIDs: []int64{1},
|
||||
},
|
||||
{
|
||||
NamePattern: `v-*`,
|
||||
AllowlistUserIDs: []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/`,
|
||||
AllowlistUserIDs: []int64{1},
|
||||
},
|
||||
{
|
||||
NamePattern: `/\Av-/`,
|
||||
AllowlistUserIDs: []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)
|
||||
}
|
||||
})
|
||||
}
|
|
@ -1498,6 +1498,7 @@ func DeleteRepository(doer *User, uid, repoID int64) error {
|
|||
&Mirror{RepoID: repoID},
|
||||
&Notification{RepoID: repoID},
|
||||
&ProtectedBranch{RepoID: repoID},
|
||||
&ProtectedTag{RepoID: repoID},
|
||||
&PullRequest{BaseRepoID: repoID},
|
||||
&PushMirror{RepoID: repoID},
|
||||
&Release{RepoID: repoID},
|
||||
|
|
|
@ -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,8 @@ func AddBindingRules() {
|
|||
addGitRefNameBindingRule()
|
||||
addValidURLBindingRule()
|
||||
addGlobPatternRule()
|
||||
addRegexPatternRule()
|
||||
addGlobOrRegexPatternRule()
|
||||
}
|
||||
|
||||
func addGitRefNameBindingRule() {
|
||||
|
@ -102,17 +107,55 @@ func addGlobPatternRule() {
|
|||
IsMatch: func(rule string) bool {
|
||||
return rule == "GlobPattern"
|
||||
},
|
||||
IsValid: globPatternValidator,
|
||||
})
|
||||
}
|
||||
|
||||
func globPatternValidator(errs binding.Errors, name string, val interface{}) (bool, binding.Errors) {
|
||||
str := fmt.Sprintf("%v", val)
|
||||
|
||||
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() {
|
||||
binding.AddRule(&binding.Rule{
|
||||
IsMatch: func(rule string) bool {
|
||||
return rule == "RegexPattern"
|
||||
},
|
||||
IsValid: regexPatternValidator,
|
||||
})
|
||||
}
|
||||
|
||||
func regexPatternValidator(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 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 := fmt.Sprintf("%v", val)
|
||||
str := strings.TrimSpace(fmt.Sprintf("%v", val))
|
||||
|
||||
if len(str) != 0 {
|
||||
if _, err := glob.Compile(str); err != nil {
|
||||
errs.Add([]string{name}, ErrGlobPattern, err.Error())
|
||||
return false, errs
|
||||
}
|
||||
if len(str) >= 2 && strings.HasPrefix(str, "/") && strings.HasSuffix(str, "/") {
|
||||
return regexPatternValidator(errs, name, str[1:len(str)-1])
|
||||
}
|
||||
|
||||
return true, errs
|
||||
return globPatternValidator(errs, name, val)
|
||||
},
|
||||
})
|
||||
}
|
||||
|
|
|
@ -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:"RegexPattern" binding:"RegexPattern"`
|
||||
}
|
||||
)
|
||||
|
||||
|
|
|
@ -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)
|
||||
})
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -83,6 +83,7 @@ add = Add
|
|||
add_all = Add All
|
||||
remove = Remove
|
||||
remove_all = Remove All
|
||||
edit = Edit
|
||||
|
||||
write = Write
|
||||
preview = Preview
|
||||
|
@ -415,6 +416,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.
|
||||
|
@ -1802,7 +1804,7 @@ settings.event_pull_request_review_desc = Pull request approved, rejected, or re
|
|||
settings.event_pull_request_sync = Pull Request Synchronized
|
||||
settings.event_pull_request_sync_desc = Pull request synchronized.
|
||||
settings.branch_filter = Branch filter
|
||||
settings.branch_filter_desc = Branch whitelist for push, branch creation and branch deletion events, specified as glob pattern. If empty or <code>*</code>, events for all branches are reported. See <a href="https://godoc.org/github.com/gobwas/glob#Compile">github.com/gobwas/glob</a> documentation for syntax. Examples: <code>master</code>, <code>{master,release*}</code>.
|
||||
settings.branch_filter_desc = Branch whitelist for push, branch creation and branch deletion events, specified as glob pattern. If empty or <code>*</code>, events for all branches are reported. See <a href="https://pkg.go.dev/github.com/gobwas/glob#Compile">github.com/gobwas/glob</a> documentation for syntax. Examples: <code>master</code>, <code>{master,release*}</code>.
|
||||
settings.active = Active
|
||||
settings.active_helper = Information about triggered events will be sent to this webhook URL.
|
||||
settings.add_hook_success = The webhook has been added.
|
||||
|
@ -1872,7 +1874,7 @@ settings.dismiss_stale_approvals_desc = When new commits that change the content
|
|||
settings.require_signed_commits = Require Signed Commits
|
||||
settings.require_signed_commits_desc = Reject pushes to this branch if they are unsigned or unverifiable.
|
||||
settings.protect_protected_file_patterns = Protected file patterns (separated using semicolon '\;'):
|
||||
settings.protect_protected_file_patterns_desc = Protected files that are not allowed to be changed directly even if user has rights to add, edit, or delete files in this branch. Multiple patterns can be separated using semicolon ('\;'). See <a href="https://godoc.org/github.com/gobwas/glob#Compile">github.com/gobwas/glob</a> documentation for pattern syntax. Examples: <code>.drone.yml</code>, <code>/docs/**/*.txt</code>.
|
||||
settings.protect_protected_file_patterns_desc = Protected files that are not allowed to be changed directly even if user has rights to add, edit, or delete files in this branch. Multiple patterns can be separated using semicolon ('\;'). See <a href="https://pkg.go.dev/github.com/gobwas/glob#Compile">github.com/gobwas/glob</a> documentation for pattern syntax. Examples: <code>.drone.yml</code>, <code>/docs/**/*.txt</code>.
|
||||
settings.add_protected_branch = Enable protection
|
||||
settings.delete_protected_branch = Disable protection
|
||||
settings.update_protect_branch_success = Branch protection for branch '%s' has been updated.
|
||||
|
@ -1891,6 +1893,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 = You can use a single name or a glob pattern or regular expression to match multiple tags. Read more in the <a target="_blank" rel="noopener" href="https://docs.gitea.io/en-us/protected-tags/">protected tags guide</a>.
|
||||
settings.bot_token = Bot Token
|
||||
settings.chat_id = Chat ID
|
||||
settings.matrix.homeserver_url = Homeserver URL
|
||||
|
@ -1904,6 +1916,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.
|
||||
|
@ -2018,6 +2031,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
|
||||
|
|
|
@ -155,125 +155,202 @@ 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.Response{
|
||||
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, private.Response{
|
||||
Err: fmt.Sprintf("branch %s is the default branch and cannot be deleted", 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, private.Response{
|
||||
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, private.Response{
|
||||
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, private.Response{
|
||||
Err: fmt.Sprintf("branch %s is protected from deletion", 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, private.Response{
|
||||
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)
|
||||
ctx.JSON(http.StatusForbidden, private.Response{
|
||||
Err: fmt.Sprintf("branch %s is protected from force push", 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)
|
||||
// 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, private.Response{
|
||||
Err: fmt.Sprintf("branch %s is protected from deletion", 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)
|
||||
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, private.Response{
|
||||
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, private.Response{
|
||||
Err: fmt.Sprintf("branch %s is protected from force push", 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)
|
||||
ctx.JSON(http.StatusInternalServerError, private.Response{
|
||||
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, private.Response{
|
||||
Err: fmt.Sprintf("branch %s is protected from unverified commit %s", branchName, unverifiedCommit),
|
||||
})
|
||||
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, private.Response{
|
||||
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 := ""
|
||||
// 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)
|
||||
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, private.Response{
|
||||
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)
|
||||
}
|
||||
|
||||
// 6. If we're not allowed to push directly
|
||||
if !canPush {
|
||||
// Is this is a merge from the UI/API?
|
||||
if opts.PullRequestID == 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, private.Response{
|
||||
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, private.Response{
|
||||
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.PullRequestID)
|
||||
if err != nil {
|
||||
log.Error("Unable to get PullRequest %d Error: %v", opts.PullRequestID, err)
|
||||
ctx.JSON(http.StatusInternalServerError, private.Response{
|
||||
Err: fmt.Sprintf("Unable to check file protection for commits from %s to %s: %v", oldCommitID, newCommitID, err),
|
||||
Err: fmt.Sprintf("Unable to get PullRequest %d Error: %v", opts.PullRequestID, 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, private.Response{
|
||||
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, private.Response{
|
||||
Err: fmt.Sprintf("Unable to get Repo permission of repo %s/%s of User %s: %v", repo.OwnerName, repo.Name, user.Name, err),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
changedProtectedfiles = true
|
||||
protectedFilePath = err.(models.ErrFilePathProtected).Path
|
||||
}
|
||||
}
|
||||
// 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, private.Response{
|
||||
Err: fmt.Sprintf("Error calculating if allowed to merge: %v", err),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// 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)
|
||||
}
|
||||
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, private.Response{
|
||||
Err: fmt.Sprintf("Not allowed to push to protected branch %s", branchName),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// 6. If we're not allowed to push directly
|
||||
if !canPush {
|
||||
// Is this is a merge from the UI/API?
|
||||
if opts.PullRequestID == 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 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, private.Response{
|
||||
|
@ -282,88 +359,44 @@ func HookPreReceive(ctx *gitea_context.PrivateContext) {
|
|||
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, private.Response{
|
||||
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.PullRequestID)
|
||||
if err != nil {
|
||||
log.Error("Unable to get PullRequest %d Error: %v", opts.PullRequestID, err)
|
||||
ctx.JSON(http.StatusInternalServerError, private.Response{
|
||||
Err: fmt.Sprintf("Unable to get PullRequest %d Error: %v", opts.PullRequestID, 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, private.Response{
|
||||
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, private.Response{
|
||||
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, private.Response{
|
||||
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, private.Response{
|
||||
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
|
||||
}
|
||||
|
||||
// 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, private.Response{
|
||||
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, private.Response{
|
||||
Err: fmt.Sprintf("Not allowed to push to protected branch %s and pr #%d is not ready to be merged: %s", branchName, opts.PullRequestID, err.Error()),
|
||||
// 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, private.Response{
|
||||
Err: fmt.Sprintf("Not allowed to push to protected branch %s and pr #%d is not ready to be merged: %s", branchName, opts.PullRequestID, 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, private.Response{
|
||||
Err: fmt.Sprintf("Unable to get status of pull request %d. Error: %v", opts.PullRequestID, err),
|
||||
})
|
||||
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)
|
||||
}
|
||||
} else if strings.HasPrefix(refFullName, git.TagPrefix) {
|
||||
tagName := strings.TrimPrefix(refFullName, git.TagPrefix)
|
||||
|
||||
isAllowed, err := models.IsUserAllowedToControlTag(protectedTags, tagName, opts.UserID)
|
||||
if err != nil {
|
||||
ctx.JSON(http.StatusInternalServerError, private.Response{
|
||||
Err: fmt.Sprintf("Unable to get status of pull request %d. Error: %v", opts.PullRequestID, err),
|
||||
Err: err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
if !isAllowed {
|
||||
log.Warn("Forbidden: Tag %s in %-v is protected", tagName, repo)
|
||||
ctx.JSON(http.StatusForbidden, private.Response{
|
||||
Err: fmt.Sprintf("Tag %s is protected", tagName),
|
||||
})
|
||||
return
|
||||
}
|
||||
} else {
|
||||
log.Error("Unexpected ref: %s", refFullName)
|
||||
ctx.JSON(http.StatusInternalServerError, private.Response{
|
||||
Err: fmt.Sprintf("Unexpected ref: %s", refFullName),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -322,6 +322,18 @@ func NewReleasePost(ctx *context.Context) {
|
|||
return
|
||||
}
|
||||
|
||||
if models.IsErrInvalidTagName(err) {
|
||||
ctx.Flash.Error(ctx.Tr("repo.release.tag_name_invalid"))
|
||||
ctx.Redirect(ctx.Repo.RepoLink + "/src/" + ctx.Repo.BranchNameSubURL())
|
||||
return
|
||||
}
|
||||
|
||||
if models.IsErrProtectedTagName(err) {
|
||||
ctx.Flash.Error(ctx.Tr("repo.release.tag_name_protected"))
|
||||
ctx.Redirect(ctx.Repo.RepoLink + "/src/" + ctx.Repo.BranchNameSubURL())
|
||||
return
|
||||
}
|
||||
|
||||
ctx.ServerError("releaseservice.CreateNewTag", err)
|
||||
return
|
||||
}
|
||||
|
@ -333,7 +345,9 @@ func NewReleasePost(ctx *context.Context) {
|
|||
|
||||
rel = &models.Release{
|
||||
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,
|
||||
|
@ -350,6 +364,8 @@ func NewReleasePost(ctx *context.Context) {
|
|||
ctx.RenderWithErr(ctx.Tr("repo.release.tag_name_already_exist"), tplReleaseNew, &form)
|
||||
case models.IsErrInvalidTagName(err):
|
||||
ctx.RenderWithErr(ctx.Tr("repo.release.tag_name_invalid"), tplReleaseNew, &form)
|
||||
case models.IsErrProtectedTagName(err):
|
||||
ctx.RenderWithErr(ctx.Tr("repo.release.tag_name_protected"), tplReleaseNew, &form)
|
||||
default:
|
||||
ctx.ServerError("CreateRelease", err)
|
||||
}
|
||||
|
|
|
@ -40,6 +40,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"
|
||||
|
|
|
@ -0,0 +1,182 @@
|
|||
// 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"
|
||||
"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)
|
||||
}
|
||||
|
||||
// NewProtectedTagPost handles creation of a protect tag
|
||||
func NewProtectedTagPost(ctx *context.Context) {
|
||||
if setTagsContext(ctx) != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if ctx.HasError() {
|
||||
ctx.HTML(http.StatusOK, tplTags)
|
||||
return
|
||||
}
|
||||
|
||||
repo := ctx.Repo.Repository
|
||||
form := web.GetForm(ctx).(*forms.ProtectTagForm)
|
||||
|
||||
pt := &models.ProtectedTag{
|
||||
RepoID: repo.ID,
|
||||
NamePattern: strings.TrimSpace(form.NamePattern),
|
||||
}
|
||||
|
||||
if strings.TrimSpace(form.AllowlistUsers) != "" {
|
||||
pt.AllowlistUserIDs, _ = base.StringsToInt64s(strings.Split(form.AllowlistUsers, ","))
|
||||
}
|
||||
if strings.TrimSpace(form.AllowlistTeams) != "" {
|
||||
pt.AllowlistTeamIDs, _ = base.StringsToInt64s(strings.Split(form.AllowlistTeams, ","))
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
// EditProtectedTag render the page to edit a protect tag
|
||||
func EditProtectedTag(ctx *context.Context) {
|
||||
if setTagsContext(ctx) != nil {
|
||||
return
|
||||
}
|
||||
|
||||
ctx.Data["PageIsEditProtectedTag"] = true
|
||||
|
||||
pt := selectProtectedTagByContext(ctx)
|
||||
if pt == nil {
|
||||
return
|
||||
}
|
||||
|
||||
ctx.Data["name_pattern"] = pt.NamePattern
|
||||
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)
|
||||
}
|
||||
|
||||
// EditProtectedTagPost handles creation of a protect tag
|
||||
func EditProtectedTagPost(ctx *context.Context) {
|
||||
if setTagsContext(ctx) != nil {
|
||||
return
|
||||
}
|
||||
|
||||
ctx.Data["PageIsEditProtectedTag"] = true
|
||||
|
||||
if ctx.HasError() {
|
||||
ctx.HTML(http.StatusOK, tplTags)
|
||||
return
|
||||
}
|
||||
|
||||
pt := selectProtectedTagByContext(ctx)
|
||||
if pt == nil {
|
||||
return
|
||||
}
|
||||
|
||||
form := web.GetForm(ctx).(*forms.ProtectTagForm)
|
||||
|
||||
pt.NamePattern = strings.TrimSpace(form.NamePattern)
|
||||
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)
|
||||
return
|
||||
}
|
||||
|
||||
ctx.Flash.Success(ctx.Tr("repo.settings.update_settings_success"))
|
||||
ctx.Redirect(ctx.Repo.Repository.Link() + "/settings/tags")
|
||||
}
|
||||
|
||||
// DeleteProtectedTagPost handles deletion of a protected tag
|
||||
func DeleteProtectedTagPost(ctx *context.Context) {
|
||||
pt := selectProtectedTagByContext(ctx)
|
||||
if pt == 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 {
|
||||
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) *models.ProtectedTag {
|
||||
id := ctx.QueryInt64("id")
|
||||
if id == 0 {
|
||||
id = ctx.ParamsInt64(":id")
|
||||
}
|
||||
|
||||
tag, err := models.GetProtectedTagByID(id)
|
||||
if err != nil {
|
||||
ctx.ServerError("GetProtectedTagByID", err)
|
||||
return nil
|
||||
}
|
||||
|
||||
if tag != nil && tag.RepoID == ctx.Repo.Repository.ID {
|
||||
return tag
|
||||
}
|
||||
|
||||
ctx.NotFound("", fmt.Errorf("ProtectedTag[%v] not associated to repository %v", id, ctx.Repo.Repository))
|
||||
|
||||
return nil
|
||||
}
|
|
@ -594,12 +594,21 @@ 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.Get("", repo.Tags)
|
||||
m.Post("", bindIgnErr(forms.ProtectTagForm{}), context.RepoMustNotBeArchived(), repo.NewProtectedTagPost)
|
||||
m.Post("/delete", context.RepoMustNotBeArchived(), repo.DeleteProtectedTagPost)
|
||||
m.Get("/{id}", repo.EditProtectedTag)
|
||||
m.Post("/{id}", bindIgnErr(forms.ProtectTagForm{}), context.RepoMustNotBeArchived(), repo.EditProtectedTagPost)
|
||||
})
|
||||
|
||||
m.Group("/hooks/git", func() {
|
||||
m.Get("", repo.GitHooks)
|
||||
m.Combo("/{name}").Get(repo.GitHooksEdit).
|
||||
|
|
|
@ -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;GlobOrRegexPattern"`
|
||||
AllowlistUsers string
|
||||
AllowlistTeams 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)
|
||||
}
|
|
@ -23,6 +23,25 @@ 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 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.ErrProtectedTagName{
|
||||
TagName: rel.TagName,
|
||||
}
|
||||
}
|
||||
|
||||
commit, err := gitRepo.GetCommit(rel.Target)
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("GetCommit: %v", err)
|
||||
|
@ -49,11 +68,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{
|
||||
|
@ -137,7 +152,9 @@ func CreateNewTag(doer *models.User, repo *models.Repository, commit, tagName, m
|
|||
|
||||
rel := &models.Release{
|
||||
RepoID: repo.ID,
|
||||
Repo: repo,
|
||||
PublisherID: doer.ID,
|
||||
Publisher: doer,
|
||||
TagName: tagName,
|
||||
Target: commit,
|
||||
IsDraft: false,
|
||||
|
|
|
@ -33,7 +33,9 @@ func TestRelease_Create(t *testing.T) {
|
|||
|
||||
assert.NoError(t, CreateRelease(gitRepo, &models.Release{
|
||||
RepoID: repo.ID,
|
||||
Repo: repo,
|
||||
PublisherID: user.ID,
|
||||
Publisher: user,
|
||||
TagName: "v0.1",
|
||||
Target: "master",
|
||||
Title: "v0.1 is released",
|
||||
|
@ -45,7 +47,9 @@ func TestRelease_Create(t *testing.T) {
|
|||
|
||||
assert.NoError(t, CreateRelease(gitRepo, &models.Release{
|
||||
RepoID: repo.ID,
|
||||
Repo: repo,
|
||||
PublisherID: user.ID,
|
||||
Publisher: user,
|
||||
TagName: "v0.1.1",
|
||||
Target: "65f1bf27bc3bf70f64657658635e66094edbcb4d",
|
||||
Title: "v0.1.1 is released",
|
||||
|
@ -57,7 +61,9 @@ func TestRelease_Create(t *testing.T) {
|
|||
|
||||
assert.NoError(t, CreateRelease(gitRepo, &models.Release{
|
||||
RepoID: repo.ID,
|
||||
Repo: repo,
|
||||
PublisherID: user.ID,
|
||||
Publisher: user,
|
||||
TagName: "v0.1.2",
|
||||
Target: "65f1bf2",
|
||||
Title: "v0.1.2 is released",
|
||||
|
@ -69,7 +75,9 @@ func TestRelease_Create(t *testing.T) {
|
|||
|
||||
assert.NoError(t, CreateRelease(gitRepo, &models.Release{
|
||||
RepoID: repo.ID,
|
||||
Repo: repo,
|
||||
PublisherID: user.ID,
|
||||
Publisher: user,
|
||||
TagName: "v0.1.3",
|
||||
Target: "65f1bf2",
|
||||
Title: "v0.1.3 is released",
|
||||
|
@ -81,7 +89,9 @@ func TestRelease_Create(t *testing.T) {
|
|||
|
||||
assert.NoError(t, CreateRelease(gitRepo, &models.Release{
|
||||
RepoID: repo.ID,
|
||||
Repo: repo,
|
||||
PublisherID: user.ID,
|
||||
Publisher: user,
|
||||
TagName: "v0.1.4",
|
||||
Target: "65f1bf2",
|
||||
Title: "v0.1.4 is released",
|
||||
|
@ -99,7 +109,9 @@ func TestRelease_Create(t *testing.T) {
|
|||
|
||||
var release = models.Release{
|
||||
RepoID: repo.ID,
|
||||
Repo: repo,
|
||||
PublisherID: user.ID,
|
||||
Publisher: user,
|
||||
TagName: "v0.1.5",
|
||||
Target: "65f1bf2",
|
||||
Title: "v0.1.5 is released",
|
||||
|
@ -125,7 +137,9 @@ 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,
|
||||
Publisher: user,
|
||||
TagName: "v1.1.1",
|
||||
Target: "master",
|
||||
Title: "v1.1.1 is released",
|
||||
|
@ -147,7 +161,9 @@ 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,
|
||||
Publisher: user,
|
||||
TagName: "v1.2.1",
|
||||
Target: "65f1bf2",
|
||||
Title: "v1.2.1 is draft",
|
||||
|
@ -169,7 +185,9 @@ 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,
|
||||
Publisher: user,
|
||||
TagName: "v1.3.1",
|
||||
Target: "65f1bf2",
|
||||
Title: "v1.3.1 is pre-released",
|
||||
|
@ -192,7 +210,9 @@ func TestRelease_Update(t *testing.T) {
|
|||
// Test create release
|
||||
release = &models.Release{
|
||||
RepoID: repo.ID,
|
||||
Repo: repo,
|
||||
PublisherID: user.ID,
|
||||
Publisher: user,
|
||||
TagName: "v1.1.2",
|
||||
Target: "master",
|
||||
Title: "v1.1.2 is released",
|
||||
|
@ -258,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",
|
||||
|
@ -280,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",
|
||||
|
@ -301,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",
|
||||
|
|
|
@ -5,6 +5,7 @@
|
|||
<li {{if .PageIsSettingsOptions}}class="current"{{end}}><a href="{{.RepoLink}}/settings">{{.i18n.Tr "repo.settings.options"}}</a></li>
|
||||
<li {{if .PageIsSettingsCollaboration}}class="current"{{end}}><a href="{{.RepoLink}}/settings/collaboration">{{.i18n.Tr "repo.settings.collaboration"}}</a></li>
|
||||
<li {{if .PageIsSettingsBranches}}class="current"{{end}}><a href="{{.RepoLink}}/settings/branches">{{.i18n.Tr "repo.settings.branches"}}</a></li>
|
||||
<li {{if .PageIsSettingsTags}}class="current"{{end}}><a href="{{.RepoLink}}/settings/tags">{{.i18n.Tr "repo.settings.tags"}}</a></li>
|
||||
{{if not DisableWebhooks}}
|
||||
<li {{if .PageIsSettingsHooks}}class="current"{{end}}><a href="{{.RepoLink}}/settings/hooks">{{.i18n.Tr "repo.settings.hooks"}}</a></li>
|
||||
{{end}}
|
||||
|
|
|
@ -11,6 +11,9 @@
|
|||
{{.i18n.Tr "repo.settings.branches"}}
|
||||
</a>
|
||||
{{end}}
|
||||
<a class="{{if .PageIsSettingsTags}}active{{end}} item" href="{{.RepoLink}}/settings/tags">
|
||||
{{.i18n.Tr "repo.settings.tags"}}
|
||||
</a>
|
||||
{{if not DisableWebhooks}}
|
||||
<a class="{{if .PageIsSettingsHooks}}active{{end}} item" href="{{.RepoLink}}/settings/hooks">
|
||||
{{.i18n.Tr "repo.settings.hooks"}}
|
||||
|
|
|
@ -0,0 +1,132 @@
|
|||
{{template "base/head" .}}
|
||||
<div class="page-content repository settings edit">
|
||||
{{template "repo/header" .}}
|
||||
{{template "repo/settings/navbar" .}}
|
||||
<div class="ui container">
|
||||
{{template "base/alert" .}}
|
||||
{{if .Repository.IsArchived}}
|
||||
<div class="ui warning message">
|
||||
{{.i18n.Tr "repo.settings.archive.tagsettings_unavailable"}}
|
||||
</div>
|
||||
{{else}}
|
||||
<h4 class="ui top attached header">
|
||||
{{.i18n.Tr "repo.settings.tags.protection"}}
|
||||
</h4>
|
||||
|
||||
<div class="ui attached segment">
|
||||
<div class="ui grid">
|
||||
<div class="eight wide column">
|
||||
<div class="ui segment">
|
||||
<form class="ui form" action="{{.Link}}" method="post">
|
||||
{{.CsrfTokenHtml}}
|
||||
<div class="field">
|
||||
<label>{{.i18n.Tr "repo.settings.tags.protection.pattern"}}</label>
|
||||
<div id="search-tag-box" class="ui search">
|
||||
<div class="ui input">
|
||||
<input class="prompt" name="name_pattern" autocomplete="off" value="{{.name_pattern}}" placeholder="v*" autofocus required>
|
||||
</div>
|
||||
<div class="help">{{.i18n.Tr "repo.settings.tags.protection.pattern.description" | Safe}}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="whitelist field">
|
||||
<label>{{.i18n.Tr "repo.settings.tags.protection.allowed.users"}}</label>
|
||||
<div class="ui multiple search selection dropdown">
|
||||
<input type="hidden" name="allowlist_users" value="{{.allowlist_users}}">
|
||||
<div class="default text">{{.i18n.Tr "repo.settings.protect_whitelist_search_users"}}</div>
|
||||
<div class="menu">
|
||||
{{range .Users}}
|
||||
<div class="item" data-value="{{.ID}}">
|
||||
{{avatar . 28 "mini"}}
|
||||
{{.GetDisplayName}}
|
||||
</div>
|
||||
{{end}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{if .Owner.IsOrganization}}
|
||||
<div class="whitelist field">
|
||||
<label>{{.i18n.Tr "repo.settings.tags.protection.allowed.teams"}}</label>
|
||||
<div class="ui multiple search selection dropdown">
|
||||
<input type="hidden" name="allowlist_teams" value="{{.allowlist_teams}}">
|
||||
<div class="default text">{{.i18n.Tr "repo.settings.protect_whitelist_search_teams"}}</div>
|
||||
<div class="menu">
|
||||
{{range .Teams}}
|
||||
<div class="item" data-value="{{.ID}}">
|
||||
{{svg "octicon-people"}}
|
||||
{{.Name}}
|
||||
</div>
|
||||
{{end}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
<div class="field">
|
||||
{{if .PageIsEditProtectedTag}}
|
||||
<button class="ui green button">
|
||||
{{$.i18n.Tr "save"}}
|
||||
</button>
|
||||
<a class="ui blue button" href="{{$.RepoLink}}/settings/tags">
|
||||
{{$.i18n.Tr "cancel"}}
|
||||
</a>
|
||||
{{else}}
|
||||
<button class="ui green button">
|
||||
{{$.i18n.Tr "repo.settings.tags.protection.create"}}
|
||||
</button>
|
||||
{{end}}
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="sixteen wide column">
|
||||
<table class="ui single line table">
|
||||
<thead>
|
||||
<th>{{.i18n.Tr "repo.settings.tags.protection.pattern"}}</th>
|
||||
<th>{{.i18n.Tr "repo.settings.tags.protection.allowed"}}</th>
|
||||
<th></th>
|
||||
</thead>
|
||||
<tbody>
|
||||
{{range .ProtectedTags}}
|
||||
<tr>
|
||||
<td><pre>{{.NamePattern}}</pre></td>
|
||||
<td>
|
||||
{{if or .AllowlistUserIDs (and $.Owner.IsOrganization .AllowlistTeamIDs)}}
|
||||
{{$userIDs := .AllowlistUserIDs}}
|
||||
{{range $.Users}}
|
||||
{{if contain $userIDs .ID }}
|
||||
<a class="ui basic image label" href="{{.HomeLink}}">{{avatar . 26}} {{.GetDisplayName}}</a>
|
||||
{{end}}
|
||||
{{end}}
|
||||
{{if $.Owner.IsOrganization}}
|
||||
{{$teamIDs := .AllowlistTeamIDs}}
|
||||
{{range $.Teams}}
|
||||
{{if contain $teamIDs .ID }}
|
||||
<a class="ui basic image label" href="{{$.Owner.OrganisationLink}}/teams/{{.LowerName}}">{{.Name}}</a>
|
||||
{{end}}
|
||||
{{end}}
|
||||
{{end}}
|
||||
{{else}}
|
||||
{{$.i18n.Tr "repo.settings.tags.protection.allowed.noone"}}
|
||||
{{end}}
|
||||
</td>
|
||||
<td class="right aligned">
|
||||
<a class="ui tiny blue button" href="{{$.RepoLink}}/settings/tags/{{.ID}}">{{$.i18n.Tr "edit"}}</a>
|
||||
<form class="dib" action="{{$.RepoLink}}/settings/tags/delete" method="post">
|
||||
{{$.CsrfTokenHtml}}
|
||||
<input type="hidden" name="id" value="{{.ID}}" />
|
||||
<button class="ui tiny red button">{{$.i18n.Tr "remove"}}</button>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
{{else}}
|
||||
<tr class="center aligned"><td colspan="3">{{.i18n.Tr "repo.settings.tags.protection.none"}}</td></tr>
|
||||
{{end}}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
</div>
|
||||
</div>
|
||||
{{template "base/footer" .}}
|
Loading…
Reference in New Issue