blob: ba5db7c09e6ab1e064d256499da5e38bdf4ecbf1 [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"
Davit Tabidze75d57c32024-07-19 19:17:55 +040015 "sync"
DTabidze0d802592024-03-19 17:42:45 +040016
17 "github.com/ncruces/go-sqlite3"
18 _ "github.com/ncruces/go-sqlite3/driver"
19 _ "github.com/ncruces/go-sqlite3/embed"
DTabidzed7744a62024-03-20 14:09:15 +040020
21 "github.com/gorilla/mux"
DTabidze0d802592024-03-19 17:42:45 +040022)
23
Giorgi Lekveishvili329af572024-03-25 20:14:41 +040024var port = flag.Int("port", 8080, "Port to listen on")
25var apiPort = flag.Int("api-port", 8081, "Port to listen on for API requests")
DTabidze0d802592024-03-19 17:42:45 +040026var dbPath = flag.String("db-path", "memberships.db", "Path to SQLite file")
27
DTabidze4b44ff42024-04-02 03:16:26 +040028//go:embed memberships-tmpl/*
29var tmpls embed.FS
DTabidze0d802592024-03-19 17:42:45 +040030
gio1bf00802024-08-17 12:31:41 +040031//go:embed stat
DTabidze0d802592024-03-19 17:42:45 +040032var staticResources embed.FS
33
34type Store interface {
Giorgi Lekveishvili942c7612024-03-22 19:27:48 +040035 // Initializes store with admin user and their groups.
gio2728e402024-08-01 18:14:21 +040036 Init(user, email string, groups []string) error
DTabidze0d802592024-03-19 17:42:45 +040037 CreateGroup(owner string, group Group) error
38 AddChildGroup(parent, child string) error
Davit Tabidzec0d2bf52024-04-03 15:39:33 +040039 AddOwnerGroup(owned_group, owner_group 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)
Davit Tabidzec0d2bf52024-04-03 15:39:33 +040044 IsMemberOfOwnerGroup(user, group string) (bool, error)
DTabidze0d802592024-03-19 17:42:45 +040045 AddGroupMember(user, group string) error
46 AddGroupOwner(user, group string) error
47 GetGroupOwners(group string) ([]string, error)
Davit Tabidzec0d2bf52024-04-03 15:39:33 +040048 GetGroupOwnerGroups(group string) ([]Group, error)
DTabidze0d802592024-03-19 17:42:45 +040049 GetGroupMembers(group string) ([]string, error)
50 GetGroupDescription(group string) (string, error)
DTabidzec0b4d8f2024-03-22 17:25:10 +040051 GetAllTransitiveGroupsForUser(user string) ([]Group, error)
52 GetGroupsGroupBelongsTo(group string) ([]Group, error)
53 GetDirectChildrenGroups(group string) ([]Group, error)
54 GetAllTransitiveGroupsForGroup(group string) ([]Group, error)
DTabidze2b224bf2024-03-27 13:25:49 +040055 RemoveFromGroupToGroup(parent, child string) error
56 RemoveUserFromTable(username, groupName, tableName string) error
Davit Tabidzec0d2bf52024-04-03 15:39:33 +040057 GetAllGroups() ([]Group, error)
Davit Tabidzef867f2d2024-07-24 18:06:25 +040058 GetUsers(username []string) ([]User, error)
Davit Tabidze75d57c32024-07-19 19:17:55 +040059 GetUser(username string) (User, error)
60 AddSSHKeyForUser(username, sshKey string) error
61 RemoveSSHKeyForUser(username, sshKey string) error
62 CreateUser(user, email string) error
DTabidze0d802592024-03-19 17:42:45 +040063}
64
65type Server struct {
Davit Tabidze75d57c32024-07-19 19:17:55 +040066 store Store
67 syncAddresses map[string]struct{}
68 mu sync.Mutex
DTabidze0d802592024-03-19 17:42:45 +040069}
70
71type Group struct {
72 Name string
73 Description string
74}
75
Davit Tabidze75d57c32024-07-19 19:17:55 +040076type User struct {
77 Username string `json:"username"`
78 Email string `json:"email"`
79 SSHPublicKeys []string `json:"sshPublicKeys,omitempty"`
80}
81
DTabidze0d802592024-03-19 17:42:45 +040082type SQLiteStore struct {
83 db *sql.DB
84}
85
Davit Tabidzec0d2bf52024-04-03 15:39:33 +040086const (
87 ErrorUniqueConstraintViolation = 2067
88 ErrorConstraintPrimaryKeyViolation = 1555
89)
90
DTabidzec0b4d8f2024-03-22 17:25:10 +040091func NewSQLiteStore(db *sql.DB) (*SQLiteStore, error) {
92 _, err := db.Exec(`
Davit Tabidzec0d2bf52024-04-03 15:39:33 +040093 CREATE TABLE IF NOT EXISTS groups (
94 name TEXT PRIMARY KEY,
95 description TEXT
96 );
97 CREATE TABLE IF NOT EXISTS owners (
98 username TEXT,
99 group_name TEXT,
100 FOREIGN KEY(group_name) REFERENCES groups(name),
101 UNIQUE (username, group_name)
102 );
103 CREATE TABLE IF NOT EXISTS owner_groups (
104 owner_group TEXT,
105 owned_group TEXT,
106 FOREIGN KEY(owner_group) REFERENCES groups(name),
107 FOREIGN KEY(owned_group) REFERENCES groups(name),
108 UNIQUE (owner_group, owned_group)
109 );
110 CREATE TABLE IF NOT EXISTS group_to_group (
111 parent_group TEXT,
112 child_group TEXT,
113 FOREIGN KEY(parent_group) REFERENCES groups(name),
114 FOREIGN KEY(child_group) REFERENCES groups(name),
115 UNIQUE (parent_group, child_group)
116 );
117 CREATE TABLE IF NOT EXISTS user_to_group (
118 username TEXT,
119 group_name TEXT,
120 FOREIGN KEY(group_name) REFERENCES groups(name),
121 UNIQUE (username, group_name)
Davit Tabidze75d57c32024-07-19 19:17:55 +0400122 );
123 CREATE TABLE IF NOT EXISTS users (
124 username TEXT PRIMARY KEY,
125 email TEXT,
126 UNIQUE (email)
127 );
128 CREATE TABLE IF NOT EXISTS user_ssh_keys (
129 username TEXT,
130 ssh_key TEXT,
131 UNIQUE (ssh_key),
132 FOREIGN KEY(username) REFERENCES users(username)
Davit Tabidzec0d2bf52024-04-03 15:39:33 +0400133 );`)
DTabidze0d802592024-03-19 17:42:45 +0400134 if err != nil {
135 return nil, err
136 }
137 return &SQLiteStore{db: db}, nil
138}
139
gio2728e402024-08-01 18:14:21 +0400140func (s *SQLiteStore) Init(user, email string, groups []string) error {
Giorgi Lekveishvili942c7612024-03-22 19:27:48 +0400141 tx, err := s.db.Begin()
142 if err != nil {
143 return err
144 }
145 defer tx.Rollback()
146 row := tx.QueryRow("SELECT COUNT(*) FROM groups")
147 var count int
148 if err := row.Scan(&count); err != nil {
149 return err
150 }
151 if count != 0 {
Davit Tabidze5f1a2c62024-07-17 17:57:27 +0400152 return fmt.Errorf("Store already initialised")
Giorgi Lekveishvili942c7612024-03-22 19:27:48 +0400153 }
gio2728e402024-08-01 18:14:21 +0400154 query := `INSERT INTO users (username, email) VALUES (?, ?)`
155 if _, err := tx.Exec(query, user, email); err != nil {
156 return err
157 }
Giorgi Lekveishvili942c7612024-03-22 19:27:48 +0400158 for _, g := range groups {
gio2728e402024-08-01 18:14:21 +0400159 query = `INSERT INTO groups (name, description) VALUES (?, '')`
Giorgi Lekveishvili942c7612024-03-22 19:27:48 +0400160 if _, err := tx.Exec(query, g); err != nil {
161 return err
162 }
163 query = `INSERT INTO owners (username, group_name) VALUES (?, ?)`
gio2728e402024-08-01 18:14:21 +0400164 if _, err := tx.Exec(query, user, g); err != nil {
Giorgi Lekveishvili942c7612024-03-22 19:27:48 +0400165 return err
166 }
Giorgi Lekveishvilid542b732024-03-25 18:17:39 +0400167 query = `INSERT INTO user_to_group (username, group_name) VALUES (?, ?)`
gio2728e402024-08-01 18:14:21 +0400168 if _, err := tx.Exec(query, user, g); err != nil {
Giorgi Lekveishvilid542b732024-03-25 18:17:39 +0400169 return err
170 }
Giorgi Lekveishvili942c7612024-03-22 19:27:48 +0400171 }
172 return tx.Commit()
173}
174
DTabidze0d802592024-03-19 17:42:45 +0400175func (s *SQLiteStore) queryGroups(query string, args ...interface{}) ([]Group, error) {
176 groups := make([]Group, 0)
177 rows, err := s.db.Query(query, args...)
178 if err != nil {
179 return nil, err
180 }
181 defer rows.Close()
182 for rows.Next() {
183 var group Group
184 if err := rows.Scan(&group.Name, &group.Description); err != nil {
185 return nil, err
186 }
187 groups = append(groups, group)
188 }
189 if err := rows.Err(); err != nil {
190 return nil, err
191 }
192 return groups, nil
193}
194
195func (s *SQLiteStore) GetGroupsOwnedBy(user string) ([]Group, error) {
196 query := `
197 SELECT groups.name, groups.description
198 FROM groups
199 JOIN owners ON groups.name = owners.group_name
200 WHERE owners.username = ?`
201 return s.queryGroups(query, user)
202}
203
DTabidzed7744a62024-03-20 14:09:15 +0400204func (s *SQLiteStore) GetGroupsUserBelongsTo(user string) ([]Group, error) {
DTabidze0d802592024-03-19 17:42:45 +0400205 query := `
206 SELECT groups.name, groups.description
207 FROM groups
208 JOIN user_to_group ON groups.name = user_to_group.group_name
209 WHERE user_to_group.username = ?`
210 return s.queryGroups(query, user)
211}
212
213func (s *SQLiteStore) CreateGroup(owner string, group Group) error {
214 tx, err := s.db.Begin()
215 if err != nil {
216 return err
217 }
218 defer tx.Rollback()
219 query := `INSERT INTO groups (name, description) VALUES (?, ?)`
220 if _, err := tx.Exec(query, group.Name, group.Description); err != nil {
221 sqliteErr, ok := err.(*sqlite3.Error)
Davit Tabidzec0d2bf52024-04-03 15:39:33 +0400222 if ok && sqliteErr.ExtendedCode() == ErrorConstraintPrimaryKeyViolation {
DTabidze0d802592024-03-19 17:42:45 +0400223 return fmt.Errorf("Group with the name %s already exists", group.Name)
224 }
225 return err
226 }
227 query = `INSERT INTO owners (username, group_name) VALUES (?, ?)`
228 if _, err := tx.Exec(query, owner, group.Name); err != nil {
229 return err
230 }
Giorgi Lekveishvili942c7612024-03-22 19:27:48 +0400231 return tx.Commit()
DTabidze0d802592024-03-19 17:42:45 +0400232}
233
234func (s *SQLiteStore) IsGroupOwner(user, group string) (bool, error) {
235 query := `
236 SELECT EXISTS (
237 SELECT 1
238 FROM owners
239 WHERE username = ? AND group_name = ?
240 )`
241 var exists bool
242 if err := s.db.QueryRow(query, user, group).Scan(&exists); err != nil {
243 return false, err
244 }
245 return exists, nil
246}
247
DTabidze0d802592024-03-19 17:42:45 +0400248func (s *SQLiteStore) AddGroupMember(user, group string) error {
Davit Tabidzec0d2bf52024-04-03 15:39:33 +0400249 _, err := s.db.Exec(`INSERT INTO user_to_group (username, group_name) VALUES (?, ?)`, user, group)
DTabidze0d802592024-03-19 17:42:45 +0400250 if err != nil {
Davit Tabidzec0d2bf52024-04-03 15:39:33 +0400251 sqliteErr, ok := err.(*sqlite3.Error)
252 if ok && sqliteErr.ExtendedCode() == ErrorUniqueConstraintViolation {
253 return fmt.Errorf("%s is already a member of group %s", user, group)
254 }
DTabidze0d802592024-03-19 17:42:45 +0400255 return err
256 }
257 return nil
258}
259
260func (s *SQLiteStore) AddGroupOwner(user, group string) error {
Davit Tabidzec0d2bf52024-04-03 15:39:33 +0400261 _, err := s.db.Exec(`INSERT INTO owners (username, group_name) VALUES (?, ?)`, user, group)
DTabidze0d802592024-03-19 17:42:45 +0400262 if err != nil {
Davit Tabidzec0d2bf52024-04-03 15:39:33 +0400263 sqliteErr, ok := err.(*sqlite3.Error)
264 if ok && sqliteErr.ExtendedCode() == ErrorUniqueConstraintViolation {
265 return fmt.Errorf("%s is already an owner of group %s", user, group)
266 }
DTabidze0d802592024-03-19 17:42:45 +0400267 return err
268 }
269 return nil
270}
271
272func (s *SQLiteStore) getUsersByGroup(table, group string) ([]string, error) {
273 query := fmt.Sprintf("SELECT username FROM %s WHERE group_name = ?", table)
274 rows, err := s.db.Query(query, group)
275 if err != nil {
276 return nil, err
277 }
278 defer rows.Close()
279 var users []string
280 for rows.Next() {
281 var username string
282 if err := rows.Scan(&username); err != nil {
283 return nil, err
284 }
285 users = append(users, username)
286 }
287 if err := rows.Err(); err != nil {
288 return nil, err
289 }
290 return users, nil
291}
292
293func (s *SQLiteStore) GetGroupOwners(group string) ([]string, error) {
294 return s.getUsersByGroup("owners", group)
295}
296
297func (s *SQLiteStore) GetGroupMembers(group string) ([]string, error) {
298 return s.getUsersByGroup("user_to_group", group)
299}
300
301func (s *SQLiteStore) GetGroupDescription(group string) (string, error) {
302 var description string
303 query := `SELECT description FROM groups WHERE name = ?`
304 if err := s.db.QueryRow(query, group).Scan(&description); err != nil {
305 return "", err
306 }
307 return description, nil
308}
309
DTabidze908bb852024-03-25 20:07:57 +0400310func (s *SQLiteStore) DoesGroupExist(group string) (bool, error) {
311 query := `SELECT EXISTS (SELECT 1 FROM groups WHERE name = ?)`
312 var exists bool
313 if err := s.db.QueryRow(query, group).Scan(&exists); err != nil {
314 return false, err
315 }
316 return exists, nil
317}
318
DTabidze0d802592024-03-19 17:42:45 +0400319func (s *SQLiteStore) AddChildGroup(parent, child string) error {
DTabidze908bb852024-03-25 20:07:57 +0400320 if parent == child {
Davit Tabidze5f1a2c62024-07-17 17:57:27 +0400321 return fmt.Errorf("Parent and child groups can not have same name")
DTabidze908bb852024-03-25 20:07:57 +0400322 }
Davit Tabidzec0d2bf52024-04-03 15:39:33 +0400323 exists, err := s.DoesGroupExist(parent)
324 if err != nil {
Davit Tabidze5f1a2c62024-07-17 17:57:27 +0400325 return fmt.Errorf("Error checking parent group existence: %v", err)
DTabidze908bb852024-03-25 20:07:57 +0400326 }
Davit Tabidzec0d2bf52024-04-03 15:39:33 +0400327 if !exists {
Davit Tabidze5f1a2c62024-07-17 17:57:27 +0400328 return fmt.Errorf("Parent group with name %s does not exist", parent)
Davit Tabidzec0d2bf52024-04-03 15:39:33 +0400329 }
330 exists, err = s.DoesGroupExist(child)
331 if err != nil {
Davit Tabidze5f1a2c62024-07-17 17:57:27 +0400332 return fmt.Errorf("Error checking child group existence: %v", err)
Davit Tabidzec0d2bf52024-04-03 15:39:33 +0400333 }
334 if !exists {
Davit Tabidze5f1a2c62024-07-17 17:57:27 +0400335 return fmt.Errorf("Child group with name %s does not exist", child)
DTabidze908bb852024-03-25 20:07:57 +0400336 }
DTabidzec0b4d8f2024-03-22 17:25:10 +0400337 parentGroups, err := s.GetAllTransitiveGroupsForGroup(parent)
338 if err != nil {
339 return err
340 }
341 for _, group := range parentGroups {
342 if group.Name == child {
Davit Tabidze5f1a2c62024-07-17 17:57:27 +0400343 return fmt.Errorf("Circular reference detected: group %s is already a parent of group %s", child, parent)
DTabidzec0b4d8f2024-03-22 17:25:10 +0400344 }
345 }
Davit Tabidzec0d2bf52024-04-03 15:39:33 +0400346 _, err = s.db.Exec(`INSERT INTO group_to_group (parent_group, child_group) VALUES (?, ?)`, parent, child)
DTabidze0d802592024-03-19 17:42:45 +0400347 if err != nil {
Davit Tabidzec0d2bf52024-04-03 15:39:33 +0400348 sqliteErr, ok := err.(*sqlite3.Error)
349 if ok && sqliteErr.ExtendedCode() == ErrorUniqueConstraintViolation {
Davit Tabidze5f1a2c62024-07-17 17:57:27 +0400350 return fmt.Errorf("Child group name %s already exists in group %s", child, parent)
Davit Tabidzec0d2bf52024-04-03 15:39:33 +0400351 }
DTabidze0d802592024-03-19 17:42:45 +0400352 return err
353 }
354 return nil
355}
356
DTabidzec0b4d8f2024-03-22 17:25:10 +0400357func (s *SQLiteStore) GetAllTransitiveGroupsForUser(user string) ([]Group, error) {
358 if groups, err := s.GetGroupsUserBelongsTo(user); err != nil {
DTabidzed7744a62024-03-20 14:09:15 +0400359 return nil, err
DTabidzec0b4d8f2024-03-22 17:25:10 +0400360 } else {
361 visited := map[string]struct{}{}
362 return s.getAllParentGroupsRecursive(groups, visited)
DTabidzed7744a62024-03-20 14:09:15 +0400363 }
DTabidzec0b4d8f2024-03-22 17:25:10 +0400364}
365
366func (s *SQLiteStore) GetAllTransitiveGroupsForGroup(group string) ([]Group, error) {
367 if p, err := s.GetGroupsGroupBelongsTo(group); err != nil {
368 return nil, err
369 } else {
370 // Mark initial group as visited
371 visited := map[string]struct{}{
372 group: struct{}{},
373 }
374 return s.getAllParentGroupsRecursive(p, visited)
375 }
376}
377
378func (s *SQLiteStore) getAllParentGroupsRecursive(groups []Group, visited map[string]struct{}) ([]Group, error) {
379 var ret []Group
380 for _, g := range groups {
381 if _, ok := visited[g.Name]; ok {
382 continue
383 }
384 visited[g.Name] = struct{}{}
385 ret = append(ret, g)
386 if p, err := s.GetGroupsGroupBelongsTo(g.Name); err != nil {
DTabidzed7744a62024-03-20 14:09:15 +0400387 return nil, err
DTabidzec0b4d8f2024-03-22 17:25:10 +0400388 } else if res, err := s.getAllParentGroupsRecursive(p, visited); err != nil {
389 return nil, err
390 } else {
391 ret = append(ret, res...)
DTabidzed7744a62024-03-20 14:09:15 +0400392 }
393 }
DTabidzec0b4d8f2024-03-22 17:25:10 +0400394 return ret, nil
DTabidzed7744a62024-03-20 14:09:15 +0400395}
396
DTabidzec0b4d8f2024-03-22 17:25:10 +0400397func (s *SQLiteStore) GetGroupsGroupBelongsTo(group string) ([]Group, error) {
398 query := `
399 SELECT groups.name, groups.description
400 FROM groups
401 JOIN group_to_group ON groups.name = group_to_group.parent_group
402 WHERE group_to_group.child_group = ?`
DTabidzed7744a62024-03-20 14:09:15 +0400403 rows, err := s.db.Query(query, group)
404 if err != nil {
405 return nil, err
406 }
407 defer rows.Close()
DTabidzec0b4d8f2024-03-22 17:25:10 +0400408 var parentGroups []Group
DTabidzed7744a62024-03-20 14:09:15 +0400409 for rows.Next() {
DTabidzec0b4d8f2024-03-22 17:25:10 +0400410 var parentGroup Group
411 if err := rows.Scan(&parentGroup.Name, &parentGroup.Description); err != nil {
DTabidzed7744a62024-03-20 14:09:15 +0400412 return nil, err
413 }
414 parentGroups = append(parentGroups, parentGroup)
415 }
416 if err := rows.Err(); err != nil {
417 return nil, err
418 }
419 return parentGroups, nil
420}
421
DTabidzec0b4d8f2024-03-22 17:25:10 +0400422func (s *SQLiteStore) GetDirectChildrenGroups(group string) ([]Group, error) {
423 query := `
424 SELECT groups.name, groups.description
425 FROM groups
426 JOIN group_to_group ON groups.name = group_to_group.child_group
427 WHERE group_to_group.parent_group = ?`
428 rows, err := s.db.Query(query, group)
429 if err != nil {
430 return nil, err
431 }
432 defer rows.Close()
433 var childrenGroups []Group
434 for rows.Next() {
435 var childGroup Group
436 if err := rows.Scan(&childGroup.Name, &childGroup.Description); err != nil {
437 return nil, err
438 }
439 childrenGroups = append(childrenGroups, childGroup)
440 }
441 if err := rows.Err(); err != nil {
442 return nil, err
443 }
444 return childrenGroups, nil
445}
446
DTabidze2b224bf2024-03-27 13:25:49 +0400447func (s *SQLiteStore) RemoveFromGroupToGroup(parent, child string) error {
448 query := `DELETE FROM group_to_group WHERE parent_group = ? AND child_group = ?`
449 rowDeleted, err := s.db.Exec(query, parent, child)
450 if err != nil {
451 return err
452 }
453 rowDeletedNumber, err := rowDeleted.RowsAffected()
454 if err != nil {
455 return err
456 }
457 if rowDeletedNumber == 0 {
Davit Tabidze5f1a2c62024-07-17 17:57:27 +0400458 return fmt.Errorf("Pair of parent '%s' and child '%s' groups not found", parent, child)
DTabidze2b224bf2024-03-27 13:25:49 +0400459 }
460 return nil
461}
462
463func (s *SQLiteStore) RemoveUserFromTable(username, groupName, tableName string) error {
464 if tableName == "owners" {
465 owners, err := s.GetGroupOwners(groupName)
466 if err != nil {
467 return err
468 }
469 if len(owners) == 1 {
Davit Tabidze5f1a2c62024-07-17 17:57:27 +0400470 return fmt.Errorf("Cannot remove the last owner of the group")
DTabidze2b224bf2024-03-27 13:25:49 +0400471 }
472 }
473 query := fmt.Sprintf("DELETE FROM %s WHERE username = ? AND group_name = ?", tableName)
474 rowDeleted, err := s.db.Exec(query, username, groupName)
475 if err != nil {
476 return err
477 }
478 rowDeletedNumber, err := rowDeleted.RowsAffected()
479 if err != nil {
480 return err
481 }
482 if rowDeletedNumber == 0 {
Davit Tabidze5f1a2c62024-07-17 17:57:27 +0400483 return fmt.Errorf("Pair of group '%s' and user '%s' not found", groupName, username)
DTabidze2b224bf2024-03-27 13:25:49 +0400484 }
485 return nil
486}
487
Davit Tabidzec0d2bf52024-04-03 15:39:33 +0400488func (s *SQLiteStore) AddOwnerGroup(owner_group, owned_group string) error {
489 if owned_group == owner_group {
Davit Tabidze5f1a2c62024-07-17 17:57:27 +0400490 return fmt.Errorf("Group can not own itself")
Davit Tabidzec0d2bf52024-04-03 15:39:33 +0400491 }
492 exists, err := s.DoesGroupExist(owned_group)
493 if err != nil {
Davit Tabidze5f1a2c62024-07-17 17:57:27 +0400494 return fmt.Errorf("Error checking owned group existence: %v", err)
Davit Tabidzec0d2bf52024-04-03 15:39:33 +0400495 }
496 if !exists {
Davit Tabidze5f1a2c62024-07-17 17:57:27 +0400497 return fmt.Errorf("Owned group with name %s does not exist", owned_group)
Davit Tabidzec0d2bf52024-04-03 15:39:33 +0400498 }
499 exists, err = s.DoesGroupExist(owner_group)
500 if err != nil {
Davit Tabidze5f1a2c62024-07-17 17:57:27 +0400501 return fmt.Errorf("Error checking owner group existence: %v", err)
Davit Tabidzec0d2bf52024-04-03 15:39:33 +0400502 }
503 if !exists {
Davit Tabidze5f1a2c62024-07-17 17:57:27 +0400504 return fmt.Errorf("Owner group with name %s does not exist", owner_group)
Davit Tabidzec0d2bf52024-04-03 15:39:33 +0400505 }
506 _, err = s.db.Exec(`INSERT INTO owner_groups (owner_group, owned_group) VALUES (?, ?)`, owner_group, owned_group)
507 if err != nil {
508 sqliteErr, ok := err.(*sqlite3.Error)
509 if ok && sqliteErr.ExtendedCode() == ErrorUniqueConstraintViolation {
Davit Tabidze5f1a2c62024-07-17 17:57:27 +0400510 return fmt.Errorf("Group named %s is already owner of a group %s", owner_group, owned_group)
Davit Tabidzec0d2bf52024-04-03 15:39:33 +0400511 }
512 return err
513 }
514 return nil
515}
516
517func (s *SQLiteStore) GetGroupOwnerGroups(group string) ([]Group, error) {
518 query := `
519 SELECT groups.name, groups.description
520 FROM groups
521 JOIN owner_groups ON groups.name = owner_groups.owner_group
522 WHERE owner_groups.owned_group = ?`
523 return s.queryGroups(query, group)
524}
525
526func (s *SQLiteStore) IsMemberOfOwnerGroup(user, group string) (bool, error) {
527 query := `
528 SELECT EXISTS (
529 SELECT 1 FROM owner_groups
530 INNER JOIN user_to_group ON owner_groups.owner_group = user_to_group.group_name
531 WHERE owner_groups.owned_group = ? AND user_to_group.username = ?)`
532 var exists bool
533 err := s.db.QueryRow(query, group, user).Scan(&exists)
534 if err != nil {
535 return false, err
536 }
537 return exists, nil
538}
539
540func (s *SQLiteStore) GetAllGroups() ([]Group, error) {
541 query := `SELECT name, description FROM groups`
542 return s.queryGroups(query)
543}
544
Davit Tabidze75d57c32024-07-19 19:17:55 +0400545func (s *SQLiteStore) AddSSHKeyForUser(username, sshKey string) error {
546 _, err := s.db.Exec(`INSERT INTO user_ssh_keys (username, ssh_key) VALUES (?, ?)`, username, sshKey)
547 if err != nil {
548 sqliteErr, ok := err.(*sqlite3.Error)
549 if ok && sqliteErr.ExtendedCode() == ErrorUniqueConstraintViolation {
550 return fmt.Errorf("%s such SSH public key already exists", sshKey)
551 }
552 return err
553 }
554 return nil
555}
556
557func (s *SQLiteStore) RemoveSSHKeyForUser(username, sshKey string) error {
558 _, err := s.db.Exec(`DELETE FROM user_ssh_keys WHERE username = ? AND ssh_key = ?`, username, sshKey)
559 if err != nil {
560 return err
561 }
562 return nil
563}
564
Davit Tabidzef867f2d2024-07-24 18:06:25 +0400565func (s *SQLiteStore) GetUsers(usernames []string) ([]User, error) {
566 var rows *sql.Rows
567 var err error
568 query := `
Davit Tabidze75d57c32024-07-19 19:17:55 +0400569 SELECT users.username, users.email, GROUP_CONCAT(user_ssh_keys.ssh_key, ',')
570 FROM users
Davit Tabidzef867f2d2024-07-24 18:06:25 +0400571 LEFT JOIN user_ssh_keys ON users.username = user_ssh_keys.username`
572 var args []interface{}
573 if usernames != nil {
574 if len(usernames) == 0 {
575 return []User{}, nil
576 }
577 query += " WHERE users.username IN ("
578 placeholders := strings.Repeat("?,", len(usernames)-1) + "?"
579 query += placeholders + ") "
580 for _, username := range usernames {
581 args = append(args, username)
582 }
583 }
584 query += " GROUP BY users.username"
585 rows, err = s.db.Query(query, args...)
Davit Tabidze75d57c32024-07-19 19:17:55 +0400586 if err != nil {
587 return nil, err
588 }
589 defer rows.Close()
590 var userInfos []User
591 for rows.Next() {
592 var username, email string
593 var sshKeys sql.NullString
594 if err := rows.Scan(&username, &email, &sshKeys); err != nil {
595 return nil, err
596 }
597 user := User{
598 Username: username,
599 Email: email,
600 }
601 if sshKeys.Valid {
602 user.SSHPublicKeys = strings.Split(sshKeys.String, ",")
603 }
604 userInfos = append(userInfos, user)
605 }
606 if err := rows.Err(); err != nil {
607 return nil, err
608 }
609 return userInfos, nil
610}
611
612func (s *SQLiteStore) GetUser(username string) (User, error) {
613 var user User
614 user.Username = username
615 query := `
616 SELECT users.email, GROUP_CONCAT(user_ssh_keys.ssh_key, ',')
617 FROM users
618 LEFT JOIN user_ssh_keys ON users.username = user_ssh_keys.username
619 WHERE users.username = ?
620 GROUP BY users.username
621 `
622 row := s.db.QueryRow(query, username)
623 var sshKeys sql.NullString
624 err := row.Scan(&user.Email, &sshKeys)
625 if err != nil {
626 if err == sql.ErrNoRows {
627 return User{}, fmt.Errorf("no user found with username %s", username)
628 }
629 return User{}, err
630 }
631 if sshKeys.Valid {
632 user.SSHPublicKeys = strings.Split(sshKeys.String, ",")
633 }
634 return user, nil
635}
636
637func (s *SQLiteStore) CreateUser(user, email string) error {
638 _, err := s.db.Exec(`INSERT INTO users (username, email) VALUES (?, ?)`, user, email)
639 if err != nil {
640 sqliteErr, ok := err.(*sqlite3.Error)
641 if ok {
642 if sqliteErr.ExtendedCode() == ErrorUniqueConstraintViolation {
643 if strings.Contains(err.Error(), "UNIQUE constraint failed: users.username") {
644 return fmt.Errorf("username %s already exists", user)
645 }
646 if strings.Contains(err.Error(), "UNIQUE constraint failed: users.email") {
647 return fmt.Errorf("email %s already exists", email)
648 }
649 }
650 }
651 return err
652 }
653 return nil
654}
655
DTabidze0d802592024-03-19 17:42:45 +0400656func getLoggedInUser(r *http.Request) (string, error) {
DTabidzec0b4d8f2024-03-22 17:25:10 +0400657 if user := r.Header.Get("X-User"); user != "" {
658 return user, nil
659 } else {
660 return "", fmt.Errorf("unauthenticated")
661 }
Davit Tabidze75d57c32024-07-19 19:17:55 +0400662 // return "tabo", nil
DTabidze0d802592024-03-19 17:42:45 +0400663}
664
665type Status int
666
667const (
668 Owner Status = iota
669 Member
670)
671
Giorgi Lekveishvili329af572024-03-25 20:14:41 +0400672func (s *Server) Start() error {
673 e := make(chan error)
674 go func() {
675 r := mux.NewRouter()
gio1bf00802024-08-17 12:31:41 +0400676 r.PathPrefix("/stat/").Handler(http.FileServer(http.FS(staticResources)))
Davit Tabidze75d57c32024-07-19 19:17:55 +0400677 r.HandleFunc("/group/{group-name}/add-user/", s.addUserToGroupHandler).Methods(http.MethodPost)
678 r.HandleFunc("/group/{parent-group}/add-child-group", s.addChildGroupHandler).Methods(http.MethodPost)
679 r.HandleFunc("/group/{owned-group}/add-owner-group", s.addOwnerGroupHandler).Methods(http.MethodPost)
680 r.HandleFunc("/group/{parent-group}/remove-child-group/{child-group}", s.removeChildGroupHandler).Methods(http.MethodPost)
681 r.HandleFunc("/group/{group-name}/remove-owner/{username}", s.removeOwnerFromGroupHandler).Methods(http.MethodPost)
682 r.HandleFunc("/group/{group-name}/remove-member/{username}", s.removeMemberFromGroupHandler).Methods(http.MethodPost)
Giorgi Lekveishvili329af572024-03-25 20:14:41 +0400683 r.HandleFunc("/group/{group-name}", s.groupHandler)
Davit Tabidze75d57c32024-07-19 19:17:55 +0400684 r.HandleFunc("/user/{username}/ssh-key", s.addSSHKeyForUserHandler).Methods(http.MethodPost)
685 r.HandleFunc("/user/{username}/remove-ssh-key", s.removeSSHKeyForUserHandler).Methods(http.MethodPost)
DTabidze5d735e32024-03-26 16:01:06 +0400686 r.HandleFunc("/user/{username}", s.userHandler)
Davit Tabidze75d57c32024-07-19 19:17:55 +0400687 r.HandleFunc("/create-group", s.createGroupHandler).Methods(http.MethodPost)
Giorgi Lekveishvili329af572024-03-25 20:14:41 +0400688 r.HandleFunc("/", s.homePageHandler)
689 e <- http.ListenAndServe(fmt.Sprintf(":%d", *port), r)
690 }()
691 go func() {
692 r := mux.NewRouter()
693 r.HandleFunc("/api/init", s.apiInitHandler)
694 r.HandleFunc("/api/user/{username}", s.apiMemberOfHandler)
Davit Tabidze75d57c32024-07-19 19:17:55 +0400695 r.HandleFunc("/api/users", s.apiGetAllUsers).Methods(http.MethodGet)
696 r.HandleFunc("/api/users", s.apiCreateUser).Methods(http.MethodPost)
Giorgi Lekveishvili329af572024-03-25 20:14:41 +0400697 e <- http.ListenAndServe(fmt.Sprintf(":%d", *apiPort), r)
698 }()
699 return <-e
DTabidze0d802592024-03-19 17:42:45 +0400700}
701
702type GroupData struct {
703 Group Group
704 Membership string
705}
706
Davit Tabidzec0d2bf52024-04-03 15:39:33 +0400707func (s *Server) checkIsOwner(w http.ResponseWriter, user, group string) error {
DTabidze0d802592024-03-19 17:42:45 +0400708 isOwner, err := s.store.IsGroupOwner(user, group)
709 if err != nil {
Davit Tabidzec0d2bf52024-04-03 15:39:33 +0400710 return err
DTabidze0d802592024-03-19 17:42:45 +0400711 }
Davit Tabidzec0d2bf52024-04-03 15:39:33 +0400712 if isOwner {
713 return nil
DTabidze0d802592024-03-19 17:42:45 +0400714 }
Davit Tabidzec0d2bf52024-04-03 15:39:33 +0400715 // TODO(dtabidze): right now this only checks if user is member of just one lvl upper group. should add transitive group check.
716 isMemberOfOwnerGroup, err := s.store.IsMemberOfOwnerGroup(user, group)
717 if err != nil {
718 return err
719 }
720 if !isMemberOfOwnerGroup {
Davit Tabidze5f1a2c62024-07-17 17:57:27 +0400721 return fmt.Errorf("You are not the owner or a member of any owner group of the group %s", group)
Davit Tabidzec0d2bf52024-04-03 15:39:33 +0400722 }
723 return nil
DTabidze0d802592024-03-19 17:42:45 +0400724}
725
DTabidze4b44ff42024-04-02 03:16:26 +0400726type templates struct {
727 group *template.Template
728 user *template.Template
729}
730
731func parseTemplates(fs embed.FS) (templates, error) {
732 base, err := template.ParseFS(fs, "memberships-tmpl/base.html")
733 if err != nil {
734 return templates{}, err
735 }
736 parse := func(path string) (*template.Template, error) {
737 if b, err := base.Clone(); err != nil {
738 return nil, err
739 } else {
740 return b.ParseFS(fs, path)
741 }
742 }
743 user, err := parse("memberships-tmpl/user.html")
744 if err != nil {
745 return templates{}, err
746 }
747 group, err := parse("memberships-tmpl/group.html")
748 if err != nil {
749 return templates{}, err
750 }
751 return templates{group, user}, nil
752}
753
DTabidze0d802592024-03-19 17:42:45 +0400754func (s *Server) homePageHandler(w http.ResponseWriter, r *http.Request) {
755 loggedInUser, err := getLoggedInUser(r)
756 if err != nil {
757 http.Error(w, "User Not Logged In", http.StatusUnauthorized)
758 return
759 }
DTabidze5d735e32024-03-26 16:01:06 +0400760 http.Redirect(w, r, "/user/"+loggedInUser, http.StatusSeeOther)
761}
762
Davit Tabidze75d57c32024-07-19 19:17:55 +0400763type UserPageData struct {
764 OwnerGroups []Group
765 MembershipGroups []Group
766 TransitiveGroups []Group
767 LoggedInUserPage bool
768 CurrentUser string
769 SSHPublicKeys []string
770 Email string
771 ErrorMessage string
772}
773
DTabidze5d735e32024-03-26 16:01:06 +0400774func (s *Server) userHandler(w http.ResponseWriter, r *http.Request) {
775 loggedInUser, err := getLoggedInUser(r)
776 if err != nil {
777 http.Error(w, "User Not Logged In", http.StatusUnauthorized)
778 return
779 }
DTabidze4b44ff42024-04-02 03:16:26 +0400780 errorMsg := r.URL.Query().Get("errorMessage")
DTabidze5d735e32024-03-26 16:01:06 +0400781 vars := mux.Vars(r)
782 user := strings.ToLower(vars["username"])
783 // TODO(dtabidze): should check if username exists or not.
784 loggedInUserPage := loggedInUser == user
785 ownerGroups, err := s.store.GetGroupsOwnedBy(user)
DTabidze0d802592024-03-19 17:42:45 +0400786 if err != nil {
787 http.Error(w, err.Error(), http.StatusInternalServerError)
788 return
789 }
DTabidze5d735e32024-03-26 16:01:06 +0400790 membershipGroups, err := s.store.GetGroupsUserBelongsTo(user)
DTabidze0d802592024-03-19 17:42:45 +0400791 if err != nil {
792 http.Error(w, err.Error(), http.StatusInternalServerError)
793 return
794 }
DTabidze5d735e32024-03-26 16:01:06 +0400795 transitiveGroups, err := s.store.GetAllTransitiveGroupsForUser(user)
DTabidzec0b4d8f2024-03-22 17:25:10 +0400796 if err != nil {
797 http.Error(w, err.Error(), http.StatusInternalServerError)
798 return
799 }
Davit Tabidze75d57c32024-07-19 19:17:55 +0400800 userInfo, err := s.store.GetUser(user)
801 if err != nil {
802 http.Error(w, err.Error(), http.StatusInternalServerError)
803 return
804 }
805 data := UserPageData{
DTabidze0d802592024-03-19 17:42:45 +0400806 OwnerGroups: ownerGroups,
807 MembershipGroups: membershipGroups,
DTabidzec0b4d8f2024-03-22 17:25:10 +0400808 TransitiveGroups: transitiveGroups,
DTabidze5d735e32024-03-26 16:01:06 +0400809 LoggedInUserPage: loggedInUserPage,
810 CurrentUser: user,
Davit Tabidze75d57c32024-07-19 19:17:55 +0400811 SSHPublicKeys: userInfo.SSHPublicKeys,
812 Email: userInfo.Email,
DTabidze4b44ff42024-04-02 03:16:26 +0400813 ErrorMessage: errorMsg,
DTabidze0d802592024-03-19 17:42:45 +0400814 }
DTabidze4b44ff42024-04-02 03:16:26 +0400815 templates, err := parseTemplates(tmpls)
816 if err != nil {
817 http.Error(w, err.Error(), http.StatusInternalServerError)
818 return
819 }
820 if err := templates.user.Execute(w, data); err != nil {
DTabidze0d802592024-03-19 17:42:45 +0400821 http.Error(w, err.Error(), http.StatusInternalServerError)
822 return
823 }
824}
825
826func (s *Server) createGroupHandler(w http.ResponseWriter, r *http.Request) {
827 loggedInUser, err := getLoggedInUser(r)
828 if err != nil {
829 http.Error(w, "User Not Logged In", http.StatusUnauthorized)
830 return
831 }
DTabidze0d802592024-03-19 17:42:45 +0400832 if err := r.ParseForm(); err != nil {
833 http.Error(w, err.Error(), http.StatusInternalServerError)
834 return
835 }
836 var group Group
837 group.Name = r.PostFormValue("group-name")
DTabidze908bb852024-03-25 20:07:57 +0400838 if err := isValidGroupName(group.Name); err != nil {
DTabidze4b44ff42024-04-02 03:16:26 +0400839 // http.Error(w, err.Error(), http.StatusBadRequest)
840 redirectURL := fmt.Sprintf("/user/%s?errorMessage=%s", loggedInUser, url.QueryEscape(err.Error()))
841 http.Redirect(w, r, redirectURL, http.StatusFound)
DTabidze908bb852024-03-25 20:07:57 +0400842 return
843 }
DTabidze0d802592024-03-19 17:42:45 +0400844 group.Description = r.PostFormValue("description")
845 if err := s.store.CreateGroup(loggedInUser, group); err != nil {
DTabidze4b44ff42024-04-02 03:16:26 +0400846 // http.Error(w, err.Error(), http.StatusInternalServerError)
847 redirectURL := fmt.Sprintf("/user/%s?errorMessage=%s", loggedInUser, url.QueryEscape(err.Error()))
848 http.Redirect(w, r, redirectURL, http.StatusFound)
DTabidze0d802592024-03-19 17:42:45 +0400849 return
850 }
851 http.Redirect(w, r, "/", http.StatusSeeOther)
852}
853
Davit Tabidze75d57c32024-07-19 19:17:55 +0400854type GroupPageData struct {
855 GroupName string
856 Description string
857 Owners []string
858 Members []string
859 AllGroups []Group
860 TransitiveGroups []Group
861 ChildGroups []Group
862 OwnerGroups []Group
863 ErrorMessage string
864}
865
DTabidze0d802592024-03-19 17:42:45 +0400866func (s *Server) groupHandler(w http.ResponseWriter, r *http.Request) {
DTabidzec0b4d8f2024-03-22 17:25:10 +0400867 _, err := getLoggedInUser(r)
868 if err != nil {
869 http.Error(w, "User Not Logged In", http.StatusUnauthorized)
870 return
871 }
DTabidze4b44ff42024-04-02 03:16:26 +0400872 errorMsg := r.URL.Query().Get("errorMessage")
DTabidzed7744a62024-03-20 14:09:15 +0400873 vars := mux.Vars(r)
874 groupName := vars["group-name"]
DTabidze908bb852024-03-25 20:07:57 +0400875 exists, err := s.store.DoesGroupExist(groupName)
876 if err != nil {
877 http.Error(w, err.Error(), http.StatusInternalServerError)
878 return
879 }
880 if !exists {
DTabidze4b44ff42024-04-02 03:16:26 +0400881 errorMsg = fmt.Sprintf("group with the name '%s' not found", groupName)
DTabidze908bb852024-03-25 20:07:57 +0400882 http.Error(w, errorMsg, http.StatusNotFound)
883 return
884 }
DTabidze0d802592024-03-19 17:42:45 +0400885 if err != nil {
886 http.Error(w, err.Error(), http.StatusInternalServerError)
887 return
888 }
889 owners, err := s.store.GetGroupOwners(groupName)
890 if err != nil {
891 http.Error(w, err.Error(), http.StatusInternalServerError)
892 return
893 }
894 members, err := s.store.GetGroupMembers(groupName)
895 if err != nil {
896 http.Error(w, err.Error(), http.StatusInternalServerError)
897 return
898 }
899 description, err := s.store.GetGroupDescription(groupName)
900 if err != nil {
901 http.Error(w, err.Error(), http.StatusInternalServerError)
902 return
903 }
Davit Tabidzec0d2bf52024-04-03 15:39:33 +0400904 allGroups, err := s.store.GetAllGroups()
DTabidze0d802592024-03-19 17:42:45 +0400905 if err != nil {
906 http.Error(w, err.Error(), http.StatusInternalServerError)
907 return
908 }
DTabidzec0b4d8f2024-03-22 17:25:10 +0400909 transitiveGroups, err := s.store.GetAllTransitiveGroupsForGroup(groupName)
910 if err != nil {
911 http.Error(w, err.Error(), http.StatusInternalServerError)
912 return
913 }
914 childGroups, err := s.store.GetDirectChildrenGroups(groupName)
915 if err != nil {
916 http.Error(w, err.Error(), http.StatusInternalServerError)
917 return
918 }
Davit Tabidzec0d2bf52024-04-03 15:39:33 +0400919 ownerGroups, err := s.store.GetGroupOwnerGroups(groupName)
920 if err != nil {
921 http.Error(w, err.Error(), http.StatusInternalServerError)
922 return
923 }
Davit Tabidze75d57c32024-07-19 19:17:55 +0400924 data := GroupPageData{
DTabidzec0b4d8f2024-03-22 17:25:10 +0400925 GroupName: groupName,
926 Description: description,
927 Owners: owners,
928 Members: members,
Davit Tabidzec0d2bf52024-04-03 15:39:33 +0400929 AllGroups: allGroups,
DTabidzec0b4d8f2024-03-22 17:25:10 +0400930 TransitiveGroups: transitiveGroups,
931 ChildGroups: childGroups,
Davit Tabidzec0d2bf52024-04-03 15:39:33 +0400932 OwnerGroups: ownerGroups,
DTabidze4b44ff42024-04-02 03:16:26 +0400933 ErrorMessage: errorMsg,
DTabidze0d802592024-03-19 17:42:45 +0400934 }
DTabidze4b44ff42024-04-02 03:16:26 +0400935 templates, err := parseTemplates(tmpls)
936 if err != nil {
937 http.Error(w, err.Error(), http.StatusInternalServerError)
938 return
939 }
940 if err := templates.group.Execute(w, data); err != nil {
DTabidze0d802592024-03-19 17:42:45 +0400941 http.Error(w, err.Error(), http.StatusInternalServerError)
942 return
943 }
944}
945
DTabidze2b224bf2024-03-27 13:25:49 +0400946func (s *Server) removeChildGroupHandler(w http.ResponseWriter, r *http.Request) {
947 loggedInUser, err := getLoggedInUser(r)
948 if err != nil {
949 http.Error(w, "User Not Logged In", http.StatusUnauthorized)
950 return
951 }
Davit Tabidze75d57c32024-07-19 19:17:55 +0400952 vars := mux.Vars(r)
953 parentGroup := vars["parent-group"]
954 childGroup := vars["child-group"]
955 if err := isValidGroupName(parentGroup); err != nil {
956 http.Error(w, err.Error(), http.StatusBadRequest)
957 return
DTabidze2b224bf2024-03-27 13:25:49 +0400958 }
Davit Tabidze75d57c32024-07-19 19:17:55 +0400959 if err := isValidGroupName(childGroup); err != nil {
960 http.Error(w, err.Error(), http.StatusBadRequest)
961 return
962 }
963 if err := s.checkIsOwner(w, loggedInUser, parentGroup); err != nil {
964 redirectURL := fmt.Sprintf("/group/%s?errorMessage=%s", parentGroup, url.QueryEscape(err.Error()))
965 http.Redirect(w, r, redirectURL, http.StatusSeeOther)
966 return
967 }
968 err = s.store.RemoveFromGroupToGroup(parentGroup, childGroup)
969 if err != nil {
970 redirectURL := fmt.Sprintf("/group/%s?errorMessage=%s", parentGroup, url.QueryEscape(err.Error()))
971 http.Redirect(w, r, redirectURL, http.StatusFound)
972 return
973 }
974 http.Redirect(w, r, "/group/"+parentGroup, http.StatusSeeOther)
DTabidze2b224bf2024-03-27 13:25:49 +0400975}
976
DTabidze078385f2024-03-27 14:49:05 +0400977func (s *Server) removeOwnerFromGroupHandler(w http.ResponseWriter, r *http.Request) {
DTabidze2b224bf2024-03-27 13:25:49 +0400978 loggedInUser, err := getLoggedInUser(r)
979 if err != nil {
980 http.Error(w, "User Not Logged In", http.StatusUnauthorized)
981 return
982 }
Davit Tabidze75d57c32024-07-19 19:17:55 +0400983 vars := mux.Vars(r)
984 username := vars["username"]
985 groupName := vars["group-name"]
986 tableName := "owners"
987 if err := isValidGroupName(groupName); err != nil {
988 http.Error(w, err.Error(), http.StatusBadRequest)
989 return
DTabidze2b224bf2024-03-27 13:25:49 +0400990 }
Davit Tabidze75d57c32024-07-19 19:17:55 +0400991 if err := s.checkIsOwner(w, loggedInUser, groupName); err != nil {
992 redirectURL := fmt.Sprintf("/group/%s?errorMessage=%s", groupName, url.QueryEscape(err.Error()))
993 http.Redirect(w, r, redirectURL, http.StatusSeeOther)
994 return
995 }
996 err = s.store.RemoveUserFromTable(username, groupName, tableName)
997 if err != nil {
998 redirectURL := fmt.Sprintf("/group/%s?errorMessage=%s", groupName, url.QueryEscape(err.Error()))
999 http.Redirect(w, r, redirectURL, http.StatusFound)
1000 return
1001 }
1002 http.Redirect(w, r, "/group/"+groupName, http.StatusSeeOther)
DTabidze2b224bf2024-03-27 13:25:49 +04001003}
1004
DTabidze078385f2024-03-27 14:49:05 +04001005func (s *Server) removeMemberFromGroupHandler(w http.ResponseWriter, r *http.Request) {
1006 loggedInUser, err := getLoggedInUser(r)
1007 if err != nil {
1008 http.Error(w, "User Not Logged In", http.StatusUnauthorized)
1009 return
1010 }
Davit Tabidze75d57c32024-07-19 19:17:55 +04001011 vars := mux.Vars(r)
1012 username := vars["username"]
1013 groupName := vars["group-name"]
1014 tableName := "user_to_group"
1015 if err := isValidGroupName(groupName); err != nil {
1016 http.Error(w, err.Error(), http.StatusBadRequest)
1017 return
DTabidze078385f2024-03-27 14:49:05 +04001018 }
Davit Tabidze75d57c32024-07-19 19:17:55 +04001019 if err := s.checkIsOwner(w, loggedInUser, groupName); err != nil {
1020 redirectURL := fmt.Sprintf("/group/%s?errorMessage=%s", groupName, url.QueryEscape(err.Error()))
1021 http.Redirect(w, r, redirectURL, http.StatusSeeOther)
1022 return
1023 }
1024 err = s.store.RemoveUserFromTable(username, groupName, tableName)
1025 if err != nil {
1026 redirectURL := fmt.Sprintf("/group/%s?errorMessage=%s", groupName, url.QueryEscape(err.Error()))
1027 http.Redirect(w, r, redirectURL, http.StatusFound)
1028 return
1029 }
1030 http.Redirect(w, r, "/group/"+groupName, http.StatusSeeOther)
DTabidze078385f2024-03-27 14:49:05 +04001031}
1032
1033func (s *Server) addUserToGroupHandler(w http.ResponseWriter, r *http.Request) {
DTabidze0d802592024-03-19 17:42:45 +04001034 loggedInUser, err := getLoggedInUser(r)
1035 if err != nil {
1036 http.Error(w, "User Not Logged In", http.StatusUnauthorized)
1037 return
1038 }
DTabidze078385f2024-03-27 14:49:05 +04001039 vars := mux.Vars(r)
1040 groupName := vars["group-name"]
DTabidze908bb852024-03-25 20:07:57 +04001041 if err := isValidGroupName(groupName); err != nil {
1042 http.Error(w, err.Error(), http.StatusBadRequest)
1043 return
1044 }
1045 username := strings.ToLower(r.FormValue("username"))
1046 if username == "" {
1047 http.Error(w, "Username parameter is required", http.StatusBadRequest)
1048 return
1049 }
DTabidze0d802592024-03-19 17:42:45 +04001050 status, err := convertStatus(r.FormValue("status"))
1051 if err != nil {
1052 http.Error(w, err.Error(), http.StatusBadRequest)
1053 return
1054 }
Davit Tabidzec0d2bf52024-04-03 15:39:33 +04001055 if err := s.checkIsOwner(w, loggedInUser, groupName); err != nil {
1056 redirectURL := fmt.Sprintf("/group/%s?errorMessage=%s", groupName, url.QueryEscape(err.Error()))
1057 http.Redirect(w, r, redirectURL, http.StatusSeeOther)
DTabidze0d802592024-03-19 17:42:45 +04001058 return
1059 }
1060 switch status {
1061 case Owner:
1062 err = s.store.AddGroupOwner(username, groupName)
1063 case Member:
1064 err = s.store.AddGroupMember(username, groupName)
1065 default:
1066 http.Error(w, "Invalid status", http.StatusBadRequest)
1067 return
1068 }
1069 if err != nil {
DTabidze4b44ff42024-04-02 03:16:26 +04001070 redirectURL := fmt.Sprintf("/group/%s?errorMessage=%s", groupName, url.QueryEscape(err.Error()))
1071 http.Redirect(w, r, redirectURL, http.StatusFound)
DTabidze0d802592024-03-19 17:42:45 +04001072 return
1073 }
1074 http.Redirect(w, r, "/group/"+groupName, http.StatusSeeOther)
1075}
1076
1077func (s *Server) addChildGroupHandler(w http.ResponseWriter, r *http.Request) {
1078 // TODO(dtabidze): In future we might need to make one group OWNER of another and not just a member.
DTabidze0d802592024-03-19 17:42:45 +04001079 loggedInUser, err := getLoggedInUser(r)
1080 if err != nil {
1081 http.Error(w, "User Not Logged In", http.StatusUnauthorized)
1082 return
1083 }
DTabidze078385f2024-03-27 14:49:05 +04001084 vars := mux.Vars(r)
1085 parentGroup := vars["parent-group"]
DTabidze908bb852024-03-25 20:07:57 +04001086 if err := isValidGroupName(parentGroup); err != nil {
1087 http.Error(w, err.Error(), http.StatusBadRequest)
1088 return
1089 }
DTabidze0d802592024-03-19 17:42:45 +04001090 childGroup := r.FormValue("child-group")
DTabidze908bb852024-03-25 20:07:57 +04001091 if err := isValidGroupName(childGroup); err != nil {
1092 http.Error(w, err.Error(), http.StatusBadRequest)
1093 return
1094 }
Davit Tabidzec0d2bf52024-04-03 15:39:33 +04001095 if err := s.checkIsOwner(w, loggedInUser, parentGroup); err != nil {
1096 redirectURL := fmt.Sprintf("/group/%s?errorMessage=%s", parentGroup, url.QueryEscape(err.Error()))
1097 http.Redirect(w, r, redirectURL, http.StatusSeeOther)
DTabidze0d802592024-03-19 17:42:45 +04001098 return
1099 }
1100 if err := s.store.AddChildGroup(parentGroup, childGroup); err != nil {
DTabidze4b44ff42024-04-02 03:16:26 +04001101 redirectURL := fmt.Sprintf("/group/%s?errorMessage=%s", parentGroup, url.QueryEscape(err.Error()))
1102 http.Redirect(w, r, redirectURL, http.StatusFound)
DTabidze0d802592024-03-19 17:42:45 +04001103 return
1104 }
1105 http.Redirect(w, r, "/group/"+parentGroup, http.StatusSeeOther)
1106}
1107
Davit Tabidzec0d2bf52024-04-03 15:39:33 +04001108func (s *Server) addOwnerGroupHandler(w http.ResponseWriter, r *http.Request) {
Davit Tabidzec0d2bf52024-04-03 15:39:33 +04001109 loggedInUser, err := getLoggedInUser(r)
1110 if err != nil {
1111 http.Error(w, "User Not Logged In", http.StatusUnauthorized)
1112 return
1113 }
1114 vars := mux.Vars(r)
1115 ownedGroup := vars["owned-group"]
1116 if err := isValidGroupName(ownedGroup); err != nil {
1117 http.Error(w, err.Error(), http.StatusBadRequest)
1118 return
1119 }
1120 ownerGroup := r.FormValue("owner-group")
1121 if err := isValidGroupName(ownerGroup); err != nil {
1122 http.Error(w, err.Error(), http.StatusBadRequest)
1123 return
1124 }
1125 if err := s.checkIsOwner(w, loggedInUser, ownedGroup); err != nil {
1126 redirectURL := fmt.Sprintf("/group/%s?errorMessage=%s", ownedGroup, url.QueryEscape(err.Error()))
1127 http.Redirect(w, r, redirectURL, http.StatusSeeOther)
1128 return
1129 }
1130 if err := s.store.AddOwnerGroup(ownerGroup, ownedGroup); err != nil {
1131 redirectURL := fmt.Sprintf("/group/%s?errorMessage=%s", ownedGroup, url.QueryEscape(err.Error()))
1132 http.Redirect(w, r, redirectURL, http.StatusFound)
1133 return
1134 }
1135 http.Redirect(w, r, "/group/"+ownedGroup, http.StatusSeeOther)
1136}
1137
Davit Tabidze75d57c32024-07-19 19:17:55 +04001138func (s *Server) addSSHKeyForUserHandler(w http.ResponseWriter, r *http.Request) {
1139 defer s.pingAllSyncAddresses()
1140 loggedInUser, err := getLoggedInUser(r)
1141 if err != nil {
1142 http.Error(w, "User Not Logged In", http.StatusUnauthorized)
1143 return
1144 }
1145 vars := mux.Vars(r)
1146 username := vars["username"]
1147 if loggedInUser != username {
1148 http.Error(w, "You are not allowed to add SSH key for someone else", http.StatusUnauthorized)
1149 return
1150 }
1151 sshKey := r.FormValue("ssh-key")
1152 if sshKey == "" {
1153 http.Error(w, "SSH key not present", http.StatusBadRequest)
1154 return
1155 }
1156 if err := s.store.AddSSHKeyForUser(username, sshKey); err != nil {
1157 redirectURL := fmt.Sprintf("/user/%s?errorMessage=%s", loggedInUser, url.QueryEscape(err.Error()))
1158 http.Redirect(w, r, redirectURL, http.StatusFound)
1159 return
1160 }
1161 http.Redirect(w, r, "/user/"+loggedInUser, http.StatusSeeOther)
1162}
1163
1164func (s *Server) removeSSHKeyForUserHandler(w http.ResponseWriter, r *http.Request) {
1165 defer s.pingAllSyncAddresses()
1166 loggedInUser, err := getLoggedInUser(r)
1167 if err != nil {
1168 http.Error(w, "User Not Logged In", http.StatusUnauthorized)
1169 return
1170 }
1171 vars := mux.Vars(r)
1172 username := vars["username"]
1173 if loggedInUser != username {
1174 http.Error(w, "You are not allowed to remove SSH key for someone else", http.StatusUnauthorized)
1175 return
1176 }
1177 if err := r.ParseForm(); err != nil {
1178 http.Error(w, "Invalid request body", http.StatusBadRequest)
1179 return
1180 }
1181 sshKey := r.FormValue("ssh-key")
1182 if sshKey == "" {
1183 http.Error(w, "SSH key not present", http.StatusBadRequest)
1184 return
1185 }
1186 if err := s.store.RemoveSSHKeyForUser(username, sshKey); err != nil {
1187 redirectURL := fmt.Sprintf("/user/%s?errorMessage=%s", loggedInUser, url.QueryEscape(err.Error()))
1188 http.Redirect(w, r, redirectURL, http.StatusFound)
1189 return
1190 }
1191 http.Redirect(w, r, "/user/"+loggedInUser, http.StatusSeeOther)
1192}
1193
Giorgi Lekveishvili942c7612024-03-22 19:27:48 +04001194type initRequest struct {
gio2728e402024-08-01 18:14:21 +04001195 User string `json:"user"`
1196 Email string `json:"email"`
Giorgi Lekveishvili942c7612024-03-22 19:27:48 +04001197 Groups []string `json:"groups"`
1198}
1199
1200func (s *Server) apiInitHandler(w http.ResponseWriter, r *http.Request) {
1201 var req initRequest
1202 if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
1203 http.Error(w, err.Error(), http.StatusBadRequest)
1204 return
1205 }
gio2728e402024-08-01 18:14:21 +04001206 if err := s.store.Init(req.User, req.Email, req.Groups); err != nil {
Giorgi Lekveishvili942c7612024-03-22 19:27:48 +04001207 http.Error(w, err.Error(), http.StatusInternalServerError)
1208 return
1209 }
1210}
1211
1212type userInfo struct {
DTabidzed7744a62024-03-20 14:09:15 +04001213 MemberOf []string `json:"memberOf"`
1214}
1215
1216func (s *Server) apiMemberOfHandler(w http.ResponseWriter, r *http.Request) {
1217 vars := mux.Vars(r)
1218 user, ok := vars["username"]
DTabidze908bb852024-03-25 20:07:57 +04001219 if !ok || user == "" {
DTabidzed7744a62024-03-20 14:09:15 +04001220 http.Error(w, "Username parameter is required", http.StatusBadRequest)
1221 return
1222 }
DTabidze908bb852024-03-25 20:07:57 +04001223 user = strings.ToLower(user)
DTabidzed7744a62024-03-20 14:09:15 +04001224 transitiveGroups, err := s.store.GetAllTransitiveGroupsForUser(user)
1225 if err != nil {
1226 http.Error(w, err.Error(), http.StatusInternalServerError)
1227 return
1228 }
DTabidzec0b4d8f2024-03-22 17:25:10 +04001229 var groupNames []string
1230 for _, group := range transitiveGroups {
1231 groupNames = append(groupNames, group.Name)
1232 }
DTabidzed7744a62024-03-20 14:09:15 +04001233 w.Header().Set("Content-Type", "application/json")
Giorgi Lekveishvili942c7612024-03-22 19:27:48 +04001234 if err := json.NewEncoder(w).Encode(userInfo{groupNames}); err != nil {
DTabidzed7744a62024-03-20 14:09:15 +04001235 http.Error(w, err.Error(), http.StatusInternalServerError)
1236 return
1237 }
1238}
1239
Davit Tabidze75d57c32024-07-19 19:17:55 +04001240func (s *Server) apiGetAllUsers(w http.ResponseWriter, r *http.Request) {
1241 defer s.pingAllSyncAddresses()
1242 selfAddress := r.FormValue("selfAddress")
1243 if selfAddress != "" {
1244 s.addSyncAddress(selfAddress)
1245 }
Davit Tabidzef867f2d2024-07-24 18:06:25 +04001246 var users []User
1247 var err error
1248 groups := r.FormValue("groups")
1249 if groups == "" {
1250 users, err = s.store.GetUsers(nil)
1251 } else {
1252 uniqueUsers := make(map[string]struct{})
1253 g := strings.Split(groups, ",")
1254 uniqueTG := make(map[string]struct{})
1255 for _, group := range g {
1256 uniqueTG[group] = struct{}{}
1257 trGroups, err := s.store.GetAllTransitiveGroupsForGroup(group)
1258 if err != nil {
1259 http.Error(w, err.Error(), http.StatusInternalServerError)
1260 return
1261 }
1262 for _, tg := range trGroups {
1263 uniqueTG[tg.Name] = struct{}{}
1264 }
1265 }
1266 for group := range uniqueTG {
1267 u, err := s.store.GetGroupMembers(group)
1268 if err != nil {
1269 http.Error(w, err.Error(), http.StatusInternalServerError)
1270 return
1271 }
1272 for _, user := range u {
1273 uniqueUsers[user] = struct{}{}
1274 }
1275 }
1276 usernames := make([]string, 0, len(uniqueUsers))
1277 for username := range uniqueUsers {
1278 usernames = append(usernames, username)
1279 }
1280 users, err = s.store.GetUsers(usernames)
1281 }
Davit Tabidze75d57c32024-07-19 19:17:55 +04001282 if err != nil {
Davit Tabidzef867f2d2024-07-24 18:06:25 +04001283 http.Error(w, "Failed to retrieve user infos", http.StatusInternalServerError)
Davit Tabidze75d57c32024-07-19 19:17:55 +04001284 return
1285 }
1286 w.Header().Set("Content-Type", "application/json")
1287 if err := json.NewEncoder(w).Encode(users); err != nil {
1288 http.Error(w, err.Error(), http.StatusInternalServerError)
1289 return
1290 }
1291}
1292
gio2728e402024-08-01 18:14:21 +04001293type createUserRequest struct {
1294 User string `json:"user"`
1295 Email string `json:"email"`
1296}
1297
Davit Tabidze75d57c32024-07-19 19:17:55 +04001298func (s *Server) apiCreateUser(w http.ResponseWriter, r *http.Request) {
1299 defer s.pingAllSyncAddresses()
gio2728e402024-08-01 18:14:21 +04001300 var req createUserRequest
1301 if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
Davit Tabidze75d57c32024-07-19 19:17:55 +04001302 http.Error(w, "Invalid request body", http.StatusBadRequest)
1303 return
1304 }
gio2728e402024-08-01 18:14:21 +04001305 if req.User == "" {
Davit Tabidze75d57c32024-07-19 19:17:55 +04001306 http.Error(w, "Username cannot be empty", http.StatusBadRequest)
1307 return
1308 }
gio2728e402024-08-01 18:14:21 +04001309 if req.Email == "" {
Davit Tabidze75d57c32024-07-19 19:17:55 +04001310 http.Error(w, "Email cannot be empty", http.StatusBadRequest)
1311 return
1312 }
gio2728e402024-08-01 18:14:21 +04001313 if err := s.store.CreateUser(strings.ToLower(req.User), strings.ToLower(req.Email)); err != nil {
Davit Tabidze75d57c32024-07-19 19:17:55 +04001314 http.Error(w, err.Error(), http.StatusInternalServerError)
1315 return
1316 }
Davit Tabidze75d57c32024-07-19 19:17:55 +04001317}
1318
1319func (s *Server) pingAllSyncAddresses() {
1320 s.mu.Lock()
1321 defer s.mu.Unlock()
1322 for address := range s.syncAddresses {
1323 resp, err := http.Get(address)
1324 if err != nil {
1325 log.Printf("Failed to ping %s: %v", address, err)
1326 continue
1327 }
1328 resp.Body.Close()
1329 if resp.StatusCode != http.StatusOK {
1330 log.Printf("Ping to %s returned status %d", address, resp.StatusCode)
1331 }
1332 }
1333}
1334
1335func (s *Server) addSyncAddress(address string) {
1336 s.mu.Lock()
1337 defer s.mu.Unlock()
1338 s.syncAddresses[address] = struct{}{}
1339}
1340
DTabidze908bb852024-03-25 20:07:57 +04001341func convertStatus(status string) (Status, error) {
1342 switch status {
1343 case "Owner":
1344 return Owner, nil
1345 case "Member":
1346 return Member, nil
1347 default:
1348 return Owner, fmt.Errorf("invalid status: %s", status)
1349 }
1350}
1351
1352func isValidGroupName(group string) error {
1353 if strings.TrimSpace(group) == "" {
Davit Tabidze5f1a2c62024-07-17 17:57:27 +04001354 return fmt.Errorf("Group name can't be empty or contain only whitespaces")
DTabidze908bb852024-03-25 20:07:57 +04001355 }
1356 validGroupName := regexp.MustCompile(`^[a-z0-9\-_:.\/ ]+$`)
1357 if !validGroupName.MatchString(group) {
Davit Tabidze5f1a2c62024-07-17 17:57:27 +04001358 return fmt.Errorf("Group name should contain only lowercase letters, digits, -, _, :, ., /")
DTabidze908bb852024-03-25 20:07:57 +04001359 }
1360 return nil
1361}
1362
DTabidze0d802592024-03-19 17:42:45 +04001363func main() {
1364 flag.Parse()
DTabidzec0b4d8f2024-03-22 17:25:10 +04001365 db, err := sql.Open("sqlite3", *dbPath)
DTabidze0d802592024-03-19 17:42:45 +04001366 if err != nil {
1367 panic(err)
1368 }
DTabidzec0b4d8f2024-03-22 17:25:10 +04001369 store, err := NewSQLiteStore(db)
1370 if err != nil {
1371 panic(err)
1372 }
Davit Tabidze75d57c32024-07-19 19:17:55 +04001373 s := Server{
1374 store: store,
1375 syncAddresses: make(map[string]struct{}),
1376 mu: sync.Mutex{},
1377 }
Giorgi Lekveishvili329af572024-03-25 20:14:41 +04001378 log.Fatal(s.Start())
DTabidze0d802592024-03-19 17:42:45 +04001379}