blob: 0a2bda42f2124eb2297053cc9dd9e26e13c7a82d [file] [log] [blame]
DTabidze0d802592024-03-19 17:42:45 +04001package main
2
3import (
4 "database/sql"
5 "embed"
DTabidzed7744a62024-03-20 14:09:15 +04006 "encoding/json"
DTabidze0d802592024-03-19 17:42:45 +04007 "flag"
8 "fmt"
9 "html/template"
10 "log"
11 "net/http"
DTabidze908bb852024-03-25 20:07:57 +040012 "regexp"
13 "strings"
DTabidze0d802592024-03-19 17:42:45 +040014
15 "github.com/ncruces/go-sqlite3"
16 _ "github.com/ncruces/go-sqlite3/driver"
17 _ "github.com/ncruces/go-sqlite3/embed"
DTabidzed7744a62024-03-20 14:09:15 +040018
19 "github.com/gorilla/mux"
DTabidze0d802592024-03-19 17:42:45 +040020)
21
22var port = flag.Int("port", 8080, "ort to listen on")
23var dbPath = flag.String("db-path", "memberships.db", "Path to SQLite file")
24
25//go:embed index.html
26var indexHTML string
27
28//go:embed group.html
29var groupHTML string
30
31//go:embed static
32var staticResources embed.FS
33
34type Store interface {
Giorgi Lekveishvili942c7612024-03-22 19:27:48 +040035 // Initializes store with admin user and their groups.
36 Init(owner string, groups []string) error
DTabidze0d802592024-03-19 17:42:45 +040037 CreateGroup(owner string, group Group) error
38 AddChildGroup(parent, child string) error
DTabidze908bb852024-03-25 20:07:57 +040039 DoesGroupExist(group string) (bool, error)
DTabidze0d802592024-03-19 17:42:45 +040040 GetGroupsOwnedBy(user string) ([]Group, error)
DTabidzed7744a62024-03-20 14:09:15 +040041 GetGroupsUserBelongsTo(user string) ([]Group, error)
DTabidze0d802592024-03-19 17:42:45 +040042 IsGroupOwner(user, group string) (bool, error)
43 AddGroupMember(user, group string) error
44 AddGroupOwner(user, group string) error
45 GetGroupOwners(group string) ([]string, error)
46 GetGroupMembers(group string) ([]string, error)
47 GetGroupDescription(group string) (string, error)
48 GetAvailableGroupsAsChild(group string) ([]string, error)
DTabidzec0b4d8f2024-03-22 17:25:10 +040049 GetAllTransitiveGroupsForUser(user string) ([]Group, error)
50 GetGroupsGroupBelongsTo(group string) ([]Group, error)
51 GetDirectChildrenGroups(group string) ([]Group, error)
52 GetAllTransitiveGroupsForGroup(group string) ([]Group, error)
DTabidze0d802592024-03-19 17:42:45 +040053}
54
55type Server struct {
56 store Store
57}
58
59type Group struct {
60 Name string
61 Description string
62}
63
64type SQLiteStore struct {
65 db *sql.DB
66}
67
DTabidzec0b4d8f2024-03-22 17:25:10 +040068func NewSQLiteStore(db *sql.DB) (*SQLiteStore, error) {
69 _, err := db.Exec(`
DTabidze0d802592024-03-19 17:42:45 +040070 CREATE TABLE IF NOT EXISTS groups (
71 name TEXT PRIMARY KEY,
72 description TEXT
73 );
74
75 CREATE TABLE IF NOT EXISTS owners (
76 username TEXT,
77 group_name TEXT,
78 FOREIGN KEY(group_name) REFERENCES groups(name)
79 );
80
81 CREATE TABLE IF NOT EXISTS group_to_group (
82 parent_group TEXT,
83 child_group TEXT,
84 FOREIGN KEY(parent_group) REFERENCES groups(name),
85 FOREIGN KEY(child_group) REFERENCES groups(name)
86 );
87
88 CREATE TABLE IF NOT EXISTS user_to_group (
89 username TEXT,
90 group_name TEXT,
91 FOREIGN KEY(group_name) REFERENCES groups(name)
92 );`)
93 if err != nil {
94 return nil, err
95 }
96 return &SQLiteStore{db: db}, nil
97}
98
Giorgi Lekveishvili942c7612024-03-22 19:27:48 +040099func (s *SQLiteStore) Init(owner string, groups []string) error {
100 tx, err := s.db.Begin()
101 if err != nil {
102 return err
103 }
104 defer tx.Rollback()
105 row := tx.QueryRow("SELECT COUNT(*) FROM groups")
106 var count int
107 if err := row.Scan(&count); err != nil {
108 return err
109 }
110 if count != 0 {
111 return fmt.Errorf("store already initialised")
112 }
113 for _, g := range groups {
114 query := `INSERT INTO groups (name, description) VALUES (?, '')`
115 if _, err := tx.Exec(query, g); err != nil {
116 return err
117 }
118 query = `INSERT INTO owners (username, group_name) VALUES (?, ?)`
119 if _, err := tx.Exec(query, owner, g); err != nil {
120 return err
121 }
Giorgi Lekveishvilid542b732024-03-25 18:17:39 +0400122 query = `INSERT INTO user_to_group (username, group_name) VALUES (?, ?)`
123 if _, err := tx.Exec(query, owner, g); err != nil {
124 return err
125 }
Giorgi Lekveishvili942c7612024-03-22 19:27:48 +0400126 }
127 return tx.Commit()
128}
129
DTabidze0d802592024-03-19 17:42:45 +0400130func (s *SQLiteStore) queryGroups(query string, args ...interface{}) ([]Group, error) {
131 groups := make([]Group, 0)
132 rows, err := s.db.Query(query, args...)
133 if err != nil {
134 return nil, err
135 }
136 defer rows.Close()
137 for rows.Next() {
138 var group Group
139 if err := rows.Scan(&group.Name, &group.Description); err != nil {
140 return nil, err
141 }
142 groups = append(groups, group)
143 }
144 if err := rows.Err(); err != nil {
145 return nil, err
146 }
147 return groups, nil
148}
149
150func (s *SQLiteStore) GetGroupsOwnedBy(user string) ([]Group, error) {
151 query := `
152 SELECT groups.name, groups.description
153 FROM groups
154 JOIN owners ON groups.name = owners.group_name
155 WHERE owners.username = ?`
156 return s.queryGroups(query, user)
157}
158
DTabidzed7744a62024-03-20 14:09:15 +0400159func (s *SQLiteStore) GetGroupsUserBelongsTo(user string) ([]Group, error) {
DTabidze0d802592024-03-19 17:42:45 +0400160 query := `
161 SELECT groups.name, groups.description
162 FROM groups
163 JOIN user_to_group ON groups.name = user_to_group.group_name
164 WHERE user_to_group.username = ?`
165 return s.queryGroups(query, user)
166}
167
168func (s *SQLiteStore) CreateGroup(owner string, group Group) error {
169 tx, err := s.db.Begin()
170 if err != nil {
171 return err
172 }
173 defer tx.Rollback()
174 query := `INSERT INTO groups (name, description) VALUES (?, ?)`
175 if _, err := tx.Exec(query, group.Name, group.Description); err != nil {
176 sqliteErr, ok := err.(*sqlite3.Error)
177 if ok && sqliteErr.ExtendedCode() == 1555 {
178 return fmt.Errorf("Group with the name %s already exists", group.Name)
179 }
180 return err
181 }
182 query = `INSERT INTO owners (username, group_name) VALUES (?, ?)`
183 if _, err := tx.Exec(query, owner, group.Name); err != nil {
184 return err
185 }
Giorgi Lekveishvili942c7612024-03-22 19:27:48 +0400186 return tx.Commit()
DTabidze0d802592024-03-19 17:42:45 +0400187}
188
189func (s *SQLiteStore) IsGroupOwner(user, group string) (bool, error) {
190 query := `
191 SELECT EXISTS (
192 SELECT 1
193 FROM owners
194 WHERE username = ? AND group_name = ?
195 )`
196 var exists bool
197 if err := s.db.QueryRow(query, user, group).Scan(&exists); err != nil {
198 return false, err
199 }
200 return exists, nil
201}
202
203func (s *SQLiteStore) userGroupPairExists(tx *sql.Tx, table, user, group string) (bool, error) {
204 query := fmt.Sprintf("SELECT EXISTS (SELECT 1 FROM %s WHERE username = ? AND group_name = ?)", table)
205 var exists bool
206 if err := tx.QueryRow(query, user, group).Scan(&exists); err != nil {
207 return false, err
208 }
209 return exists, nil
210}
211
212func (s *SQLiteStore) AddGroupMember(user, group string) error {
213 tx, err := s.db.Begin()
214 if err != nil {
215 return err
216 }
217 defer tx.Rollback()
218 existsInUserToGroup, err := s.userGroupPairExists(tx, "user_to_group", user, group)
219 if err != nil {
220 return err
221 }
222 if existsInUserToGroup {
223 return fmt.Errorf("%s is already a member of group %s", user, group)
224 }
225 if _, err := tx.Exec(`INSERT INTO user_to_group (username, group_name) VALUES (?, ?)`, user, group); err != nil {
226 return err
227 }
228 if err := tx.Commit(); err != nil {
229 return err
230 }
231 return nil
232}
233
234func (s *SQLiteStore) AddGroupOwner(user, group string) error {
235 tx, err := s.db.Begin()
236 if err != nil {
237 return err
238 }
239 defer tx.Rollback()
240 existsInOwners, err := s.userGroupPairExists(tx, "owners", user, group)
241 if err != nil {
242 return err
243 }
244 if existsInOwners {
245 return fmt.Errorf("%s is already an owner of group %s", user, group)
246 }
247 if _, err = tx.Exec(`INSERT INTO owners (username, group_name) VALUES (?, ?)`, user, group); err != nil {
248 return err
249 }
250 if err := tx.Commit(); err != nil {
251 return err
252 }
253 return nil
254}
255
256func (s *SQLiteStore) getUsersByGroup(table, group string) ([]string, error) {
257 query := fmt.Sprintf("SELECT username FROM %s WHERE group_name = ?", table)
258 rows, err := s.db.Query(query, group)
259 if err != nil {
260 return nil, err
261 }
262 defer rows.Close()
263 var users []string
264 for rows.Next() {
265 var username string
266 if err := rows.Scan(&username); err != nil {
267 return nil, err
268 }
269 users = append(users, username)
270 }
271 if err := rows.Err(); err != nil {
272 return nil, err
273 }
274 return users, nil
275}
276
277func (s *SQLiteStore) GetGroupOwners(group string) ([]string, error) {
278 return s.getUsersByGroup("owners", group)
279}
280
281func (s *SQLiteStore) GetGroupMembers(group string) ([]string, error) {
282 return s.getUsersByGroup("user_to_group", group)
283}
284
285func (s *SQLiteStore) GetGroupDescription(group string) (string, error) {
286 var description string
287 query := `SELECT description FROM groups WHERE name = ?`
288 if err := s.db.QueryRow(query, group).Scan(&description); err != nil {
289 return "", err
290 }
291 return description, nil
292}
293
294func (s *SQLiteStore) parentChildGroupPairExists(tx *sql.Tx, parent, child string) (bool, error) {
295 query := `SELECT EXISTS (SELECT 1 FROM group_to_group WHERE parent_group = ? AND child_group = ?)`
296 var exists bool
297 if err := tx.QueryRow(query, parent, child).Scan(&exists); err != nil {
298 return false, err
299 }
300 return exists, nil
301}
302
DTabidze908bb852024-03-25 20:07:57 +0400303func (s *SQLiteStore) DoesGroupExist(group string) (bool, error) {
304 query := `SELECT EXISTS (SELECT 1 FROM groups WHERE name = ?)`
305 var exists bool
306 if err := s.db.QueryRow(query, group).Scan(&exists); err != nil {
307 return false, err
308 }
309 return exists, nil
310}
311
DTabidze0d802592024-03-19 17:42:45 +0400312func (s *SQLiteStore) AddChildGroup(parent, child string) error {
DTabidze908bb852024-03-25 20:07:57 +0400313 if parent == child {
314 return fmt.Errorf("parent and child groups can not have same name")
315 }
316 if _, err := s.DoesGroupExist(parent); err != nil {
317 return fmt.Errorf("parent group name %s does not exist", parent)
318 }
319 if _, err := s.DoesGroupExist(child); err != nil {
320 return fmt.Errorf("child group name %s does not exist", child)
321 }
DTabidzec0b4d8f2024-03-22 17:25:10 +0400322 parentGroups, err := s.GetAllTransitiveGroupsForGroup(parent)
323 if err != nil {
324 return err
325 }
326 for _, group := range parentGroups {
327 if group.Name == child {
328 return fmt.Errorf("circular reference detected: group %s is already a parent of group %s", child, parent)
329 }
330 }
DTabidze0d802592024-03-19 17:42:45 +0400331 tx, err := s.db.Begin()
332 if err != nil {
333 return err
334 }
335 defer tx.Rollback()
336 existsInGroupToGroup, err := s.parentChildGroupPairExists(tx, parent, child)
337 if err != nil {
338 return err
339 }
340 if existsInGroupToGroup {
341 return fmt.Errorf("child group name %s already exists in group %s", child, parent)
342 }
343 if _, err := tx.Exec(`INSERT INTO group_to_group (parent_group, child_group) VALUES (?, ?)`, parent, child); err != nil {
344 return err
345 }
346 if err := tx.Commit(); err != nil {
347 return err
348 }
349 return nil
350}
351
352func (s *SQLiteStore) GetAvailableGroupsAsChild(group string) ([]string, error) {
353 // TODO(dtabidze): Might have to add further logic to filter available groups as children.
354 query := `
355 SELECT name FROM groups
356 WHERE name != ? AND name NOT IN (
357 SELECT child_group FROM group_to_group WHERE parent_group = ?
DTabidze908bb852024-03-25 20:07:57 +0400358 )`
DTabidze0d802592024-03-19 17:42:45 +0400359 rows, err := s.db.Query(query, group, group)
360 if err != nil {
361 return nil, err
362 }
363 defer rows.Close()
364 var availableGroups []string
365 for rows.Next() {
366 var groupName string
367 if err := rows.Scan(&groupName); err != nil {
368 return nil, err
369 }
370 availableGroups = append(availableGroups, groupName)
371 }
372 return availableGroups, nil
373}
374
DTabidzec0b4d8f2024-03-22 17:25:10 +0400375func (s *SQLiteStore) GetAllTransitiveGroupsForUser(user string) ([]Group, error) {
376 if groups, err := s.GetGroupsUserBelongsTo(user); err != nil {
DTabidzed7744a62024-03-20 14:09:15 +0400377 return nil, err
DTabidzec0b4d8f2024-03-22 17:25:10 +0400378 } else {
379 visited := map[string]struct{}{}
380 return s.getAllParentGroupsRecursive(groups, visited)
DTabidzed7744a62024-03-20 14:09:15 +0400381 }
DTabidzec0b4d8f2024-03-22 17:25:10 +0400382}
383
384func (s *SQLiteStore) GetAllTransitiveGroupsForGroup(group string) ([]Group, error) {
385 if p, err := s.GetGroupsGroupBelongsTo(group); err != nil {
386 return nil, err
387 } else {
388 // Mark initial group as visited
389 visited := map[string]struct{}{
390 group: struct{}{},
391 }
392 return s.getAllParentGroupsRecursive(p, visited)
393 }
394}
395
396func (s *SQLiteStore) getAllParentGroupsRecursive(groups []Group, visited map[string]struct{}) ([]Group, error) {
397 var ret []Group
398 for _, g := range groups {
399 if _, ok := visited[g.Name]; ok {
400 continue
401 }
402 visited[g.Name] = struct{}{}
403 ret = append(ret, g)
404 if p, err := s.GetGroupsGroupBelongsTo(g.Name); err != nil {
DTabidzed7744a62024-03-20 14:09:15 +0400405 return nil, err
DTabidzec0b4d8f2024-03-22 17:25:10 +0400406 } else if res, err := s.getAllParentGroupsRecursive(p, visited); err != nil {
407 return nil, err
408 } else {
409 ret = append(ret, res...)
DTabidzed7744a62024-03-20 14:09:15 +0400410 }
411 }
DTabidzec0b4d8f2024-03-22 17:25:10 +0400412 return ret, nil
DTabidzed7744a62024-03-20 14:09:15 +0400413}
414
DTabidzec0b4d8f2024-03-22 17:25:10 +0400415func (s *SQLiteStore) GetGroupsGroupBelongsTo(group string) ([]Group, error) {
416 query := `
417 SELECT groups.name, groups.description
418 FROM groups
419 JOIN group_to_group ON groups.name = group_to_group.parent_group
420 WHERE group_to_group.child_group = ?`
DTabidzed7744a62024-03-20 14:09:15 +0400421 rows, err := s.db.Query(query, group)
422 if err != nil {
423 return nil, err
424 }
425 defer rows.Close()
DTabidzec0b4d8f2024-03-22 17:25:10 +0400426 var parentGroups []Group
DTabidzed7744a62024-03-20 14:09:15 +0400427 for rows.Next() {
DTabidzec0b4d8f2024-03-22 17:25:10 +0400428 var parentGroup Group
429 if err := rows.Scan(&parentGroup.Name, &parentGroup.Description); err != nil {
DTabidzed7744a62024-03-20 14:09:15 +0400430 return nil, err
431 }
432 parentGroups = append(parentGroups, parentGroup)
433 }
434 if err := rows.Err(); err != nil {
435 return nil, err
436 }
437 return parentGroups, nil
438}
439
DTabidzec0b4d8f2024-03-22 17:25:10 +0400440func (s *SQLiteStore) GetDirectChildrenGroups(group string) ([]Group, error) {
441 query := `
442 SELECT groups.name, groups.description
443 FROM groups
444 JOIN group_to_group ON groups.name = group_to_group.child_group
445 WHERE group_to_group.parent_group = ?`
446 rows, err := s.db.Query(query, group)
447 if err != nil {
448 return nil, err
449 }
450 defer rows.Close()
451 var childrenGroups []Group
452 for rows.Next() {
453 var childGroup Group
454 if err := rows.Scan(&childGroup.Name, &childGroup.Description); err != nil {
455 return nil, err
456 }
457 childrenGroups = append(childrenGroups, childGroup)
458 }
459 if err := rows.Err(); err != nil {
460 return nil, err
461 }
462 return childrenGroups, nil
463}
464
DTabidze0d802592024-03-19 17:42:45 +0400465func getLoggedInUser(r *http.Request) (string, error) {
DTabidzec0b4d8f2024-03-22 17:25:10 +0400466 if user := r.Header.Get("X-User"); user != "" {
467 return user, nil
468 } else {
469 return "", fmt.Errorf("unauthenticated")
470 }
DTabidze0d802592024-03-19 17:42:45 +0400471}
472
473type Status int
474
475const (
476 Owner Status = iota
477 Member
478)
479
DTabidze0d802592024-03-19 17:42:45 +0400480func (s *Server) Start() {
DTabidzed7744a62024-03-20 14:09:15 +0400481 router := mux.NewRouter()
482 router.PathPrefix("/static/").Handler(http.FileServer(http.FS(staticResources)))
483 router.HandleFunc("/group/{group-name}", s.groupHandler)
484 router.HandleFunc("/create-group", s.createGroupHandler)
485 router.HandleFunc("/add-user", s.addUserHandler)
486 router.HandleFunc("/add-child-group", s.addChildGroupHandler)
Giorgi Lekveishvili942c7612024-03-22 19:27:48 +0400487 router.HandleFunc("/api/init", s.apiInitHandler)
DTabidzed7744a62024-03-20 14:09:15 +0400488 router.HandleFunc("/api/user/{username}", s.apiMemberOfHandler)
489 router.HandleFunc("/", s.homePageHandler)
490 log.Fatal(http.ListenAndServe(fmt.Sprintf(":%d", *port), router))
DTabidze0d802592024-03-19 17:42:45 +0400491}
492
493type GroupData struct {
494 Group Group
495 Membership string
496}
497
498func (s *Server) checkIsOwner(w http.ResponseWriter, user, group string) (bool, error) {
499 isOwner, err := s.store.IsGroupOwner(user, group)
500 if err != nil {
501 http.Error(w, err.Error(), http.StatusInternalServerError)
502 return false, err
503 }
504 if !isOwner {
505 http.Error(w, fmt.Sprintf("You are not the owner of the group %s", group), http.StatusUnauthorized)
506 return false, nil
507 }
508 return true, nil
509}
510
511func (s *Server) homePageHandler(w http.ResponseWriter, r *http.Request) {
512 loggedInUser, err := getLoggedInUser(r)
513 if err != nil {
514 http.Error(w, "User Not Logged In", http.StatusUnauthorized)
515 return
516 }
517 ownerGroups, err := s.store.GetGroupsOwnedBy(loggedInUser)
518 if err != nil {
519 http.Error(w, err.Error(), http.StatusInternalServerError)
520 return
521 }
DTabidzed7744a62024-03-20 14:09:15 +0400522 membershipGroups, err := s.store.GetGroupsUserBelongsTo(loggedInUser)
DTabidze0d802592024-03-19 17:42:45 +0400523 if err != nil {
524 http.Error(w, err.Error(), http.StatusInternalServerError)
525 return
526 }
527 tmpl, err := template.New("index").Parse(indexHTML)
528 if err != nil {
529 http.Error(w, err.Error(), http.StatusInternalServerError)
530 return
531 }
DTabidzec0b4d8f2024-03-22 17:25:10 +0400532 transitiveGroups, err := s.store.GetAllTransitiveGroupsForUser(loggedInUser)
533 if err != nil {
534 http.Error(w, err.Error(), http.StatusInternalServerError)
535 return
536 }
DTabidze0d802592024-03-19 17:42:45 +0400537 data := struct {
538 OwnerGroups []Group
539 MembershipGroups []Group
DTabidzec0b4d8f2024-03-22 17:25:10 +0400540 TransitiveGroups []Group
DTabidze0d802592024-03-19 17:42:45 +0400541 }{
542 OwnerGroups: ownerGroups,
543 MembershipGroups: membershipGroups,
DTabidzec0b4d8f2024-03-22 17:25:10 +0400544 TransitiveGroups: transitiveGroups,
DTabidze0d802592024-03-19 17:42:45 +0400545 }
546 w.Header().Set("Content-Type", "text/html")
547 if err := tmpl.Execute(w, data); err != nil {
548 http.Error(w, err.Error(), http.StatusInternalServerError)
549 return
550 }
551}
552
553func (s *Server) createGroupHandler(w http.ResponseWriter, r *http.Request) {
554 loggedInUser, err := getLoggedInUser(r)
555 if err != nil {
556 http.Error(w, "User Not Logged In", http.StatusUnauthorized)
557 return
558 }
559 if r.Method != http.MethodPost {
560 http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
561 return
562 }
563 if err := r.ParseForm(); err != nil {
564 http.Error(w, err.Error(), http.StatusInternalServerError)
565 return
566 }
567 var group Group
568 group.Name = r.PostFormValue("group-name")
DTabidze908bb852024-03-25 20:07:57 +0400569 if err := isValidGroupName(group.Name); err != nil {
570 http.Error(w, err.Error(), http.StatusBadRequest)
571 return
572 }
DTabidze0d802592024-03-19 17:42:45 +0400573 group.Description = r.PostFormValue("description")
574 if err := s.store.CreateGroup(loggedInUser, group); err != nil {
575 http.Error(w, err.Error(), http.StatusInternalServerError)
576 return
577 }
578 http.Redirect(w, r, "/", http.StatusSeeOther)
579}
580
581func (s *Server) groupHandler(w http.ResponseWriter, r *http.Request) {
DTabidzec0b4d8f2024-03-22 17:25:10 +0400582 _, err := getLoggedInUser(r)
583 if err != nil {
584 http.Error(w, "User Not Logged In", http.StatusUnauthorized)
585 return
586 }
DTabidzed7744a62024-03-20 14:09:15 +0400587 vars := mux.Vars(r)
588 groupName := vars["group-name"]
DTabidze908bb852024-03-25 20:07:57 +0400589 exists, err := s.store.DoesGroupExist(groupName)
590 if err != nil {
591 http.Error(w, err.Error(), http.StatusInternalServerError)
592 return
593 }
594 if !exists {
595 errorMsg := fmt.Sprintf("group with the name '%s' not found", groupName)
596 http.Error(w, errorMsg, http.StatusNotFound)
597 return
598 }
DTabidze0d802592024-03-19 17:42:45 +0400599 tmpl, err := template.New("group").Parse(groupHTML)
600 if err != nil {
601 http.Error(w, err.Error(), http.StatusInternalServerError)
602 return
603 }
604 owners, err := s.store.GetGroupOwners(groupName)
605 if err != nil {
606 http.Error(w, err.Error(), http.StatusInternalServerError)
607 return
608 }
609 members, err := s.store.GetGroupMembers(groupName)
610 if err != nil {
611 http.Error(w, err.Error(), http.StatusInternalServerError)
612 return
613 }
614 description, err := s.store.GetGroupDescription(groupName)
615 if err != nil {
616 http.Error(w, err.Error(), http.StatusInternalServerError)
617 return
618 }
619 availableGroups, err := s.store.GetAvailableGroupsAsChild(groupName)
620 if err != nil {
621 http.Error(w, err.Error(), http.StatusInternalServerError)
622 return
623 }
DTabidzec0b4d8f2024-03-22 17:25:10 +0400624 transitiveGroups, err := s.store.GetAllTransitiveGroupsForGroup(groupName)
625 if err != nil {
626 http.Error(w, err.Error(), http.StatusInternalServerError)
627 return
628 }
629 childGroups, err := s.store.GetDirectChildrenGroups(groupName)
630 if err != nil {
631 http.Error(w, err.Error(), http.StatusInternalServerError)
632 return
633 }
DTabidze0d802592024-03-19 17:42:45 +0400634 data := struct {
DTabidzec0b4d8f2024-03-22 17:25:10 +0400635 GroupName string
636 Description string
637 Owners []string
638 Members []string
639 AvailableGroups []string
640 TransitiveGroups []Group
641 ChildGroups []Group
DTabidze0d802592024-03-19 17:42:45 +0400642 }{
DTabidzec0b4d8f2024-03-22 17:25:10 +0400643 GroupName: groupName,
644 Description: description,
645 Owners: owners,
646 Members: members,
647 AvailableGroups: availableGroups,
648 TransitiveGroups: transitiveGroups,
649 ChildGroups: childGroups,
DTabidze0d802592024-03-19 17:42:45 +0400650 }
651 if err := tmpl.Execute(w, data); err != nil {
652 http.Error(w, err.Error(), http.StatusInternalServerError)
653 return
654 }
655}
656
657func (s *Server) addUserHandler(w http.ResponseWriter, r *http.Request) {
658 if r.Method != http.MethodPost {
659 http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
660 return
661 }
662 loggedInUser, err := getLoggedInUser(r)
663 if err != nil {
664 http.Error(w, "User Not Logged In", http.StatusUnauthorized)
665 return
666 }
667 groupName := r.FormValue("group")
DTabidze908bb852024-03-25 20:07:57 +0400668 if err := isValidGroupName(groupName); err != nil {
669 http.Error(w, err.Error(), http.StatusBadRequest)
670 return
671 }
672 username := strings.ToLower(r.FormValue("username"))
673 if username == "" {
674 http.Error(w, "Username parameter is required", http.StatusBadRequest)
675 return
676 }
DTabidze0d802592024-03-19 17:42:45 +0400677 status, err := convertStatus(r.FormValue("status"))
678 if err != nil {
679 http.Error(w, err.Error(), http.StatusBadRequest)
680 return
681 }
682 if _, err := s.checkIsOwner(w, loggedInUser, groupName); err != nil {
683 return
684 }
685 switch status {
686 case Owner:
687 err = s.store.AddGroupOwner(username, groupName)
688 case Member:
689 err = s.store.AddGroupMember(username, groupName)
690 default:
691 http.Error(w, "Invalid status", http.StatusBadRequest)
692 return
693 }
694 if err != nil {
695 http.Error(w, err.Error(), http.StatusInternalServerError)
696 return
697 }
698 http.Redirect(w, r, "/group/"+groupName, http.StatusSeeOther)
699}
700
701func (s *Server) addChildGroupHandler(w http.ResponseWriter, r *http.Request) {
702 // TODO(dtabidze): In future we might need to make one group OWNER of another and not just a member.
703 if r.Method != http.MethodPost {
704 http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
705 return
706 }
707 loggedInUser, err := getLoggedInUser(r)
708 if err != nil {
709 http.Error(w, "User Not Logged In", http.StatusUnauthorized)
710 return
711 }
712 parentGroup := r.FormValue("parent-group")
DTabidze908bb852024-03-25 20:07:57 +0400713 if err := isValidGroupName(parentGroup); err != nil {
714 http.Error(w, err.Error(), http.StatusBadRequest)
715 return
716 }
DTabidze0d802592024-03-19 17:42:45 +0400717 childGroup := r.FormValue("child-group")
DTabidze908bb852024-03-25 20:07:57 +0400718 if err := isValidGroupName(childGroup); err != nil {
719 http.Error(w, err.Error(), http.StatusBadRequest)
720 return
721 }
DTabidze0d802592024-03-19 17:42:45 +0400722 if _, err := s.checkIsOwner(w, loggedInUser, parentGroup); err != nil {
723 return
724 }
725 if err := s.store.AddChildGroup(parentGroup, childGroup); err != nil {
726 http.Error(w, err.Error(), http.StatusInternalServerError)
727 return
728 }
729 http.Redirect(w, r, "/group/"+parentGroup, http.StatusSeeOther)
730}
731
Giorgi Lekveishvili942c7612024-03-22 19:27:48 +0400732type initRequest struct {
733 Owner string `json:"owner"`
734 Groups []string `json:"groups"`
735}
736
737func (s *Server) apiInitHandler(w http.ResponseWriter, r *http.Request) {
738 var req initRequest
739 if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
740 http.Error(w, err.Error(), http.StatusBadRequest)
741 return
742 }
743 if err := s.store.Init(req.Owner, req.Groups); err != nil {
744 http.Error(w, err.Error(), http.StatusInternalServerError)
745 return
746 }
747}
748
749type userInfo struct {
DTabidzed7744a62024-03-20 14:09:15 +0400750 MemberOf []string `json:"memberOf"`
751}
752
753func (s *Server) apiMemberOfHandler(w http.ResponseWriter, r *http.Request) {
754 vars := mux.Vars(r)
755 user, ok := vars["username"]
DTabidze908bb852024-03-25 20:07:57 +0400756 if !ok || user == "" {
DTabidzed7744a62024-03-20 14:09:15 +0400757 http.Error(w, "Username parameter is required", http.StatusBadRequest)
758 return
759 }
DTabidze908bb852024-03-25 20:07:57 +0400760 user = strings.ToLower(user)
DTabidzed7744a62024-03-20 14:09:15 +0400761 transitiveGroups, err := s.store.GetAllTransitiveGroupsForUser(user)
762 if err != nil {
763 http.Error(w, err.Error(), http.StatusInternalServerError)
764 return
765 }
DTabidzec0b4d8f2024-03-22 17:25:10 +0400766 var groupNames []string
767 for _, group := range transitiveGroups {
768 groupNames = append(groupNames, group.Name)
769 }
DTabidzed7744a62024-03-20 14:09:15 +0400770 w.Header().Set("Content-Type", "application/json")
Giorgi Lekveishvili942c7612024-03-22 19:27:48 +0400771 if err := json.NewEncoder(w).Encode(userInfo{groupNames}); err != nil {
DTabidzed7744a62024-03-20 14:09:15 +0400772 http.Error(w, err.Error(), http.StatusInternalServerError)
773 return
774 }
775}
776
DTabidze908bb852024-03-25 20:07:57 +0400777func convertStatus(status string) (Status, error) {
778 switch status {
779 case "Owner":
780 return Owner, nil
781 case "Member":
782 return Member, nil
783 default:
784 return Owner, fmt.Errorf("invalid status: %s", status)
785 }
786}
787
788func isValidGroupName(group string) error {
789 if strings.TrimSpace(group) == "" {
790 return fmt.Errorf("group name can't be empty or contain only whitespaces")
791 }
792 validGroupName := regexp.MustCompile(`^[a-z0-9\-_:.\/ ]+$`)
793 if !validGroupName.MatchString(group) {
794 return fmt.Errorf("group name should contain only lowercase letters, digits, -, _, :, ., /")
795 }
796 return nil
797}
798
DTabidze0d802592024-03-19 17:42:45 +0400799func main() {
800 flag.Parse()
DTabidzec0b4d8f2024-03-22 17:25:10 +0400801 db, err := sql.Open("sqlite3", *dbPath)
DTabidze0d802592024-03-19 17:42:45 +0400802 if err != nil {
803 panic(err)
804 }
DTabidzec0b4d8f2024-03-22 17:25:10 +0400805 store, err := NewSQLiteStore(db)
806 if err != nil {
807 panic(err)
808 }
809 s := Server{store}
DTabidze0d802592024-03-19 17:42:45 +0400810 s.Start()
811}