Skip to content

Commit 234be2a

Browse files
authored
Merge branch 'main' into fix-6087-heatmap
2 parents fe1485e + 44b8b07 commit 234be2a

27 files changed

+1220
-182
lines changed

cmd/hook.go

+4-4
Original file line numberDiff line numberDiff line change
@@ -221,16 +221,16 @@ Gitea or set your environment appropriately.`, "")
221221
total++
222222
lastline++
223223

224-
// If the ref is a branch, check if it's protected
225-
if strings.HasPrefix(refFullName, git.BranchPrefix) {
224+
// If the ref is a branch or tag, check if it's protected
225+
if strings.HasPrefix(refFullName, git.BranchPrefix) || strings.HasPrefix(refFullName, git.TagPrefix) {
226226
oldCommitIDs[count] = oldCommitID
227227
newCommitIDs[count] = newCommitID
228228
refFullNames[count] = refFullName
229229
count++
230230
fmt.Fprintf(out, "*")
231231

232232
if count >= hookBatchSize {
233-
fmt.Fprintf(out, " Checking %d branches\n", count)
233+
fmt.Fprintf(out, " Checking %d references\n", count)
234234

235235
hookOptions.OldCommitIDs = oldCommitIDs
236236
hookOptions.NewCommitIDs = newCommitIDs
@@ -261,7 +261,7 @@ Gitea or set your environment appropriately.`, "")
261261
hookOptions.NewCommitIDs = newCommitIDs[:count]
262262
hookOptions.RefFullNames = refFullNames[:count]
263263

264-
fmt.Fprintf(out, " Checking %d branches\n", count)
264+
fmt.Fprintf(out, " Checking %d references\n", count)
265265

