blob: 2e523235e60ff2cf9a901e1484cf9b5d64fcceb3 [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) {
giodd213152024-09-27 11:26:59 +0200657 if user := r.Header.Get("X-Forwarded-User"); user != "" {
DTabidzec0b4d8f2024-03-22 17:25:10 +0400658 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()
gio59946282024-10-07 12:55:51 +0400676 r.PathPrefix("/static/").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)
gio7fbd4ad2024-08-27 10:06:39 +0400694 // TODO(gio): change to /api/users/{username}
695 r.HandleFunc("/api/users/{username}/keys", s.apiAddUserKey).Methods(http.MethodPost)
Giorgi Lekveishvili329af572024-03-25 20:14:41 +0400696 r.HandleFunc("/api/user/{username}", s.apiMemberOfHandler)
Davit Tabidze75d57c32024-07-19 19:17:55 +0400697 r.HandleFunc("/api/users", s.apiGetAllUsers).Methods(http.MethodGet)
698 r.HandleFunc("/api/users", s.apiCreateUser).Methods(http.MethodPost)
Giorgi Lekveishvili329af572024-03-25 20:14:41 +0400699 e <- http.ListenAndServe(fmt.Sprintf(":%d", *apiPort), r)
700 }()
701 return <-e
DTabidze0d802592024-03-19 17:42:45 +0400702}
703
704type GroupData struct {
705 Group Group
706 Membership string
707}
708
Davit Tabidzec0d2bf52024-04-03 15:39:33 +0400709func (s *Server) checkIsOwner(w http.ResponseWriter, user, group string) error {
DTabidze0d802592024-03-19 17:42:45 +0400710 isOwner, err := s.store.IsGroupOwner(user, group)
711 if err != nil {
Davit Tabidzec0d2bf52024-04-03 15:39:33 +0400712 return err
DTabidze0d802592024-03-19 17:42:45 +0400713 }
Davit Tabidzec0d2bf52024-04-03 15:39:33 +0400714 if isOwner {
715 return nil
DTabidze0d802592024-03-19 17:42:45 +0400716 }
Davit Tabidzec0d2bf52024-04-03 15:39:33 +0400717 // TODO(dtabidze): right now this only checks if user is member of just one lvl upper group. should add transitive group check.
718 isMemberOfOwnerGroup, err := s.store.IsMemberOfOwnerGroup(user, group)
719 if err != nil {
720 return err
721 }
722 if !isMemberOfOwnerGroup {
Davit Tabidze5f1a2c62024-07-17 17:57:27 +0400723 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 +0400724 }
725 return nil
DTabidze0d802592024-03-19 17:42:45 +0400726}
727
DTabidze4b44ff42024-04-02 03:16:26 +0400728type templates struct {
729 group *template.Template
730 user *template.Template
731}
732
733func parseTemplates(fs embed.FS) (templates, error) {
734 base, err := template.ParseFS(fs, "memberships-tmpl/base.html")
735 if err != nil {
736 return templates{}, err
737 }
738 parse := func(path string) (*template.Template, error) {
739 if b, err := base.Clone(); err != nil {
740 return nil, err
741 } else {
742 return b.ParseFS(fs, path)
743 }
744 }
745 user, err := parse("memberships-tmpl/user.html")
746 if err != nil {
747 return templates{}, err
748 }
749 group, err := parse("memberships-tmpl/group.html")
750 if err != nil {
751 return templates{}, err
752 }
753 return templates{group, user}, nil
754}
755
DTabidze0d802592024-03-19 17:42:45 +0400756func (s *Server) homePageHandler(w http.ResponseWriter, r *http.Request) {
757 loggedInUser, err := getLoggedInUser(r)
758 if err != nil {
759 http.Error(w, "User Not Logged In", http.StatusUnauthorized)
760 return
761 }
DTabidze5d735e32024-03-26 16:01:06 +0400762 http.Redirect(w, r, "/user/"+loggedInUser, http.StatusSeeOther)
763}
764
Davit Tabidze75d57c32024-07-19 19:17:55 +0400765type UserPageData struct {
766 OwnerGroups []Group
767 MembershipGroups []Group
768 TransitiveGroups []Group
769 LoggedInUserPage bool
770 CurrentUser string
771 SSHPublicKeys []string
772 Email string
773 ErrorMessage string
774}
775
DTabidze5d735e32024-03-26 16:01:06 +0400776func (s *Server) userHandler(w http.ResponseWriter, r *http.Request) {
777 loggedInUser, err := getLoggedInUser(r)
778 if err != nil {
779 http.Error(w, "User Not Logged In", http.StatusUnauthorized)
780 return
781 }
DTabidze4b44ff42024-04-02 03:16:26 +0400782 errorMsg := r.URL.Query().Get("errorMessage")
DTabidze5d735e32024-03-26 16:01:06 +0400783 vars := mux.Vars(r)
784 user := strings.ToLower(vars["username"])
785 // TODO(dtabidze): should check if username exists or not.
786 loggedInUserPage := loggedInUser == user
787 ownerGroups, err := s.store.GetGroupsOwnedBy(user)
DTabidze0d802592024-03-19 17:42:45 +0400788 if err != nil {
789 http.Error(w, err.Error(), http.StatusInternalServerError)
790 return
791 }
DTabidze5d735e32024-03-26 16:01:06 +0400792 membershipGroups, err := s.store.GetGroupsUserBelongsTo(user)
DTabidze0d802592024-03-19 17:42:45 +0400793 if err != nil {
794 http.Error(w, err.Error(), http.StatusInternalServerError)
795 return
796 }
DTabidze5d735e32024-03-26 16:01:06 +0400797 transitiveGroups, err := s.store.GetAllTransitiveGroupsForUser(user)
DTabidzec0b4d8f2024-03-22 17:25:10 +0400798 if err != nil {
799 http.Error(w, err.Error(), http.StatusInternalServerError)
800 return
801 }
Davit Tabidze75d57c32024-07-19 19:17:55 +0400802 userInfo, err := s.store.GetUser(user)
803 if err != nil {
804 http.Error(w, err.Error(), http.StatusInternalServerError)
805 return
806 }
807 data := UserPageData{
DTabidze0d802592024-03-19 17:42:45 +0400808 OwnerGroups: ownerGroups,
809 MembershipGroups: membershipGroups,
DTabidzec0b4d8f2024-03-22 17:25:10 +0400810 TransitiveGroups: transitiveGroups,
DTabidze5d735e32024-03-26 16:01:06 +0400811 LoggedInUserPage: loggedInUserPage,
812 CurrentUser: user,
Davit Tabidze75d57c32024-07-19 19:17:55 +0400813 SSHPublicKeys: userInfo.SSHPublicKeys,
814 Email: userInfo.Email,
DTabidze4b44ff42024-04-02 03:16:26 +0400815 ErrorMessage: errorMsg,
DTabidze0d802592024-03-19 17:42:45 +0400816 }
DTabidze4b44ff42024-04-02 03:16:26 +0400817 templates, err := parseTemplates(tmpls)
818 if err != nil {
819 http.Error(w, err.Error(), http.StatusInternalServerError)
820 return
821 }
822 if err := templates.user.Execute(w, data); err != nil {
DTabidze0d802592024-03-19 17:42:45 +0400823 http.Error(w, err.Error(), http.StatusInternalServerError)
824 return
825 }
826}
827
828func (s *Server) createGroupHandler(w http.ResponseWriter, r *http.Request) {
829 loggedInUser, err := getLoggedInUser(r)
830 if err != nil {
831 http.Error(w, "User Not Logged In", http.StatusUnauthorized)
832 return
833 }
DTabidze0d802592024-03-19 17:42:45 +0400834 if err := r.ParseForm(); err != nil {
835 http.Error(w, err.Error(), http.StatusInternalServerError)
836 return
837 }
838 var group Group
839 group.Name = r.PostFormValue("group-name")
DTabidze908bb852024-03-25 20:07:57 +0400840 if err := isValidGroupName(group.Name); err != nil {
DTabidze4b44ff42024-04-02 03:16:26 +0400841 // http.Error(w, err.Error(), http.StatusBadRequest)
842 redirectURL := fmt.Sprintf("/user/%s?errorMessage=%s", loggedInUser, url.QueryEscape(err.Error()))
843 http.Redirect(w, r, redirectURL, http.StatusFound)
DTabidze908bb852024-03-25 20:07:57 +0400844 return
845 }
DTabidze0d802592024-03-19 17:42:45 +0400846 group.Description = r.PostFormValue("description")
847 if err := s.store.CreateGroup(loggedInUser, group); err != nil {
DTabidze4b44ff42024-04-02 03:16:26 +0400848 // http.Error(w, err.Error(), http.StatusInternalServerError)
849 redirectURL := fmt.Sprintf("/user/%s?errorMessage=%s", loggedInUser, url.QueryEscape(err.Error()))
850 http.Redirect(w, r, redirectURL, http.StatusFound)
DTabidze0d802592024-03-19 17:42:45 +0400851 return
852 }
853 http.Redirect(w, r, "/", http.StatusSeeOther)
854}
855
Davit Tabidze75d57c32024-07-19 19:17:55 +0400856type GroupPageData struct {
857 GroupName string
858 Description string
859 Owners []string
860 Members []string
861 AllGroups []Group
862 TransitiveGroups []Group
863 ChildGroups []Group
864 OwnerGroups []Group
865 ErrorMessage string
866}
867
DTabidze0d802592024-03-19 17:42:45 +0400868func (s *Server) groupHandler(w http.ResponseWriter, r *http.Request) {
DTabidzec0b4d8f2024-03-22 17:25:10 +0400869 _, err := getLoggedInUser(r)
870 if err != nil {
871 http.Error(w, "User Not Logged In", http.StatusUnauthorized)
872 return
873 }
DTabidze4b44ff42024-04-02 03:16:26 +0400874 errorMsg := r.URL.Query().Get("errorMessage")
DTabidzed7744a62024-03-20 14:09:15 +0400875 vars := mux.Vars(r)
876 groupName := vars["group-name"]
DTabidze908bb852024-03-25 20:07:57 +0400877 exists, err := s.store.DoesGroupExist(groupName)
878 if err != nil {
879 http.Error(w, err.Error(), http.StatusInternalServerError)
880 return
881 }
882 if !exists {
DTabidze4b44ff42024-04-02 03:16:26 +0400883 errorMsg = fmt.Sprintf("group with the name '%s' not found", groupName)
DTabidze908bb852024-03-25 20:07:57 +0400884 http.Error(w, errorMsg, http.StatusNotFound)
885 return
886 }
DTabidze0d802592024-03-19 17:42:45 +0400887 if err != nil {
888 http.Error(w, err.Error(), http.StatusInternalServerError)
889 return
890 }
891 owners, err := s.store.GetGroupOwners(groupName)
892 if err != nil {
893 http.Error(w, err.Error(), http.StatusInternalServerError)
894 return
895 }
896 members, err := s.store.GetGroupMembers(groupName)
897 if err != nil {
898 http.Error(w, err.Error(), http.StatusInternalServerError)
899 return
900 }
901 description, err := s.store.GetGroupDescription(groupName)
902 if err != nil {
903 http.Error(w, err.Error(), http.StatusInternalServerError)
904 return
905 }
Davit Tabidzec0d2bf52024-04-03 15:39:33 +0400906 allGroups, err := s.store.GetAllGroups()
DTabidze0d802592024-03-19 17:42:45 +0400907 if err != nil {
908 http.Error(w, err.Error(), http.StatusInternalServerError)
909 return
910 }
DTabidzec0b4d8f2024-03-22 17:25:10 +0400911 transitiveGroups, err := s.store.GetAllTransitiveGroupsForGroup(groupName)
912 if err != nil {
913 http.Error(w, err.Error(), http.StatusInternalServerError)
914 return
915 }
916 childGroups, err := s.store.GetDirectChildrenGroups(groupName)
917 if err != nil {
918 http.Error(w, err.Error(), http.StatusInternalServerError)
919 return
920 }
Davit Tabidzec0d2bf52024-04-03 15:39:33 +0400921 ownerGroups, err := s.store.GetGroupOwnerGroups(groupName)
922 if err != nil {
923 http.Error(w, err.Error(), http.StatusInternalServerError)
924 return
925 }
Davit Tabidze75d57c32024-07-19 19:17:55 +0400926 data := GroupPageData{
DTabidzec0b4d8f2024-03-22 17:25:10 +0400927 GroupName: groupName,
928 Description: description,
929 Owners: owners,
930 Members: members,
Davit Tabidzec0d2bf52024-04-03 15:39:33 +0400931 AllGroups: allGroups,
DTabidzec0b4d8f2024-03-22 17:25:10 +0400932 TransitiveGroups: transitiveGroups,
933 ChildGroups: childGroups,
Davit Tabidzec0d2bf52024-04-03 15:39:33 +0400934 OwnerGroups: ownerGroups,
DTabidze4b44ff42024-04-02 03:16:26 +0400935 ErrorMessage: errorMsg,
DTabidze0d802592024-03-19 17:42:45 +0400936 }
DTabidze4b44ff42024-04-02 03:16:26 +0400937 templates, err := parseTemplates(tmpls)
938 if err != nil {
939 http.Error(w, err.Error(), http.StatusInternalServerError)
940 return
941 }
942 if err := templates.group.Execute(w, data); err != nil {
DTabidze0d802592024-03-19 17:42:45 +0400943 http.Error(w, err.Error(), http.StatusInternalServerError)
944 return
945 }
946}
947
DTabidze2b224bf2024-03-27 13:25:49 +0400948func (s *Server) removeChildGroupHandler(w http.ResponseWriter, r *http.Request) {
949 loggedInUser, err := getLoggedInUser(r)
950 if err != nil {
951 http.Error(w, "User Not Logged In", http.StatusUnauthorized)
952 return
953 }
Davit Tabidze75d57c32024-07-19 19:17:55 +0400954 vars := mux.Vars(r)
955 parentGroup := vars["parent-group"]
956 childGroup := vars["child-group"]
957 if err := isValidGroupName(parentGroup); err != nil {
958 http.Error(w, err.Error(), http.StatusBadRequest)
959 return
DTabidze2b224bf2024-03-27 13:25:49 +0400960 }
Davit Tabidze75d57c32024-07-19 19:17:55 +0400961 if err := isValidGroupName(childGroup); err != nil {
962 http.Error(w, err.Error(), http.StatusBadRequest)
963 return
964 }
965 if err := s.checkIsOwner(w, loggedInUser, parentGroup); err != nil {
966 redirectURL := fmt.Sprintf("/group/%s?errorMessage=%s", parentGroup, url.QueryEscape(err.Error()))
967 http.Redirect(w, r, redirectURL, http.StatusSeeOther)
968 return
969 }
970 err = s.store.RemoveFromGroupToGroup(parentGroup, childGroup)
971 if err != nil {
972 redirectURL := fmt.Sprintf("/group/%s?errorMessage=%s", parentGroup, url.QueryEscape(err.Error()))
973 http.Redirect(w, r, redirectURL, http.StatusFound)
974 return
975 }
976 http.Redirect(w, r, "/group/"+parentGroup, http.StatusSeeOther)
DTabidze2b224bf2024-03-27 13:25:49 +0400977}
978
DTabidze078385f2024-03-27 14:49:05 +0400979func (s *Server) removeOwnerFromGroupHandler(w http.ResponseWriter, r *http.Request) {
DTabidze2b224bf2024-03-27 13:25:49 +0400980 loggedInUser, err := getLoggedInUser(r)
981 if err != nil {
982 http.Error(w, "User Not Logged In", http.StatusUnauthorized)
983 return
984 }
Davit Tabidze75d57c32024-07-19 19:17:55 +0400985 vars := mux.Vars(r)
986 username := vars["username"]
987 groupName := vars["group-name"]
988 tableName := "owners"
989 if err := isValidGroupName(groupName); err != nil {
990 http.Error(w, err.Error(), http.StatusBadRequest)
991 return
DTabidze2b224bf2024-03-27 13:25:49 +0400992 }
Davit Tabidze75d57c32024-07-19 19:17:55 +0400993 if err := s.checkIsOwner(w, loggedInUser, groupName); err != nil {
994 redirectURL := fmt.Sprintf("/group/%s?errorMessage=%s", groupName, url.QueryEscape(err.Error()))
995 http.Redirect(w, r, redirectURL, http.StatusSeeOther)
996 return
997 }
998 err = s.store.RemoveUserFromTable(username, groupName, tableName)
999 if err != nil {
1000 redirectURL := fmt.Sprintf("/group/%s?errorMessage=%s", groupName, url.QueryEscape(err.Error()))
1001 http.Redirect(w, r, redirectURL, http.StatusFound)
1002 return
1003 }
1004 http.Redirect(w, r, "/group/"+groupName, http.StatusSeeOther)
DTabidze2b224bf2024-03-27 13:25:49 +04001005}
1006
DTabidze078385f2024-03-27 14:49:05 +04001007func (s *Server) removeMemberFromGroupHandler(w http.ResponseWriter, r *http.Request) {
1008 loggedInUser, err := getLoggedInUser(r)
1009 if err != nil {
1010 http.Error(w, "User Not Logged In", http.StatusUnauthorized)
1011 return
1012 }
Davit Tabidze75d57c32024-07-19 19:17:55 +04001013 vars := mux.Vars(r)
1014 username := vars["username"]
1015 groupName := vars["group-name"]
1016 tableName := "user_to_group"
1017 if err := isValidGroupName(groupName); err != nil {
1018 http.Error(w, err.Error(), http.StatusBadRequest)
1019 return
DTabidze078385f2024-03-27 14:49:05 +04001020 }
Davit Tabidze75d57c32024-07-19 19:17:55 +04001021 if err := s.checkIsOwner(w, loggedInUser, groupName); err != nil {
1022 redirectURL := fmt.Sprintf("/group/%s?errorMessage=%s", groupName, url.QueryEscape(err.Error()))
1023 http.Redirect(w, r, redirectURL, http.StatusSeeOther)
1024 return
1025 }
1026 err = s.store.RemoveUserFromTable(username, groupName, tableName)
1027 if err != nil {
1028 redirectURL := fmt.Sprintf("/group/%s?errorMessage=%s", groupName, url.QueryEscape(err.Error()))
1029 http.Redirect(w, r, redirectURL, http.StatusFound)
1030 return
1031 }
1032 http.Redirect(w, r, "/group/"+groupName, http.StatusSeeOther)
DTabidze078385f2024-03-27 14:49:05 +04001033}
1034
1035func (s *Server) addUserToGroupHandler(w http.ResponseWriter, r *http.Request) {
DTabidze0d802592024-03-19 17:42:45 +04001036 loggedInUser, err := getLoggedInUser(r)
1037 if err != nil {
1038 http.Error(w, "User Not Logged In", http.StatusUnauthorized)
1039 return
1040 }
DTabidze078385f2024-03-27 14:49:05 +04001041 vars := mux.Vars(r)
1042 groupName := vars["group-name"]
DTabidze908bb852024-03-25 20:07:57 +04001043 if err := isValidGroupName(groupName); err != nil {
1044 http.Error(w, err.Error(), http.StatusBadRequest)
1045 return
1046 }
1047 username := strings.ToLower(r.FormValue("username"))
1048 if username == "" {
1049 http.Error(w, "Username parameter is required", http.StatusBadRequest)
1050 return
1051 }
DTabidze0d802592024-03-19 17:42:45 +04001052 status, err := convertStatus(r.FormValue("status"))
1053 if err != nil {
1054 http.Error(w, err.Error(), http.StatusBadRequest)
1055 return
1056 }
Davit Tabidzec0d2bf52024-04-03 15:39:33 +04001057 if err := s.checkIsOwner(w, loggedInUser, groupName); err != nil {
1058 redirectURL := fmt.Sprintf("/group/%s?errorMessage=%s", groupName, url.QueryEscape(err.Error()))
1059 http.Redirect(w, r, redirectURL, http.StatusSeeOther)
DTabidze0d802592024-03-19 17:42:45 +04001060 return
1061 }
1062 switch status {
1063 case Owner:
1064 err = s.store.AddGroupOwner(username, groupName)
1065 case Member:
1066 err = s.store.AddGroupMember(username, groupName)
1067 default:
1068 http.Error(w, "Invalid status", http.StatusBadRequest)
1069 return
1070 }
1071 if err != nil {
DTabidze4b44ff42024-04-02 03:16:26 +04001072 redirectURL := fmt.Sprintf("/group/%s?errorMessage=%s", groupName, url.QueryEscape(err.Error()))
1073 http.Redirect(w, r, redirectURL, http.StatusFound)
DTabidze0d802592024-03-19 17:42:45 +04001074 return
1075 }
1076 http.Redirect(w, r, "/group/"+groupName, http.StatusSeeOther)
1077}
1078
1079func (s *Server) addChildGroupHandler(w http.ResponseWriter, r *http.Request) {
1080 // 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 +04001081 loggedInUser, err := getLoggedInUser(r)
1082 if err != nil {
1083 http.Error(w, "User Not Logged In", http.StatusUnauthorized)
1084 return
1085 }
DTabidze078385f2024-03-27 14:49:05 +04001086 vars := mux.Vars(r)
1087 parentGroup := vars["parent-group"]
DTabidze908bb852024-03-25 20:07:57 +04001088 if err := isValidGroupName(parentGroup); err != nil {
1089 http.Error(w, err.Error(), http.StatusBadRequest)
1090 return
1091 }
DTabidze0d802592024-03-19 17:42:45 +04001092 childGroup := r.FormValue("child-group")
DTabidze908bb852024-03-25 20:07:57 +04001093 if err := isValidGroupName(childGroup); err != nil {
1094 http.Error(w, err.Error(), http.StatusBadRequest)
1095 return
1096 }
Davit Tabidzec0d2bf52024-04-03 15:39:33 +04001097 if err := s.checkIsOwner(w, loggedInUser, parentGroup); err != nil {
1098 redirectURL := fmt.Sprintf("/group/%s?errorMessage=%s", parentGroup, url.QueryEscape(err.Error()))
1099 http.Redirect(w, r, redirectURL, http.StatusSeeOther)
DTabidze0d802592024-03-19 17:42:45 +04001100 return
1101 }
1102 if err := s.store.AddChildGroup(parentGroup, childGroup); err != nil {
DTabidze4b44ff42024-04-02 03:16:26 +04001103 redirectURL := fmt.Sprintf("/group/%s?errorMessage=%s", parentGroup, url.QueryEscape(err.Error()))
1104 http.Redirect(w, r, redirectURL, http.StatusFound)
DTabidze0d802592024-03-19 17:42:45 +04001105 return
1106 }
1107 http.Redirect(w, r, "/group/"+parentGroup, http.StatusSeeOther)
1108}
1109
Davit Tabidzec0d2bf52024-04-03 15:39:33 +04001110func (s *Server) addOwnerGroupHandler(w http.ResponseWriter, r *http.Request) {
Davit Tabidzec0d2bf52024-04-03 15:39:33 +04001111 loggedInUser, err := getLoggedInUser(r)
1112 if err != nil {
1113 http.Error(w, "User Not Logged In", http.StatusUnauthorized)
1114 return
1115 }
1116 vars := mux.Vars(r)
1117 ownedGroup := vars["owned-group"]
1118 if err := isValidGroupName(ownedGroup); err != nil {
1119 http.Error(w, err.Error(), http.StatusBadRequest)
1120 return
1121 }
1122 ownerGroup := r.FormValue("owner-group")
1123 if err := isValidGroupName(ownerGroup); err != nil {
1124 http.Error(w, err.Error(), http.StatusBadRequest)
1125 return
1126 }
1127 if err := s.checkIsOwner(w, loggedInUser, ownedGroup); err != nil {
1128 redirectURL := fmt.Sprintf("/group/%s?errorMessage=%s", ownedGroup, url.QueryEscape(err.Error()))
1129 http.Redirect(w, r, redirectURL, http.StatusSeeOther)
1130 return
1131 }
1132 if err := s.store.AddOwnerGroup(ownerGroup, ownedGroup); err != nil {
1133 redirectURL := fmt.Sprintf("/group/%s?errorMessage=%s", ownedGroup, url.QueryEscape(err.Error()))
1134 http.Redirect(w, r, redirectURL, http.StatusFound)
1135 return
1136 }
1137 http.Redirect(w, r, "/group/"+ownedGroup, http.StatusSeeOther)
1138}
1139
Davit Tabidze75d57c32024-07-19 19:17:55 +04001140func (s *Server) addSSHKeyForUserHandler(w http.ResponseWriter, r *http.Request) {
1141 defer s.pingAllSyncAddresses()
1142 loggedInUser, err := getLoggedInUser(r)
1143 if err != nil {
1144 http.Error(w, "User Not Logged In", http.StatusUnauthorized)
1145 return
1146 }
1147 vars := mux.Vars(r)
1148 username := vars["username"]
1149 if loggedInUser != username {
1150 http.Error(w, "You are not allowed to add SSH key for someone else", http.StatusUnauthorized)
1151 return
1152 }
1153 sshKey := r.FormValue("ssh-key")
1154 if sshKey == "" {
1155 http.Error(w, "SSH key not present", http.StatusBadRequest)
1156 return
1157 }
gio7fbd4ad2024-08-27 10:06:39 +04001158 if err := s.store.AddSSHKeyForUser(strings.ToLower(username), sshKey); err != nil {
Davit Tabidze75d57c32024-07-19 19:17:55 +04001159 redirectURL := fmt.Sprintf("/user/%s?errorMessage=%s", loggedInUser, url.QueryEscape(err.Error()))
1160 http.Redirect(w, r, redirectURL, http.StatusFound)
1161 return
1162 }
1163 http.Redirect(w, r, "/user/"+loggedInUser, http.StatusSeeOther)
1164}
1165
1166func (s *Server) removeSSHKeyForUserHandler(w http.ResponseWriter, r *http.Request) {
1167 defer s.pingAllSyncAddresses()
1168 loggedInUser, err := getLoggedInUser(r)
1169 if err != nil {
1170 http.Error(w, "User Not Logged In", http.StatusUnauthorized)
1171 return
1172 }
1173 vars := mux.Vars(r)
1174 username := vars["username"]
1175 if loggedInUser != username {
1176 http.Error(w, "You are not allowed to remove SSH key for someone else", http.StatusUnauthorized)
1177 return
1178 }
1179 if err := r.ParseForm(); err != nil {
1180 http.Error(w, "Invalid request body", http.StatusBadRequest)
1181 return
1182 }
1183 sshKey := r.FormValue("ssh-key")
1184 if sshKey == "" {
1185 http.Error(w, "SSH key not present", http.StatusBadRequest)
1186 return
1187 }
1188 if err := s.store.RemoveSSHKeyForUser(username, sshKey); err != nil {
1189 redirectURL := fmt.Sprintf("/user/%s?errorMessage=%s", loggedInUser, url.QueryEscape(err.Error()))
1190 http.Redirect(w, r, redirectURL, http.StatusFound)
1191 return
1192 }
1193 http.Redirect(w, r, "/user/"+loggedInUser, http.StatusSeeOther)
1194}
1195
Giorgi Lekveishvili942c7612024-03-22 19:27:48 +04001196type initRequest struct {
gio2728e402024-08-01 18:14:21 +04001197 User string `json:"user"`
1198 Email string `json:"email"`
Giorgi Lekveishvili942c7612024-03-22 19:27:48 +04001199 Groups []string `json:"groups"`
1200}
1201
1202func (s *Server) apiInitHandler(w http.ResponseWriter, r *http.Request) {
1203 var req initRequest
1204 if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
1205 http.Error(w, err.Error(), http.StatusBadRequest)
1206 return
1207 }
gio2728e402024-08-01 18:14:21 +04001208 if err := s.store.Init(req.User, req.Email, req.Groups); err != nil {
Giorgi Lekveishvili942c7612024-03-22 19:27:48 +04001209 http.Error(w, err.Error(), http.StatusInternalServerError)
1210 return
1211 }
1212}
1213
1214type userInfo struct {
DTabidzed7744a62024-03-20 14:09:15 +04001215 MemberOf []string `json:"memberOf"`
1216}
1217
1218func (s *Server) apiMemberOfHandler(w http.ResponseWriter, r *http.Request) {
1219 vars := mux.Vars(r)
1220 user, ok := vars["username"]
DTabidze908bb852024-03-25 20:07:57 +04001221 if !ok || user == "" {
DTabidzed7744a62024-03-20 14:09:15 +04001222 http.Error(w, "Username parameter is required", http.StatusBadRequest)
1223 return
1224 }
DTabidze908bb852024-03-25 20:07:57 +04001225 user = strings.ToLower(user)
DTabidzed7744a62024-03-20 14:09:15 +04001226 transitiveGroups, err := s.store.GetAllTransitiveGroupsForUser(user)
1227 if err != nil {
1228 http.Error(w, err.Error(), http.StatusInternalServerError)
1229 return
1230 }
DTabidzec0b4d8f2024-03-22 17:25:10 +04001231 var groupNames []string
1232 for _, group := range transitiveGroups {
1233 groupNames = append(groupNames, group.Name)
1234 }
DTabidzed7744a62024-03-20 14:09:15 +04001235 w.Header().Set("Content-Type", "application/json")
Giorgi Lekveishvili942c7612024-03-22 19:27:48 +04001236 if err := json.NewEncoder(w).Encode(userInfo{groupNames}); err != nil {
DTabidzed7744a62024-03-20 14:09:15 +04001237 http.Error(w, err.Error(), http.StatusInternalServerError)
1238 return
1239 }
1240}
1241
Davit Tabidze75d57c32024-07-19 19:17:55 +04001242func (s *Server) apiGetAllUsers(w http.ResponseWriter, r *http.Request) {
gio7fbd4ad2024-08-27 10:06:39 +04001243 s.addSyncAddress(r.FormValue("selfAddress"))
Davit Tabidzef867f2d2024-07-24 18:06:25 +04001244 var users []User
1245 var err error
1246 groups := r.FormValue("groups")
1247 if groups == "" {
1248 users, err = s.store.GetUsers(nil)
1249 } else {
1250 uniqueUsers := make(map[string]struct{})
1251 g := strings.Split(groups, ",")
1252 uniqueTG := make(map[string]struct{})
1253 for _, group := range g {
1254 uniqueTG[group] = struct{}{}
1255 trGroups, err := s.store.GetAllTransitiveGroupsForGroup(group)
1256 if err != nil {
1257 http.Error(w, err.Error(), http.StatusInternalServerError)
1258 return
1259 }
1260 for _, tg := range trGroups {
1261 uniqueTG[tg.Name] = struct{}{}
1262 }
1263 }
1264 for group := range uniqueTG {
1265 u, err := s.store.GetGroupMembers(group)
1266 if err != nil {
1267 http.Error(w, err.Error(), http.StatusInternalServerError)
1268 return
1269 }
1270 for _, user := range u {
1271 uniqueUsers[user] = struct{}{}
1272 }
1273 }
1274 usernames := make([]string, 0, len(uniqueUsers))
1275 for username := range uniqueUsers {
1276 usernames = append(usernames, username)
1277 }
1278 users, err = s.store.GetUsers(usernames)
1279 }
Davit Tabidze75d57c32024-07-19 19:17:55 +04001280 if err != nil {
Davit Tabidzef867f2d2024-07-24 18:06:25 +04001281 http.Error(w, "Failed to retrieve user infos", http.StatusInternalServerError)
Davit Tabidze75d57c32024-07-19 19:17:55 +04001282 return
1283 }
1284 w.Header().Set("Content-Type", "application/json")
1285 if err := json.NewEncoder(w).Encode(users); err != nil {
1286 http.Error(w, err.Error(), http.StatusInternalServerError)
1287 return
1288 }
1289}
1290
gio2728e402024-08-01 18:14:21 +04001291type createUserRequest struct {
1292 User string `json:"user"`
1293 Email string `json:"email"`
1294}
1295
Davit Tabidze75d57c32024-07-19 19:17:55 +04001296func (s *Server) apiCreateUser(w http.ResponseWriter, r *http.Request) {
1297 defer s.pingAllSyncAddresses()
gio2728e402024-08-01 18:14:21 +04001298 var req createUserRequest
1299 if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
Davit Tabidze75d57c32024-07-19 19:17:55 +04001300 http.Error(w, "Invalid request body", http.StatusBadRequest)
1301 return
1302 }
gio2728e402024-08-01 18:14:21 +04001303 if req.User == "" {
Davit Tabidze75d57c32024-07-19 19:17:55 +04001304 http.Error(w, "Username cannot be empty", http.StatusBadRequest)
1305 return
1306 }
gio2728e402024-08-01 18:14:21 +04001307 if req.Email == "" {
Davit Tabidze75d57c32024-07-19 19:17:55 +04001308 http.Error(w, "Email cannot be empty", http.StatusBadRequest)
1309 return
1310 }
gio2728e402024-08-01 18:14:21 +04001311 if err := s.store.CreateUser(strings.ToLower(req.User), strings.ToLower(req.Email)); err != nil {
Davit Tabidze75d57c32024-07-19 19:17:55 +04001312 http.Error(w, err.Error(), http.StatusInternalServerError)
1313 return
1314 }
Davit Tabidze75d57c32024-07-19 19:17:55 +04001315}
1316
gio7fbd4ad2024-08-27 10:06:39 +04001317type addUserKeyRequest struct {
1318 User string `json:"user"`
1319 PublicKey string `json:"publicKey"`
1320}
1321
1322func (s *Server) apiAddUserKey(w http.ResponseWriter, r *http.Request) {
1323 var req addUserKeyRequest
1324 if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
1325 http.Error(w, "Invalid request body", http.StatusBadRequest)
1326 return
1327 }
1328 if req.User == "" {
1329 http.Error(w, "Username cannot be empty", http.StatusBadRequest)
1330 return
1331 }
1332 if req.PublicKey == "" {
1333 http.Error(w, "PublicKey cannot be empty", http.StatusBadRequest)
1334 return
1335 }
1336 if err := s.store.AddSSHKeyForUser(strings.ToLower(req.User), req.PublicKey); err != nil {
1337 http.Error(w, err.Error(), http.StatusInternalServerError)
1338 return
1339 }
1340}
1341
1342// TODO(gio): enque sync event instead of directly reaching out to clients.
1343// This will allow to deduplicate sync events and save resources.
Davit Tabidze75d57c32024-07-19 19:17:55 +04001344func (s *Server) pingAllSyncAddresses() {
1345 s.mu.Lock()
1346 defer s.mu.Unlock()
1347 for address := range s.syncAddresses {
gio7fbd4ad2024-08-27 10:06:39 +04001348 go func(address string) {
1349 log.Printf("Pinging %s", address)
1350 resp, err := http.Get(address)
1351 if err != nil {
1352 // TODO(gio): remove sync address after N number of failures.
1353 log.Printf("Failed to ping %s: %v", address, err)
1354 return
1355 }
1356 defer resp.Body.Close()
1357 if resp.StatusCode != http.StatusOK {
1358 log.Printf("Ping to %s returned status %d", address, resp.StatusCode)
1359 }
1360 }(address)
Davit Tabidze75d57c32024-07-19 19:17:55 +04001361 }
1362}
1363
1364func (s *Server) addSyncAddress(address string) {
gio7fbd4ad2024-08-27 10:06:39 +04001365 if address == "" {
1366 return
1367 }
1368 fmt.Printf("Adding sync address: %s\n", address)
Davit Tabidze75d57c32024-07-19 19:17:55 +04001369 s.mu.Lock()
1370 defer s.mu.Unlock()
1371 s.syncAddresses[address] = struct{}{}
1372}
1373
DTabidze908bb852024-03-25 20:07:57 +04001374func convertStatus(status string) (Status, error) {
1375 switch status {
1376 case "Owner":
1377 return Owner, nil
1378 case "Member":
1379 return Member, nil
1380 default:
1381 return Owner, fmt.Errorf("invalid status: %s", status)
1382 }
1383}
1384
1385func isValidGroupName(group string) error {
1386 if strings.TrimSpace(group) == "" {
Davit Tabidze5f1a2c62024-07-17 17:57:27 +04001387 return fmt.Errorf("Group name can't be empty or contain only whitespaces")
DTabidze908bb852024-03-25 20:07:57 +04001388 }
1389 validGroupName := regexp.MustCompile(`^[a-z0-9\-_:.\/ ]+$`)
1390 if !validGroupName.MatchString(group) {
Davit Tabidze5f1a2c62024-07-17 17:57:27 +04001391 return fmt.Errorf("Group name should contain only lowercase letters, digits, -, _, :, ., /")
DTabidze908bb852024-03-25 20:07:57 +04001392 }
1393 return nil
1394}
1395
DTabidze0d802592024-03-19 17:42:45 +04001396func main() {
1397 flag.Parse()
DTabidzec0b4d8f2024-03-22 17:25:10 +04001398 db, err := sql.Open("sqlite3", *dbPath)
DTabidze0d802592024-03-19 17:42:45 +04001399 if err != nil {
1400 panic(err)
1401 }
DTabidzec0b4d8f2024-03-22 17:25:10 +04001402 store, err := NewSQLiteStore(db)
1403 if err != nil {
1404 panic(err)
1405 }
Davit Tabidze75d57c32024-07-19 19:17:55 +04001406 s := Server{
1407 store: store,
1408 syncAddresses: make(map[string]struct{}),
1409 mu: sync.Mutex{},
1410 }
Giorgi Lekveishvili329af572024-03-25 20:14:41 +04001411 log.Fatal(s.Start())
DTabidze0d802592024-03-19 17:42:45 +04001412}