blob: bc8f38451f307fb3439ecfbf3f13a58c1425d2ce [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)
487 r.HandleFunc("/create-group", s.createGroupHandler)
488 r.HandleFunc("/add-user", s.addUserHandler)
489 r.HandleFunc("/add-child-group", s.addChildGroupHandler)
490 r.HandleFunc("/", s.homePageHandler)
491 e <- http.ListenAndServe(fmt.Sprintf(":%d", *port), r)
492 }()
493 go func() {
494 r := mux.NewRouter()
495 r.HandleFunc("/api/init", s.apiInitHandler)
496 r.HandleFunc("/api/user/{username}", s.apiMemberOfHandler)
497 e <- http.ListenAndServe(fmt.Sprintf(":%d", *apiPort), r)
498 }()
499 return <-e
DTabidze0d802592024-03-19 17:42:45 +0400500}
501
502type GroupData struct {
503 Group Group
504 Membership string
505}
506
507func (s *Server) checkIsOwner(w http.ResponseWriter, user, group string) (bool, error) {
508 isOwner, err := s.store.IsGroupOwner(user, group)
509 if err != nil {
510 http.Error(w, err.Error(), http.StatusInternalServerError)
511 return false, err
512 }
513 if !isOwner {
514 http.Error(w, fmt.Sprintf("You are not the owner of the group %s", group), http.StatusUnauthorized)
515 return false, nil
516 }
517 return true, nil
518}
519
520func (s *Server) homePageHandler(w http.ResponseWriter, r *http.Request) {
521 loggedInUser, err := getLoggedInUser(r)
522 if err != nil {
523 http.Error(w, "User Not Logged In", http.StatusUnauthorized)
524 return
525 }
526 ownerGroups, err := s.store.GetGroupsOwnedBy(loggedInUser)
527 if err != nil {
528 http.Error(w, err.Error(), http.StatusInternalServerError)
529 return
530 }
DTabidzed7744a62024-03-20 14:09:15 +0400531 membershipGroups, err := s.store.GetGroupsUserBelongsTo(loggedInUser)
DTabidze0d802592024-03-19 17:42:45 +0400532 if err != nil {
533 http.Error(w, err.Error(), http.StatusInternalServerError)
534 return
535 }
536 tmpl, err := template.New("index").Parse(indexHTML)
537 if err != nil {
538 http.Error(w, err.Error(), http.StatusInternalServerError)
539 return
540 }
DTabidzec0b4d8f2024-03-22 17:25:10 +0400541 transitiveGroups, err := s.store.GetAllTransitiveGroupsForUser(loggedInUser)
542 if err != nil {
543 http.Error(w, err.Error(), http.StatusInternalServerError)
544 return
545 }
DTabidze0d802592024-03-19 17:42:45 +0400546 data := struct {
547 OwnerGroups []Group
548 MembershipGroups []Group
DTabidzec0b4d8f2024-03-22 17:25:10 +0400549 TransitiveGroups []Group
DTabidze0d802592024-03-19 17:42:45 +0400550 }{
551 OwnerGroups: ownerGroups,
552 MembershipGroups: membershipGroups,
DTabidzec0b4d8f2024-03-22 17:25:10 +0400553 TransitiveGroups: transitiveGroups,
DTabidze0d802592024-03-19 17:42:45 +0400554 }
555 w.Header().Set("Content-Type", "text/html")
556 if err := tmpl.Execute(w, data); err != nil {
557 http.Error(w, err.Error(), http.StatusInternalServerError)
558 return
559 }
560}
561
562func (s *Server) createGroupHandler(w http.ResponseWriter, r *http.Request) {
563 loggedInUser, err := getLoggedInUser(r)
564 if err != nil {
565 http.Error(w, "User Not Logged In", http.StatusUnauthorized)
566 return
567 }
568 if r.Method != http.MethodPost {
569 http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
570 return
571 }
572 if err := r.ParseForm(); err != nil {
573 http.Error(w, err.Error(), http.StatusInternalServerError)
574 return
575 }
576 var group Group
577 group.Name = r.PostFormValue("group-name")
DTabidze908bb852024-03-25 20:07:57 +0400578 if err := isValidGroupName(group.Name); err != nil {
579 http.Error(w, err.Error(), http.StatusBadRequest)
580 return
581 }
DTabidze0d802592024-03-19 17:42:45 +0400582 group.Description = r.PostFormValue("description")
583 if err := s.store.CreateGroup(loggedInUser, group); err != nil {
584 http.Error(w, err.Error(), http.StatusInternalServerError)
585 return
586 }
587 http.Redirect(w, r, "/", http.StatusSeeOther)
588}
589
590func (s *Server) groupHandler(w http.ResponseWriter, r *http.Request) {
DTabidzec0b4d8f2024-03-22 17:25:10 +0400591 _, err := getLoggedInUser(r)
592 if err != nil {
593 http.Error(w, "User Not Logged In", http.StatusUnauthorized)
594 return
595 }
DTabidzed7744a62024-03-20 14:09:15 +0400596 vars := mux.Vars(r)
597 groupName := vars["group-name"]
DTabidze908bb852024-03-25 20:07:57 +0400598 exists, err := s.store.DoesGroupExist(groupName)
599 if err != nil {
600 http.Error(w, err.Error(), http.StatusInternalServerError)
601 return
602 }
603 if !exists {
604 errorMsg := fmt.Sprintf("group with the name '%s' not found", groupName)
605 http.Error(w, errorMsg, http.StatusNotFound)
606 return
607 }
DTabidze0d802592024-03-19 17:42:45 +0400608 tmpl, err := template.New("group").Parse(groupHTML)
609 if err != nil {
610 http.Error(w, err.Error(), http.StatusInternalServerError)
611 return
612 }
613 owners, err := s.store.GetGroupOwners(groupName)
614 if err != nil {
615 http.Error(w, err.Error(), http.StatusInternalServerError)
616 return
617 }
618 members, err := s.store.GetGroupMembers(groupName)
619 if err != nil {
620 http.Error(w, err.Error(), http.StatusInternalServerError)
621 return
622 }
623 description, err := s.store.GetGroupDescription(groupName)
624 if err != nil {
625 http.Error(w, err.Error(), http.StatusInternalServerError)
626 return
627 }
628 availableGroups, err := s.store.GetAvailableGroupsAsChild(groupName)
629 if err != nil {
630 http.Error(w, err.Error(), http.StatusInternalServerError)
631 return
632 }
DTabidzec0b4d8f2024-03-22 17:25:10 +0400633 transitiveGroups, err := s.store.GetAllTransitiveGroupsForGroup(groupName)
634 if err != nil {
635 http.Error(w, err.Error(), http.StatusInternalServerError)
636 return
637 }
638 childGroups, err := s.store.GetDirectChildrenGroups(groupName)
639 if err != nil {
640 http.Error(w, err.Error(), http.StatusInternalServerError)
641 return
642 }
DTabidze0d802592024-03-19 17:42:45 +0400643 data := struct {
DTabidzec0b4d8f2024-03-22 17:25:10 +0400644 GroupName string
645 Description string
646 Owners []string
647 Members []string
648 AvailableGroups []string
649 TransitiveGroups []Group
650 ChildGroups []Group
DTabidze0d802592024-03-19 17:42:45 +0400651 }{
DTabidzec0b4d8f2024-03-22 17:25:10 +0400652 GroupName: groupName,
653 Description: description,
654 Owners: owners,
655 Members: members,
656 AvailableGroups: availableGroups,
657 TransitiveGroups: transitiveGroups,
658 ChildGroups: childGroups,
DTabidze0d802592024-03-19 17:42:45 +0400659 }
660 if err := tmpl.Execute(w, data); err != nil {
661 http.Error(w, err.Error(), http.StatusInternalServerError)
662 return
663 }
664}
665
666func (s *Server) addUserHandler(w http.ResponseWriter, r *http.Request) {
667 if r.Method != http.MethodPost {
668 http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
669 return
670 }
671 loggedInUser, err := getLoggedInUser(r)
672 if err != nil {
673 http.Error(w, "User Not Logged In", http.StatusUnauthorized)
674 return
675 }
676 groupName := r.FormValue("group")
DTabidze908bb852024-03-25 20:07:57 +0400677 if err := isValidGroupName(groupName); err != nil {
678 http.Error(w, err.Error(), http.StatusBadRequest)
679 return
680 }
681 username := strings.ToLower(r.FormValue("username"))
682 if username == "" {
683 http.Error(w, "Username parameter is required", http.StatusBadRequest)
684 return
685 }
DTabidze0d802592024-03-19 17:42:45 +0400686 status, err := convertStatus(r.FormValue("status"))
687 if err != nil {
688 http.Error(w, err.Error(), http.StatusBadRequest)
689 return
690 }
691 if _, err := s.checkIsOwner(w, loggedInUser, groupName); err != nil {
692 return
693 }
694 switch status {
695 case Owner:
696 err = s.store.AddGroupOwner(username, groupName)
697 case Member:
698 err = s.store.AddGroupMember(username, groupName)
699 default:
700 http.Error(w, "Invalid status", http.StatusBadRequest)
701 return
702 }
703 if err != nil {
704 http.Error(w, err.Error(), http.StatusInternalServerError)
705 return
706 }
707 http.Redirect(w, r, "/group/"+groupName, http.StatusSeeOther)
708}
709
710func (s *Server) addChildGroupHandler(w http.ResponseWriter, r *http.Request) {
711 // TODO(dtabidze): In future we might need to make one group OWNER of another and not just a member.
712 if r.Method != http.MethodPost {
713 http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
714 return
715 }
716 loggedInUser, err := getLoggedInUser(r)
717 if err != nil {
718 http.Error(w, "User Not Logged In", http.StatusUnauthorized)
719 return
720 }
721 parentGroup := r.FormValue("parent-group")
DTabidze908bb852024-03-25 20:07:57 +0400722 if err := isValidGroupName(parentGroup); err != nil {
723 http.Error(w, err.Error(), http.StatusBadRequest)
724 return
725 }
DTabidze0d802592024-03-19 17:42:45 +0400726 childGroup := r.FormValue("child-group")
DTabidze908bb852024-03-25 20:07:57 +0400727 if err := isValidGroupName(childGroup); err != nil {
728 http.Error(w, err.Error(), http.StatusBadRequest)
729 return
730 }
DTabidze0d802592024-03-19 17:42:45 +0400731 if _, err := s.checkIsOwner(w, loggedInUser, parentGroup); err != nil {
732 return
733 }
734 if err := s.store.AddChildGroup(parentGroup, childGroup); err != nil {
735 http.Error(w, err.Error(), http.StatusInternalServerError)
736 return
737 }
738 http.Redirect(w, r, "/group/"+parentGroup, http.StatusSeeOther)
739}
740
Giorgi Lekveishvili942c7612024-03-22 19:27:48 +0400741type initRequest struct {
742 Owner string `json:"owner"`
743 Groups []string `json:"groups"`
744}
745
746func (s *Server) apiInitHandler(w http.ResponseWriter, r *http.Request) {
747 var req initRequest
748 if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
749 http.Error(w, err.Error(), http.StatusBadRequest)
750 return
751 }
752 if err := s.store.Init(req.Owner, req.Groups); err != nil {
753 http.Error(w, err.Error(), http.StatusInternalServerError)
754 return
755 }
756}
757
758type userInfo struct {
DTabidzed7744a62024-03-20 14:09:15 +0400759 MemberOf []string `json:"memberOf"`
760}
761
762func (s *Server) apiMemberOfHandler(w http.ResponseWriter, r *http.Request) {
763 vars := mux.Vars(r)
764 user, ok := vars["username"]
DTabidze908bb852024-03-25 20:07:57 +0400765 if !ok || user == "" {
DTabidzed7744a62024-03-20 14:09:15 +0400766 http.Error(w, "Username parameter is required", http.StatusBadRequest)
767 return
768 }
DTabidze908bb852024-03-25 20:07:57 +0400769 user = strings.ToLower(user)
DTabidzed7744a62024-03-20 14:09:15 +0400770 transitiveGroups, err := s.store.GetAllTransitiveGroupsForUser(user)
771 if err != nil {
772 http.Error(w, err.Error(), http.StatusInternalServerError)
773 return
774 }
DTabidzec0b4d8f2024-03-22 17:25:10 +0400775 var groupNames []string
776 for _, group := range transitiveGroups {
777 groupNames = append(groupNames, group.Name)
778 }
DTabidzed7744a62024-03-20 14:09:15 +0400779 w.Header().Set("Content-Type", "application/json")
Giorgi Lekveishvili942c7612024-03-22 19:27:48 +0400780 if err := json.NewEncoder(w).Encode(userInfo{groupNames}); err != nil {
DTabidzed7744a62024-03-20 14:09:15 +0400781 http.Error(w, err.Error(), http.StatusInternalServerError)
782 return
783 }
784}
785
DTabidze908bb852024-03-25 20:07:57 +0400786func convertStatus(status string) (Status, error) {
787 switch status {
788 case "Owner":
789 return Owner, nil
790 case "Member":
791 return Member, nil
792 default:
793 return Owner, fmt.Errorf("invalid status: %s", status)
794 }
795}
796
797func isValidGroupName(group string) error {
798 if strings.TrimSpace(group) == "" {
799 return fmt.Errorf("group name can't be empty or contain only whitespaces")
800 }
801 validGroupName := regexp.MustCompile(`^[a-z0-9\-_:.\/ ]+$`)
802 if !validGroupName.MatchString(group) {
803 return fmt.Errorf("group name should contain only lowercase letters, digits, -, _, :, ., /")
804 }
805 return nil
806}
807
DTabidze0d802592024-03-19 17:42:45 +0400808func main() {
809 flag.Parse()
DTabidzec0b4d8f2024-03-22 17:25:10 +0400810 db, err := sql.Open("sqlite3", *dbPath)
DTabidze0d802592024-03-19 17:42:45 +0400811 if err != nil {
812 panic(err)
813 }
DTabidzec0b4d8f2024-03-22 17:25:10 +0400814 store, err := NewSQLiteStore(db)
815 if err != nil {
816 panic(err)
817 }
818 s := Server{store}
Giorgi Lekveishvili329af572024-03-25 20:14:41 +0400819 log.Fatal(s.Start())
DTabidze0d802592024-03-19 17:42:45 +0400820}