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
62type SimpleUser struct {
63 Pseudo string
64 Email string
65 Ssh string
66}
67
68func CreateFileUser(defaultBranch string, users ...SimpleUser) ([]byte, error) {
69 u := make([]User, len(users))
70 for i, j := range users {
71 u[i] = User{
72 Pseudo: j.Pseudo,
73 Avatar: "",
74 Emails: []string{j.Email},
75 Ssh: []string{j.Ssh},
76 }
77 }
78 g := Group{
79 Name: "owner",
80 Branches: []Branch{{Name: defaultBranch}},
81 Users: u,
82 }
83 content, err := yaml.Marshal(map[string]Group{"owner": g})
84 return content, oops.Wrapf(err, "can't marshal yaml users")
85}
86
87func AppendUserToGroup(file []byte, groupName string, users ...SimpleUser) ([]byte, error) {
88 urlPath, err := yaml.PathString(fmt.Sprintf("$.%s.users", groupName))
89 if err != nil {
90 return nil, oops.Wrapf(err, "invalid path yaml")
91 }
92 f, err := parser.ParseBytes(file, 0)
93 if err != nil {
94 return nil, oops.Wrapf(err, "can't parse file")
95 }
96 u := make([]User, 0)
97 for _, j := range users {
98 if !bytes.Contains(file, []byte(j.Ssh)) {
99 u = append(u, User{
100 Pseudo: j.Pseudo,
101 Avatar: "",
102 Emails: []string{j.Email},
103 Ssh: []string{j.Ssh},
104 })
105 }
106 }
107 content, err := yaml.Marshal(u)
108 if err != nil {
109 return nil, oops.Wrapf(err, "can't marshal u")
110 }
111 err = urlPath.MergeFromReader(f, bytes.NewReader(content))
112 return []byte(f.String()), oops.Wrapf(err, "can't merge yaml")
113}
114
115func AppendBranchToGroup(file []byte, groupName string, branches ...string) ([]byte, error) {
116 urlPath, err := yaml.PathString(fmt.Sprintf("$.%s.branches", groupName))
117 if err != nil {
118 return nil, oops.Wrapf(err, "invalid path yaml")
119 }
120 f, err := parser.ParseBytes(file, 0)
121 if err != nil {
122 return nil, oops.Wrapf(err, "can't parse file")
123 }
124 u := make([]Branch, len(branches))
125 for i, j := range branches {
126 u[i] = Branch{
127 Name: j,
128 }
129 }
130 content, err := yaml.Marshal(u)
131 if err != nil {
132 return nil, oops.Wrapf(err, "can't marshal u")
133 }
134 err = urlPath.MergeFromReader(f, bytes.NewReader(content))
135 return []byte(f.String()), oops.Wrapf(err, "can't merge yaml")
136}
137
138func AppendBranch(file []byte, groupName string, users ...SimpleUser) ([]byte, error) {
139 u := make([]User, len(users))
140 for i, j := range users {
141 u[i] = User{
142 Pseudo: j.Pseudo,
143 Avatar: "",
144 Emails: []string{j.Email},
145 Ssh: []string{j.Ssh},
146 }
147 }
148 group := Group{Name: groupName, Branches: []Branch{{Name: groupName}}, Users: u}
149 content, err := yaml.Marshal(map[string]Group{groupName: group})
150 if err != nil {
151 return nil, oops.Wrapf(err, "can't marshal u")
152 }
153
154 return AppendBranchToGroup(append(file, content...), "owner", groupName)
155}
156
157func FindUser(file []byte, branch string, sshKey string) (*GitRootUser, error) {
158 groups, err := ParseGroups(file)
159 if err != nil {
160 return nil, oops.Wrapf(err, "can't read group")
161 }
162 found := false
163 var goodUser User
164 var goodGroup *GitRootGroup
165 allGroups := make([]*GitRootGroup, 0)
166 for _, g := range groups {
167 branches := make([]string, len(g.Branches))
168 for i, b := range g.Branches {
169 branches[i] = b.Name
170 }
171 currentGroup := &GitRootGroup{
172 GroupName: g.Name,
173 Branches: branches,
174 }
175 allGroups = append(allGroups, currentGroup)
176 if slices.Contains(currentGroup.Branches, branch) {
177 for _, u := range g.Users {
178 for _, s := range u.Ssh {
179 if s == sshKey {
180 found = true
181 goodUser = u
182 goodGroup = currentGroup
183 break
184 }
185 }
186 if found {
187 break
188 }
189 }
190 }
191
192 }
193 if !found {
194 return &GitRootUser{
195 Group: nil,
196 Email: "",
197 Pseudo: "",
198 AllGroups: allGroups,
199 PubKey: sshKey,
200 }, nil
201 }
202 return &GitRootUser{
203 Group: goodGroup,
204 Email: goodUser.Emails[0],
205 Pseudo: goodUser.Pseudo,
206 AllGroups: allGroups,
207 PubKey: sshKey,
208 }, nil
209}
210
211func (u *GitRootUser) CanWrite(branch string) bool {
212 // user is in the group of the branch
213 if u.Group != nil { // new user
214 for _, b := range u.Group.Branches {
215 if b == branch || b == "*" {
216 return true
217 }
218 }
219 }
220 // other group have right for this branch
221 for _, g := range u.AllGroups {
222 if g.GroupName == "*" {
223 return false
224 }
225 if u.Group == nil || u.Group.GroupName != g.GroupName {
226 for _, b := range g.Branches {
227 if b == branch {
228 return false
229 }
230 }
231 }
232 }
233 // nobody take care of this branch
234 return true
235}