GitRoot

craft your forge, build your project, grow your community freely
  1// SPDX-FileCopyrightText: 2025 Romain Maneschi <romain@gitroot.dev>
  2//
  3// SPDX-License-Identifier: EUPL-1.2
  4
  5package user
  6
  7import (
  8	"bytes"
  9	"fmt"
 10	"slices"
 11
 12	"github.com/goccy/go-yaml"
 13	"github.com/goccy/go-yaml/parser"
 14	"github.com/samber/oops"
 15)
 16
 17type GitRootGroup struct {
 18	GroupName string
 19	Branches  []string
 20}
 21
 22type GitRootUser struct {
 23	Group     *GitRootGroup
 24	Email     string
 25	Pseudo    string
 26	AllGroups []*GitRootGroup
 27	PubKey    string
 28}
 29
 30type Group struct {
 31	Name     string `yaml:"-"`
 32	Branches []Branch
 33	Users    []User
 34}
 35
 36type Branch struct {
 37	Name string
 38	Not  []string `yaml:",omitempty"`
 39	Only []string `yaml:",omitempty"`
 40}
 41
 42type User struct {
 43	Pseudo string
 44	Avatar string
 45	Emails []string
 46	Ssh    []string
 47}
 48
 49func ParseGroups(fileContent []byte) ([]Group, error) {
 50	groups := make([]Group, 0)
 51	groupsByName := make(map[string]Group)
 52	if err := yaml.UnmarshalWithOptions(fileContent, &groupsByName, yaml.UseOrderedMap()); err != nil {
 53		return nil, oops.Wrapf(err, "can't parse yaml users")
 54	}
 55	for name, group := range groupsByName {
 56		group.Name = name
 57		groups = append(groups, group)
 58	}
 59	return groups, nil
 60}
 61
 62func WriteGroups(groups []Group) ([]byte, error) {
 63	groupsByName := make(map[string]Group)
 64	for _, group := range groups {
 65		groupsByName[group.Name] = group
 66	}
 67	content, err := yaml.Marshal(groupsByName)
 68	return content, oops.Wrapf(err, "can't marshal yaml groups")
 69}
 70
 71type SimpleUser struct {
 72	Pseudo string
 73	Email  string
 74	Ssh    string
 75}
 76
 77func CreateFileUser(defaultBranch string, users ...SimpleUser) ([]byte, error) {
 78	u := make([]User, len(users))
 79	for i, j := range users {
 80		u[i] = User{
 81			Pseudo: j.Pseudo,
 82			Avatar: "",
 83			Emails: []string{j.Email},
 84			Ssh:    []string{j.Ssh},
 85		}
 86	}
 87	g := Group{
 88		Name:     "owner",
 89		Branches: []Branch{{Name: defaultBranch}},
 90		Users:    u,
 91	}
 92	content, err := yaml.Marshal(map[string]Group{"owner": g})
 93	return content, oops.Wrapf(err, "can't marshal yaml users")
 94}
 95
 96func AppendUserToGroup(file []byte, groupName string, users ...SimpleUser) ([]byte, error) {
 97	urlPath, err := yaml.PathString(fmt.Sprintf("$.%s.users", groupName))
 98	if err != nil {
 99		return nil, oops.Wrapf(err, "invalid path yaml")
100	}
101	f, err := parser.ParseBytes(file, 0)
102	if err != nil {
103		return nil, oops.Wrapf(err, "can't parse file")
104	}
105	u := make([]User, 0)
106	for _, j := range users {
107		if !bytes.Contains(file, []byte(j.Ssh)) {
108			u = append(u, User{
109				Pseudo: j.Pseudo,
110				Avatar: "",
111				Emails: []string{j.Email},
112				Ssh:    []string{j.Ssh},
113			})
114		}
115	}
116	content, err := yaml.Marshal(u)
117	if err != nil {
118		return nil, oops.Wrapf(err, "can't marshal u")
119	}
120	err = urlPath.MergeFromReader(f, bytes.NewReader(content))
121	return []byte(f.String()), oops.Wrapf(err, "can't merge yaml")
122}
123
124func AppendBranchToGroup(file []byte, groupName string, branches ...string) ([]byte, error) {
125	urlPath, err := yaml.PathString(fmt.Sprintf("$.%s.branches", groupName))
126	if err != nil {
127		return nil, oops.Wrapf(err, "invalid path yaml")
128	}
129	f, err := parser.ParseBytes(file, 0)
130	if err != nil {
131		return nil, oops.Wrapf(err, "can't parse file")
132	}
133	u := make([]Branch, len(branches))
134	for i, j := range branches {
135		u[i] = Branch{
136			Name: j,
137		}
138	}
139	content, err := yaml.Marshal(u)
140	if err != nil {
141		return nil, oops.Wrapf(err, "can't marshal u")
142	}
143	err = urlPath.MergeFromReader(f, bytes.NewReader(content))
144	return []byte(f.String()), oops.Wrapf(err, "can't merge yaml")
145}
146
147func AppendBranch(file []byte, groupName string, users ...SimpleUser) ([]byte, error) {
148	u := make([]User, len(users))
149	for i, j := range users {
150		u[i] = User{
151			Pseudo: j.Pseudo,
152			Avatar: "",
153			Emails: []string{j.Email},
154			Ssh:    []string{j.Ssh},
155		}
156	}
157	group := Group{Name: groupName, Branches: []Branch{{Name: groupName}}, Users: u}
158	content, err := yaml.Marshal(map[string]Group{groupName: group})
159	if err != nil {
160		return nil, oops.Wrapf(err, "can't marshal u")
161	}
162
163	return AppendBranchToGroup(append(file, content...), "owner", groupName)
164}
165
166func DeleteGroupAndBranch(file []byte, groupName string) ([]byte, error) {
167	groups, err := ParseGroups(file)
168	if err != nil {
169		return nil, oops.Wrapf(err, "can't read group")
170	}
171
172	goodGroups := make([]Group, 0)
173	for _, g := range groups {
174		if g.Name != groupName {
175			if idx := slices.IndexFunc(g.Branches, func(b Branch) bool { return b.Name == groupName }); idx > -1 {
176				goodGroups = append(goodGroups, Group{
177					Name:     g.Name,
178					Users:    g.Users,
179					Branches: append(g.Branches[:idx], g.Branches[idx+1:]...),
180				})
181			} else {
182				goodGroups = append(goodGroups, g)
183			}
184		}
185	}
186
187	return WriteGroups(goodGroups)
188}
189
190func FindUser(file []byte, branch string, sshKey string) (*GitRootUser, error) {
191	groups, err := ParseGroups(file)
192	if err != nil {
193		return nil, oops.Wrapf(err, "can't read group")
194	}
195	found := false
196	var goodUser User
197	var goodGroup *GitRootGroup
198	allGroups := make([]*GitRootGroup, 0)
199	for _, g := range groups {
200		branches := make([]string, len(g.Branches))
201		for i, b := range g.Branches {
202			branches[i] = b.Name
203		}
204		currentGroup := &GitRootGroup{
205			GroupName: g.Name,
206			Branches:  branches,
207		}
208		allGroups = append(allGroups, currentGroup)
209		if slices.Contains(currentGroup.Branches, branch) {
210			for _, u := range g.Users {
211				for _, s := range u.Ssh {
212					if s == sshKey {
213						found = true
214						goodUser = u
215						goodGroup = currentGroup
216						break
217					}
218				}
219				if found {
220					break
221				}
222			}
223		}
224
225	}
226	if !found {
227		return &GitRootUser{
228			Group:     nil,
229			Email:     "",
230			Pseudo:    "",
231			AllGroups: allGroups,
232			PubKey:    sshKey,
233		}, nil
234	}
235	return &GitRootUser{
236		Group:     goodGroup,
237		Email:     goodUser.Emails[0],
238		Pseudo:    goodUser.Pseudo,
239		AllGroups: allGroups,
240		PubKey:    sshKey,
241	}, nil
242}
243
244func (u *GitRootUser) CanWrite(branch string) bool {
245	// user is in the group of the branch
246	if u.Group != nil { // new user
247		for _, b := range u.Group.Branches {
248			if b == branch || b == "*" {
249				return true
250			}
251		}
252	}
253	// other group have right for this branch
254	for _, g := range u.AllGroups {
255		if g.GroupName == "*" {
256			return false
257		}
258		if u.Group == nil || u.Group.GroupName != g.GroupName {
259			for _, b := range g.Branches {
260				if b == branch {
261					return false
262				}
263			}
264		}
265	}
266	// nobody take care of this branch
267	return true
268}