blob: f8f2cff41400c5b7ca38cb9af28b9326f340f4b5 [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
Giorgi Lekveishvili329af572024-03-25 20:14:41 +040022var port = flag.Int("port", 8080, "Port to listen on")
23var apiPort = flag.Int("api-port", 8081, "Port to listen on for API requests")
DTabidze0d802592024-03-19 17:42:45 +040024var dbPath = flag.String("db-path", "memberships.db", "Path to SQLite file")
25
26//go:embed index.html
27var indexHTML string
28
29//go:embed group.html
30var groupHTML string
31
32//go:embed static
33var staticResources embed.FS
34
35type Store interface {
Giorgi Lekveishvili942c7612024-03-22 19:27:48 +040036 // Initializes store with admin user and their groups.
37 Init(owner string, groups []string) error
DTabidze0d802592024-03-19 17:42:45 +040038 CreateGroup(owner string, group Group) error
39 AddChildGroup(parent, child string) error
DTabidze908bb852024-03-25 20:07:57 +040040 DoesGroupExist(group string) (bool, error)
DTabidze0d802592024-03-19 17:42:45 +040041 GetGroupsOwnedBy(user string) ([]Group, error)
DTabidzed7744a62024-03-20 14:09:15 +040042 GetGroupsUserBelongsTo(user string) ([]Group, error)
DTabidze0d802592024-03-19 17:42:45 +040043 IsGroupOwner(user, group string) (bool, error)
44 AddGroupMember(user, group string) error
45 AddGroupOwner(user, group string) error
46 GetGroupOwners(group string) ([]string, error)
47 GetGroupMembers(group string) ([]string, error)
48 GetGroupDescription(group string) (string, error)
49 GetAvailableGroupsAsChild(group string) ([]string, error)
DTabidzec0b4d8f2024-03-22 17:25:10 +040050 GetAllTransitiveGroupsForUser(user string) ([]Group, error)
51 GetGroupsGroupBelongsTo(group string) ([]Group, error)
52 GetDirectChildrenGroups(group string) ([]Group, error)
53 GetAllTransitiveGroupsForGroup(group string) ([]Group, error)
DTabidze2b224bf2024-03-27 13:25:49 +040054 RemoveFromGroupToGroup(parent, child string) error
55 RemoveUserFromTable(username, groupName, tableName string) error
DTabidze0d802592024-03-19 17:42:45 +040056}
57
58type Server struct {
59 store Store
60}
61
62type Group struct {
63 Name string
64 Description string
65}
66
67type SQLiteStore struct {
68 db *sql.DB
69}
70
DTabidzec0b4d8f2024-03-22 17:25:10 +040071func NewSQLiteStore(db *sql.DB) (*SQLiteStore, error) {
72 _, err := db.Exec(`
DTabidze0d802592024-03-19 17:42:45 +040073 CREATE TABLE IF NOT EXISTS groups (
74 name TEXT PRIMARY KEY,
75 description TEXT
76 );
77
78 CREATE TABLE IF NOT EXISTS owners (
79 username TEXT,
80 group_name TEXT,
81 FOREIGN KEY(group_name) REFERENCES groups(name)
82 );
83
84 CREATE TABLE IF NOT EXISTS group_to_group (
85 parent_group TEXT,
86 child_group TEXT,
87 FOREIGN KEY(parent_group) REFERENCES groups(name),
88 FOREIGN KEY(child_group) REFERENCES groups(name)
89 );
90
91 CREATE TABLE IF NOT EXISTS user_to_group (
92 username TEXT,
93 group_name TEXT,
94 FOREIGN KEY(group_name) REFERENCES groups(name)
95 );`)
96 if err != nil {
97 return nil, err
98 }
99 return &SQLiteStore{db: db}, nil
100}
101
Giorgi Lekveishvili942c7612024-03-22 19:27:48 +0400102func (s *SQLiteStore) Init(owner string, groups []string) error {
103 tx, err := s.db.Begin()
104 if err != nil {
105 return err
106 }
107 defer tx.Rollback()
108 row := tx.QueryRow("SELECT COUNT(*) FROM groups")
109 var count int
110 if err := row.Scan(&count); err != nil {
111 return err
112 }
113 if count != 0 {
114 return fmt.Errorf("store already initialised")
115 }
116 for _, g := range groups {
117 query := `INSERT INTO groups (name, description) VALUES (?, '')`
118 if _, err := tx.Exec(query, g); err != nil {
119 return err
120 }
121 query = `INSERT INTO owners (username, group_name) VALUES (?, ?)`
122 if _, err := tx.Exec(query, owner, g); err != nil {
123 return err
124 }
Giorgi Lekveishvilid542b732024-03-25 18:17:39 +0400125 query = `INSERT INTO user_to_group (username, group_name) VALUES (?, ?)`
126 if _, err := tx.Exec(query, owner, g); err != nil {
127 return err
128 }
Giorgi Lekveishvili942c7612024-03-22 19:27:48 +0400129 }
130 return tx.Commit()
131}
132
DTabidze0d802592024-03-19 17:42:45 +0400133func (s *SQLiteStore) queryGroups(query string, args ...interface{}) ([]Group, error) {
134 groups := make([]Group, 0)
135 rows, err := s.db.Query(query, args...)
136 if err != nil {
137 return nil, err
138 }
139 defer rows.Close()
140 for rows.Next() {
141 var group Group
142 if err := rows.Scan(&group.Name, &group.Description); err != nil {
143 return nil, err
144 }
145 groups = append(groups, group)
146 }
147 if err := rows.Err(); err != nil {
148 return nil, err
149 }
150 return groups, nil
151}
152
153func (s *SQLiteStore) GetGroupsOwnedBy(user string) ([]Group, error) {
154 query := `
155 SELECT groups.name, groups.description
156 FROM groups
157 JOIN owners ON groups.name = owners.group_name
158 WHERE owners.username = ?`
159 return s.queryGroups(query, user)
160}
161
DTabidzed7744a62024-03-20 14:09:15 +0400162func (s *SQLiteStore) GetGroupsUserBelongsTo(user string) ([]Group, error) {
DTabidze0d802592024-03-19 17:42:45 +0400163 query := `
164 SELECT groups.name, groups.description
165 FROM groups
166 JOIN user_to_group ON groups.name = user_to_group.group_name
167 WHERE user_to_group.username = ?`
168 return s.queryGroups(query, user)
169}
170
171func (s *SQLiteStore) CreateGroup(owner string, group Group) error {
172 tx, err := s.db.Begin()
173 if err != nil {
174 return err
175 }
176 defer tx.Rollback()
177 query := `INSERT INTO groups (name, description) VALUES (?, ?)`
178 if _, err := tx.Exec(query, group.Name, group.Description); err != nil {
179 sqliteErr, ok := err.(*sqlite3.Error)
180 if ok && sqliteErr.ExtendedCode() == 1555 {
181 return fmt.Errorf("Group with the name %s already exists", group.Name)
182 }
183 return err
184 }
185 query = `INSERT INTO owners (username, group_name) VALUES (?, ?)`
186 if _, err := tx.Exec(query, owner, group.Name); err != nil {
187 return err
188 }
Giorgi Lekveishvili942c7612024-03-22 19:27:48 +0400189 return tx.Commit()
DTabidze0d802592024-03-19 17:42:45 +0400190}
191
192func (s *SQLiteStore) IsGroupOwner(user, group string) (bool, error) {
193 query := `
194 SELECT EXISTS (
195 SELECT 1
196 FROM owners
197 WHERE username = ? AND group_name = ?
198 )`
199 var exists bool
200 if err := s.db.QueryRow(query, user, group).Scan(&exists); err != nil {
201 return false, err
202 }
203 return exists, nil
204}
205
206func (s *SQLiteStore) userGroupPairExists(tx *sql.Tx, table, user, group string) (bool, error) {
207 query := fmt.Sprintf("SELECT EXISTS (SELECT 1 FROM %s WHERE username = ? AND group_name = ?)", table)
208 var exists bool
209 if err := tx.QueryRow(query, user, group).Scan(&exists); err != nil {
210 return false, err
211 }
212 return exists, nil
213}
214
215func (s *SQLiteStore) AddGroupMember(user, group string) error {
216 tx, err := s.db.Begin()
217 if err != nil {
218 return err
219 }
220 defer tx.Rollback()
221 existsInUserToGroup, err := s.userGroupPairExists(tx, "user_to_group", user, group)
222 if err != nil {
223 return err
224 }
225 if existsInUserToGroup {
226 return fmt.Errorf("%s is already a member of group %s", user, group)
227 }
228 if _, err := tx.Exec(`INSERT INTO user_to_group (username, group_name) VALUES (?, ?)`, user, group); err != nil {
229 return err
230 }
231 if err := tx.Commit(); err != nil {
232 return err
233 }
234 return nil
235}
236
237func (s *SQLiteStore) AddGroupOwner(user, group string) error {
238 tx, err := s.db.Begin()
239 if err != nil {
240 return err
241 }
242 defer tx.Rollback()
243 existsInOwners, err := s.userGroupPairExists(tx, "owners", user, group)
244 if err != nil {
245 return err
246 }
247 if existsInOwners {
248 return fmt.Errorf("%s is already an owner of group %s", user, group)
249 }
250 if _, err = tx.Exec(`INSERT INTO owners (username, group_name) VALUES (?, ?)`, user, group); err != nil {
251 return err
252 }
253 if err := tx.Commit(); err != nil {
254 return err
255 }
256 return nil
257}
258
259func (s *SQLiteStore) getUsersByGroup(table, group string) ([]string, error) {
260 query := fmt.Sprintf("SELECT username FROM %s WHERE group_name = ?", table)
261 rows, err := s.db.Query(query, group)
262 if err != nil {
263 return nil, err
264 }
265 defer rows.Close()
266 var users []string
267 for rows.Next() {
268 var username string
269 if err := rows.Scan(&username); err != nil {
270 return nil, err
271 }
272 users = append(users, username)
273 }
274 if err := rows.Err(); err != nil {
275 return nil, err
276 }
277 return users, nil
278}
279
280func (s *SQLiteStore) GetGroupOwners(group string) ([]string, error) {
281 return s.getUsersByGroup("owners", group)
282}
283
284func (s *SQLiteStore) GetGroupMembers(group string) ([]string, error) {
285 return s.getUsersByGroup("user_to_group", group)
286}
287
288func (s *SQLiteStore) GetGroupDescription(group string) (string, error) {
289 var description string
290 query := `SELECT description FROM groups WHERE name = ?`
291 if err := s.db.QueryRow(query, group).Scan(&description); err != nil {
292 return "", err
293 }
294 return description, nil
295}
296
297func (s *SQLiteStore) parentChildGroupPairExists(tx *sql.Tx, parent, child string) (bool, error) {
298 query := `SELECT EXISTS (SELECT 1 FROM group_to_group WHERE parent_group = ? AND child_group = ?)`
299 var exists bool
300 if err := tx.QueryRow(query, parent, child).Scan(&exists); err != nil {
301 return false, err
302 }
303 return exists, nil
304}
305
DTabidze908bb852024-03-25 20:07:57 +0400306func (s *SQLiteStore) DoesGroupExist(group string) (bool, error) {
307 query := `SELECT EXISTS (SELECT 1 FROM groups WHERE name = ?)`
308 var exists bool
309 if err := s.db.QueryRow(query, group).Scan(&exists); err != nil {
310 return false, err
311 }
312 return exists, nil
313}
314
DTabidze0d802592024-03-19 17:42:45 +0400315func (s *SQLiteStore) AddChildGroup(parent, child string) error {
DTabidze908bb852024-03-25 20:07:57 +0400316 if parent == child {
317 return fmt.Errorf("parent and child groups can not have same name")
318 }
319 if _, err := s.DoesGroupExist(parent); err != nil {
320 return fmt.Errorf("parent group name %s does not exist", parent)
321 }
322 if _, err := s.DoesGroupExist(child); err != nil {
323 return fmt.Errorf("child group name %s does not exist", child)
324 }
DTabidzec0b4d8f2024-03-22 17:25:10 +0400325 parentGroups, err := s.GetAllTransitiveGroupsForGroup(parent)
326 if err != nil {
327 return err
328 }
329 for _, group := range parentGroups {
330 if group.Name == child {
331 return fmt.Errorf("circular reference detected: group %s is already a parent of group %s", child, parent)
332 }
333 }
DTabidze0d802592024-03-19 17:42:45 +0400334 tx, err := s.db.Begin()
335 if err != nil {
336 return err
337 }
338 defer tx.Rollback()
339 existsInGroupToGroup, err := s.parentChildGroupPairExists(tx, parent, child)
340 if err != nil {
341 return err
342 }
343 if existsInGroupToGroup {
344 return fmt.Errorf("child group name %s already exists in group %s", child, parent)
345 }
346 if _, err := tx.Exec(`INSERT INTO group_to_group (parent_group, child_group) VALUES (?, ?)`, parent, child); err != nil {
347 return err
348 }
349 if err := tx.Commit(); err != nil {
350 return err
351 }
352 return nil
353}
354
355func (s *SQLiteStore) GetAvailableGroupsAsChild(group string) ([]string, error) {
356 // TODO(dtabidze): Might have to add further logic to filter available groups as children.
357 query := `
358 SELECT name FROM groups
359 WHERE name != ? AND name NOT IN (
360 SELECT child_group FROM group_to_group WHERE parent_group = ?
DTabidze908bb852024-03-25 20:07:57 +0400361 )`
DTabidze0d802592024-03-19 17:42:45 +0400362 rows, err := s.db.Query(query, group, group)
363 if err != nil {
364 return nil, err
365 }
366 defer rows.Close()
367 var availableGroups []string
368 for rows.Next() {
369 var groupName string
370 if err := rows.Scan(&groupName); err != nil {
371 return nil, err
372 }
373 availableGroups = append(availableGroups, groupName)
374 }
375 return availableGroups, nil
376}
377
DTabidzec0b4d8f2024-03-22 17:25:10 +0400378func (s *SQLiteStore) GetAllTransitiveGroupsForUser(user string) ([]Group, error) {
379 if groups, err := s.GetGroupsUserBelongsTo(user); err != nil {
DTabidzed7744a62024-03-20 14:09:15 +0400380 return nil, err
DTabidzec0b4d8f2024-03-22 17:25:10 +0400381 } else {
382 visited := map[string]struct{}{}
383 return s.getAllParentGroupsRecursive(groups, visited)
DTabidzed7744a62024-03-20 14:09:15 +0400384 }
DTabidzec0b4d8f2024-03-22 17:25:10 +0400385}
386
387func (s *SQLiteStore) GetAllTransitiveGroupsForGroup(group string) ([]Group, error) {
388 if p, err := s.GetGroupsGroupBelongsTo(group); err != nil {
389 return nil, err
390 } else {
391 // Mark initial group as visited
392 visited := map[string]struct{}{
393 group: struct{}{},
394 }
395 return s.getAllParentGroupsRecursive(p, visited)
396 }
397}
398
399func (s *SQLiteStore) getAllParentGroupsRecursive(groups []Group, visited map[string]struct{}) ([]Group, error) {
400 var ret []Group
401 for _, g := range groups {
402 if _, ok := visited[g.Name]; ok {
403 continue
404 }
405 visited[g.Name] = struct{}{}
406 ret = append(ret, g)
407 if p, err := s.GetGroupsGroupBelongsTo(g.Name); err != nil {
DTabidzed7744a62024-03-20 14:09:15 +0400408 return nil, err
DTabidzec0b4d8f2024-03-22 17:25:10 +0400409 } else if res, err := s.getAllParentGroupsRecursive(p, visited); err != nil {
410 return nil, err
411 } else {
412 ret = append(ret, res...)
DTabidzed7744a62024-03-20 14:09:15 +0400413 }
414 }
DTabidzec0b4d8f2024-03-22 17:25:10 +0400415 return ret, nil
DTabidzed7744a62024-03-20 14:09:15 +0400416}
417
DTabidzec0b4d8f2024-03-22 17:25:10 +0400418func (s *SQLiteStore) GetGroupsGroupBelongsTo(group string) ([]Group, error) {
419 query := `
420 SELECT groups.name, groups.description
421 FROM groups
422 JOIN group_to_group ON groups.name = group_to_group.parent_group
423 WHERE group_to_group.child_group = ?`
DTabidzed7744a62024-03-20 14:09:15 +0400424 rows, err := s.db.Query(query, group)
425 if err != nil {
426 return nil, err
427 }
428 defer rows.Close()
DTabidzec0b4d8f2024-03-22 17:25:10 +0400429 var parentGroups []Group
DTabidzed7744a62024-03-20 14:09:15 +0400430 for rows.Next() {
DTabidzec0b4d8f2024-03-22 17:25:10 +0400431 var parentGroup Group
432 if err := rows.Scan(&parentGroup.Name, &parentGroup.Description); err != nil {
DTabidzed7744a62024-03-20 14:09:15 +0400433 return nil, err
434 }
435 parentGroups = append(parentGroups, parentGroup)
436 }
437 if err := rows.Err(); err != nil {
438 return nil, err
439 }
440 return parentGroups, nil
441}
442
DTabidzec0b4d8f2024-03-22 17:25:10 +0400443func (s *SQLiteStore) GetDirectChildrenGroups(group string) ([]Group, error) {
444 query := `
445 SELECT groups.name, groups.description
446 FROM groups
447 JOIN group_to_group ON groups.name = group_to_group.child_group
448 WHERE group_to_group.parent_group = ?`
449 rows, err := s.db.Query(query, group)
450 if err != nil {
451 return nil, err
452 }
453 defer rows.Close()
454 var childrenGroups []Group
455 for rows.Next() {
456 var childGroup Group
457 if err := rows.Scan(&childGroup.Name, &childGroup.Description); err != nil {
458 return nil, err
459 }
460 childrenGroups = append(childrenGroups, childGroup)
461 }
462 if err := rows.Err(); err != nil {
463 return nil, err
464 }
465 return childrenGroups, nil
466}
467
DTabidze2b224bf2024-03-27 13:25:49 +0400468func (s *SQLiteStore) RemoveFromGroupToGroup(parent, child string) error {
469 query := `DELETE FROM group_to_group WHERE parent_group = ? AND child_group = ?`
470 rowDeleted, err := s.db.Exec(query, parent, child)
471 if err != nil {
472 return err
473 }
474 rowDeletedNumber, err := rowDeleted.RowsAffected()
475 if err != nil {
476 return err
477 }
478 if rowDeletedNumber == 0 {
479 return fmt.Errorf("pair of parent '%s' and child '%s' groups not found", parent, child)
480 }
481 return nil
482}
483
484func (s *SQLiteStore) RemoveUserFromTable(username, groupName, tableName string) error {
485 if tableName == "owners" {
486 owners, err := s.GetGroupOwners(groupName)
487 if err != nil {
488 return err
489 }
490 if len(owners) == 1 {
491 return fmt.Errorf("cannot remove the last owner of the group")
492 }
493 }
494 query := fmt.Sprintf("DELETE FROM %s WHERE username = ? AND group_name = ?", tableName)
495 rowDeleted, err := s.db.Exec(query, username, groupName)
496 if err != nil {
497 return err
498 }
499 rowDeletedNumber, err := rowDeleted.RowsAffected()
500 if err != nil {
501 return err
502 }
503 if rowDeletedNumber == 0 {
504 return fmt.Errorf("pair of group '%s' and user '%s' not found", groupName, username)
505 }
506 return nil
507}
508
DTabidze0d802592024-03-19 17:42:45 +0400509func getLoggedInUser(r *http.Request) (string, error) {
DTabidzec0b4d8f2024-03-22 17:25:10 +0400510 if user := r.Header.Get("X-User"); user != "" {
511 return user, nil
512 } else {
513 return "", fmt.Errorf("unauthenticated")
514 }
DTabidze0d802592024-03-19 17:42:45 +0400515}
516
517type Status int
518
519const (
520 Owner Status = iota
521 Member
522)
523
Giorgi Lekveishvili329af572024-03-25 20:14:41 +0400524func (s *Server) Start() error {
525 e := make(chan error)
526 go func() {
527 r := mux.NewRouter()
528 r.PathPrefix("/static/").Handler(http.FileServer(http.FS(staticResources)))
DTabidze078385f2024-03-27 14:49:05 +0400529 r.HandleFunc("/group/{group-name}/add-user/", s.addUserToGroupHandler)
530 r.HandleFunc("/group/{parent-group}/add-child-group", s.addChildGroupHandler)
531 r.HandleFunc("/group/{parent-group}/remove-child-group/{child-group}", s.removeChildGroupHandler)
532 r.HandleFunc("/group/{group-name}/remove-owner/{username}", s.removeOwnerFromGroupHandler)
533 r.HandleFunc("/group/{group-name}/remove-member/{username}", s.removeMemberFromGroupHandler)
Giorgi Lekveishvili329af572024-03-25 20:14:41 +0400534 r.HandleFunc("/group/{group-name}", s.groupHandler)
DTabidze5d735e32024-03-26 16:01:06 +0400535 r.HandleFunc("/user/{username}", s.userHandler)
Giorgi Lekveishvili329af572024-03-25 20:14:41 +0400536 r.HandleFunc("/create-group", s.createGroupHandler)
Giorgi Lekveishvili329af572024-03-25 20:14:41 +0400537 r.HandleFunc("/", s.homePageHandler)
538 e <- http.ListenAndServe(fmt.Sprintf(":%d", *port), r)
539 }()
540 go func() {
541 r := mux.NewRouter()
542 r.HandleFunc("/api/init", s.apiInitHandler)
543 r.HandleFunc("/api/user/{username}", s.apiMemberOfHandler)
544 e <- http.ListenAndServe(fmt.Sprintf(":%d", *apiPort), r)
545 }()
546 return <-e
DTabidze0d802592024-03-19 17:42:45 +0400547}
548
549type GroupData struct {
550 Group Group
551 Membership string
552}
553
554func (s *Server) checkIsOwner(w http.ResponseWriter, user, group string) (bool, error) {
555 isOwner, err := s.store.IsGroupOwner(user, group)
556 if err != nil {
557 http.Error(w, err.Error(), http.StatusInternalServerError)
558 return false, err
559 }
560 if !isOwner {
DTabidze2b224bf2024-03-27 13:25:49 +0400561 return false, fmt.Errorf("you are not the owner of the group %s", group)
DTabidze0d802592024-03-19 17:42:45 +0400562 }
563 return true, nil
564}
565
566func (s *Server) homePageHandler(w http.ResponseWriter, r *http.Request) {
567 loggedInUser, err := getLoggedInUser(r)
568 if err != nil {
569 http.Error(w, "User Not Logged In", http.StatusUnauthorized)
570 return
571 }
DTabidze5d735e32024-03-26 16:01:06 +0400572 http.Redirect(w, r, "/user/"+loggedInUser, http.StatusSeeOther)
573}
574
575func (s *Server) userHandler(w http.ResponseWriter, r *http.Request) {
576 loggedInUser, err := getLoggedInUser(r)
577 if err != nil {
578 http.Error(w, "User Not Logged In", http.StatusUnauthorized)
579 return
580 }
581 vars := mux.Vars(r)
582 user := strings.ToLower(vars["username"])
583 // TODO(dtabidze): should check if username exists or not.
584 loggedInUserPage := loggedInUser == user
585 ownerGroups, err := s.store.GetGroupsOwnedBy(user)
DTabidze0d802592024-03-19 17:42:45 +0400586 if err != nil {
587 http.Error(w, err.Error(), http.StatusInternalServerError)
588 return
589 }
DTabidze5d735e32024-03-26 16:01:06 +0400590 membershipGroups, err := s.store.GetGroupsUserBelongsTo(user)
DTabidze0d802592024-03-19 17:42:45 +0400591 if err != nil {
592 http.Error(w, err.Error(), http.StatusInternalServerError)
593 return
594 }
595 tmpl, err := template.New("index").Parse(indexHTML)
596 if err != nil {
597 http.Error(w, err.Error(), http.StatusInternalServerError)
598 return
599 }
DTabidze5d735e32024-03-26 16:01:06 +0400600 transitiveGroups, err := s.store.GetAllTransitiveGroupsForUser(user)
DTabidzec0b4d8f2024-03-22 17:25:10 +0400601 if err != nil {
602 http.Error(w, err.Error(), http.StatusInternalServerError)
603 return
604 }
DTabidze0d802592024-03-19 17:42:45 +0400605 data := struct {
606 OwnerGroups []Group
607 MembershipGroups []Group
DTabidzec0b4d8f2024-03-22 17:25:10 +0400608 TransitiveGroups []Group
DTabidze5d735e32024-03-26 16:01:06 +0400609 LoggedInUserPage bool
610 CurrentUser string
DTabidze0d802592024-03-19 17:42:45 +0400611 }{
612 OwnerGroups: ownerGroups,
613 MembershipGroups: membershipGroups,
DTabidzec0b4d8f2024-03-22 17:25:10 +0400614 TransitiveGroups: transitiveGroups,
DTabidze5d735e32024-03-26 16:01:06 +0400615 LoggedInUserPage: loggedInUserPage,
616 CurrentUser: user,
DTabidze0d802592024-03-19 17:42:45 +0400617 }
618 w.Header().Set("Content-Type", "text/html")
619 if err := tmpl.Execute(w, data); err != nil {
620 http.Error(w, err.Error(), http.StatusInternalServerError)
621 return
622 }
623}
624
625func (s *Server) createGroupHandler(w http.ResponseWriter, r *http.Request) {
626 loggedInUser, err := getLoggedInUser(r)
627 if err != nil {
628 http.Error(w, "User Not Logged In", http.StatusUnauthorized)
629 return
630 }
631 if r.Method != http.MethodPost {
632 http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
633 return
634 }
635 if err := r.ParseForm(); err != nil {
636 http.Error(w, err.Error(), http.StatusInternalServerError)
637 return
638 }
639 var group Group
640 group.Name = r.PostFormValue("group-name")
DTabidze908bb852024-03-25 20:07:57 +0400641 if err := isValidGroupName(group.Name); err != nil {
642 http.Error(w, err.Error(), http.StatusBadRequest)
643 return
644 }
DTabidze0d802592024-03-19 17:42:45 +0400645 group.Description = r.PostFormValue("description")
646 if err := s.store.CreateGroup(loggedInUser, group); err != nil {
647 http.Error(w, err.Error(), http.StatusInternalServerError)
648 return
649 }
650 http.Redirect(w, r, "/", http.StatusSeeOther)
651}
652
653func (s *Server) groupHandler(w http.ResponseWriter, r *http.Request) {
DTabidzec0b4d8f2024-03-22 17:25:10 +0400654 _, err := getLoggedInUser(r)
655 if err != nil {
656 http.Error(w, "User Not Logged In", http.StatusUnauthorized)
657 return
658 }
DTabidzed7744a62024-03-20 14:09:15 +0400659 vars := mux.Vars(r)
660 groupName := vars["group-name"]
DTabidze908bb852024-03-25 20:07:57 +0400661 exists, err := s.store.DoesGroupExist(groupName)
662 if err != nil {
663 http.Error(w, err.Error(), http.StatusInternalServerError)
664 return
665 }
666 if !exists {
667 errorMsg := fmt.Sprintf("group with the name '%s' not found", groupName)
668 http.Error(w, errorMsg, http.StatusNotFound)
669 return
670 }
DTabidze0d802592024-03-19 17:42:45 +0400671 tmpl, err := template.New("group").Parse(groupHTML)
672 if err != nil {
673 http.Error(w, err.Error(), http.StatusInternalServerError)
674 return
675 }
676 owners, err := s.store.GetGroupOwners(groupName)
677 if err != nil {
678 http.Error(w, err.Error(), http.StatusInternalServerError)
679 return
680 }
681 members, err := s.store.GetGroupMembers(groupName)
682 if err != nil {
683 http.Error(w, err.Error(), http.StatusInternalServerError)
684 return
685 }
686 description, err := s.store.GetGroupDescription(groupName)
687 if err != nil {
688 http.Error(w, err.Error(), http.StatusInternalServerError)
689 return
690 }
691 availableGroups, err := s.store.GetAvailableGroupsAsChild(groupName)
692 if err != nil {
693 http.Error(w, err.Error(), http.StatusInternalServerError)
694 return
695 }
DTabidzec0b4d8f2024-03-22 17:25:10 +0400696 transitiveGroups, err := s.store.GetAllTransitiveGroupsForGroup(groupName)
697 if err != nil {
698 http.Error(w, err.Error(), http.StatusInternalServerError)
699 return
700 }
701 childGroups, err := s.store.GetDirectChildrenGroups(groupName)
702 if err != nil {
703 http.Error(w, err.Error(), http.StatusInternalServerError)
704 return
705 }
DTabidze0d802592024-03-19 17:42:45 +0400706 data := struct {
DTabidzec0b4d8f2024-03-22 17:25:10 +0400707 GroupName string
708 Description string
709 Owners []string
710 Members []string
711 AvailableGroups []string
712 TransitiveGroups []Group
713 ChildGroups []Group
DTabidze0d802592024-03-19 17:42:45 +0400714 }{
DTabidzec0b4d8f2024-03-22 17:25:10 +0400715 GroupName: groupName,
716 Description: description,
717 Owners: owners,
718 Members: members,
719 AvailableGroups: availableGroups,
720 TransitiveGroups: transitiveGroups,
721 ChildGroups: childGroups,
DTabidze0d802592024-03-19 17:42:45 +0400722 }
723 if err := tmpl.Execute(w, data); err != nil {
724 http.Error(w, err.Error(), http.StatusInternalServerError)
725 return
726 }
727}
728
DTabidze2b224bf2024-03-27 13:25:49 +0400729func (s *Server) removeChildGroupHandler(w http.ResponseWriter, r *http.Request) {
730 loggedInUser, err := getLoggedInUser(r)
731 if err != nil {
732 http.Error(w, "User Not Logged In", http.StatusUnauthorized)
733 return
734 }
735 if r.Method == http.MethodPost {
736 vars := mux.Vars(r)
737 parentGroup := vars["parent-group"]
738 childGroup := vars["child-group"]
739 if err := isValidGroupName(parentGroup); err != nil {
740 http.Error(w, err.Error(), http.StatusBadRequest)
741 return
742 }
743 if err := isValidGroupName(childGroup); err != nil {
744 http.Error(w, err.Error(), http.StatusBadRequest)
745 return
746 }
747 if _, err := s.checkIsOwner(w, loggedInUser, parentGroup); err != nil {
748 http.Error(w, err.Error(), http.StatusUnauthorized)
749 return
750 }
751 err := s.store.RemoveFromGroupToGroup(parentGroup, childGroup)
752 if err != nil {
753 http.Error(w, err.Error(), http.StatusInternalServerError)
754 return
755 }
756 http.Redirect(w, r, "/group/"+parentGroup, http.StatusSeeOther)
757 }
758}
759
DTabidze078385f2024-03-27 14:49:05 +0400760func (s *Server) removeOwnerFromGroupHandler(w http.ResponseWriter, r *http.Request) {
DTabidze2b224bf2024-03-27 13:25:49 +0400761 loggedInUser, err := getLoggedInUser(r)
762 if err != nil {
763 http.Error(w, "User Not Logged In", http.StatusUnauthorized)
764 return
765 }
766 if r.Method == http.MethodPost {
767 vars := mux.Vars(r)
768 username := vars["username"]
769 groupName := vars["group-name"]
DTabidze078385f2024-03-27 14:49:05 +0400770 tableName := "owners"
DTabidze2b224bf2024-03-27 13:25:49 +0400771 if err := isValidGroupName(groupName); err != nil {
772 http.Error(w, err.Error(), http.StatusBadRequest)
773 return
774 }
775 if _, err := s.checkIsOwner(w, loggedInUser, groupName); err != nil {
776 http.Error(w, err.Error(), http.StatusUnauthorized)
777 return
778 }
779 err := s.store.RemoveUserFromTable(username, groupName, tableName)
780 if err != nil {
781 http.Error(w, err.Error(), http.StatusInternalServerError)
782 return
783 }
784 http.Redirect(w, r, "/group/"+groupName, http.StatusSeeOther)
785 }
786}
787
DTabidze078385f2024-03-27 14:49:05 +0400788func (s *Server) removeMemberFromGroupHandler(w http.ResponseWriter, r *http.Request) {
789 loggedInUser, err := getLoggedInUser(r)
790 if err != nil {
791 http.Error(w, "User Not Logged In", http.StatusUnauthorized)
792 return
793 }
794 if r.Method == http.MethodPost {
795 vars := mux.Vars(r)
796 username := vars["username"]
797 groupName := vars["group-name"]
798 tableName := "user_to_group"
799 if err := isValidGroupName(groupName); err != nil {
800 http.Error(w, err.Error(), http.StatusBadRequest)
801 return
802 }
803 if _, err := s.checkIsOwner(w, loggedInUser, groupName); err != nil {
804 http.Error(w, err.Error(), http.StatusUnauthorized)
805 return
806 }
807 err := s.store.RemoveUserFromTable(username, groupName, tableName)
808 if err != nil {
809 http.Error(w, err.Error(), http.StatusInternalServerError)
810 return
811 }
812 http.Redirect(w, r, "/group/"+groupName, http.StatusSeeOther)
813 }
814}
815
816func (s *Server) addUserToGroupHandler(w http.ResponseWriter, r *http.Request) {
DTabidze0d802592024-03-19 17:42:45 +0400817 if r.Method != http.MethodPost {
818 http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
819 return
820 }
821 loggedInUser, err := getLoggedInUser(r)
822 if err != nil {
823 http.Error(w, "User Not Logged In", http.StatusUnauthorized)
824 return
825 }
DTabidze078385f2024-03-27 14:49:05 +0400826 vars := mux.Vars(r)
827 groupName := vars["group-name"]
DTabidze908bb852024-03-25 20:07:57 +0400828 if err := isValidGroupName(groupName); err != nil {
829 http.Error(w, err.Error(), http.StatusBadRequest)
830 return
831 }
832 username := strings.ToLower(r.FormValue("username"))
833 if username == "" {
834 http.Error(w, "Username parameter is required", http.StatusBadRequest)
835 return
836 }
DTabidze0d802592024-03-19 17:42:45 +0400837 status, err := convertStatus(r.FormValue("status"))
838 if err != nil {
839 http.Error(w, err.Error(), http.StatusBadRequest)
840 return
841 }
842 if _, err := s.checkIsOwner(w, loggedInUser, groupName); err != nil {
DTabidze2b224bf2024-03-27 13:25:49 +0400843 http.Error(w, err.Error(), http.StatusUnauthorized)
DTabidze0d802592024-03-19 17:42:45 +0400844 return
845 }
846 switch status {
847 case Owner:
848 err = s.store.AddGroupOwner(username, groupName)
849 case Member:
850 err = s.store.AddGroupMember(username, groupName)
851 default:
852 http.Error(w, "Invalid status", http.StatusBadRequest)
853 return
854 }
855 if err != nil {
856 http.Error(w, err.Error(), http.StatusInternalServerError)
857 return
858 }
859 http.Redirect(w, r, "/group/"+groupName, http.StatusSeeOther)
860}
861
862func (s *Server) addChildGroupHandler(w http.ResponseWriter, r *http.Request) {
863 // TODO(dtabidze): In future we might need to make one group OWNER of another and not just a member.
864 if r.Method != http.MethodPost {
865 http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
866 return
867 }
868 loggedInUser, err := getLoggedInUser(r)
869 if err != nil {
870 http.Error(w, "User Not Logged In", http.StatusUnauthorized)
871 return
872 }
DTabidze078385f2024-03-27 14:49:05 +0400873 vars := mux.Vars(r)
874 parentGroup := vars["parent-group"]
DTabidze908bb852024-03-25 20:07:57 +0400875 if err := isValidGroupName(parentGroup); err != nil {
876 http.Error(w, err.Error(), http.StatusBadRequest)
877 return
878 }
DTabidze0d802592024-03-19 17:42:45 +0400879 childGroup := r.FormValue("child-group")
DTabidze908bb852024-03-25 20:07:57 +0400880 if err := isValidGroupName(childGroup); err != nil {
881 http.Error(w, err.Error(), http.StatusBadRequest)
882 return
883 }
DTabidze0d802592024-03-19 17:42:45 +0400884 if _, err := s.checkIsOwner(w, loggedInUser, parentGroup); err != nil {
DTabidze2b224bf2024-03-27 13:25:49 +0400885 http.Error(w, err.Error(), http.StatusUnauthorized)
DTabidze0d802592024-03-19 17:42:45 +0400886 return
887 }
888 if err := s.store.AddChildGroup(parentGroup, childGroup); err != nil {
889 http.Error(w, err.Error(), http.StatusInternalServerError)
890 return
891 }
892 http.Redirect(w, r, "/group/"+parentGroup, http.StatusSeeOther)
893}
894
Giorgi Lekveishvili942c7612024-03-22 19:27:48 +0400895type initRequest struct {
896 Owner string `json:"owner"`
897 Groups []string `json:"groups"`
898}
899
900func (s *Server) apiInitHandler(w http.ResponseWriter, r *http.Request) {
901 var req initRequest
902 if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
903 http.Error(w, err.Error(), http.StatusBadRequest)
904 return
905 }
906 if err := s.store.Init(req.Owner, req.Groups); err != nil {
907 http.Error(w, err.Error(), http.StatusInternalServerError)
908 return
909 }
910}
911
912type userInfo struct {
DTabidzed7744a62024-03-20 14:09:15 +0400913 MemberOf []string `json:"memberOf"`
914}
915
916func (s *Server) apiMemberOfHandler(w http.ResponseWriter, r *http.Request) {
917 vars := mux.Vars(r)
918 user, ok := vars["username"]
DTabidze908bb852024-03-25 20:07:57 +0400919 if !ok || user == "" {
DTabidzed7744a62024-03-20 14:09:15 +0400920 http.Error(w, "Username parameter is required", http.StatusBadRequest)
921 return
922 }
DTabidze908bb852024-03-25 20:07:57 +0400923 user = strings.ToLower(user)
DTabidzed7744a62024-03-20 14:09:15 +0400924 transitiveGroups, err := s.store.GetAllTransitiveGroupsForUser(user)
925 if err != nil {
926 http.Error(w, err.Error(), http.StatusInternalServerError)
927 return
928 }
DTabidzec0b4d8f2024-03-22 17:25:10 +0400929 var groupNames []string
930 for _, group := range transitiveGroups {
931 groupNames = append(groupNames, group.Name)
932 }
DTabidzed7744a62024-03-20 14:09:15 +0400933 w.Header().Set("Content-Type", "application/json")
Giorgi Lekveishvili942c7612024-03-22 19:27:48 +0400934 if err := json.NewEncoder(w).Encode(userInfo{groupNames}); err != nil {
DTabidzed7744a62024-03-20 14:09:15 +0400935 http.Error(w, err.Error(), http.StatusInternalServerError)
936 return
937 }
938}
939
DTabidze908bb852024-03-25 20:07:57 +0400940func convertStatus(status string) (Status, error) {
941 switch status {
942 case "Owner":
943 return Owner, nil
944 case "Member":
945 return Member, nil
946 default:
947 return Owner, fmt.Errorf("invalid status: %s", status)
948 }
949}
950
951func isValidGroupName(group string) error {
952 if strings.TrimSpace(group) == "" {
953 return fmt.Errorf("group name can't be empty or contain only whitespaces")
954 }
955 validGroupName := regexp.MustCompile(`^[a-z0-9\-_:.\/ ]+$`)
956 if !validGroupName.MatchString(group) {
957 return fmt.Errorf("group name should contain only lowercase letters, digits, -, _, :, ., /")
958 }
959 return nil
960}
961
DTabidze0d802592024-03-19 17:42:45 +0400962func main() {
963 flag.Parse()
DTabidzec0b4d8f2024-03-22 17:25:10 +0400964 db, err := sql.Open("sqlite3", *dbPath)
DTabidze0d802592024-03-19 17:42:45 +0400965 if err != nil {
966 panic(err)
967 }
DTabidzec0b4d8f2024-03-22 17:25:10 +0400968 store, err := NewSQLiteStore(db)
969 if err != nil {
970 panic(err)
971 }
972 s := Server{store}
Giorgi Lekveishvili329af572024-03-25 20:14:41 +0400973 log.Fatal(s.Start())
DTabidze0d802592024-03-19 17:42:45 +0400974}