blob: 8df935651a32a29c4f71a8877ab1defe5d34f183 [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)))
DTabidze2b224bf2024-03-27 13:25:49 +0400529 r.HandleFunc("/remove-child-group/{parent-group}/{child-group}", s.removeChildGroupHandler)
530 r.HandleFunc("/remove-{action}/{group-name}/{username}", s.removeUserFromGroupHandler)
Giorgi Lekveishvili329af572024-03-25 20:14:41 +0400531 r.HandleFunc("/group/{group-name}", s.groupHandler)
DTabidze5d735e32024-03-26 16:01:06 +0400532 r.HandleFunc("/user/{username}", s.userHandler)
Giorgi Lekveishvili329af572024-03-25 20:14:41 +0400533 r.HandleFunc("/create-group", s.createGroupHandler)
534 r.HandleFunc("/add-user", s.addUserHandler)
535 r.HandleFunc("/add-child-group", s.addChildGroupHandler)
536 r.HandleFunc("/", s.homePageHandler)
537 e <- http.ListenAndServe(fmt.Sprintf(":%d", *port), r)
538 }()
539 go func() {
540 r := mux.NewRouter()
541 r.HandleFunc("/api/init", s.apiInitHandler)
542 r.HandleFunc("/api/user/{username}", s.apiMemberOfHandler)
543 e <- http.ListenAndServe(fmt.Sprintf(":%d", *apiPort), r)
544 }()
545 return <-e
DTabidze0d802592024-03-19 17:42:45 +0400546}
547
548type GroupData struct {
549 Group Group
550 Membership string
551}
552
553func (s *Server) checkIsOwner(w http.ResponseWriter, user, group string) (bool, error) {
554 isOwner, err := s.store.IsGroupOwner(user, group)
555 if err != nil {
556 http.Error(w, err.Error(), http.StatusInternalServerError)
557 return false, err
558 }
559 if !isOwner {
DTabidze2b224bf2024-03-27 13:25:49 +0400560 return false, fmt.Errorf("you are not the owner of the group %s", group)
DTabidze0d802592024-03-19 17:42:45 +0400561 }
562 return true, nil
563}
564
565func (s *Server) homePageHandler(w http.ResponseWriter, r *http.Request) {
566 loggedInUser, err := getLoggedInUser(r)
567 if err != nil {
568 http.Error(w, "User Not Logged In", http.StatusUnauthorized)
569 return
570 }
DTabidze5d735e32024-03-26 16:01:06 +0400571 http.Redirect(w, r, "/user/"+loggedInUser, http.StatusSeeOther)
572}
573
574func (s *Server) userHandler(w http.ResponseWriter, r *http.Request) {
575 loggedInUser, err := getLoggedInUser(r)
576 if err != nil {
577 http.Error(w, "User Not Logged In", http.StatusUnauthorized)
578 return
579 }
580 vars := mux.Vars(r)
581 user := strings.ToLower(vars["username"])
582 // TODO(dtabidze): should check if username exists or not.
583 loggedInUserPage := loggedInUser == user
584 ownerGroups, err := s.store.GetGroupsOwnedBy(user)
DTabidze0d802592024-03-19 17:42:45 +0400585 if err != nil {
586 http.Error(w, err.Error(), http.StatusInternalServerError)
587 return
588 }
DTabidze5d735e32024-03-26 16:01:06 +0400589 membershipGroups, err := s.store.GetGroupsUserBelongsTo(user)
DTabidze0d802592024-03-19 17:42:45 +0400590 if err != nil {
591 http.Error(w, err.Error(), http.StatusInternalServerError)
592 return
593 }
594 tmpl, err := template.New("index").Parse(indexHTML)
595 if err != nil {
596 http.Error(w, err.Error(), http.StatusInternalServerError)
597 return
598 }
DTabidze5d735e32024-03-26 16:01:06 +0400599 transitiveGroups, err := s.store.GetAllTransitiveGroupsForUser(user)
DTabidzec0b4d8f2024-03-22 17:25:10 +0400600 if err != nil {
601 http.Error(w, err.Error(), http.StatusInternalServerError)
602 return
603 }
DTabidze0d802592024-03-19 17:42:45 +0400604 data := struct {
605 OwnerGroups []Group
606 MembershipGroups []Group
DTabidzec0b4d8f2024-03-22 17:25:10 +0400607 TransitiveGroups []Group
DTabidze5d735e32024-03-26 16:01:06 +0400608 LoggedInUserPage bool
609 CurrentUser string
DTabidze0d802592024-03-19 17:42:45 +0400610 }{
611 OwnerGroups: ownerGroups,
612 MembershipGroups: membershipGroups,
DTabidzec0b4d8f2024-03-22 17:25:10 +0400613 TransitiveGroups: transitiveGroups,
DTabidze5d735e32024-03-26 16:01:06 +0400614 LoggedInUserPage: loggedInUserPage,
615 CurrentUser: user,
DTabidze0d802592024-03-19 17:42:45 +0400616 }
617 w.Header().Set("Content-Type", "text/html")
618 if err := tmpl.Execute(w, data); err != nil {
619 http.Error(w, err.Error(), http.StatusInternalServerError)
620 return
621 }
622}
623
624func (s *Server) createGroupHandler(w http.ResponseWriter, r *http.Request) {
625 loggedInUser, err := getLoggedInUser(r)
626 if err != nil {
627 http.Error(w, "User Not Logged In", http.StatusUnauthorized)
628 return
629 }
630 if r.Method != http.MethodPost {
631 http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
632 return
633 }
634 if err := r.ParseForm(); err != nil {
635 http.Error(w, err.Error(), http.StatusInternalServerError)
636 return
637 }
638 var group Group
639 group.Name = r.PostFormValue("group-name")
DTabidze908bb852024-03-25 20:07:57 +0400640 if err := isValidGroupName(group.Name); err != nil {
641 http.Error(w, err.Error(), http.StatusBadRequest)
642 return
643 }
DTabidze0d802592024-03-19 17:42:45 +0400644 group.Description = r.PostFormValue("description")
645 if err := s.store.CreateGroup(loggedInUser, group); err != nil {
646 http.Error(w, err.Error(), http.StatusInternalServerError)
647 return
648 }
649 http.Redirect(w, r, "/", http.StatusSeeOther)
650}
651
652func (s *Server) groupHandler(w http.ResponseWriter, r *http.Request) {
DTabidzec0b4d8f2024-03-22 17:25:10 +0400653 _, err := getLoggedInUser(r)
654 if err != nil {
655 http.Error(w, "User Not Logged In", http.StatusUnauthorized)
656 return
657 }
DTabidzed7744a62024-03-20 14:09:15 +0400658 vars := mux.Vars(r)
659 groupName := vars["group-name"]
DTabidze908bb852024-03-25 20:07:57 +0400660 exists, err := s.store.DoesGroupExist(groupName)
661 if err != nil {
662 http.Error(w, err.Error(), http.StatusInternalServerError)
663 return
664 }
665 if !exists {
666 errorMsg := fmt.Sprintf("group with the name '%s' not found", groupName)
667 http.Error(w, errorMsg, http.StatusNotFound)
668 return
669 }
DTabidze0d802592024-03-19 17:42:45 +0400670 tmpl, err := template.New("group").Parse(groupHTML)
671 if err != nil {
672 http.Error(w, err.Error(), http.StatusInternalServerError)
673 return
674 }
675 owners, err := s.store.GetGroupOwners(groupName)
676 if err != nil {
677 http.Error(w, err.Error(), http.StatusInternalServerError)
678 return
679 }
680 members, err := s.store.GetGroupMembers(groupName)
681 if err != nil {
682 http.Error(w, err.Error(), http.StatusInternalServerError)
683 return
684 }
685 description, err := s.store.GetGroupDescription(groupName)
686 if err != nil {
687 http.Error(w, err.Error(), http.StatusInternalServerError)
688 return
689 }
690 availableGroups, err := s.store.GetAvailableGroupsAsChild(groupName)
691 if err != nil {
692 http.Error(w, err.Error(), http.StatusInternalServerError)
693 return
694 }
DTabidzec0b4d8f2024-03-22 17:25:10 +0400695 transitiveGroups, err := s.store.GetAllTransitiveGroupsForGroup(groupName)
696 if err != nil {
697 http.Error(w, err.Error(), http.StatusInternalServerError)
698 return
699 }
700 childGroups, err := s.store.GetDirectChildrenGroups(groupName)
701 if err != nil {
702 http.Error(w, err.Error(), http.StatusInternalServerError)
703 return
704 }
DTabidze0d802592024-03-19 17:42:45 +0400705 data := struct {
DTabidzec0b4d8f2024-03-22 17:25:10 +0400706 GroupName string
707 Description string
708 Owners []string
709 Members []string
710 AvailableGroups []string
711 TransitiveGroups []Group
712 ChildGroups []Group
DTabidze0d802592024-03-19 17:42:45 +0400713 }{
DTabidzec0b4d8f2024-03-22 17:25:10 +0400714 GroupName: groupName,
715 Description: description,
716 Owners: owners,
717 Members: members,
718 AvailableGroups: availableGroups,
719 TransitiveGroups: transitiveGroups,
720 ChildGroups: childGroups,
DTabidze0d802592024-03-19 17:42:45 +0400721 }
722 if err := tmpl.Execute(w, data); err != nil {
723 http.Error(w, err.Error(), http.StatusInternalServerError)
724 return
725 }
726}
727
DTabidze2b224bf2024-03-27 13:25:49 +0400728func (s *Server) removeChildGroupHandler(w http.ResponseWriter, r *http.Request) {
729 loggedInUser, err := getLoggedInUser(r)
730 if err != nil {
731 http.Error(w, "User Not Logged In", http.StatusUnauthorized)
732 return
733 }
734 if r.Method == http.MethodPost {
735 vars := mux.Vars(r)
736 parentGroup := vars["parent-group"]
737 childGroup := vars["child-group"]
738 if err := isValidGroupName(parentGroup); err != nil {
739 http.Error(w, err.Error(), http.StatusBadRequest)
740 return
741 }
742 if err := isValidGroupName(childGroup); err != nil {
743 http.Error(w, err.Error(), http.StatusBadRequest)
744 return
745 }
746 if _, err := s.checkIsOwner(w, loggedInUser, parentGroup); err != nil {
747 http.Error(w, err.Error(), http.StatusUnauthorized)
748 return
749 }
750 err := s.store.RemoveFromGroupToGroup(parentGroup, childGroup)
751 if err != nil {
752 http.Error(w, err.Error(), http.StatusInternalServerError)
753 return
754 }
755 http.Redirect(w, r, "/group/"+parentGroup, http.StatusSeeOther)
756 }
757}
758
759func (s *Server) removeUserFromGroupHandler(w http.ResponseWriter, r *http.Request) {
760 loggedInUser, err := getLoggedInUser(r)
761 if err != nil {
762 http.Error(w, "User Not Logged In", http.StatusUnauthorized)
763 return
764 }
765 if r.Method == http.MethodPost {
766 vars := mux.Vars(r)
767 username := vars["username"]
768 groupName := vars["group-name"]
769 action := vars["action"]
770 var tableName string
771 switch action {
772 case "group-owner":
773 tableName = "owners"
774 case "group-member":
775 tableName = "user_to_group"
776 default:
777 http.Error(w, "action not found", http.StatusBadRequest)
778 return
779 }
780 if err := isValidGroupName(groupName); err != nil {
781 http.Error(w, err.Error(), http.StatusBadRequest)
782 return
783 }
784 if _, err := s.checkIsOwner(w, loggedInUser, groupName); err != nil {
785 http.Error(w, err.Error(), http.StatusUnauthorized)
786 return
787 }
788 err := s.store.RemoveUserFromTable(username, groupName, tableName)
789 if err != nil {
790 http.Error(w, err.Error(), http.StatusInternalServerError)
791 return
792 }
793 http.Redirect(w, r, "/group/"+groupName, http.StatusSeeOther)
794 }
795}
796
DTabidze0d802592024-03-19 17:42:45 +0400797func (s *Server) addUserHandler(w http.ResponseWriter, r *http.Request) {
798 if r.Method != http.MethodPost {
799 http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
800 return
801 }
802 loggedInUser, err := getLoggedInUser(r)
803 if err != nil {
804 http.Error(w, "User Not Logged In", http.StatusUnauthorized)
805 return
806 }
807 groupName := r.FormValue("group")
DTabidze908bb852024-03-25 20:07:57 +0400808 if err := isValidGroupName(groupName); err != nil {
809 http.Error(w, err.Error(), http.StatusBadRequest)
810 return
811 }
812 username := strings.ToLower(r.FormValue("username"))
813 if username == "" {
814 http.Error(w, "Username parameter is required", http.StatusBadRequest)
815 return
816 }
DTabidze0d802592024-03-19 17:42:45 +0400817 status, err := convertStatus(r.FormValue("status"))
818 if err != nil {
819 http.Error(w, err.Error(), http.StatusBadRequest)
820 return
821 }
822 if _, err := s.checkIsOwner(w, loggedInUser, groupName); err != nil {
DTabidze2b224bf2024-03-27 13:25:49 +0400823 http.Error(w, err.Error(), http.StatusUnauthorized)
DTabidze0d802592024-03-19 17:42:45 +0400824 return
825 }
826 switch status {
827 case Owner:
828 err = s.store.AddGroupOwner(username, groupName)
829 case Member:
830 err = s.store.AddGroupMember(username, groupName)
831 default:
832 http.Error(w, "Invalid status", http.StatusBadRequest)
833 return
834 }
835 if err != nil {
836 http.Error(w, err.Error(), http.StatusInternalServerError)
837 return
838 }
839 http.Redirect(w, r, "/group/"+groupName, http.StatusSeeOther)
840}
841
842func (s *Server) addChildGroupHandler(w http.ResponseWriter, r *http.Request) {
843 // TODO(dtabidze): In future we might need to make one group OWNER of another and not just a member.
844 if r.Method != http.MethodPost {
845 http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
846 return
847 }
848 loggedInUser, err := getLoggedInUser(r)
849 if err != nil {
850 http.Error(w, "User Not Logged In", http.StatusUnauthorized)
851 return
852 }
853 parentGroup := r.FormValue("parent-group")
DTabidze908bb852024-03-25 20:07:57 +0400854 if err := isValidGroupName(parentGroup); err != nil {
855 http.Error(w, err.Error(), http.StatusBadRequest)
856 return
857 }
DTabidze0d802592024-03-19 17:42:45 +0400858 childGroup := r.FormValue("child-group")
DTabidze908bb852024-03-25 20:07:57 +0400859 if err := isValidGroupName(childGroup); err != nil {
860 http.Error(w, err.Error(), http.StatusBadRequest)
861 return
862 }
DTabidze0d802592024-03-19 17:42:45 +0400863 if _, err := s.checkIsOwner(w, loggedInUser, parentGroup); err != nil {
DTabidze2b224bf2024-03-27 13:25:49 +0400864 http.Error(w, err.Error(), http.StatusUnauthorized)
DTabidze0d802592024-03-19 17:42:45 +0400865 return
866 }
867 if err := s.store.AddChildGroup(parentGroup, childGroup); err != nil {
868 http.Error(w, err.Error(), http.StatusInternalServerError)
869 return
870 }
871 http.Redirect(w, r, "/group/"+parentGroup, http.StatusSeeOther)
872}
873
Giorgi Lekveishvili942c7612024-03-22 19:27:48 +0400874type initRequest struct {
875 Owner string `json:"owner"`
876 Groups []string `json:"groups"`
877}
878
879func (s *Server) apiInitHandler(w http.ResponseWriter, r *http.Request) {
880 var req initRequest
881 if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
882 http.Error(w, err.Error(), http.StatusBadRequest)
883 return
884 }
885 if err := s.store.Init(req.Owner, req.Groups); err != nil {
886 http.Error(w, err.Error(), http.StatusInternalServerError)
887 return
888 }
889}
890
891type userInfo struct {
DTabidzed7744a62024-03-20 14:09:15 +0400892 MemberOf []string `json:"memberOf"`
893}
894
895func (s *Server) apiMemberOfHandler(w http.ResponseWriter, r *http.Request) {
896 vars := mux.Vars(r)
897 user, ok := vars["username"]
DTabidze908bb852024-03-25 20:07:57 +0400898 if !ok || user == "" {
DTabidzed7744a62024-03-20 14:09:15 +0400899 http.Error(w, "Username parameter is required", http.StatusBadRequest)
900 return
901 }
DTabidze908bb852024-03-25 20:07:57 +0400902 user = strings.ToLower(user)
DTabidzed7744a62024-03-20 14:09:15 +0400903 transitiveGroups, err := s.store.GetAllTransitiveGroupsForUser(user)
904 if err != nil {
905 http.Error(w, err.Error(), http.StatusInternalServerError)
906 return
907 }
DTabidzec0b4d8f2024-03-22 17:25:10 +0400908 var groupNames []string
909 for _, group := range transitiveGroups {
910 groupNames = append(groupNames, group.Name)
911 }
DTabidzed7744a62024-03-20 14:09:15 +0400912 w.Header().Set("Content-Type", "application/json")
Giorgi Lekveishvili942c7612024-03-22 19:27:48 +0400913 if err := json.NewEncoder(w).Encode(userInfo{groupNames}); err != nil {
DTabidzed7744a62024-03-20 14:09:15 +0400914 http.Error(w, err.Error(), http.StatusInternalServerError)
915 return
916 }
917}
918
DTabidze908bb852024-03-25 20:07:57 +0400919func convertStatus(status string) (Status, error) {
920 switch status {
921 case "Owner":
922 return Owner, nil
923 case "Member":
924 return Member, nil
925 default:
926 return Owner, fmt.Errorf("invalid status: %s", status)
927 }
928}
929
930func isValidGroupName(group string) error {
931 if strings.TrimSpace(group) == "" {
932 return fmt.Errorf("group name can't be empty or contain only whitespaces")
933 }
934 validGroupName := regexp.MustCompile(`^[a-z0-9\-_:.\/ ]+$`)
935 if !validGroupName.MatchString(group) {
936 return fmt.Errorf("group name should contain only lowercase letters, digits, -, _, :, ., /")
937 }
938 return nil
939}
940
DTabidze0d802592024-03-19 17:42:45 +0400941func main() {
942 flag.Parse()
DTabidzec0b4d8f2024-03-22 17:25:10 +0400943 db, err := sql.Open("sqlite3", *dbPath)
DTabidze0d802592024-03-19 17:42:45 +0400944 if err != nil {
945 panic(err)
946 }
DTabidzec0b4d8f2024-03-22 17:25:10 +0400947 store, err := NewSQLiteStore(db)
948 if err != nil {
949 panic(err)
950 }
951 s := Server{store}
Giorgi Lekveishvili329af572024-03-25 20:14:41 +0400952 log.Fatal(s.Start())
DTabidze0d802592024-03-19 17:42:45 +0400953}