summaryrefslogtreecommitdiffstats
path: root/models
diff options
context:
space:
mode:
Diffstat (limited to 'models')
-rw-r--r--models/error.go15
-rw-r--r--models/migrations/migrations.go2
-rw-r--r--models/migrations/v186.go26
-rw-r--r--models/models.go1
-rw-r--r--models/protected_tag.go131
-rw-r--r--models/protected_tag_test.go162
-rw-r--r--models/repo.go1
7 files changed, 338 insertions, 0 deletions
diff --git a/models/error.go b/models/error.go
index 501bf86869..513effdb02 100644
--- a/models/error.go
+++ b/models/error.go
@@ -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
diff --git a/models/migrations/migrations.go b/models/migrations/migrations.go
index 4e17a6a2c8..978ba6368f 100644
--- a/models/migrations/migrations.go
+++ b/models/migrations/migrations.go
@@ -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
diff --git a/models/migrations/v186.go b/models/migrations/v186.go
new file mode 100644
index 0000000000..eb6ec7118c
--- /dev/null
+++ b/models/migrations/v186.go
@@ -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))
+}
diff --git a/models/models.go b/models/models.go
index 3266be0f4a..610933d327 100644
--- a/models/models.go
+++ b/models/models.go
@@ -137,6 +137,7 @@ func init() {
new(IssueIndex),
new(PushMirror),
new(RepoArchiver),
+ new(ProtectedTag),
)
gonicNames := []string{"SSL", "UID"}
diff --git a/models/protected_tag.go b/models/protected_tag.go
new file mode 100644
index 0000000000..88f20dd29a
--- /dev/null
+++ b/models/protected_tag.go
@@ -0,0 +1,131 @@
+// Copyright 2021 The Gitea Authors. All rights reserved.
+// Use of this source code is governed by a MIT-style
+// license that can be found in the LICENSE file.
+
+package 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)
+}
diff --git a/models/protected_tag_test.go b/models/protected_tag_test.go
new file mode 100644
index 0000000000..3dc895c69f
--- /dev/null
+++ b/models/protected_tag_test.go
@@ -0,0 +1,162 @@
+// Copyright 2021 The Gitea Authors. All rights reserved.
+// Use of this source code is governed by a MIT-style
+// license that can be found in the LICENSE file.
+
+package 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)
+ }
+ })
+}
diff --git a/models/repo.go b/models/repo.go
index 2baf6e9bdd..4ce3d4839b 100644
--- a/models/repo.go
+++ b/models/repo.go
@@ -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},