blob: 0c1d104e2f5ebfea5a56de989f3a6af4373008fb [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"
DTabidze0d802592024-03-19 17:42:45 +040012
13 "github.com/ncruces/go-sqlite3"
14 _ "github.com/ncruces/go-sqlite3/driver"
15 _ "github.com/ncruces/go-sqlite3/embed"
DTabidzed7744a62024-03-20 14:09:15 +040016
17 "github.com/gorilla/mux"
DTabidze0d802592024-03-19 17:42:45 +040018)
19
20var port = flag.Int("port", 8080, "ort to listen on")
21var dbPath = flag.String("db-path", "memberships.db", "Path to SQLite file")
22
23//go:embed index.html
24var indexHTML string
25
26//go:embed group.html
27var groupHTML string
28
29//go:embed static
30var staticResources embed.FS
31
32type Store interface {
33 CreateGroup(owner string, group Group) error
34 AddChildGroup(parent, child string) error
35 GetGroupsOwnedBy(user string) ([]Group, error)
DTabidzed7744a62024-03-20 14:09:15 +040036 GetGroupsUserBelongsTo(user string) ([]Group, error)
DTabidze0d802592024-03-19 17:42:45 +040037 IsGroupOwner(user, group string) (bool, error)
38 AddGroupMember(user, group string) error
39 AddGroupOwner(user, group string) error
40 GetGroupOwners(group string) ([]string, error)
41 GetGroupMembers(group string) ([]string, error)
42 GetGroupDescription(group string) (string, error)
43 GetAvailableGroupsAsChild(group string) ([]string, error)
DTabidzed7744a62024-03-20 14:09:15 +040044 GetAllTransitiveGroupsForUser(user string) ([]string, error)
45 GetGroupsGroupBelongsTo(group string) ([]string, error)
DTabidze0d802592024-03-19 17:42:45 +040046}
47
48type Server struct {
49 store Store
50}
51
52type Group struct {
53 Name string
54 Description string
55}
56
57type SQLiteStore struct {
58 db *sql.DB
59}
60
61func NewSQLiteStore(path string) (*SQLiteStore, error) {
62 db, err := sql.Open("sqlite3", path)
63 if err != nil {
64 return nil, err
65 }
66 _, err = db.Exec(`
67 CREATE TABLE IF NOT EXISTS groups (
68 name TEXT PRIMARY KEY,
69 description TEXT
70 );
71
72 CREATE TABLE IF NOT EXISTS owners (
73 username TEXT,
74 group_name TEXT,
75 FOREIGN KEY(group_name) REFERENCES groups(name)
76 );
77
78 CREATE TABLE IF NOT EXISTS group_to_group (
79 parent_group TEXT,
80 child_group TEXT,
81 FOREIGN KEY(parent_group) REFERENCES groups(name),
82 FOREIGN KEY(child_group) REFERENCES groups(name)
83 );
84
85 CREATE TABLE IF NOT EXISTS user_to_group (
86 username TEXT,
87 group_name TEXT,
88 FOREIGN KEY(group_name) REFERENCES groups(name)
89 );`)
90 if err != nil {
91 return nil, err
92 }
93 return &SQLiteStore{db: db}, nil
94}
95
96func (s *SQLiteStore) queryGroups(query string, args ...interface{}) ([]Group, error) {
97 groups := make([]Group, 0)
98 rows, err := s.db.Query(query, args...)
99 if err != nil {
100 return nil, err
101 }
102 defer rows.Close()
103 for rows.Next() {
104 var group Group
105 if err := rows.Scan(&group.Name, &group.Description); err != nil {
106 return nil, err
107 }
108 groups = append(groups, group)
109 }
110 if err := rows.Err(); err != nil {
111 return nil, err
112 }
113 return groups, nil
114}
115
116func (s *SQLiteStore) GetGroupsOwnedBy(user string) ([]Group, error) {
117 query := `
118 SELECT groups.name, groups.description
119 FROM groups
120 JOIN owners ON groups.name = owners.group_name
121 WHERE owners.username = ?`
122 return s.queryGroups(query, user)
123}
124
DTabidzed7744a62024-03-20 14:09:15 +0400125func (s *SQLiteStore) GetGroupsUserBelongsTo(user string) ([]Group, error) {
DTabidze0d802592024-03-19 17:42:45 +0400126 query := `
127 SELECT groups.name, groups.description
128 FROM groups
129 JOIN user_to_group ON groups.name = user_to_group.group_name
130 WHERE user_to_group.username = ?`
131 return s.queryGroups(query, user)
132}
133
134func (s *SQLiteStore) CreateGroup(owner string, group Group) error {
135 tx, err := s.db.Begin()
136 if err != nil {
137 return err
138 }
139 defer tx.Rollback()
140 query := `INSERT INTO groups (name, description) VALUES (?, ?)`
141 if _, err := tx.Exec(query, group.Name, group.Description); err != nil {
142 sqliteErr, ok := err.(*sqlite3.Error)
143 if ok && sqliteErr.ExtendedCode() == 1555 {
144 return fmt.Errorf("Group with the name %s already exists", group.Name)
145 }
146 return err
147 }
148 query = `INSERT INTO owners (username, group_name) VALUES (?, ?)`
149 if _, err := tx.Exec(query, owner, group.Name); err != nil {
150 return err
151 }
152 if err := tx.Commit(); err != nil {
153 return err
154 }
155 return nil
156}
157
158func (s *SQLiteStore) IsGroupOwner(user, group string) (bool, error) {
159 query := `
160 SELECT EXISTS (
161 SELECT 1
162 FROM owners
163 WHERE username = ? AND group_name = ?
164 )`
165 var exists bool
166 if err := s.db.QueryRow(query, user, group).Scan(&exists); err != nil {
167 return false, err
168 }
169 return exists, nil
170}
171
172func (s *SQLiteStore) userGroupPairExists(tx *sql.Tx, table, user, group string) (bool, error) {
173 query := fmt.Sprintf("SELECT EXISTS (SELECT 1 FROM %s WHERE username = ? AND group_name = ?)", table)
174 var exists bool
175 if err := tx.QueryRow(query, user, group).Scan(&exists); err != nil {
176 return false, err
177 }
178 return exists, nil
179}
180
181func (s *SQLiteStore) AddGroupMember(user, group string) error {
182 tx, err := s.db.Begin()
183 if err != nil {
184 return err
185 }
186 defer tx.Rollback()
187 existsInUserToGroup, err := s.userGroupPairExists(tx, "user_to_group", user, group)
188 if err != nil {
189 return err
190 }
191 if existsInUserToGroup {
192 return fmt.Errorf("%s is already a member of group %s", user, group)
193 }
194 if _, err := tx.Exec(`INSERT INTO user_to_group (username, group_name) VALUES (?, ?)`, user, group); err != nil {
195 return err
196 }
197 if err := tx.Commit(); err != nil {
198 return err
199 }
200 return nil
201}
202
203func (s *SQLiteStore) AddGroupOwner(user, group string) error {
204 tx, err := s.db.Begin()
205 if err != nil {
206 return err
207 }
208 defer tx.Rollback()
209 existsInOwners, err := s.userGroupPairExists(tx, "owners", user, group)
210 if err != nil {
211 return err
212 }
213 if existsInOwners {
214 return fmt.Errorf("%s is already an owner of group %s", user, group)
215 }
216 if _, err = tx.Exec(`INSERT INTO owners (username, group_name) VALUES (?, ?)`, user, group); err != nil {
217 return err
218 }
219 if err := tx.Commit(); err != nil {
220 return err
221 }
222 return nil
223}
224
225func (s *SQLiteStore) getUsersByGroup(table, group string) ([]string, error) {
226 query := fmt.Sprintf("SELECT username FROM %s WHERE group_name = ?", table)
227 rows, err := s.db.Query(query, group)
228 if err != nil {
229 return nil, err
230 }
231 defer rows.Close()
232 var users []string
233 for rows.Next() {
234 var username string
235 if err := rows.Scan(&username); err != nil {
236 return nil, err
237 }
238 users = append(users, username)
239 }
240 if err := rows.Err(); err != nil {
241 return nil, err
242 }
243 return users, nil
244}
245
246func (s *SQLiteStore) GetGroupOwners(group string) ([]string, error) {
247 return s.getUsersByGroup("owners", group)
248}
249
250func (s *SQLiteStore) GetGroupMembers(group string) ([]string, error) {
251 return s.getUsersByGroup("user_to_group", group)
252}
253
254func (s *SQLiteStore) GetGroupDescription(group string) (string, error) {
255 var description string
256 query := `SELECT description FROM groups WHERE name = ?`
257 if err := s.db.QueryRow(query, group).Scan(&description); err != nil {
258 return "", err
259 }
260 return description, nil
261}
262
263func (s *SQLiteStore) parentChildGroupPairExists(tx *sql.Tx, parent, child string) (bool, error) {
264 query := `SELECT EXISTS (SELECT 1 FROM group_to_group WHERE parent_group = ? AND child_group = ?)`
265 var exists bool
266 if err := tx.QueryRow(query, parent, child).Scan(&exists); err != nil {
267 return false, err
268 }
269 return exists, nil
270}
271
272func (s *SQLiteStore) AddChildGroup(parent, child string) error {
273 tx, err := s.db.Begin()
274 if err != nil {
275 return err
276 }
277 defer tx.Rollback()
278 existsInGroupToGroup, err := s.parentChildGroupPairExists(tx, parent, child)
279 if err != nil {
280 return err
281 }
282 if existsInGroupToGroup {
283 return fmt.Errorf("child group name %s already exists in group %s", child, parent)
284 }
285 if _, err := tx.Exec(`INSERT INTO group_to_group (parent_group, child_group) VALUES (?, ?)`, parent, child); err != nil {
286 return err
287 }
288 if err := tx.Commit(); err != nil {
289 return err
290 }
291 return nil
292}
293
294func (s *SQLiteStore) GetAvailableGroupsAsChild(group string) ([]string, error) {
295 // TODO(dtabidze): Might have to add further logic to filter available groups as children.
296 query := `
297 SELECT name FROM groups
298 WHERE name != ? AND name NOT IN (
299 SELECT child_group FROM group_to_group WHERE parent_group = ?
300 )
301 `
302 rows, err := s.db.Query(query, group, group)
303 if err != nil {
304 return nil, err
305 }
306 defer rows.Close()
307 var availableGroups []string
308 for rows.Next() {
309 var groupName string
310 if err := rows.Scan(&groupName); err != nil {
311 return nil, err
312 }
313 availableGroups = append(availableGroups, groupName)
314 }
315 return availableGroups, nil
316}
317
DTabidzed7744a62024-03-20 14:09:15 +0400318func (s *SQLiteStore) GetAllTransitiveGroupsForUser(user string) ([]string, error) {
319 directGroups, err := s.GetGroupsUserBelongsTo(user)
320 if err != nil {
321 return nil, err
322 }
323 allGroups := make(map[string]bool)
324 for _, group := range directGroups {
325 if err := s.getParentGroups(group.Name, allGroups); err != nil {
326 return nil, err
327 }
328 }
329 var result []string
330 for group := range allGroups {
331 result = append(result, group)
332 }
333 return result, nil
334}
335
336func (s *SQLiteStore) getParentGroups(group string, allGroups map[string]bool) error {
337 if allGroups[group] {
338 return nil
339 }
340 allGroups[group] = true
341 parentGroups, err := s.GetGroupsGroupBelongsTo(group)
342 if err != nil {
343 return err
344 }
345 for _, parentGroup := range parentGroups {
346 if err := s.getParentGroups(parentGroup, allGroups); err != nil {
347 return err
348 }
349 }
350 return nil
351}
352
353func (s *SQLiteStore) GetGroupsGroupBelongsTo(group string) ([]string, error) {
354 query := "SELECT parent_group FROM group_to_group WHERE child_group = ?"
355 rows, err := s.db.Query(query, group)
356 if err != nil {
357 return nil, err
358 }
359 defer rows.Close()
360 var parentGroups []string
361 for rows.Next() {
362 var parentGroup string
363 if err := rows.Scan(&parentGroup); err != nil {
364 return nil, err
365 }
366 parentGroups = append(parentGroups, parentGroup)
367 }
368 if err := rows.Err(); err != nil {
369 return nil, err
370 }
371 return parentGroups, nil
372}
373
DTabidze0d802592024-03-19 17:42:45 +0400374func getLoggedInUser(r *http.Request) (string, error) {
375 // TODO(dtabidze): should make a request to get loggedin user
376 return "tabo", nil
377}
378
379type Status int
380
381const (
382 Owner Status = iota
383 Member
384)
385
386func convertStatus(status string) (Status, error) {
387 switch status {
388 case "Owner":
389 return Owner, nil
390 case "Member":
391 return Member, nil
392 default:
393 return Owner, fmt.Errorf("invalid status: %s", status)
394 }
395}
396
397func (s *Server) Start() {
DTabidzed7744a62024-03-20 14:09:15 +0400398 router := mux.NewRouter()
399 router.PathPrefix("/static/").Handler(http.FileServer(http.FS(staticResources)))
400 router.HandleFunc("/group/{group-name}", s.groupHandler)
401 router.HandleFunc("/create-group", s.createGroupHandler)
402 router.HandleFunc("/add-user", s.addUserHandler)
403 router.HandleFunc("/add-child-group", s.addChildGroupHandler)
404 router.HandleFunc("/api/user/{username}", s.apiMemberOfHandler)
405 router.HandleFunc("/", s.homePageHandler)
406 log.Fatal(http.ListenAndServe(fmt.Sprintf(":%d", *port), router))
DTabidze0d802592024-03-19 17:42:45 +0400407}
408
409type GroupData struct {
410 Group Group
411 Membership string
412}
413
414func (s *Server) checkIsOwner(w http.ResponseWriter, user, group string) (bool, error) {
415 isOwner, err := s.store.IsGroupOwner(user, group)
416 if err != nil {
417 http.Error(w, err.Error(), http.StatusInternalServerError)
418 return false, err
419 }
420 if !isOwner {
421 http.Error(w, fmt.Sprintf("You are not the owner of the group %s", group), http.StatusUnauthorized)
422 return false, nil
423 }
424 return true, nil
425}
426
427func (s *Server) homePageHandler(w http.ResponseWriter, r *http.Request) {
428 loggedInUser, err := getLoggedInUser(r)
429 if err != nil {
430 http.Error(w, "User Not Logged In", http.StatusUnauthorized)
431 return
432 }
433 ownerGroups, err := s.store.GetGroupsOwnedBy(loggedInUser)
434 if err != nil {
435 http.Error(w, err.Error(), http.StatusInternalServerError)
436 return
437 }
DTabidzed7744a62024-03-20 14:09:15 +0400438 membershipGroups, err := s.store.GetGroupsUserBelongsTo(loggedInUser)
DTabidze0d802592024-03-19 17:42:45 +0400439 if err != nil {
440 http.Error(w, err.Error(), http.StatusInternalServerError)
441 return
442 }
443 tmpl, err := template.New("index").Parse(indexHTML)
444 if err != nil {
445 http.Error(w, err.Error(), http.StatusInternalServerError)
446 return
447 }
448 data := struct {
449 OwnerGroups []Group
450 MembershipGroups []Group
451 }{
452 OwnerGroups: ownerGroups,
453 MembershipGroups: membershipGroups,
454 }
455 w.Header().Set("Content-Type", "text/html")
456 if err := tmpl.Execute(w, data); err != nil {
457 http.Error(w, err.Error(), http.StatusInternalServerError)
458 return
459 }
460}
461
462func (s *Server) createGroupHandler(w http.ResponseWriter, r *http.Request) {
463 loggedInUser, err := getLoggedInUser(r)
464 if err != nil {
465 http.Error(w, "User Not Logged In", http.StatusUnauthorized)
466 return
467 }
468 if r.Method != http.MethodPost {
469 http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
470 return
471 }
472 if err := r.ParseForm(); err != nil {
473 http.Error(w, err.Error(), http.StatusInternalServerError)
474 return
475 }
476 var group Group
477 group.Name = r.PostFormValue("group-name")
478 group.Description = r.PostFormValue("description")
479 if err := s.store.CreateGroup(loggedInUser, group); err != nil {
480 http.Error(w, err.Error(), http.StatusInternalServerError)
481 return
482 }
483 http.Redirect(w, r, "/", http.StatusSeeOther)
484}
485
486func (s *Server) groupHandler(w http.ResponseWriter, r *http.Request) {
DTabidzed7744a62024-03-20 14:09:15 +0400487 // groupName := strings.TrimPrefix(r.URL.Path, "/group/")
488 vars := mux.Vars(r)
489 groupName := vars["group-name"]
DTabidze0d802592024-03-19 17:42:45 +0400490 tmpl, err := template.New("group").Parse(groupHTML)
491 if err != nil {
492 http.Error(w, err.Error(), http.StatusInternalServerError)
493 return
494 }
495 owners, err := s.store.GetGroupOwners(groupName)
496 if err != nil {
497 http.Error(w, err.Error(), http.StatusInternalServerError)
498 return
499 }
500 members, err := s.store.GetGroupMembers(groupName)
501 if err != nil {
502 http.Error(w, err.Error(), http.StatusInternalServerError)
503 return
504 }
505 description, err := s.store.GetGroupDescription(groupName)
506 if err != nil {
507 http.Error(w, err.Error(), http.StatusInternalServerError)
508 return
509 }
510 availableGroups, err := s.store.GetAvailableGroupsAsChild(groupName)
511 if err != nil {
512 http.Error(w, err.Error(), http.StatusInternalServerError)
513 return
514 }
515 data := struct {
516 GroupName string
517 Description string
518 Owners []string
519 Members []string
520 AvailableGroups []string
521 }{
522 GroupName: groupName,
523 Description: description,
524 Owners: owners,
525 Members: members,
526 AvailableGroups: availableGroups,
527 }
528 if err := tmpl.Execute(w, data); err != nil {
529 http.Error(w, err.Error(), http.StatusInternalServerError)
530 return
531 }
532}
533
534func (s *Server) addUserHandler(w http.ResponseWriter, r *http.Request) {
535 if r.Method != http.MethodPost {
536 http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
537 return
538 }
539 loggedInUser, err := getLoggedInUser(r)
540 if err != nil {
541 http.Error(w, "User Not Logged In", http.StatusUnauthorized)
542 return
543 }
544 groupName := r.FormValue("group")
545 username := r.FormValue("username")
546 status, err := convertStatus(r.FormValue("status"))
547 if err != nil {
548 http.Error(w, err.Error(), http.StatusBadRequest)
549 return
550 }
551 if _, err := s.checkIsOwner(w, loggedInUser, groupName); err != nil {
552 return
553 }
554 switch status {
555 case Owner:
556 err = s.store.AddGroupOwner(username, groupName)
557 case Member:
558 err = s.store.AddGroupMember(username, groupName)
559 default:
560 http.Error(w, "Invalid status", http.StatusBadRequest)
561 return
562 }
563 if err != nil {
564 http.Error(w, err.Error(), http.StatusInternalServerError)
565 return
566 }
567 http.Redirect(w, r, "/group/"+groupName, http.StatusSeeOther)
568}
569
570func (s *Server) addChildGroupHandler(w http.ResponseWriter, r *http.Request) {
571 // TODO(dtabidze): In future we might need to make one group OWNER of another and not just a member.
572 if r.Method != http.MethodPost {
573 http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
574 return
575 }
576 loggedInUser, err := getLoggedInUser(r)
577 if err != nil {
578 http.Error(w, "User Not Logged In", http.StatusUnauthorized)
579 return
580 }
581 parentGroup := r.FormValue("parent-group")
582 childGroup := r.FormValue("child-group")
583 if _, err := s.checkIsOwner(w, loggedInUser, parentGroup); err != nil {
584 return
585 }
586 if err := s.store.AddChildGroup(parentGroup, childGroup); err != nil {
587 http.Error(w, err.Error(), http.StatusInternalServerError)
588 return
589 }
590 http.Redirect(w, r, "/group/"+parentGroup, http.StatusSeeOther)
591}
592
DTabidzed7744a62024-03-20 14:09:15 +0400593type UserInfo struct {
594 MemberOf []string `json:"memberOf"`
595}
596
597func (s *Server) apiMemberOfHandler(w http.ResponseWriter, r *http.Request) {
598 vars := mux.Vars(r)
599 user, ok := vars["username"]
600 if !ok {
601 http.Error(w, "Username parameter is required", http.StatusBadRequest)
602 return
603 }
604 transitiveGroups, err := s.store.GetAllTransitiveGroupsForUser(user)
605 if err != nil {
606 http.Error(w, err.Error(), http.StatusInternalServerError)
607 return
608 }
609 w.Header().Set("Content-Type", "application/json")
610 if err := json.NewEncoder(w).Encode(UserInfo{transitiveGroups}); err != nil {
611 http.Error(w, err.Error(), http.StatusInternalServerError)
612 return
613 }
614}
615
DTabidze0d802592024-03-19 17:42:45 +0400616func main() {
617 flag.Parse()
618 db, err := NewSQLiteStore(*dbPath)
619 if err != nil {
620 panic(err)
621 }
622 s := Server{store: db}
623 s.Start()
624}