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