266266
statusCode, msg := private.HookPreReceive(username, reponame, hookOptions)
267267
switch statusCode {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
---
2+
date: "2021-05-14T00:00:00-00:00"
3+
title: "Protected tags"
4+
slug: "protected-tags"
5+
weight: 45
6+
toc: false
7+
draft: false
8+
menu:
9+
sidebar:
10+
parent: "advanced"
11+
name: "Protected tags"
12+
weight: 45
13+
identifier: "protected-tags"
14+
---
15+
16+
# Protected tags
17+
18+
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.
19+
20+
**Table of Contents**
21+
22+
{{< toc >}}
23+
24+
## Setting up protected tags
25+
26+
To protect a tag, you need to follow these steps:
27+
28+
1. Go to the repository’s **Settings** > **Tags** page.
29+
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.
30+
1. Choose the allowed users and/or teams. If you leave these fields empty noone is allowed to create or modify this tag.
31+
1. Select **Save** to save the configuration.
32+
33+
## Pattern protected tags
34+
35+
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.
36+
37+
Examples:
38+
39+
| Type | Pattern Protected Tag | Possible Matching Tags |
40+
| ----- | ------------------------ | --------------------------------------- |
41+
| Glob | `v*` | `v`, `v-1`, `version2` |
42+
| Glob | `v[0-9]` | `v0`, `v1` up to `v9` |
43+
| Glob | `*-release` | `2.1-release`, `final-release` |
44+
| Glob | `gitea` | only `gitea` |
45+
| Glob | `*gitea*` | `gitea`, `2.1-gitea`, `1_gitea-release` |
46+
| Glob | `{v,rel}-*` | `v-`, `v-1`, `v-final`, `rel-`, `rel-x` |
47+
| Glob | `*` | matches all possible tag names |
48+
| Regex | `/\Av/` | `v`, `v-1`, `version2` |
49+
| Regex | `/\Av[0-9]\z/` | `v0`, `v1` up to `v9` |
50+
| Regex | `/\Av\d+\.\d+\.\d+\z/` | `v1.0.17`, `v2.1.0` |
51+
| Regex | `/\Av\d+(\.\d+){0,2}\z/` | `v1`, `v2.1`, `v1.2.34` |
52+
| Regex | `/-release\z/` | `2.1-release`, `final-release` |
53+
| Regex | `/gitea/` | `gitea`, `2.1-gitea`, `1_gitea-release` |
54+
| Regex | `/\Agitea\z/` | only `gitea` |
55+
| Regex | `/^gitea$/` | only `gitea` |
56+
| Regex | `/\A(v\|rel)-/` | `v-`, `v-1`, `v-final`, `rel-`, `rel-x` |
57+
| Regex | `/.+/` | matches all possible tag names |

integrations/mirror_pull_test.go

+2
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,9 @@ func TestMirrorPull(t *testing.T) {
5959

6060
assert.NoError(t, release_service.CreateRelease(gitRepo, &models.Release{
6161
RepoID: repo.ID,
62+
Repo: repo,
6263
PublisherID: user.ID,
64+
Publisher: user,
6365
TagName: "v0.2",
6466
Target: "master",
6567
Title: "v0.2 is released",

integrations/repo_tag_test.go

+74
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
// Copyright 2021 The Gitea Authors. All rights reserved.
2+
// Use of this source code is governed by a MIT-style
3+
// license that can be found in the LICENSE file.
4+
5+
package integrations
6+
7+
import (
8+
"io/ioutil"
9+
"net/url"
10+
"testing"
11+
12+
"code.gitea.io/gitea/models"
13+
"code.gitea.io/gitea/modules/git"
14+
"code.gitea.io/gitea/modules/util"
15+
"code.gitea.io/gitea/services/release"
16+
17+
"github.com/stretchr/testify/assert"
18+
)
19+
20+
func TestCreateNewTagProtected(t *testing.T) {
21+
defer prepareTestEnv(t)()
22+
23+
repo := models.AssertExistsAndLoadBean(t, &models.Repository{ID: 1}).(*models.Repository)
24+
owner := models.AssertExistsAndLoadBean(t, &models.User{ID: repo.OwnerID}).(*models.User)
25+
26+
t.Run("API", func(t *testing.T) {
27+
defer PrintCurrentTest(t)()
28+
29+
err := release.CreateNewTag(owner, repo, "master", "v-1", "first tag")
30+
assert.NoError(t, err)
31+
32+
err = models.InsertProtectedTag(&models.ProtectedTag{
33+
RepoID: repo.ID,
34+
NamePattern: "v-*",
35+
})
36+
assert.NoError(t, err)
37+
err = models.InsertProtectedTag(&models.ProtectedTag{
38+
RepoID: repo.ID,
39+
NamePattern: "v-1.1",
40+
AllowlistUserIDs: []int64{repo.OwnerID},
41+
})
42+
assert.NoError(t, err)
43+
44+
err = release.CreateNewTag(owner, repo, "master", "v-2", "second tag")
45+
assert.Error(t, err)
46+
assert.True(t, models.IsErrProtectedTagName(err))
47+
48+
err = release.CreateNewTag(owner, repo, "master", "v-1.1", "third tag")
49+
assert.NoError(t, err)
50+
})
51+
52+
t.Run("Git", func(t *testing.T) {
53+
onGiteaRun(t, func(t *testing.T, u *url.URL) {
54+
username := "user2"
55+
httpContext := NewAPITestContext(t, username, "repo1")
56+
57+
dstPath, err := ioutil.TempDir("", httpContext.Reponame)
58+
assert.NoError(t, err)
59+
defer util.RemoveAll(dstPath)
60+
61+
u.Path = httpContext.GitPath()
62+
u.User = url.UserPassword(username, userPassword)
63+
64+
doGitClone(dstPath, u)(t)
65+
66+
_, err = git.NewCommand("tag", "v-2").RunInDir(dstPath)
67+
assert.NoError(t, err)
68+
69+
_, err = git.NewCommand("push", "--tags").RunInDir(dstPath)
70+
assert.Error(t, err)
71+
assert.Contains(t, err.Error(), "Tag v-2 is protected")
72+
})
73+
})
74+
}

models/error.go

+15
Original file line numberDiff line numberDiff line change
@@ -985,6 +985,21 @@ func (err ErrInvalidTagName) Error() string {
985985
return fmt.Sprintf("release tag name is not valid [tag_name: %s]", err.TagName)
986986
}
987987

988+
// ErrProtectedTagName represents a "ProtectedTagName" kind of error.
989+
type ErrProtectedTagName struct {
990+
TagName string
991+
}
992+
993+
// IsErrProtectedTagName checks if an error is a ErrProtectedTagName.
994+
func IsErrProtectedTagName(err error) bool {
995+
_, ok := err.(ErrProtectedTagName)
996+
return ok
997+
}
998+
999+
func (err ErrProtectedTagName) Error() string {
1000+
return fmt.Sprintf("release tag name is protected [tag_name: %s]", err.TagName)
1001+
}
1002+
9881003
// ErrRepoFileAlreadyExists represents a "RepoFileAlreadyExist" kind of error.
9891004
type ErrRepoFileAlreadyExists struct {
9901005
Path string

models/migrations/migrations.go

+2
Original file line numberDiff line numberDiff line change
@@ -321,6 +321,8 @@ var migrations = []Migration{
321321
NewMigration("Rename Task errors to message", renameTaskErrorsToMessage),
322322
// v185 -> v186
323323
NewMigration("Add new table repo_archiver", addRepoArchiver),
324+
// v186 -> v187
325+
NewMigration("Create protected tag table", createProtectedTagTable),
324326
}
325327

326328
// GetCurrentDBVersion returns the current db version

models/migrations/v186.go

+26
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
// Copyright 2021 The Gitea Authors. All rights reserved.
2+
// Use of this source code is governed by a MIT-style
3+
// license that can be found in the LICENSE file.
4+
5+
package migrations
6+
7+
import (
8+
"code.gitea.io/gitea/modules/timeutil"
9+
10+
"xorm.io/xorm"
11+
)
12+
13+
func createProtectedTagTable(x *xorm.Engine) error {
14+
type ProtectedTag struct {
15+
ID int64 `xorm:"pk autoincr"`
16+
RepoID int64
17+
NamePattern string
18+
AllowlistUserIDs []int64 `xorm:"JSON TEXT"`
19+
AllowlistTeamIDs []int64 `xorm:"JSON TEXT"`
20+
21+
CreatedUnix timeutil.TimeStamp `xorm:"created"`
22+
UpdatedUnix timeutil.TimeStamp `xorm:"updated"`
23+
}
24+
25+
return x.Sync2(new(ProtectedTag))
26+
}

models/models.go

+1
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,7 @@ func init() {
137137
new(IssueIndex),
138138
new(PushMirror),
139139
new(RepoArchiver),
140+
new(ProtectedTag),
140141
)
141142

142143
gonicNames := []string{"SSL", "UID"}

models/protected_tag.go

+131
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
// Copyright 2021 The Gitea Authors. All rights reserved.
2+
// Use of this source code is governed by a MIT-style
3+
// license that can be found in the LICENSE file.
4+
5+
package models
6+
7+
import (
8+
"regexp"
9+
"strings"
10+
11+
"code.gitea.io/gitea/modules/base"
12+
"code.gitea.io/gitea/modules/timeutil"
13+
14+
"github.com/gobwas/glob"
15+
)
16+
17+
// ProtectedTag struct
18+
type ProtectedTag struct {
19+
ID int64 `xorm:"pk autoincr"`
20+
RepoID int64
21+
NamePattern string
22+
RegexPattern *regexp.Regexp `xorm:"-"`
23+
GlobPattern glob.Glob `xorm:"-"`
24+
AllowlistUserIDs []int64 `xorm:"JSON TEXT"`
25+
AllowlistTeamIDs []int64 `xorm:"JSON TEXT"`
26+
27+
CreatedUnix timeutil.TimeStamp `xorm:"created"`
28+
UpdatedUnix timeutil.TimeStamp `xorm:"updated"`
29+
}
30+
31+
// InsertProtectedTag inserts a protected tag to database
32+
func InsertProtectedTag(pt *ProtectedTag) error {
33+
_, err := x.Insert(pt)
34+
return err
35+
}
36+
37+
// UpdateProtectedTag updates the protected tag
38+
func UpdateProtectedTag(pt *ProtectedTag) error {
39+
_, err := x.ID(pt.ID).AllCols().Update(pt)
40+
return err
41+
}
42+
43+
// DeleteProtectedTag deletes a protected tag by ID
44+
func DeleteProtectedTag(pt *ProtectedTag) error {
45+
_, err := x.ID(pt.ID).Delete(&ProtectedTag{})
46+
return err
47+
}
48+
49+
// EnsureCompiledPattern ensures the glob pattern is compiled
50+
func (pt *ProtectedTag) EnsureCompiledPattern() error {
51+
if pt.RegexPattern != nil || pt.GlobPattern != nil {
52+
return nil
53+
}
54+
55+
var err error
56+
if len(pt.NamePattern) >= 2 && strings.HasPrefix(pt.NamePattern, "/") && strings.HasSuffix(pt.NamePattern, "/") {
57+
pt.RegexPattern, err = regexp.Compile(pt.NamePattern[1 : len(pt.NamePattern)-1])
58+
} else {
59+
pt.GlobPattern, err = glob.Compile(pt.NamePattern)
60+
}
61+
return err
62+
}
63+
64+
// IsUserAllowed returns true if the user is allowed to modify the tag
65+
func (pt *ProtectedTag) IsUserAllowed(userID int64) (bool, error) {
66+
if base.Int64sContains(pt.AllowlistUserIDs, userID) {
67+
return true, nil
68+
}
69+
70+
if len(pt.AllowlistTeamIDs) == 0 {
71+
return false, nil
72+
}
73+
74+
in, err := IsUserInTeams(userID, pt.AllowlistTeamIDs)
75+
if err != nil {
76+
return false, err
77+
}
78+
return in, nil
79+
}
80+
81+
// GetProtectedTags gets all protected tags of the repository
82+
func (repo *Repository) GetProtectedTags() ([]*ProtectedTag, error) {
83+
tags := make([]*ProtectedTag, 0)
84+
return tags, x.Find(&tags, &ProtectedTag{RepoID: repo.ID})
85+
}
86+
87+
// GetProtectedTagByID gets the protected tag with the specific id
88+
func GetProtectedTagByID(id int64) (*ProtectedTag, error) {
89+
tag := new(ProtectedTag)
90+
has, err := x.ID(id).Get(tag)
91+
if err != nil {
92+
return nil, err
93+
}
94+
if !has {
95+
return nil, nil
96+
}
97+
return tag, nil
98+
}
99+
100+
// IsUserAllowedToControlTag checks if a user can control the specific tag.
101+
// It returns true if the tag name is not protected or the user is allowed to control it.
102+
func IsUserAllowedToControlTag(tags []*ProtectedTag, tagName string, userID int64) (bool, error) {
103+
isAllowed := true
104+
for _, tag := range tags {
105+
err := tag.EnsureCompiledPattern()
106+
if err != nil {
107+
return false, err
108+
}
109+
110+
if !tag.matchString(tagName) {
111+
continue
112+
}
113+
114+
isAllowed, err = tag.IsUserAllowed(userID)
115+
if err != nil {
116+
return false, err
117+
}
118+
if isAllowed {
119+
break
120+
}
121+
}
122+
123+
return isAllowed, nil
124+
}
125+
126+
func (pt *ProtectedTag) matchString(name string) bool {
127+
if pt.RegexPattern != nil {
128+
return pt.RegexPattern.MatchString(name)
129+
}
130+
return pt.GlobPattern.Match(name)
131+
}

0 commit comments

Comments
 (0)