blob: 7cb75380fb52dd1b96bf907ded14e6097710488d [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"
DTabidze4b44ff42024-04-02 03:16:26 +040012 "net/url"
DTabidze908bb852024-03-25 20:07:57 +040013 "regexp"
14 "strings"
DTabidze0d802592024-03-19 17:42:45 +040015
16 "github.com/ncruces/go-sqlite3"
17 _ "github.com/ncruces/go-sqlite3/driver"
18 _ "github.com/ncruces/go-sqlite3/embed"
DTabidzed7744a62024-03-20 14:09:15 +040019
20 "github.com/gorilla/mux"
DTabidze0d802592024-03-19 17:42:45 +040021)
22
Giorgi Lekveishvili329af572024-03-25 20:14:41 +040023var port = flag.Int("port", 8080, "Port to listen on")
24var apiPort = flag.Int("api-port", 8081, "Port to listen on for API requests")
DTabidze0d802592024-03-19 17:42:45 +040025var dbPath = flag.String("db-path", "memberships.db", "Path to SQLite file")
26
DTabidze4b44ff42024-04-02 03:16:26 +040027//go:embed memberships-tmpl/*
28var tmpls embed.FS
DTabidze0d802592024-03-19 17:42:45 +040029
30//go:embed static
31var staticResources embed.FS
32
33type Store interface {
Giorgi Lekveishvili942c7612024-03-22 19:27:48 +040034 // Initializes store with admin user and their groups.
35 Init(owner string, groups []string) error
DTabidze0d802592024-03-19 17:42:45 +040036 CreateGroup(owner string, group Group) error
37 AddChildGroup(parent, child string) error
DTabidze908bb852024-03-25 20:07:57 +040038 DoesGroupExist(group string) (bool, error)
DTabidze0d802592024-03-19 17:42:45 +040039 GetGroupsOwnedBy(user string) ([]Group, error)
DTabidzed7744a62024-03-20 14:09:15 +040040 GetGroupsUserBelongsTo(user string) ([]Group, error)
DTabidze0d802592024-03-19 17:42:45 +040041 IsGroupOwner(user, group string) (bool, error)
42 AddGroupMember(user, group string) error
43 AddGroupOwner(user, group string) error
44 GetGroupOwners(group string) ([]string, error)
45 GetGroupMembers(group string) ([]string, error)
46 GetGroupDescription(group string) (string, error)
47 GetAvailableGroupsAsChild(group string) ([]string, error)
DTabidzec0b4d8f2024-03-22 17:25:10 +040048 GetAllTransitiveGroupsForUser(user string) ([]Group, error)
49 GetGroupsGroupBelongsTo(group string) ([]Group, error)
50 GetDirectChildrenGroups(group string) ([]Group, error)
51 GetAllTransitiveGroupsForGroup(group string) ([]Group, error)
DTabidze2b224bf2024-03-27 13:25:49 +040052 RemoveFromGroupToGroup(parent, child string) error
53 RemoveUserFromTable(username, groupName, tableName string) error
DTabidze0d802592024-03-19 17:42:45 +040054}
55
56type Server struct {
57 store Store
58}
59
60type Group struct {
61 Name string
62 Description string
63}
64
65type SQLiteStore struct {
66 db *sql.DB
67}
68
DTabidzec0b4d8f2024-03-22 17:25:10 +040069func NewSQLiteStore(db *sql.DB) (*SQLiteStore, error) {
70 _, err := db.Exec(`
DTabidze0d802592024-03-19 17:42:45 +040071 CREATE TABLE IF NOT EXISTS groups (
72 name TEXT PRIMARY KEY,
73 description TEXT
74 );
75
76 CREATE TABLE IF NOT EXISTS owners (
77 username TEXT,
78 group_name TEXT,
79 FOREIGN KEY(group_name) REFERENCES groups(name)
80 );
81
82 CREATE TABLE IF NOT EXISTS group_to_group (
83 parent_group TEXT,
84 child_group TEXT,
85 FOREIGN KEY(parent_group) REFERENCES groups(name),
86 FOREIGN KEY(child_group) REFERENCES groups(name)
87 );
88
89 CREATE TABLE IF NOT EXISTS user_to_group (
90 username TEXT,
91 group_name TEXT,
92 FOREIGN KEY(group_name) REFERENCES groups(name)
93 );`)
94 if err != nil {
95 return nil, err
96 }
97 return &SQLiteStore{db: db}, nil
98}
99
Giorgi Lekveishvili942c7612024-03-22 19:27:48 +0400100func (s *SQLiteStore) Init(owner string, groups []string) error {
101 tx, err := s.db.Begin()
102 if err != nil {
103 return err
104 }
105 defer tx.Rollback()
106 row := tx.QueryRow("SELECT COUNT(*) FROM groups")
107 var count int
108 if err := row.Scan(&count); err != nil {
109 return err
110 }
111 if count != 0 {
112 return fmt.Errorf("store already initialised")
113 }
114 for _, g := range groups {
115 query := `INSERT INTO groups (name, description) VALUES (?, '')`
116 if _, err := tx.Exec(query, g); err != nil {
117 return err
118 }
119 query = `INSERT INTO owners (username, group_name) VALUES (?, ?)`
120 if _, err := tx.Exec(query, owner, g); err != nil {
121 return err
122 }
Giorgi Lekveishvilid542b732024-03-25 18:17:39 +0400123 query = `INSERT INTO user_to_group (username, group_name) VALUES (?, ?)`
124 if _, err := tx.Exec(query, owner, g); err != nil {
125 return err
126 }
Giorgi Lekveishvili942c7612024-03-22 19:27:48 +0400127 }
128 return tx.Commit()
129}
130
DTabidze0d802592024-03-19 17:42:45 +0400131func (s *SQLiteStore) queryGroups(query string, args ...interface{}) ([]Group, error) {
132 groups := make([]Group, 0)
133 rows, err := s.db.Query(query, args...)
134 if err != nil {
135 return nil, err
136 }
137 defer rows.Close()
138 for rows.Next() {
139 var group Group
140 if err := rows.Scan(&group.Name, &group.Description); err != nil {
141 return nil, err
142 }
143 groups = append(groups, group)
144 }
145 if err := rows.Err(); err != nil {
146 return nil, err
147 }
148 return groups, nil
149}
150
151func (s *SQLiteStore) GetGroupsOwnedBy(user string) ([]Group, error) {
152 query := `
153 SELECT groups.name, groups.description
154 FROM groups
155 JOIN owners ON groups.name = owners.group_name
156 WHERE owners.username = ?`
157 return s.queryGroups(query, user)
158}
159
DTabidzed7744a62024-03-20 14:09:15 +0400160func (s *SQLiteStore) GetGroupsUserBelongsTo(user string) ([]Group, error) {
DTabidze0d802592024-03-19 17:42:45 +0400161 query := `
162 SELECT groups.name, groups.description
163 FROM groups
164 JOIN user_to_group ON groups.name = user_to_group.group_name
165 WHERE user_to_group.username = ?`
166 return s.queryGroups(query, user)
167}
168
169func (s *SQLiteStore) CreateGroup(owner string, group Group) error {
170 tx, err := s.db.Begin()
171 if err != nil {
172 return err
173 }
174 defer tx.Rollback()
175 query := `INSERT INTO groups (name, description) VALUES (?, ?)`
176 if _, err := tx.Exec(query, group.Name, group.Description); err != nil {
177 sqliteErr, ok := err.(*sqlite3.Error)
178 if ok && sqliteErr.ExtendedCode() == 1555 {
179 return fmt.Errorf("Group with the name %s already exists", group.Name)
180 }
181 return err
182 }
183 query = `INSERT INTO owners (username, group_name) VALUES (?, ?)`
184 if _, err := tx.Exec(query, owner, group.Name); err != nil {
185 return err
186 }
Giorgi Lekveishvili942c7612024-03-22 19:27:48 +0400187 return tx.Commit()
DTabidze0d802592024-03-19 17:42:45 +0400188}
189
190func (s *SQLiteStore) IsGroupOwner(user, group string) (bool, error) {
191 query := `
192 SELECT EXISTS (
193 SELECT 1
194 FROM owners
195 WHERE username = ? AND group_name = ?
196 )`
197 var exists bool
198 if err := s.db.QueryRow(query, user, group).Scan(&exists); err != nil {
199 return false, err
200 }
201 return exists, nil
202}
203
204func (s *SQLiteStore) userGroupPairExists(tx *sql.Tx, table, user, group string) (bool, error) {
205 query := fmt.Sprintf("SELECT EXISTS (SELECT 1 FROM %s WHERE username = ? AND group_name = ?)", table)
206 var exists bool
207 if err := tx.QueryRow(query, user, group).Scan(&exists); err != nil {
208 return false, err
209 }
210 return exists, nil
211}
212
213func (s *SQLiteStore) AddGroupMember(user, group string) error {
214 tx, err := s.db.Begin()
215 if err != nil {
216 return err
217 }
218 defer tx.Rollback()
219 existsInUserToGroup, err := s.userGroupPairExists(tx, "user_to_group", user, group)
220 if err != nil {
221 return err
222 }
223 if existsInUserToGroup {
224 return fmt.Errorf("%s is already a member of group %s", user, group)
225 }
226 if _, err := tx.Exec(`INSERT INTO user_to_group (username, group_name) VALUES (?, ?)`, user, group); err != nil {
227 return err
228 }
229 if err := tx.Commit(); err != nil {
230 return err
231 }
232 return nil
233}
234
235func (s *SQLiteStore) AddGroupOwner(user, group string) error {
236 tx, err := s.db.Begin()
237 if err != nil {
238 return err
239 }
240 defer tx.Rollback()
241 existsInOwners, err := s.userGroupPairExists(tx, "owners", user, group)
242 if err != nil {
243 return err
244 }
245 if existsInOwners {
246 return fmt.Errorf("%s is already an owner of group %s", user, group)
247 }
248 if _, err = tx.Exec(`INSERT INTO owners (username, group_name) VALUES (?, ?)`, user, group); err != nil {
249 return err
250 }
251 if err := tx.Commit(); err != nil {
252 return err
253 }
254 return nil
255}
256
257func (s *SQLiteStore) getUsersByGroup(table, group string) ([]string, error) {
258 query := fmt.Sprintf("SELECT username FROM %s WHERE group_name = ?", table)
259 rows, err := s.db.Query(query, group)
260 if err != nil {
261 return nil, err
262 }
263 defer rows.Close()
264 var users []string
265 for rows.Next() {
266 var username string
267 if err := rows.Scan(&username); err != nil {
268 return nil, err
269 }
270 users = append(users, username)
271 }
272 if err := rows.Err(); err != nil {
273 return nil, err
274 }
275 return users, nil
276}
277
278func (s *SQLiteStore) GetGroupOwners(group string) ([]string, error) {
279 return s.getUsersByGroup("owners", group)
280}
281
282func (s *SQLiteStore) GetGroupMembers(group string) ([]string, error) {
283 return s.getUsersByGroup("user_to_group", group)
284}
285
286func (s *SQLiteStore) GetGroupDescription(group string) (string, error) {
287 var description string
288 query := `SELECT description FROM groups WHERE name = ?`
289 if err := s.db.QueryRow(query, group).Scan(&description); err != nil {
290 return "", err
291 }
292 return description, nil
293}
294
295func (s *SQLiteStore) parentChildGroupPairExists(tx *sql.Tx, parent, child string) (bool, error) {
296 query := `SELECT EXISTS (SELECT 1 FROM group_to_group WHERE parent_group = ? AND child_group = ?)`
297 var exists bool
298 if err := tx.QueryRow(query, parent, child).Scan(&exists); err != nil {
299 return false, err
300 }
301 return exists, nil
302}
303
DTabidze908bb852024-03-25 20:07:57 +0400304func (s *SQLiteStore) DoesGroupExist(group string) (bool, error) {
305 query := `SELECT EXISTS (SELECT 1 FROM groups WHERE name = ?)`
306 var exists bool
307 if err := s.db.QueryRow(query, group).Scan(&exists); err != nil {
308 return false, err
309 }
310 return exists, nil
311}
312
DTabidze0d802592024-03-19 17:42:45 +0400313func (s *SQLiteStore) AddChildGroup(parent, child string) error {
DTabidze908bb852024-03-25 20:07:57 +0400314 if parent == child {
315 return fmt.Errorf("parent and child groups can not have same name")
316 }
317 if _, err := s.DoesGroupExist(parent); err != nil {
318 return fmt.Errorf("parent group name %s does not exist", parent)
319 }
320 if _, err := s.DoesGroupExist(child); err != nil {
321 return fmt.Errorf("child group name %s does not exist", child)
322 }
DTabidzec0b4d8f2024-03-22 17:25:10 +0400323 parentGroups, err := s.GetAllTransitiveGroupsForGroup(parent)
324 if err != nil {
325 return err
326 }
327 for _, group := range parentGroups {
328 if group.Name == child {
329 return fmt.Errorf("circular reference detected: group %s is already a parent of group %s", child, parent)
330 }
331 }
DTabidze0d802592024-03-19 17:42:45 +0400332 tx, err := s.db.Begin()
333 if err != nil {
334 return err
335 }
336 defer tx.Rollback()
337 existsInGroupToGroup, err := s.parentChildGroupPairExists(tx, parent, child)
338 if err != nil {
339 return err
340 }
341 if existsInGroupToGroup {
342 return fmt.Errorf("child group name %s already exists in group %s", child, parent)
343 }
344 if _, err := tx.Exec(`INSERT INTO group_to_group (parent_group, child_group) VALUES (?, ?)`, parent, child); err != nil {
345 return err
346 }
347 if err := tx.Commit(); err != nil {
348 return err
349 }
350 return nil
351}
352
353func (s *SQLiteStore) GetAvailableGroupsAsChild(group string) ([]string, error) {
354 // TODO(dtabidze): Might have to add further logic to filter available groups as children.
355 query := `
356 SELECT name FROM groups
357 WHERE name != ? AND name NOT IN (
358 SELECT child_group FROM group_to_group WHERE parent_group = ?
DTabidze908bb852024-03-25 20:07:57 +0400359 )`
DTabidze0d802592024-03-19 17:42:45 +0400360 rows, err := s.db.Query(query, group, group)
361 if err != nil {
362 return nil, err
363 }
364 defer rows.Close()
365 var availableGroups []string
366 for rows.Next() {
367 var groupName string
368 if err := rows.Scan(&groupName); err != nil {
369 return nil, err
370 }
371 availableGroups = append(availableGroups, groupName)
372 }
373 return availableGroups, nil
374}
375
DTabidzec0b4d8f2024-03-22 17:25:10 +0400376func (s *SQLiteStore) GetAllTransitiveGroupsForUser(user string) ([]Group, error) {
377 if groups, err := s.GetGroupsUserBelongsTo(user); err != nil {
DTabidzed7744a62024-03-20 14:09:15 +0400378 return nil, err
DTabidzec0b4d8f2024-03-22 17:25:10 +0400379 } else {
380 visited := map[string]struct{}{}
381 return s.getAllParentGroupsRecursive(groups, visited)
DTabidzed7744a62024-03-20 14:09:15 +0400382 }
DTabidzec0b4d8f2024-03-22 17:25:10 +0400383}
384
385func (s *SQLiteStore) GetAllTransitiveGroupsForGroup(group string) ([]Group, error) {
386 if p, err := s.GetGroupsGroupBelongsTo(group); err != nil {
387 return nil, err
388 } else {
389 // Mark initial group as visited
390 visited := map[string]struct{}{
391 group: struct{}{},
392 }
393 return s.getAllParentGroupsRecursive(p, visited)
394 }
395}
396
397func (s *SQLiteStore) getAllParentGroupsRecursive(groups []Group, visited map[string]struct{}) ([]Group, error) {
398 var ret []Group
399 for _, g := range groups {
400 if _, ok := visited[g.Name]; ok {
401 continue
402 }
403 visited[g.Name] = struct{}{}
404 ret = append(ret, g)
405 if p, err := s.GetGroupsGroupBelongsTo(g.Name); err != nil {
DTabidzed7744a62024-03-20 14:09:15 +0400406 return nil, err
DTabidzec0b4d8f2024-03-22 17:25:10 +0400407 } else if res, err := s.getAllParentGroupsRecursive(p, visited); err != nil {
408 return nil, err
409 } else {
410 ret = append(ret, res...)
DTabidzed7744a62024-03-20 14:09:15 +0400411 }
412 }
DTabidzec0b4d8f2024-03-22 17:25:10 +0400413 return ret, nil
DTabidzed7744a62024-03-20 14:09:15 +0400414}
415
DTabidzec0b4d8f2024-03-22 17:25:10 +0400416func (s *SQLiteStore) GetGroupsGroupBelongsTo(group string) ([]Group, error) {
417 query := `
418 SELECT groups.name, groups.description
419 FROM groups
420 JOIN group_to_group ON groups.name = group_to_group.parent_group
421 WHERE group_to_group.child_group = ?`
DTabidzed7744a62024-03-20 14:09:15 +0400422 rows, err := s.db.Query(query, group)
423 if err != nil {
424 return nil, err
425 }
426 defer rows.Close()
DTabidzec0b4d8f2024-03-22 17:25:10 +0400427 var parentGroups []Group
DTabidzed7744a62024-03-20 14:09:15 +0400428 for rows.Next() {
DTabidzec0b4d8f2024-03-22 17:25:10 +0400429 var parentGroup Group
430 if err := rows.Scan(&parentGroup.Name, &parentGroup.Description); err != nil {
DTabidzed7744a62024-03-20 14:09:15 +0400431 return nil, err
432 }
433 parentGroups = append(parentGroups, parentGroup)
434 }
435 if err := rows.Err(); err != nil {
436 return nil, err
437 }
438 return parentGroups, nil
439}
440
DTabidzec0b4d8f2024-03-22 17:25:10 +0400441func (s *SQLiteStore) GetDirectChildrenGroups(group string) ([]Group, error) {
442 query := `
443 SELECT groups.name, groups.description
444 FROM groups
445 JOIN group_to_group ON groups.name = group_to_group.child_group
446 WHERE group_to_group.parent_group = ?`
447 rows, err := s.db.Query(query, group)
448 if err != nil {
449 return nil, err
450 }
451 defer rows.Close()
452 var childrenGroups []Group
453 for rows.Next() {
454 var childGroup Group
455 if err := rows.Scan(&childGroup.Name, &childGroup.Description); err != nil {
456 return nil, err
457 }
458 childrenGroups = append(childrenGroups, childGroup)
459 }
460 if err := rows.Err(); err != nil {
461 return nil, err
462 }
463 return childrenGroups, nil
464}
465
DTabidze2b224bf2024-03-27 13:25:49 +0400466func (s *SQLiteStore) RemoveFromGroupToGroup(parent, child string) error {
467 query := `DELETE FROM group_to_group WHERE parent_group = ? AND child_group = ?`
468 rowDeleted, err := s.db.Exec(query, parent, child)
469 if err != nil {
470 return err
471 }
472 rowDeletedNumber, err := rowDeleted.RowsAffected()
473 if err != nil {
474 return err
475 }
476 if rowDeletedNumber == 0 {
477 return fmt.Errorf("pair of parent '%s' and child '%s' groups not found", parent, child)
478 }
479 return nil
480}
481
482func (s *SQLiteStore) RemoveUserFromTable(username, groupName, tableName string) error {
483 if tableName == "owners" {
484 owners, err := s.GetGroupOwners(groupName)
485 if err != nil {
486 return err
487 }
488 if len(owners) == 1 {
489 return fmt.Errorf("cannot remove the last owner of the group")
490 }
491 }
492 query := fmt.Sprintf("DELETE FROM %s WHERE username = ? AND group_name = ?", tableName)
493 rowDeleted, err := s.db.Exec(query, username, groupName)
494 if err != nil {
495 return err
496 }
497 rowDeletedNumber, err := rowDeleted.RowsAffected()
498 if err != nil {
499 return err
500 }
501 if rowDeletedNumber == 0 {
502 return fmt.Errorf("pair of group '%s' and user '%s' not found", groupName, username)
503 }
504 return nil
505}
506
DTabidze0d802592024-03-19 17:42:45 +0400507func getLoggedInUser(r *http.Request) (string, error) {
DTabidzec0b4d8f2024-03-22 17:25:10 +0400508 if user := r.Header.Get("X-User"); user != "" {
509 return user, nil
510 } else {
511 return "", fmt.Errorf("unauthenticated")
512 }
DTabidze0d802592024-03-19 17:42:45 +0400513}
514
515type Status int
516
517const (
518 Owner Status = iota
519 Member
520)
521
Giorgi Lekveishvili329af572024-03-25 20:14:41 +0400522func (s *Server) Start() error {
523 e := make(chan error)
524 go func() {
525 r := mux.NewRouter()
526 r.PathPrefix("/static/").Handler(http.FileServer(http.FS(staticResources)))
DTabidze078385f2024-03-27 14:49:05 +0400527 r.HandleFunc("/group/{group-name}/add-user/", s.addUserToGroupHandler)
528 r.HandleFunc("/group/{parent-group}/add-child-group", s.addChildGroupHandler)
529 r.HandleFunc("/group/{parent-group}/remove-child-group/{child-group}", s.removeChildGroupHandler)
530 r.HandleFunc("/group/{group-name}/remove-owner/{username}", s.removeOwnerFromGroupHandler)
531 r.HandleFunc("/group/{group-name}/remove-member/{username}", s.removeMemberFromGroupHandler)
Giorgi Lekveishvili329af572024-03-25 20:14:41 +0400532 r.HandleFunc("/group/{group-name}", s.groupHandler)
DTabidze5d735e32024-03-26 16:01:06 +0400533 r.HandleFunc("/user/{username}", s.userHandler)
Giorgi Lekveishvili329af572024-03-25 20:14:41 +0400534 r.HandleFunc("/create-group", s.createGroupHandler)
Giorgi Lekveishvili329af572024-03-25 20:14:41 +0400535 r.HandleFunc("/", s.homePageHandler)
536 e <- http.ListenAndServe(fmt.Sprintf(":%d", *port), r)
537 }()
538 go func() {
539 r := mux.NewRouter()
540 r.HandleFunc("/api/init", s.apiInitHandler)
541 r.HandleFunc("/api/user/{username}", s.apiMemberOfHandler)
542 e <- http.ListenAndServe(fmt.Sprintf(":%d", *apiPort), r)
543 }()
544 return <-e
DTabidze0d802592024-03-19 17:42:45 +0400545}
546
547type GroupData struct {
548 Group Group
549 Membership string
550}
551
552func (s *Server) checkIsOwner(w http.ResponseWriter, user, group string) (bool, error) {
553 isOwner, err := s.store.IsGroupOwner(user, group)
554 if err != nil {
555 http.Error(w, err.Error(), http.StatusInternalServerError)
556 return false, err
557 }
558 if !isOwner {
DTabidze2b224bf2024-03-27 13:25:49 +0400559 return false, fmt.Errorf("you are not the owner of the group %s", group)
DTabidze0d802592024-03-19 17:42:45 +0400560 }
561 return true, nil
562}
563
DTabidze4b44ff42024-04-02 03:16:26 +0400564type templates struct {
565 group *template.Template
566 user *template.Template
567}
568
569func parseTemplates(fs embed.FS) (templates, error) {
570 base, err := template.ParseFS(fs, "memberships-tmpl/base.html")
571 if err != nil {
572 return templates{}, err
573 }
574 parse := func(path string) (*template.Template, error) {
575 if b, err := base.Clone(); err != nil {
576 return nil, err
577 } else {
578 return b.ParseFS(fs, path)
579 }
580 }
581 user, err := parse("memberships-tmpl/user.html")
582 if err != nil {
583 return templates{}, err
584 }
585 group, err := parse("memberships-tmpl/group.html")
586 if err != nil {
587 return templates{}, err
588 }
589 return templates{group, user}, nil
590}
591
DTabidze0d802592024-03-19 17:42:45 +0400592func (s *Server) homePageHandler(w http.ResponseWriter, r *http.Request) {
593 loggedInUser, err := getLoggedInUser(r)
594 if err != nil {
595 http.Error(w, "User Not Logged In", http.StatusUnauthorized)
596 return
597 }
DTabidze5d735e32024-03-26 16:01:06 +0400598 http.Redirect(w, r, "/user/"+loggedInUser, http.StatusSeeOther)
599}
600
601func (s *Server) userHandler(w http.ResponseWriter, r *http.Request) {
602 loggedInUser, err := getLoggedInUser(r)
603 if err != nil {
604 http.Error(w, "User Not Logged In", http.StatusUnauthorized)
605 return
606 }
DTabidze4b44ff42024-04-02 03:16:26 +0400607 errorMsg := r.URL.Query().Get("errorMessage")
DTabidze5d735e32024-03-26 16:01:06 +0400608 vars := mux.Vars(r)
609 user := strings.ToLower(vars["username"])
610 // TODO(dtabidze): should check if username exists or not.
611 loggedInUserPage := loggedInUser == user
612 ownerGroups, err := s.store.GetGroupsOwnedBy(user)
DTabidze0d802592024-03-19 17:42:45 +0400613 if err != nil {
614 http.Error(w, err.Error(), http.StatusInternalServerError)
615 return
616 }
DTabidze5d735e32024-03-26 16:01:06 +0400617 membershipGroups, err := s.store.GetGroupsUserBelongsTo(user)
DTabidze0d802592024-03-19 17:42:45 +0400618 if err != nil {
619 http.Error(w, err.Error(), http.StatusInternalServerError)
620 return
621 }
DTabidze5d735e32024-03-26 16:01:06 +0400622 transitiveGroups, err := s.store.GetAllTransitiveGroupsForUser(user)
DTabidzec0b4d8f2024-03-22 17:25:10 +0400623 if err != nil {
624 http.Error(w, err.Error(), http.StatusInternalServerError)
625 return
626 }
DTabidze0d802592024-03-19 17:42:45 +0400627 data := struct {
628 OwnerGroups []Group
629 MembershipGroups []Group
DTabidzec0b4d8f2024-03-22 17:25:10 +0400630 TransitiveGroups []Group
DTabidze5d735e32024-03-26 16:01:06 +0400631 LoggedInUserPage bool
632 CurrentUser string
DTabidze4b44ff42024-04-02 03:16:26 +0400633 ErrorMessage string
634 AdditionalCSS bool
DTabidze0d802592024-03-19 17:42:45 +0400635 }{
636 OwnerGroups: ownerGroups,
637 MembershipGroups: membershipGroups,
DTabidzec0b4d8f2024-03-22 17:25:10 +0400638 TransitiveGroups: transitiveGroups,
DTabidze5d735e32024-03-26 16:01:06 +0400639 LoggedInUserPage: loggedInUserPage,
640 CurrentUser: user,
DTabidze4b44ff42024-04-02 03:16:26 +0400641 ErrorMessage: errorMsg,
642 AdditionalCSS: false,
DTabidze0d802592024-03-19 17:42:45 +0400643 }
DTabidze4b44ff42024-04-02 03:16:26 +0400644 templates, err := parseTemplates(tmpls)
645 if err != nil {
646 http.Error(w, err.Error(), http.StatusInternalServerError)
647 return
648 }
649 if err := templates.user.Execute(w, data); err != nil {
DTabidze0d802592024-03-19 17:42:45 +0400650 http.Error(w, err.Error(), http.StatusInternalServerError)
651 return
652 }
653}
654
655func (s *Server) createGroupHandler(w http.ResponseWriter, r *http.Request) {
656 loggedInUser, err := getLoggedInUser(r)
657 if err != nil {
658 http.Error(w, "User Not Logged In", http.StatusUnauthorized)
659 return
660 }
661 if r.Method != http.MethodPost {
662 http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
663 return
664 }
665 if err := r.ParseForm(); err != nil {
666 http.Error(w, err.Error(), http.StatusInternalServerError)
667 return
668 }
669 var group Group
670 group.Name = r.PostFormValue("group-name")
DTabidze908bb852024-03-25 20:07:57 +0400671 if err := isValidGroupName(group.Name); err != nil {
DTabidze4b44ff42024-04-02 03:16:26 +0400672 // http.Error(w, err.Error(), http.StatusBadRequest)
673 redirectURL := fmt.Sprintf("/user/%s?errorMessage=%s", loggedInUser, url.QueryEscape(err.Error()))
674 http.Redirect(w, r, redirectURL, http.StatusFound)
DTabidze908bb852024-03-25 20:07:57 +0400675 return
676 }
DTabidze0d802592024-03-19 17:42:45 +0400677 group.Description = r.PostFormValue("description")
678 if err := s.store.CreateGroup(loggedInUser, group); err != nil {
DTabidze4b44ff42024-04-02 03:16:26 +0400679 // http.Error(w, err.Error(), http.StatusInternalServerError)
680 redirectURL := fmt.Sprintf("/user/%s?errorMessage=%s", loggedInUser, url.QueryEscape(err.Error()))
681 http.Redirect(w, r, redirectURL, http.StatusFound)
DTabidze0d802592024-03-19 17:42:45 +0400682 return
683 }
684 http.Redirect(w, r, "/", http.StatusSeeOther)
685}
686
687func (s *Server) groupHandler(w http.ResponseWriter, r *http.Request) {
DTabidzec0b4d8f2024-03-22 17:25:10 +0400688 _, err := getLoggedInUser(r)
689 if err != nil {
690 http.Error(w, "User Not Logged In", http.StatusUnauthorized)
691 return
692 }
DTabidze4b44ff42024-04-02 03:16:26 +0400693 errorMsg := r.URL.Query().Get("errorMessage")
DTabidzed7744a62024-03-20 14:09:15 +0400694 vars := mux.Vars(r)
695 groupName := vars["group-name"]
DTabidze908bb852024-03-25 20:07:57 +0400696 exists, err := s.store.DoesGroupExist(groupName)
697 if err != nil {
698 http.Error(w, err.Error(), http.StatusInternalServerError)
699 return
700 }
701 if !exists {
DTabidze4b44ff42024-04-02 03:16:26 +0400702 errorMsg = fmt.Sprintf("group with the name '%s' not found", groupName)
DTabidze908bb852024-03-25 20:07:57 +0400703 http.Error(w, errorMsg, http.StatusNotFound)
704 return
705 }
DTabidze4b44ff42024-04-02 03:16:26 +0400706 // tmpl, err := template.New("group").Parse(groupHTML)
DTabidze0d802592024-03-19 17:42:45 +0400707 if err != nil {
708 http.Error(w, err.Error(), http.StatusInternalServerError)
709 return
710 }
711 owners, err := s.store.GetGroupOwners(groupName)
712 if err != nil {
713 http.Error(w, err.Error(), http.StatusInternalServerError)
714 return
715 }
716 members, err := s.store.GetGroupMembers(groupName)
717 if err != nil {
718 http.Error(w, err.Error(), http.StatusInternalServerError)
719 return
720 }
721 description, err := s.store.GetGroupDescription(groupName)
722 if err != nil {
723 http.Error(w, err.Error(), http.StatusInternalServerError)
724 return
725 }
726 availableGroups, err := s.store.GetAvailableGroupsAsChild(groupName)
727 if err != nil {
728 http.Error(w, err.Error(), http.StatusInternalServerError)
729 return
730 }
DTabidzec0b4d8f2024-03-22 17:25:10 +0400731 transitiveGroups, err := s.store.GetAllTransitiveGroupsForGroup(groupName)
732 if err != nil {
733 http.Error(w, err.Error(), http.StatusInternalServerError)
734 return
735 }
736 childGroups, err := s.store.GetDirectChildrenGroups(groupName)
737 if err != nil {
738 http.Error(w, err.Error(), http.StatusInternalServerError)
739 return
740 }
DTabidze0d802592024-03-19 17:42:45 +0400741 data := struct {
DTabidzec0b4d8f2024-03-22 17:25:10 +0400742 GroupName string
743 Description string
744 Owners []string
745 Members []string
746 AvailableGroups []string
747 TransitiveGroups []Group
748 ChildGroups []Group
DTabidze4b44ff42024-04-02 03:16:26 +0400749 ErrorMessage string
DTabidze0d802592024-03-19 17:42:45 +0400750 }{
DTabidzec0b4d8f2024-03-22 17:25:10 +0400751 GroupName: groupName,
752 Description: description,
753 Owners: owners,
754 Members: members,
755 AvailableGroups: availableGroups,
756 TransitiveGroups: transitiveGroups,
757 ChildGroups: childGroups,
DTabidze4b44ff42024-04-02 03:16:26 +0400758 ErrorMessage: errorMsg,
DTabidze0d802592024-03-19 17:42:45 +0400759 }
DTabidze4b44ff42024-04-02 03:16:26 +0400760 templates, err := parseTemplates(tmpls)
761 if err != nil {
762 http.Error(w, err.Error(), http.StatusInternalServerError)
763 return
764 }
765 if err := templates.group.Execute(w, data); err != nil {
DTabidze0d802592024-03-19 17:42:45 +0400766 http.Error(w, err.Error(), http.StatusInternalServerError)
767 return
768 }
769}
770
DTabidze2b224bf2024-03-27 13:25:49 +0400771func (s *Server) removeChildGroupHandler(w http.ResponseWriter, r *http.Request) {
772 loggedInUser, err := getLoggedInUser(r)
773 if err != nil {
774 http.Error(w, "User Not Logged In", http.StatusUnauthorized)
775 return
776 }
777 if r.Method == http.MethodPost {
778 vars := mux.Vars(r)
779 parentGroup := vars["parent-group"]
780 childGroup := vars["child-group"]
781 if err := isValidGroupName(parentGroup); err != nil {
782 http.Error(w, err.Error(), http.StatusBadRequest)
783 return
784 }
785 if err := isValidGroupName(childGroup); err != nil {
786 http.Error(w, err.Error(), http.StatusBadRequest)
787 return
788 }
789 if _, err := s.checkIsOwner(w, loggedInUser, parentGroup); err != nil {
790 http.Error(w, err.Error(), http.StatusUnauthorized)
791 return
792 }
793 err := s.store.RemoveFromGroupToGroup(parentGroup, childGroup)
794 if err != nil {
DTabidze4b44ff42024-04-02 03:16:26 +0400795 redirectURL := fmt.Sprintf("/group/%s?errorMessage=%s", parentGroup, url.QueryEscape(err.Error()))
796 http.Redirect(w, r, redirectURL, http.StatusFound)
DTabidze2b224bf2024-03-27 13:25:49 +0400797 return
798 }
799 http.Redirect(w, r, "/group/"+parentGroup, http.StatusSeeOther)
800 }
801}
802
DTabidze078385f2024-03-27 14:49:05 +0400803func (s *Server) removeOwnerFromGroupHandler(w http.ResponseWriter, r *http.Request) {
DTabidze2b224bf2024-03-27 13:25:49 +0400804 loggedInUser, err := getLoggedInUser(r)
805 if err != nil {
806 http.Error(w, "User Not Logged In", http.StatusUnauthorized)
807 return
808 }
809 if r.Method == http.MethodPost {
810 vars := mux.Vars(r)
811 username := vars["username"]
812 groupName := vars["group-name"]
DTabidze078385f2024-03-27 14:49:05 +0400813 tableName := "owners"
DTabidze2b224bf2024-03-27 13:25:49 +0400814 if err := isValidGroupName(groupName); err != nil {
815 http.Error(w, err.Error(), http.StatusBadRequest)
816 return
817 }
818 if _, err := s.checkIsOwner(w, loggedInUser, groupName); err != nil {
819 http.Error(w, err.Error(), http.StatusUnauthorized)
820 return
821 }
822 err := s.store.RemoveUserFromTable(username, groupName, tableName)
823 if err != nil {
DTabidze4b44ff42024-04-02 03:16:26 +0400824 redirectURL := fmt.Sprintf("/group/%s?errorMessage=%s", groupName, url.QueryEscape(err.Error()))
825 http.Redirect(w, r, redirectURL, http.StatusFound)
DTabidze2b224bf2024-03-27 13:25:49 +0400826 return
827 }
828 http.Redirect(w, r, "/group/"+groupName, http.StatusSeeOther)
829 }
830}
831
DTabidze078385f2024-03-27 14:49:05 +0400832func (s *Server) removeMemberFromGroupHandler(w http.ResponseWriter, r *http.Request) {
833 loggedInUser, err := getLoggedInUser(r)
834 if err != nil {
835 http.Error(w, "User Not Logged In", http.StatusUnauthorized)
836 return
837 }
838 if r.Method == http.MethodPost {
839 vars := mux.Vars(r)
840 username := vars["username"]
841 groupName := vars["group-name"]
842 tableName := "user_to_group"
843 if err := isValidGroupName(groupName); err != nil {
844 http.Error(w, err.Error(), http.StatusBadRequest)
845 return
846 }
847 if _, err := s.checkIsOwner(w, loggedInUser, groupName); err != nil {
848 http.Error(w, err.Error(), http.StatusUnauthorized)
849 return
850 }
851 err := s.store.RemoveUserFromTable(username, groupName, tableName)
852 if err != nil {
DTabidze4b44ff42024-04-02 03:16:26 +0400853 redirectURL := fmt.Sprintf("/group/%s?errorMessage=%s", groupName, url.QueryEscape(err.Error()))
854 http.Redirect(w, r, redirectURL, http.StatusFound)
DTabidze078385f2024-03-27 14:49:05 +0400855 return
856 }
857 http.Redirect(w, r, "/group/"+groupName, http.StatusSeeOther)
858 }
859}
860
861func (s *Server) addUserToGroupHandler(w http.ResponseWriter, r *http.Request) {
DTabidze0d802592024-03-19 17:42:45 +0400862 if r.Method != http.MethodPost {
863 http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
864 return
865 }
866 loggedInUser, err := getLoggedInUser(r)
867 if err != nil {
868 http.Error(w, "User Not Logged In", http.StatusUnauthorized)
869 return
870 }
DTabidze078385f2024-03-27 14:49:05 +0400871 vars := mux.Vars(r)
872 groupName := vars["group-name"]
DTabidze908bb852024-03-25 20:07:57 +0400873 if err := isValidGroupName(groupName); err != nil {
874 http.Error(w, err.Error(), http.StatusBadRequest)
875 return
876 }
877 username := strings.ToLower(r.FormValue("username"))
878 if username == "" {
879 http.Error(w, "Username parameter is required", http.StatusBadRequest)
880 return
881 }
DTabidze0d802592024-03-19 17:42:45 +0400882 status, err := convertStatus(r.FormValue("status"))
883 if err != nil {
884 http.Error(w, err.Error(), http.StatusBadRequest)
885 return
886 }
887 if _, err := s.checkIsOwner(w, loggedInUser, groupName); err != nil {
DTabidze2b224bf2024-03-27 13:25:49 +0400888 http.Error(w, err.Error(), http.StatusUnauthorized)
DTabidze0d802592024-03-19 17:42:45 +0400889 return
890 }
891 switch status {
892 case Owner:
893 err = s.store.AddGroupOwner(username, groupName)
894 case Member:
895 err = s.store.AddGroupMember(username, groupName)
896 default:
897 http.Error(w, "Invalid status", http.StatusBadRequest)
898 return
899 }
900 if err != nil {
DTabidze4b44ff42024-04-02 03:16:26 +0400901 redirectURL := fmt.Sprintf("/group/%s?errorMessage=%s", groupName, url.QueryEscape(err.Error()))
902 http.Redirect(w, r, redirectURL, http.StatusFound)
DTabidze0d802592024-03-19 17:42:45 +0400903 return
904 }
905 http.Redirect(w, r, "/group/"+groupName, http.StatusSeeOther)
906}
907
908func (s *Server) addChildGroupHandler(w http.ResponseWriter, r *http.Request) {
909 // TODO(dtabidze): In future we might need to make one group OWNER of another and not just a member.
910 if r.Method != http.MethodPost {
911 http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
912 return
913 }
914 loggedInUser, err := getLoggedInUser(r)
915 if err != nil {
916 http.Error(w, "User Not Logged In", http.StatusUnauthorized)
917 return
918 }
DTabidze078385f2024-03-27 14:49:05 +0400919 vars := mux.Vars(r)
920 parentGroup := vars["parent-group"]
DTabidze908bb852024-03-25 20:07:57 +0400921 if err := isValidGroupName(parentGroup); err != nil {
922 http.Error(w, err.Error(), http.StatusBadRequest)
923 return
924 }
DTabidze0d802592024-03-19 17:42:45 +0400925 childGroup := r.FormValue("child-group")
DTabidze908bb852024-03-25 20:07:57 +0400926 if err := isValidGroupName(childGroup); err != nil {
927 http.Error(w, err.Error(), http.StatusBadRequest)
928 return
929 }
DTabidze0d802592024-03-19 17:42:45 +0400930 if _, err := s.checkIsOwner(w, loggedInUser, parentGroup); err != nil {
DTabidze2b224bf2024-03-27 13:25:49 +0400931 http.Error(w, err.Error(), http.StatusUnauthorized)
DTabidze0d802592024-03-19 17:42:45 +0400932 return
933 }
934 if err := s.store.AddChildGroup(parentGroup, childGroup); err != nil {
DTabidze4b44ff42024-04-02 03:16:26 +0400935 redirectURL := fmt.Sprintf("/group/%s?errorMessage=%s", parentGroup, url.QueryEscape(err.Error()))
936 http.Redirect(w, r, redirectURL, http.StatusFound)
DTabidze0d802592024-03-19 17:42:45 +0400937 return
938 }
939 http.Redirect(w, r, "/group/"+parentGroup, http.StatusSeeOther)
940}
941
Giorgi Lekveishvili942c7612024-03-22 19:27:48 +0400942type initRequest struct {
943 Owner string `json:"owner"`
944 Groups []string `json:"groups"`
945}
946
947func (s *Server) apiInitHandler(w http.ResponseWriter, r *http.Request) {
948 var req initRequest
949 if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
950 http.Error(w, err.Error(), http.StatusBadRequest)
951 return
952 }
953 if err := s.store.Init(req.Owner, req.Groups); err != nil {
954 http.Error(w, err.Error(), http.StatusInternalServerError)
955 return
956 }
957}
958
959type userInfo struct {
DTabidzed7744a62024-03-20 14:09:15 +0400960 MemberOf []string `json:"memberOf"`
961}
962
963func (s *Server) apiMemberOfHandler(w http.ResponseWriter, r *http.Request) {
964 vars := mux.Vars(r)
965 user, ok := vars["username"]
DTabidze908bb852024-03-25 20:07:57 +0400966 if !ok || user == "" {
DTabidzed7744a62024-03-20 14:09:15 +0400967 http.Error(w, "Username parameter is required", http.StatusBadRequest)
968 return
969 }
DTabidze908bb852024-03-25 20:07:57 +0400970 user = strings.ToLower(user)
DTabidzed7744a62024-03-20 14:09:15 +0400971 transitiveGroups, err := s.store.GetAllTransitiveGroupsForUser(user)
972 if err != nil {
973 http.Error(w, err.Error(), http.StatusInternalServerError)
974 return
975 }
DTabidzec0b4d8f2024-03-22 17:25:10 +0400976 var groupNames []string
977 for _, group := range transitiveGroups {
978 groupNames = append(groupNames, group.Name)
979 }
DTabidzed7744a62024-03-20 14:09:15 +0400980 w.Header().Set("Content-Type", "application/json")
Giorgi Lekveishvili942c7612024-03-22 19:27:48 +0400981 if err := json.NewEncoder(w).Encode(userInfo{groupNames}); err != nil {
DTabidzed7744a62024-03-20 14:09:15 +0400982 http.Error(w, err.Error(), http.StatusInternalServerError)
983 return
984 }
985}
986
DTabidze908bb852024-03-25 20:07:57 +0400987func convertStatus(status string) (Status, error) {
988 switch status {
989 case "Owner":
990 return Owner, nil
991 case "Member":
992 return Member, nil
993 default:
994 return Owner, fmt.Errorf("invalid status: %s", status)
995 }
996}
997
998func isValidGroupName(group string) error {
999 if strings.TrimSpace(group) == "" {
1000 return fmt.Errorf("group name can't be empty or contain only whitespaces")
1001 }
1002 validGroupName := regexp.MustCompile(`^[a-z0-9\-_:.\/ ]+$`)
1003 if !validGroupName.MatchString(group) {
1004 return fmt.Errorf("group name should contain only lowercase letters, digits, -, _, :, ., /")
1005 }
1006 return nil
1007}
1008
DTabidze0d802592024-03-19 17:42:45 +04001009func main() {
1010 flag.Parse()
DTabidzec0b4d8f2024-03-22 17:25:10 +04001011 db, err := sql.Open("sqlite3", *dbPath)
DTabidze0d802592024-03-19 17:42:45 +04001012 if err != nil {
1013 panic(err)
1014 }
DTabidzec0b4d8f2024-03-22 17:25:10 +04001015 store, err := NewSQLiteStore(db)
1016 if err != nil {
1017 panic(err)
1018 }
1019 s := Server{store}
Giorgi Lekveishvili329af572024-03-25 20:14:41 +04001020 log.Fatal(s.Start())
DTabidze0d802592024-03-19 17:42:45 +04001021}