blob: a645777d88222aa43c80404be61e58af4e9457c7 [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)
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
DTabidze0d802592024-03-19 17:42:45 +0400466func getLoggedInUser(r *http.Request) (string, error) {
DTabidzec0b4d8f2024-03-22 17:25:10 +0400467 if user := r.Header.Get("X-User"); user != "" {
468 return user, nil
469 } else {
470 return "", fmt.Errorf("unauthenticated")
471 }
DTabidze0d802592024-03-19 17:42:45 +0400472}
473
474type Status int
475
476const (
477 Owner Status = iota
478 Member
479)
480
Giorgi Lekveishvili329af572024-03-25 20:14:41 +0400481func (s *Server) Start() error {
482 e := make(chan error)
483 go func() {
484 r := mux.NewRouter()
485 r.PathPrefix("/static/").Handler(http.FileServer(http.FS(staticResources)))
486 r.HandleFunc("/group/{group-name}", s.groupHandler)
DTabidze5d735e32024-03-26 16:01:06 +0400487 r.HandleFunc("/user/{username}", s.userHandler)
Giorgi Lekveishvili329af572024-03-25 20:14:41 +0400488 r.HandleFunc("/create-group", s.createGroupHandler)
489 r.HandleFunc("/add-user", s.addUserHandler)
490 r.HandleFunc("/add-child-group", s.addChildGroupHandler)
491 r.HandleFunc("/", s.homePageHandler)
492 e <- http.ListenAndServe(fmt.Sprintf(":%d", *port), r)
493 }()
494 go func() {
495 r := mux.NewRouter()
496 r.HandleFunc("/api/init", s.apiInitHandler)
497 r.HandleFunc("/api/user/{username}", s.apiMemberOfHandler)
498 e <- http.ListenAndServe(fmt.Sprintf(":%d", *apiPort), r)
499 }()
500 return <-e
DTabidze0d802592024-03-19 17:42:45 +0400501}
502
503type GroupData struct {
504 Group Group
505 Membership string
506}
507
508func (s *Server) checkIsOwner(w http.ResponseWriter, user, group string) (bool, error) {
509 isOwner, err := s.store.IsGroupOwner(user, group)
510 if err != nil {
511 http.Error(w, err.Error(), http.StatusInternalServerError)
512 return false, err
513 }
514 if !isOwner {
515 http.Error(w, fmt.Sprintf("You are not the owner of the group %s", group), http.StatusUnauthorized)
516 return false, nil
517 }
518 return true, nil
519}
520
521func (s *Server) homePageHandler(w http.ResponseWriter, r *http.Request) {
522 loggedInUser, err := getLoggedInUser(r)
523 if err != nil {
524 http.Error(w, "User Not Logged In", http.StatusUnauthorized)
525 return
526 }
DTabidze5d735e32024-03-26 16:01:06 +0400527 http.Redirect(w, r, "/user/"+loggedInUser, http.StatusSeeOther)
528}
529
530func (s *Server) userHandler(w http.ResponseWriter, r *http.Request) {
531 loggedInUser, err := getLoggedInUser(r)
532 if err != nil {
533 http.Error(w, "User Not Logged In", http.StatusUnauthorized)
534 return
535 }
536 vars := mux.Vars(r)
537 user := strings.ToLower(vars["username"])
538 // TODO(dtabidze): should check if username exists or not.
539 loggedInUserPage := loggedInUser == user
540 ownerGroups, err := s.store.GetGroupsOwnedBy(user)
DTabidze0d802592024-03-19 17:42:45 +0400541 if err != nil {
542 http.Error(w, err.Error(), http.StatusInternalServerError)
543 return
544 }
DTabidze5d735e32024-03-26 16:01:06 +0400545 membershipGroups, err := s.store.GetGroupsUserBelongsTo(user)
DTabidze0d802592024-03-19 17:42:45 +0400546 if err != nil {
547 http.Error(w, err.Error(), http.StatusInternalServerError)
548 return
549 }
550 tmpl, err := template.New("index").Parse(indexHTML)
551 if err != nil {
552 http.Error(w, err.Error(), http.StatusInternalServerError)
553 return
554 }
DTabidze5d735e32024-03-26 16:01:06 +0400555 transitiveGroups, err := s.store.GetAllTransitiveGroupsForUser(user)
DTabidzec0b4d8f2024-03-22 17:25:10 +0400556 if err != nil {
557 http.Error(w, err.Error(), http.StatusInternalServerError)
558 return
559 }
DTabidze0d802592024-03-19 17:42:45 +0400560 data := struct {
561 OwnerGroups []Group
562 MembershipGroups []Group
DTabidzec0b4d8f2024-03-22 17:25:10 +0400563 TransitiveGroups []Group
DTabidze5d735e32024-03-26 16:01:06 +0400564 LoggedInUserPage bool
565 CurrentUser string
DTabidze0d802592024-03-19 17:42:45 +0400566 }{
567 OwnerGroups: ownerGroups,
568 MembershipGroups: membershipGroups,
DTabidzec0b4d8f2024-03-22 17:25:10 +0400569 TransitiveGroups: transitiveGroups,
DTabidze5d735e32024-03-26 16:01:06 +0400570 LoggedInUserPage: loggedInUserPage,
571 CurrentUser: user,
DTabidze0d802592024-03-19 17:42:45 +0400572 }
573 w.Header().Set("Content-Type", "text/html")
574 if err := tmpl.Execute(w, data); err != nil {
575 http.Error(w, err.Error(), http.StatusInternalServerError)
576 return
577 }
578}
579
580func (s *Server) createGroupHandler(w http.ResponseWriter, r *http.Request) {
581 loggedInUser, err := getLoggedInUser(r)
582 if err != nil {
583 http.Error(w, "User Not Logged In", http.StatusUnauthorized)
584 return
585 }
586 if r.Method != http.MethodPost {
587 http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
588 return
589 }
590 if err := r.ParseForm(); err != nil {
591 http.Error(w, err.Error(), http.StatusInternalServerError)
592 return
593 }
594 var group Group
595 group.Name = r.PostFormValue("group-name")
DTabidze908bb852024-03-25 20:07:57 +0400596 if err := isValidGroupName(group.Name); err != nil {
597 http.Error(w, err.Error(), http.StatusBadRequest)
598 return
599 }
DTabidze0d802592024-03-19 17:42:45 +0400600 group.Description = r.PostFormValue("description")
601 if err := s.store.CreateGroup(loggedInUser, group); err != nil {
602 http.Error(w, err.Error(), http.StatusInternalServerError)
603 return
604 }
605 http.Redirect(w, r, "/", http.StatusSeeOther)
606}
607
608func (s *Server) groupHandler(w http.ResponseWriter, r *http.Request) {
DTabidzec0b4d8f2024-03-22 17:25:10 +0400609 _, err := getLoggedInUser(r)
610 if err != nil {
611 http.Error(w, "User Not Logged In", http.StatusUnauthorized)
612 return
613 }
DTabidzed7744a62024-03-20 14:09:15 +0400614 vars := mux.Vars(r)
615 groupName := vars["group-name"]
DTabidze908bb852024-03-25 20:07:57 +0400616 exists, err := s.store.DoesGroupExist(groupName)
617 if err != nil {
618 http.Error(w, err.Error(), http.StatusInternalServerError)
619 return
620 }
621 if !exists {
622 errorMsg := fmt.Sprintf("group with the name '%s' not found", groupName)
623 http.Error(w, errorMsg, http.StatusNotFound)
624 return
625 }
DTabidze0d802592024-03-19 17:42:45 +0400626 tmpl, err := template.New("group").Parse(groupHTML)
627 if err != nil {
628 http.Error(w, err.Error(), http.StatusInternalServerError)
629 return
630 }
631 owners, err := s.store.GetGroupOwners(groupName)
632 if err != nil {
633 http.Error(w, err.Error(), http.StatusInternalServerError)
634 return
635 }
636 members, err := s.store.GetGroupMembers(groupName)
637 if err != nil {
638 http.Error(w, err.Error(), http.StatusInternalServerError)
639 return
640 }
641 description, err := s.store.GetGroupDescription(groupName)
642 if err != nil {
643 http.Error(w, err.Error(), http.StatusInternalServerError)
644 return
645 }
646 availableGroups, err := s.store.GetAvailableGroupsAsChild(groupName)
647 if err != nil {
648 http.Error(w, err.Error(), http.StatusInternalServerError)
649 return
650 }
DTabidzec0b4d8f2024-03-22 17:25:10 +0400651 transitiveGroups, err := s.store.GetAllTransitiveGroupsForGroup(groupName)
652 if err != nil {
653 http.Error(w, err.Error(), http.StatusInternalServerError)
654 return
655 }
656 childGroups, err := s.store.GetDirectChildrenGroups(groupName)
657 if err != nil {
658 http.Error(w, err.Error(), http.StatusInternalServerError)
659 return
660 }
DTabidze0d802592024-03-19 17:42:45 +0400661 data := struct {
DTabidzec0b4d8f2024-03-22 17:25:10 +0400662 GroupName string
663 Description string
664 Owners []string
665 Members []string
666 AvailableGroups []string
667 TransitiveGroups []Group
668 ChildGroups []Group
DTabidze0d802592024-03-19 17:42:45 +0400669 }{
DTabidzec0b4d8f2024-03-22 17:25:10 +0400670 GroupName: groupName,
671 Description: description,
672 Owners: owners,
673 Members: members,
674 AvailableGroups: availableGroups,
675 TransitiveGroups: transitiveGroups,
676 ChildGroups: childGroups,
DTabidze0d802592024-03-19 17:42:45 +0400677 }
678 if err := tmpl.Execute(w, data); err != nil {
679 http.Error(w, err.Error(), http.StatusInternalServerError)
680 return
681 }
682}
683
684func (s *Server) addUserHandler(w http.ResponseWriter, r *http.Request) {
685 if r.Method != http.MethodPost {
686 http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
687 return
688 }
689 loggedInUser, err := getLoggedInUser(r)
690 if err != nil {
691 http.Error(w, "User Not Logged In", http.StatusUnauthorized)
692 return
693 }
694 groupName := r.FormValue("group")
DTabidze908bb852024-03-25 20:07:57 +0400695 if err := isValidGroupName(groupName); err != nil {
696 http.Error(w, err.Error(), http.StatusBadRequest)
697 return
698 }
699 username := strings.ToLower(r.FormValue("username"))
700 if username == "" {
701 http.Error(w, "Username parameter is required", http.StatusBadRequest)
702 return
703 }
DTabidze0d802592024-03-19 17:42:45 +0400704 status, err := convertStatus(r.FormValue("status"))
705 if err != nil {
706 http.Error(w, err.Error(), http.StatusBadRequest)
707 return
708 }
709 if _, err := s.checkIsOwner(w, loggedInUser, groupName); err != nil {
710 return
711 }
712 switch status {
713 case Owner:
714 err = s.store.AddGroupOwner(username, groupName)
715 case Member:
716 err = s.store.AddGroupMember(username, groupName)
717 default:
718 http.Error(w, "Invalid status", http.StatusBadRequest)
719 return
720 }
721 if err != nil {
722 http.Error(w, err.Error(), http.StatusInternalServerError)
723 return
724 }
725 http.Redirect(w, r, "/group/"+groupName, http.StatusSeeOther)
726}
727
728func (s *Server) addChildGroupHandler(w http.ResponseWriter, r *http.Request) {
729 // TODO(dtabidze): In future we might need to make one group OWNER of another and not just a member.
730 if r.Method != http.MethodPost {
731 http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
732 return
733 }
734 loggedInUser, err := getLoggedInUser(r)
735 if err != nil {
736 http.Error(w, "User Not Logged In", http.StatusUnauthorized)
737 return
738 }
739 parentGroup := r.FormValue("parent-group")
DTabidze908bb852024-03-25 20:07:57 +0400740 if err := isValidGroupName(parentGroup); err != nil {
741 http.Error(w, err.Error(), http.StatusBadRequest)
742 return
743 }
DTabidze0d802592024-03-19 17:42:45 +0400744 childGroup := r.FormValue("child-group")
DTabidze908bb852024-03-25 20:07:57 +0400745 if err := isValidGroupName(childGroup); err != nil {
746 http.Error(w, err.Error(), http.StatusBadRequest)
747 return
748 }
DTabidze0d802592024-03-19 17:42:45 +0400749 if _, err := s.checkIsOwner(w, loggedInUser, parentGroup); err != nil {
750 return
751 }
752 if err := s.store.AddChildGroup(parentGroup, childGroup); err != nil {
753 http.Error(w, err.Error(), http.StatusInternalServerError)
754 return
755 }
756 http.Redirect(w, r, "/group/"+parentGroup, http.StatusSeeOther)
757}
758
Giorgi Lekveishvili942c7612024-03-22 19:27:48 +0400759type initRequest struct {
760 Owner string `json:"owner"`
761 Groups []string `json:"groups"`
762}
763
764func (s *Server) apiInitHandler(w http.ResponseWriter, r *http.Request) {
765 var req initRequest
766 if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
767 http.Error(w, err.Error(), http.StatusBadRequest)
768 return
769 }
770 if err := s.store.Init(req.Owner, req.Groups); err != nil {
771 http.Error(w, err.Error(), http.StatusInternalServerError)
772 return
773 }
774}
775
776type userInfo struct {
DTabidzed7744a62024-03-20 14:09:15 +0400777 MemberOf []string `json:"memberOf"`
778}
779
780func (s *Server) apiMemberOfHandler(w http.ResponseWriter, r *http.Request) {
781 vars := mux.Vars(r)
782 user, ok := vars["username"]
DTabidze908bb852024-03-25 20:07:57 +0400783 if !ok || user == "" {
DTabidzed7744a62024-03-20 14:09:15 +0400784 http.Error(w, "Username parameter is required", http.StatusBadRequest)
785 return
786 }
DTabidze908bb852024-03-25 20:07:57 +0400787 user = strings.ToLower(user)
DTabidzed7744a62024-03-20 14:09:15 +0400788 transitiveGroups, err := s.store.GetAllTransitiveGroupsForUser(user)
789 if err != nil {
790 http.Error(w, err.Error(), http.StatusInternalServerError)
791 return
792 }
DTabidzec0b4d8f2024-03-22 17:25:10 +0400793 var groupNames []string
794 for _, group := range transitiveGroups {
795 groupNames = append(groupNames, group.Name)
796 }
DTabidzed7744a62024-03-20 14:09:15 +0400797 w.Header().Set("Content-Type", "application/json")
Giorgi Lekveishvili942c7612024-03-22 19:27:48 +0400798 if err := json.NewEncoder(w).Encode(userInfo{groupNames}); err != nil {
DTabidzed7744a62024-03-20 14:09:15 +0400799 http.Error(w, err.Error(), http.StatusInternalServerError)
800 return
801 }
802}
803
DTabidze908bb852024-03-25 20:07:57 +0400804func convertStatus(status string) (Status, error) {
805 switch status {
806 case "Owner":
807 return Owner, nil
808 case "Member":
809 return Member, nil
810 default:
811 return Owner, fmt.Errorf("invalid status: %s", status)
812 }
813}
814
815func isValidGroupName(group string) error {
816 if strings.TrimSpace(group) == "" {
817 return fmt.Errorf("group name can't be empty or contain only whitespaces")
818 }
819 validGroupName := regexp.MustCompile(`^[a-z0-9\-_:.\/ ]+$`)
820 if !validGroupName.MatchString(group) {
821 return fmt.Errorf("group name should contain only lowercase letters, digits, -, _, :, ., /")
822 }
823 return nil
824}
825
DTabidze0d802592024-03-19 17:42:45 +0400826func main() {
827 flag.Parse()
DTabidzec0b4d8f2024-03-22 17:25:10 +0400828 db, err := sql.Open("sqlite3", *dbPath)
DTabidze0d802592024-03-19 17:42:45 +0400829 if err != nil {
830 panic(err)
831 }
DTabidzec0b4d8f2024-03-22 17:25:10 +0400832 store, err := NewSQLiteStore(db)
833 if err != nil {
834 panic(err)
835 }
836 s := Server{store}
Giorgi Lekveishvili329af572024-03-25 20:14:41 +0400837 log.Fatal(s.Start())
DTabidze0d802592024-03-19 17:42:45 +0400838}