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