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}