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 4 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
6 changes: 3 additions & 3 deletions cmd/hook.go
Original file line number Diff line number Diff line change
Expand Up @@ -222,15 +222,15 @@ Gitea or set your environment appropriately.`, "")
lastline++

// If the ref is a branch, check if it's protected
KN4CK3R marked this conversation as resolved.
Show resolved Hide resolved
if strings.HasPrefix(refFullName, git.BranchPrefix) {
if strings.HasPrefix(refFullName, git.BranchPrefix) || strings.HasPrefix(refFullName, git.TagPrefix) {
oldCommitIDs[count] = oldCommitID
newCommitIDs[count] = newCommitID
refFullNames[count] = refFullName
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
162 changes: 162 additions & 0 deletions integrations/repo_tag_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
// Copyright 2021 The Gitea Authors. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.

package integrations

import (
"io/ioutil"
"net/url"
"testing"

"code.gitea.io/gitea/models"
"code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/services/release"

"github.com/stretchr/testify/assert"
)

func TestIsUserAllowedToControlTag(t *testing.T) {
KN4CK3R marked this conversation as resolved.
Show resolved Hide resolved
protectedTags := []*models.ProtectedTag{
{
NamePattern: "*gitea",
WhitelistUserIDs: []int64{1},
},
{
NamePattern: "v-*",
WhitelistUserIDs: []int64{2},
},
{
NamePattern: "release",
},
}

cases := []struct {
name string
userid int64
allowed bool
}{
{
name: "test",
userid: 1,
allowed: true,
},
{
name: "test",
userid: 3,
allowed: true,
},
{
name: "gitea",
userid: 1,
allowed: true,
},
{
name: "gitea",
userid: 3,
allowed: false,
},
{
name: "test-gitea",
userid: 1,
allowed: true,
},
{
name: "test-gitea",
userid: 3,
allowed: false,
},
{
name: "gitea-test",
userid: 1,
allowed: true,
},
{
name: "gitea-test",
userid: 3,
allowed: true,
},
{
name: "v-1",
userid: 1,
allowed: false,
},
{
name: "v-1",
userid: 2,
allowed: true,
},
{
name: "release",
userid: 1,
allowed: false,
},
}

for n, c := range cases {
isAllowed, err := models.IsUserAllowedToControlTag(protectedTags, c.name, c.userid)
assert.NoError(t, err)
assert.Equal(t, c.allowed, isAllowed, "case %d: error should match", n)
}
}

func TestCreateNewTagProtected(t *testing.T) {
defer prepareTestEnv(t)()

repo := models.AssertExistsAndLoadBean(t, &models.Repository{ID: 1}).(*models.Repository)
owner := models.AssertExistsAndLoadBean(t, &models.User{ID: repo.OwnerID}).(*models.User)

t.Run("API", func(t *testing.T) {
defer PrintCurrentTest(t)()

err := release.CreateNewTag(owner, repo, "master", "v-1", "first tag")
assert.NoError(t, err)

err = models.InsertProtectedTag(&models.ProtectedTag{
RepoID: repo.ID,
NamePattern: "v-*",
})
assert.NoError(t, err)
err = models.InsertProtectedTag(&models.ProtectedTag{
RepoID: repo.ID,
NamePattern: "v-1.1",
WhitelistUserIDs: []int64{repo.OwnerID},
})
assert.NoError(t, err)

err = release.CreateNewTag(owner, repo, "master", "v-2", "second tag")
assert.Error(t, err)
assert.True(t, models.IsErrInvalidTagName(err))
e := err.(models.ErrInvalidTagName)
assert.True(t, e.Protected)

err = release.CreateNewTag(owner, repo, "master", "v-1.1", "third tag")
assert.NoError(t, err)
})

t.Run("Git", func(t *testing.T) {
defer PrintCurrentTest(t)()

onGiteaRun(t, func(t *testing.T, u *url.URL) {
KN4CK3R marked this conversation as resolved.
Show resolved Hide resolved
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")
})
})
}
6 changes: 5 additions & 1 deletion models/error.go
Original file line number Diff line number Diff line change
Expand Up @@ -957,7 +957,8 @@ func (err ErrReleaseNotExist) Error() string {

// ErrInvalidTagName represents a "InvalidTagName" kind of error.
type ErrInvalidTagName struct {
TagName string
TagName string
Protected bool
KN4CK3R marked this conversation as resolved.
Show resolved Hide resolved
}

// IsErrInvalidTagName checks if an error is a ErrInvalidTagName.
Expand All @@ -967,6 +968,9 @@ func IsErrInvalidTagName(err error) bool {
}

func (err ErrInvalidTagName) Error() string {
if err.Protected {
return fmt.Sprintf("release tag name is protected [tag_name: %s]", err.TagName)
}
return fmt.Sprintf("release tag name is not valid [tag_name: %s]", err.TagName)
}

Expand Down
2 changes: 2 additions & 0 deletions models/migrations/migrations.go
Original file line number Diff line number Diff line change
Expand Up @@ -309,6 +309,8 @@ var migrations = []Migration{
NewMigration("Add LFS columns to Mirror", addLFSMirrorColumns),
// v179 -> v180
NewMigration("Convert avatar url to text", convertAvatarURLToText),
// v180 -> v181
NewMigration("Create protected tag table", createProtectedTagTable),
}

// GetCurrentDBVersion returns the current db version
Expand Down
38 changes: 38 additions & 0 deletions models/migrations/v180.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
// Copyright 2021 The Gitea Authors. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.

package migrations

import (
"fmt"

"code.gitea.io/gitea/modules/timeutil"

"xorm.io/xorm"
)

func createProtectedTagTable(x *xorm.Engine) error {
type ProtectedTag struct {
ID int64 `xorm:"pk autoincr"`
RepoID int64 `xorm:"UNIQUE(s)"`
NamePattern string `xorm:"UNIQUE(s)"`
WhitelistUserIDs []int64 `xorm:"JSON TEXT"`
WhitelistTeamIDs []int64 `xorm:"JSON TEXT"`

CreatedUnix timeutil.TimeStamp `xorm:"created"`
UpdatedUnix timeutil.TimeStamp `xorm:"updated"`
}

sess := x.NewSession()
defer sess.Close()
if err := sess.Begin(); err != nil {
return err
}
KN4CK3R marked this conversation as resolved.
Show resolved Hide resolved

if err := sess.Sync2(new(ProtectedTag)); err != nil {
return fmt.Errorf("Sync2: %v", err)
}

return sess.Commit()
}
1 change: 1 addition & 0 deletions models/models.go
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,7 @@ func init() {
new(ProjectIssue),
new(Session),
new(RepoTransfer),
new(ProtectedTag),
)

gonicNames := []string{"SSL", "UID"}
Expand Down
117 changes: 117 additions & 0 deletions models/tags.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
// 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 (
"strings"

"code.gitea.io/gitea/modules/base"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/timeutil"

"github.com/gobwas/glob"
)

// ProtectedTag struct
type ProtectedTag struct {
KN4CK3R marked this conversation as resolved.
Show resolved Hide resolved
ID int64 `xorm:"pk autoincr"`
RepoID int64 `xorm:"UNIQUE(s)"`
NamePattern string `xorm:"UNIQUE(s)"`
NameGlob glob.Glob `xorm:"-"`
WhitelistUserIDs []int64 `xorm:"JSON TEXT"`
WhitelistTeamIDs []int64 `xorm:"JSON TEXT"`

CreatedUnix timeutil.TimeStamp `xorm:"created"`
UpdatedUnix timeutil.TimeStamp `xorm:"updated"`
}

// BeforeInsert will be invoked by XORM before inserting a record
func (pt *ProtectedTag) BeforeInsert() {
pt.CreatedUnix = timeutil.TimeStampNow()
pt.UpdatedUnix = timeutil.TimeStampNow()
}

// BeforeUpdate is invoked from XORM before updating this object.
func (pt *ProtectedTag) BeforeUpdate() {
pt.UpdatedUnix = timeutil.TimeStampNow()
}

// InsertProtectedTag inserts a protected tag to database
func InsertProtectedTag(pt *ProtectedTag) error {
_, err := x.Insert(pt)
return err
}

// UpdateProtectedTag updates the protected tag
func UpdateProtectedTag(pt *ProtectedTag) error {
_, err := x.ID(pt.ID).AllCols().Update(pt)
return err
}

// DeleteProtectedTag deletes a protected tag by ID
func DeleteProtectedTag(pt *ProtectedTag) error {
_, err := x.Delete(&ProtectedTag{ID: pt.ID})
return err
}

// EnsureCompiledPattern returns if the branch is protected
func (pt *ProtectedTag) EnsureCompiledPattern() error {
if pt.NameGlob != nil {
return nil
}

expr := strings.TrimSpace(pt.NamePattern)

var err error
pt.NameGlob, err = glob.Compile(expr)
return err
}

// IsUserAllowed returns true if the user is allowed to modify the tag
func (pt *ProtectedTag) IsUserAllowed(userID int64) bool {
if base.Int64sContains(pt.WhitelistUserIDs, userID) {
return true
}

if len(pt.WhitelistTeamIDs) == 0 {
return false
}

in, err := IsUserInTeams(userID, pt.WhitelistTeamIDs)
if err != nil {
log.Error("IsUserInTeams: %v", err)
KN4CK3R marked this conversation as resolved.
Show resolved Hide resolved
return false
}
return in
}

// GetProtectedTags gets all protected tags
func (repo *Repository) GetProtectedTags() ([]*ProtectedTag, error) {
tags := make([]*ProtectedTag, 0)
return tags, x.Find(&tags, &ProtectedTag{RepoID: repo.ID})
}

// IsUserAllowedToControlTag checks if a user can control the specific tag.
// It returns true if the tag name is not protected or the user is allowed to control it.
func IsUserAllowedToControlTag(tags []*ProtectedTag, tagName string, userID int64) (bool, error) {
isAllowed := true
for _, tag := range tags {
if err := tag.EnsureCompiledPattern(); err != nil {
log.Error("EnsureCompiledPattern failed: %v", err)
return false, err
}

if !tag.NameGlob.Match(tagName) {
continue
}

isAllowed = tag.IsUserAllowed(userID)
if isAllowed {
break
}
}

return isAllowed, nil
}
Loading