Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add tag protection #15629

Merged
merged 50 commits into from
Jun 25, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
50 commits
Select commit Hold shift + click to select a range
98c8760
Added tag protection in hook.
KN4CK3R Apr 26, 2021
4718c2a
Prevent UI tag creation if protected.
KN4CK3R Apr 26, 2021
911fec6
Added settings page.
KN4CK3R Apr 26, 2021
e20ddac
Added tests.
KN4CK3R Apr 26, 2021
751c13b
Added suggestions.
KN4CK3R Apr 27, 2021
932a677
Renamed file.
KN4CK3R Apr 27, 2021
922cab1
Added suggestions.
KN4CK3R Apr 27, 2021
15ac4b4
Moved tests.
KN4CK3R Apr 28, 2021
2943590
Merge branch 'master' of https://github.com/go-gitea/gitea into featu…
KN4CK3R Apr 28, 2021
afc7673
Use individual errors.
KN4CK3R Apr 28, 2021
3ee5427
Removed unneeded methods.
KN4CK3R Apr 28, 2021
dfbff8c
Switched delete selector.
KN4CK3R Apr 28, 2021
46d4e29
Changed method names.
KN4CK3R Apr 28, 2021
784b16b
Changed url.
KN4CK3R Apr 28, 2021
2324fa8
Removed fix.
KN4CK3R Apr 28, 2021
3c4d57c
No reason to be unique.
KN4CK3R Apr 30, 2021
739939b
Allow editing of protected tags.
KN4CK3R Apr 30, 2021
a4d0953
Merge branch 'master' of https://github.com/go-gitea/gitea into featu…
KN4CK3R May 1, 2021
e5a6804
lint
KN4CK3R May 1, 2021
0540877
Merge branch 'master' into feature-tag-protection
zeripath May 1, 2021
f3a4d02
Removed unique key from migration.
KN4CK3R May 11, 2021
7080e28
Apply suggestion.
KN4CK3R May 11, 2021
970a19b
Fixed comment.
KN4CK3R May 12, 2021
9120297
Merge branch 'main' of https://github.com/go-gitea/gitea into feature…
KN4CK3R May 12, 2021
7466925
Get tag by id.
KN4CK3R May 13, 2021
a5ecd2c
Added docs page.
KN4CK3R May 13, 2021
d814f02
Changed date.
KN4CK3R May 14, 2021
7cbbe4e
Respond with 404 to not found tags.
KN4CK3R May 18, 2021
deebd92
Handle id = 0.
KN4CK3R May 18, 2021
4b94720
Lint
KN4CK3R May 18, 2021
ddf3ece
Replaced glob with regex pattern.
KN4CK3R May 25, 2021
6c4be57
Merge branch 'main' of https://github.com/go-gitea/gitea into feature…
KN4CK3R Jun 9, 2021
3063811
Added support for glob and regex pattern.
KN4CK3R Jun 9, 2021
b707196
Updated documentation.
KN4CK3R Jun 9, 2021
6c1c35f
Added suggestions.
KN4CK3R Jun 9, 2021
16a360e
Fixed tests.
KN4CK3R Jun 9, 2021
b59f11b
Merge branch 'main' of https://github.com/go-gitea/gitea into feature…
KN4CK3R Jun 14, 2021
b8441e7
Merge branch 'main' of https://github.com/go-gitea/gitea into feature…
KN4CK3R Jun 15, 2021
46eb17e
Changed white* to allow*.
KN4CK3R Jun 16, 2021
5e0b73e
Merge branch 'main' of https://github.com/go-gitea/gitea into feature…
KN4CK3R Jun 16, 2021
b584cd9
Merge branch 'main' of https://github.com/go-gitea/gitea into feature…
KN4CK3R Jun 17, 2021
b70268f
Fixed edit button link.
KN4CK3R Jun 17, 2021
bdabf96
Added cancel button.
KN4CK3R Jun 17, 2021
54917a2
Merge branch 'main' of https://github.com/go-gitea/gitea into feature…
KN4CK3R Jun 23, 2021
34280e8
Merge branch 'main' into feature-tag-protection
zeripath Jun 23, 2021
2b42948
Merge branch 'main' into feature-tag-protection
lunny Jun 24, 2021
6e768b0
Merge branch 'main' into feature-tag-protection
6543 Jun 24, 2021
384f27f
Merge branch 'main' into feature-tag-protection
zeripath Jun 24, 2021
1e413e5
Fixed binding name.
KN4CK3R Jun 24, 2021
9694fca
Merge branch 'main' into feature-tag-protection
6543 Jun 25, 2021
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions cmd/hook.go
Original file line number Diff line number Diff line change
Expand Up @@ -221,16 +221,16 @@ 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
count++
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
Expand Down Expand Up @@ -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 {
Expand Down
57 changes: 57 additions & 0 deletions docs/content/doc/advanced/protected-tags.en-us.md
Original file line number Diff line number Diff line change
@@ -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 |
2 changes: 2 additions & 0 deletions integrations/mirror_pull_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
74 changes: 74 additions & 0 deletions integrations/repo_tag_test.go
Original file line number Diff line number Diff line change
@@ -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")
})
})
}
15 changes: 15 additions & 0 deletions models/error.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions models/migrations/migrations.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
26 changes: 26 additions & 0 deletions models/migrations/v186.go
Original file line number Diff line number Diff line change
@@ -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"`
6543 marked this conversation as resolved.
Show resolved Hide resolved
UpdatedUnix timeutil.TimeStamp `xorm:"updated"`
}

return x.Sync2(new(ProtectedTag))
}
1 change: 1 addition & 0 deletions models/models.go
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,7 @@ func init() {
new(IssueIndex),
new(PushMirror),
new(RepoArchiver),
new(ProtectedTag),
)

gonicNames := []string{"SSL", "UID"}
Expand Down
131 changes: 131 additions & 0 deletions models/protected_tag.go
Original file line number Diff line number Diff line change
@@ -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
KN4CK3R marked this conversation as resolved.
Show resolved Hide resolved
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)
}
Loading