blob: c7df14ef31520e632d3b88b9c4c5c2630271c8b6 [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
gio4a297752025-07-15 13:24:57 +040031//go:embed static/*
DTabidze0d802592024-03-19 17:42:45 +040032var staticResources embed.FS
33
gio4a297752025-07-15 13:24:57 +040034type cachingHandler struct {
35 h http.Handler
36}
37
38func (h cachingHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
39 w.Header().Set("Cache-Control", "max-age=604800")
40 h.h.ServeHTTP(w, r)
41}
42
DTabidze0d802592024-03-19 17:42:45 +040043type Store interface {
Giorgi Lekveishvili942c7612024-03-22 19:27:48 +040044 // Initializes store with admin user and their groups.
gio2728e402024-08-01 18:14:21 +040045 Init(user, email string, groups []string) error
DTabidze0d802592024-03-19 17:42:45 +040046 CreateGroup(owner string, group Group) error
47 AddChildGroup(parent, child string) error
Davit Tabidzec0d2bf52024-04-03 15:39:33 +040048 AddOwnerGroup(owned_group, owner_group string) error
DTabidze908bb852024-03-25 20:07:57 +040049 DoesGroupExist(group string) (bool, error)
DTabidze0d802592024-03-19 17:42:45 +040050 GetGroupsOwnedBy(user string) ([]Group, error)
DTabidzed7744a62024-03-20 14:09:15 +040051 GetGroupsUserBelongsTo(user string) ([]Group, error)
DTabidze0d802592024-03-19 17:42:45 +040052 IsGroupOwner(user, group string) (bool, error)
Davit Tabidzec0d2bf52024-04-03 15:39:33 +040053 IsMemberOfOwnerGroup(user, group string) (bool, error)
DTabidze0d802592024-03-19 17:42:45 +040054 AddGroupMember(user, group string) error
55 AddGroupOwner(user, group string) error
56 GetGroupOwners(group string) ([]string, error)
Davit Tabidzec0d2bf52024-04-03 15:39:33 +040057 GetGroupOwnerGroups(group string) ([]Group, error)
DTabidze0d802592024-03-19 17:42:45 +040058 GetGroupMembers(group string) ([]string, error)
59 GetGroupDescription(group string) (string, error)
DTabidzec0b4d8f2024-03-22 17:25:10 +040060 GetAllTransitiveGroupsForUser(user string) ([]Group, error)
61 GetGroupsGroupBelongsTo(group string) ([]Group, error)
62 GetDirectChildrenGroups(group string) ([]Group, error)
63 GetAllTransitiveGroupsForGroup(group string) ([]Group, error)
DTabidze2b224bf2024-03-27 13:25:49 +040064 RemoveFromGroupToGroup(parent, child string) error
65 RemoveUserFromTable(username, groupName, tableName string) error
Davit Tabidzec0d2bf52024-04-03 15:39:33 +040066 GetAllGroups() ([]Group, error)
Davit Tabidzef867f2d2024-07-24 18:06:25 +040067 GetUsers(username []string) ([]User, error)
Davit Tabidze75d57c32024-07-19 19:17:55 +040068 GetUser(username string) (User, error)
69 AddSSHKeyForUser(username, sshKey string) error
70 RemoveSSHKeyForUser(username, sshKey string) error
71 CreateUser(user, email string) error
DTabidze0d802592024-03-19 17:42:45 +040072}
73
74type Server struct {
Davit Tabidze75d57c32024-07-19 19:17:55 +040075 store Store
76 syncAddresses map[string]struct{}
77 mu sync.Mutex
DTabidze0d802592024-03-19 17:42:45 +040078}
79
80type Group struct {
81 Name string
82 Description string
83}
84
Davit Tabidze75d57c32024-07-19 19:17:55 +040085type User struct {
86 Username string `json:"username"`
87 Email string `json:"email"`
88 SSHPublicKeys []string `json:"sshPublicKeys,omitempty"`
89}
90
DTabidze0d802592024-03-19 17:42:45 +040091type SQLiteStore struct {
92 db *sql.DB
93}
94
Davit Tabidzec0d2bf52024-04-03 15:39:33 +040095const (
96 ErrorUniqueConstraintViolation = 2067
97 ErrorConstraintPrimaryKeyViolation = 1555
98)
99
DTabidzec0b4d8f2024-03-22 17:25:10 +0400100func NewSQLiteStore(db *sql.DB) (*SQLiteStore, error) {
101 _, err := db.Exec(`
Davit Tabidzec0d2bf52024-04-03 15:39:33 +0400102 CREATE TABLE IF NOT EXISTS groups (
103 name TEXT PRIMARY KEY,
104 description TEXT
105 );
106 CREATE TABLE IF NOT EXISTS owners (
107 username TEXT,
108 group_name TEXT,
109 FOREIGN KEY(group_name) REFERENCES groups(name),
110 UNIQUE (username, group_name)
111 );
112 CREATE TABLE IF NOT EXISTS owner_groups (
113 owner_group TEXT,
114 owned_group TEXT,
115 FOREIGN KEY(owner_group) REFERENCES groups(name),
116 FOREIGN KEY(owned_group) REFERENCES groups(name),
117 UNIQUE (owner_group, owned_group)
118 );
119 CREATE TABLE IF NOT EXISTS group_to_group (
120 parent_group TEXT,
121 child_group TEXT,
122 FOREIGN KEY(parent_group) REFERENCES groups(name),
123 FOREIGN KEY(child_group) REFERENCES groups(name),
124 UNIQUE (parent_group, child_group)
125 );
126 CREATE TABLE IF NOT EXISTS user_to_group (
127 username TEXT,
128 group_name TEXT,
129 FOREIGN KEY(group_name) REFERENCES groups(name),
130 UNIQUE (username, group_name)
Davit Tabidze75d57c32024-07-19 19:17:55 +0400131 );
132 CREATE TABLE IF NOT EXISTS users (
133 username TEXT PRIMARY KEY,
134 email TEXT,
135 UNIQUE (email)
136 );
137 CREATE TABLE IF NOT EXISTS user_ssh_keys (
138 username TEXT,
139 ssh_key TEXT,
140 UNIQUE (ssh_key),
141 FOREIGN KEY(username) REFERENCES users(username)
Davit Tabidzec0d2bf52024-04-03 15:39:33 +0400142 );`)
DTabidze0d802592024-03-19 17:42:45 +0400143 if err != nil {
144 return nil, err
145 }
146 return &SQLiteStore{db: db}, nil
147}
148
gio2728e402024-08-01 18:14:21 +0400149func (s *SQLiteStore) Init(user, email string, groups []string) error {
Giorgi Lekveishvili942c7612024-03-22 19:27:48 +0400150 tx, err := s.db.Begin()
151 if err != nil {
152 return err
153 }
154 defer tx.Rollback()
155 row := tx.QueryRow("SELECT COUNT(*) FROM groups")
156 var count int
157 if err := row.Scan(&count); err != nil {
158 return err
159 }
160 if count != 0 {
Davit Tabidze5f1a2c62024-07-17 17:57:27 +0400161 return fmt.Errorf("Store already initialised")
Giorgi Lekveishvili942c7612024-03-22 19:27:48 +0400162 }
gio2728e402024-08-01 18:14:21 +0400163 query := `INSERT INTO users (username, email) VALUES (?, ?)`
164 if _, err := tx.Exec(query, user, email); err != nil {
165 return err
166 }
Giorgi Lekveishvili942c7612024-03-22 19:27:48 +0400167 for _, g := range groups {
gio2728e402024-08-01 18:14:21 +0400168 query = `INSERT INTO groups (name, description) VALUES (?, '')`
Giorgi Lekveishvili942c7612024-03-22 19:27:48 +0400169 if _, err := tx.Exec(query, g); err != nil {
170 return err
171 }
172 query = `INSERT INTO owners (username, group_name) VALUES (?, ?)`
gio2728e402024-08-01 18:14:21 +0400173 if _, err := tx.Exec(query, user, g); err != nil {
Giorgi Lekveishvili942c7612024-03-22 19:27:48 +0400174 return err
175 }
Giorgi Lekveishvilid542b732024-03-25 18:17:39 +0400176 query = `INSERT INTO user_to_group (username, group_name) VALUES (?, ?)`
gio2728e402024-08-01 18:14:21 +0400177 if _, err := tx.Exec(query, user, g); err != nil {
Giorgi Lekveishvilid542b732024-03-25 18:17:39 +0400178 return err
179 }
Giorgi Lekveishvili942c7612024-03-22 19:27:48 +0400180 }
181 return tx.Commit()
182}
183
DTabidze0d802592024-03-19 17:42:45 +0400184func (s *SQLiteStore) queryGroups(query string, args ...interface{}) ([]Group, error) {
185 groups := make([]Group, 0)
186 rows, err := s.db.Query(query, args...)
187 if err != nil {
188 return nil, err
189 }
190 defer rows.Close()
191 for rows.Next() {
192 var group Group
193 if err := rows.Scan(&group.Name, &group.Description); err != nil {
194 return nil, err
195 }
196 groups = append(groups, group)
197 }
198 if err := rows.Err(); err != nil {
199 return nil, err
200 }
201 return groups, nil
202}
203
204func (s *SQLiteStore) GetGroupsOwnedBy(user string) ([]Group, error) {
205 query := `
206 SELECT groups.name, groups.description
207 FROM groups
208 JOIN owners ON groups.name = owners.group_name
209 WHERE owners.username = ?`
210 return s.queryGroups(query, user)
211}
212
DTabidzed7744a62024-03-20 14:09:15 +0400213func (s *SQLiteStore) GetGroupsUserBelongsTo(user string) ([]Group, error) {
DTabidze0d802592024-03-19 17:42:45 +0400214 query := `
215 SELECT groups.name, groups.description
216 FROM groups
217 JOIN user_to_group ON groups.name = user_to_group.group_name
218 WHERE user_to_group.username = ?`
219 return s.queryGroups(query, user)
220}
221
222func (s *SQLiteStore) CreateGroup(owner string, group Group) error {
223 tx, err := s.db.Begin()
224 if err != nil {
225 return err
226 }
227 defer tx.Rollback()
228 query := `INSERT INTO groups (name, description) VALUES (?, ?)`
229 if _, err := tx.Exec(query, group.Name, group.Description); err != nil {
230 sqliteErr, ok := err.(*sqlite3.Error)
Davit Tabidzec0d2bf52024-04-03 15:39:33 +0400231 if ok && sqliteErr.ExtendedCode() == ErrorConstraintPrimaryKeyViolation {
DTabidze0d802592024-03-19 17:42:45 +0400232 return fmt.Errorf("Group with the name %s already exists", group.Name)
233 }
234 return err
235 }
236 query = `INSERT INTO owners (username, group_name) VALUES (?, ?)`
237 if _, err := tx.Exec(query, owner, group.Name); err != nil {
238 return err
239 }
Giorgi Lekveishvili942c7612024-03-22 19:27:48 +0400240 return tx.Commit()
DTabidze0d802592024-03-19 17:42:45 +0400241}
242
243func (s *SQLiteStore) IsGroupOwner(user, group string) (bool, error) {
244 query := `
245 SELECT EXISTS (
246 SELECT 1
247 FROM owners
248 WHERE username = ? AND group_name = ?
249 )`
250 var exists bool
251 if err := s.db.QueryRow(query, user, group).Scan(&exists); err != nil {
252 return false, err
253 }
254 return exists, nil
255}
256
DTabidze0d802592024-03-19 17:42:45 +0400257func (s *SQLiteStore) AddGroupMember(user, group string) error {
Davit Tabidzec0d2bf52024-04-03 15:39:33 +0400258 _, err := s.db.Exec(`INSERT INTO user_to_group (username, group_name) VALUES (?, ?)`, user, group)
DTabidze0d802592024-03-19 17:42:45 +0400259 if err != nil {
Davit Tabidzec0d2bf52024-04-03 15:39:33 +0400260 sqliteErr, ok := err.(*sqlite3.Error)
261 if ok && sqliteErr.ExtendedCode() == ErrorUniqueConstraintViolation {
262 return fmt.Errorf("%s is already a member of group %s", user, group)
263 }
DTabidze0d802592024-03-19 17:42:45 +0400264 return err
265 }
266 return nil
267}
268
269func (s *SQLiteStore) AddGroupOwner(user, group string) error {
Davit Tabidzec0d2bf52024-04-03 15:39:33 +0400270 _, err := s.db.Exec(`INSERT INTO owners (username, group_name) VALUES (?, ?)`, user, group)
DTabidze0d802592024-03-19 17:42:45 +0400271 if err != nil {
Davit Tabidzec0d2bf52024-04-03 15:39:33 +0400272 sqliteErr, ok := err.(*sqlite3.Error)
273 if ok && sqliteErr.ExtendedCode() == ErrorUniqueConstraintViolation {
274 return fmt.Errorf("%s is already an owner of group %s", user, group)
275 }
DTabidze0d802592024-03-19 17:42:45 +0400276 return err
277 }
278 return nil
279}
280
281func (s *SQLiteStore) getUsersByGroup(table, group string) ([]string, error) {
282 query := fmt.Sprintf("SELECT username FROM %s WHERE group_name = ?", table)
283 rows, err := s.db.Query(query, group)
284 if err != nil {
285 return nil, err
286 }
287 defer rows.Close()
288 var users []string
289 for rows.Next() {
290 var username string
291 if err := rows.Scan(&username); err != nil {
292 return nil, err
293 }
294 users = append(users, username)
295 }
296 if err := rows.Err(); err != nil {
297 return nil, err
298 }
299 return users, nil
300}
301
302func (s *SQLiteStore) GetGroupOwners(group string) ([]string, error) {
303 return s.getUsersByGroup("owners", group)
304}
305
306func (s *SQLiteStore) GetGroupMembers(group string) ([]string, error) {
307 return s.getUsersByGroup("user_to_group", group)
308}
309
310func (s *SQLiteStore) GetGroupDescription(group string) (string, error) {
311 var description string
312 query := `SELECT description FROM groups WHERE name = ?`
313 if err := s.db.QueryRow(query, group).Scan(&description); err != nil {
314 return "", err
315 }
316 return description, nil
317}
318
DTabidze908bb852024-03-25 20:07:57 +0400319func (s *SQLiteStore) DoesGroupExist(group string) (bool, error) {
320 query := `SELECT EXISTS (SELECT 1 FROM groups WHERE name = ?)`
321 var exists bool
322 if err := s.db.QueryRow(query, group).Scan(&exists); err != nil {
323 return false, err
324 }
325 return exists, nil
326}
327
DTabidze0d802592024-03-19 17:42:45 +0400328func (s *SQLiteStore) AddChildGroup(parent, child string) error {
DTabidze908bb852024-03-25 20:07:57 +0400329 if parent == child {
Davit Tabidze5f1a2c62024-07-17 17:57:27 +0400330 return fmt.Errorf("Parent and child groups can not have same name")
DTabidze908bb852024-03-25 20:07:57 +0400331 }
Davit Tabidzec0d2bf52024-04-03 15:39:33 +0400332 exists, err := s.DoesGroupExist(parent)
333 if err != nil {
Davit Tabidze5f1a2c62024-07-17 17:57:27 +0400334 return fmt.Errorf("Error checking parent group existence: %v", err)
DTabidze908bb852024-03-25 20:07:57 +0400335 }
Davit Tabidzec0d2bf52024-04-03 15:39:33 +0400336 if !exists {
Davit Tabidze5f1a2c62024-07-17 17:57:27 +0400337 return fmt.Errorf("Parent group with name %s does not exist", parent)
Davit Tabidzec0d2bf52024-04-03 15:39:33 +0400338 }
339 exists, err = s.DoesGroupExist(child)
340 if err != nil {
Davit Tabidze5f1a2c62024-07-17 17:57:27 +0400341 return fmt.Errorf("Error checking child group existence: %v", err)
Davit Tabidzec0d2bf52024-04-03 15:39:33 +0400342 }
343 if !exists {
Davit Tabidze5f1a2c62024-07-17 17:57:27 +0400344 return fmt.Errorf("Child group with name %s does not exist", child)
DTabidze908bb852024-03-25 20:07:57 +0400345 }
DTabidzec0b4d8f2024-03-22 17:25:10 +0400346 parentGroups, err := s.GetAllTransitiveGroupsForGroup(parent)
347 if err != nil {
348 return err
349 }
350 for _, group := range parentGroups {
351 if group.Name == child {
Davit Tabidze5f1a2c62024-07-17 17:57:27 +0400352 return fmt.Errorf("Circular reference detected: group %s is already a parent of group %s", child, parent)
DTabidzec0b4d8f2024-03-22 17:25:10 +0400353 }
354 }
Davit Tabidzec0d2bf52024-04-03 15:39:33 +0400355 _, err = s.db.Exec(`INSERT INTO group_to_group (parent_group, child_group) VALUES (?, ?)`, parent, child)
DTabidze0d802592024-03-19 17:42:45 +0400356 if err != nil {
Davit Tabidzec0d2bf52024-04-03 15:39:33 +0400357 sqliteErr, ok := err.(*sqlite3.Error)
358 if ok && sqliteErr.ExtendedCode() == ErrorUniqueConstraintViolation {
Davit Tabidze5f1a2c62024-07-17 17:57:27 +0400359 return fmt.Errorf("Child group name %s already exists in group %s", child, parent)
Davit Tabidzec0d2bf52024-04-03 15:39:33 +0400360 }
DTabidze0d802592024-03-19 17:42:45 +0400361 return err
362 }
363 return nil
364}
365
DTabidzec0b4d8f2024-03-22 17:25:10 +0400366func (s *SQLiteStore) GetAllTransitiveGroupsForUser(user string) ([]Group, error) {
367 if groups, err := s.GetGroupsUserBelongsTo(user); err != nil {
DTabidzed7744a62024-03-20 14:09:15 +0400368 return nil, err
DTabidzec0b4d8f2024-03-22 17:25:10 +0400369 } else {
370 visited := map[string]struct{}{}
371 return s.getAllParentGroupsRecursive(groups, visited)
DTabidzed7744a62024-03-20 14:09:15 +0400372 }
DTabidzec0b4d8f2024-03-22 17:25:10 +0400373}
374
375func (s *SQLiteStore) GetAllTransitiveGroupsForGroup(group string) ([]Group, error) {
376 if p, err := s.GetGroupsGroupBelongsTo(group); err != nil {
377 return nil, err
378 } else {
379 // Mark initial group as visited
380 visited := map[string]struct{}{
381 group: struct{}{},
382 }
383 return s.getAllParentGroupsRecursive(p, visited)
384 }
385}
386
387func (s *SQLiteStore) getAllParentGroupsRecursive(groups []Group, visited map[string]struct{}) ([]Group, error) {
388 var ret []Group
389 for _, g := range groups {
390 if _, ok := visited[g.Name]; ok {
391 continue
392 }
393 visited[g.Name] = struct{}{}
394 ret = append(ret, g)
395 if p, err := s.GetGroupsGroupBelongsTo(g.Name); err != nil {
DTabidzed7744a62024-03-20 14:09:15 +0400396 return nil, err
DTabidzec0b4d8f2024-03-22 17:25:10 +0400397 } else if res, err := s.getAllParentGroupsRecursive(p, visited); err != nil {
398 return nil, err
399 } else {
400 ret = append(ret, res...)
DTabidzed7744a62024-03-20 14:09:15 +0400401 }
402 }
DTabidzec0b4d8f2024-03-22 17:25:10 +0400403 return ret, nil
DTabidzed7744a62024-03-20 14:09:15 +0400404}
405
DTabidzec0b4d8f2024-03-22 17:25:10 +0400406func (s *SQLiteStore) GetGroupsGroupBelongsTo(group string) ([]Group, error) {
407 query := `
408 SELECT groups.name, groups.description
409 FROM groups
410 JOIN group_to_group ON groups.name = group_to_group.parent_group
411 WHERE group_to_group.child_group = ?`
DTabidzed7744a62024-03-20 14:09:15 +0400412 rows, err := s.db.Query(query, group)
413 if err != nil {
414 return nil, err
415 }
416 defer rows.Close()
DTabidzec0b4d8f2024-03-22 17:25:10 +0400417 var parentGroups []Group
DTabidzed7744a62024-03-20 14:09:15 +0400418 for rows.Next() {
DTabidzec0b4d8f2024-03-22 17:25:10 +0400419 var parentGroup Group
420 if err := rows.Scan(&parentGroup.Name, &parentGroup.Description); err != nil {
DTabidzed7744a62024-03-20 14:09:15 +0400421 return nil, err
422 }
423 parentGroups = append(parentGroups, parentGroup)
424 }
425 if err := rows.Err(); err != nil {
426 return nil, err
427 }
428 return parentGroups, nil
429}
430
DTabidzec0b4d8f2024-03-22 17:25:10 +0400431func (s *SQLiteStore) GetDirectChildrenGroups(group string) ([]Group, error) {
432 query := `
433 SELECT groups.name, groups.description
434 FROM groups
435 JOIN group_to_group ON groups.name = group_to_group.child_group
436 WHERE group_to_group.parent_group = ?`
437 rows, err := s.db.Query(query, group)
438 if err != nil {
439 return nil, err
440 }
441 defer rows.Close()
442 var childrenGroups []Group
443 for rows.Next() {
444 var childGroup Group
445 if err := rows.Scan(&childGroup.Name, &childGroup.Description); err != nil {
446 return nil, err
447 }
448 childrenGroups = append(childrenGroups, childGroup)
449 }
450 if err := rows.Err(); err != nil {
451 return nil, err
452 }
453 return childrenGroups, nil
454}
455
DTabidze2b224bf2024-03-27 13:25:49 +0400456func (s *SQLiteStore) RemoveFromGroupToGroup(parent, child string) error {
457 query := `DELETE FROM group_to_group WHERE parent_group = ? AND child_group = ?`
458 rowDeleted, err := s.db.Exec(query, parent, child)
459 if err != nil {
460 return err
461 }
462 rowDeletedNumber, err := rowDeleted.RowsAffected()
463 if err != nil {
464 return err
465 }
466 if rowDeletedNumber == 0 {
Davit Tabidze5f1a2c62024-07-17 17:57:27 +0400467 return fmt.Errorf("Pair of parent '%s' and child '%s' groups not found", parent, child)
DTabidze2b224bf2024-03-27 13:25:49 +0400468 }
469 return nil
470}
471
472func (s *SQLiteStore) RemoveUserFromTable(username, groupName, tableName string) error {
473 if tableName == "owners" {
474 owners, err := s.GetGroupOwners(groupName)
475 if err != nil {
476 return err
477 }
478 if len(owners) == 1 {
Davit Tabidze5f1a2c62024-07-17 17:57:27 +0400479 return fmt.Errorf("Cannot remove the last owner of the group")
DTabidze2b224bf2024-03-27 13:25:49 +0400480 }
481 }
482 query := fmt.Sprintf("DELETE FROM %s WHERE username = ? AND group_name = ?", tableName)
483 rowDeleted, err := s.db.Exec(query, username, groupName)
484 if err != nil {
485 return err
486 }
487 rowDeletedNumber, err := rowDeleted.RowsAffected()
488 if err != nil {
489 return err
490 }
491 if rowDeletedNumber == 0 {
Davit Tabidze5f1a2c62024-07-17 17:57:27 +0400492 return fmt.Errorf("Pair of group '%s' and user '%s' not found", groupName, username)
DTabidze2b224bf2024-03-27 13:25:49 +0400493 }
494 return nil
495}
496
Davit Tabidzec0d2bf52024-04-03 15:39:33 +0400497func (s *SQLiteStore) AddOwnerGroup(owner_group, owned_group string) error {
498 if owned_group == owner_group {
Davit Tabidze5f1a2c62024-07-17 17:57:27 +0400499 return fmt.Errorf("Group can not own itself")
Davit Tabidzec0d2bf52024-04-03 15:39:33 +0400500 }
501 exists, err := s.DoesGroupExist(owned_group)
502 if err != nil {
Davit Tabidze5f1a2c62024-07-17 17:57:27 +0400503 return fmt.Errorf("Error checking owned group existence: %v", err)
Davit Tabidzec0d2bf52024-04-03 15:39:33 +0400504 }
505 if !exists {
Davit Tabidze5f1a2c62024-07-17 17:57:27 +0400506 return fmt.Errorf("Owned group with name %s does not exist", owned_group)
Davit Tabidzec0d2bf52024-04-03 15:39:33 +0400507 }
508 exists, err = s.DoesGroupExist(owner_group)
509 if err != nil {
Davit Tabidze5f1a2c62024-07-17 17:57:27 +0400510 return fmt.Errorf("Error checking owner group existence: %v", err)
Davit Tabidzec0d2bf52024-04-03 15:39:33 +0400511 }
512 if !exists {
Davit Tabidze5f1a2c62024-07-17 17:57:27 +0400513 return fmt.Errorf("Owner group with name %s does not exist", owner_group)
Davit Tabidzec0d2bf52024-04-03 15:39:33 +0400514 }
515 _, err = s.db.Exec(`INSERT INTO owner_groups (owner_group, owned_group) VALUES (?, ?)`, owner_group, owned_group)
516 if err != nil {
517 sqliteErr, ok := err.(*sqlite3.Error)
518 if ok && sqliteErr.ExtendedCode() == ErrorUniqueConstraintViolation {
Davit Tabidze5f1a2c62024-07-17 17:57:27 +0400519 return fmt.Errorf("Group named %s is already owner of a group %s", owner_group, owned_group)
Davit Tabidzec0d2bf52024-04-03 15:39:33 +0400520 }
521 return err
522 }
523 return nil
524}
525
526func (s *SQLiteStore) GetGroupOwnerGroups(group string) ([]Group, error) {
527 query := `
528 SELECT groups.name, groups.description
529 FROM groups
530 JOIN owner_groups ON groups.name = owner_groups.owner_group
531 WHERE owner_groups.owned_group = ?`
532 return s.queryGroups(query, group)
533}
534
535func (s *SQLiteStore) IsMemberOfOwnerGroup(user, group string) (bool, error) {
536 query := `
537 SELECT EXISTS (
538 SELECT 1 FROM owner_groups
539 INNER JOIN user_to_group ON owner_groups.owner_group = user_to_group.group_name
540 WHERE owner_groups.owned_group = ? AND user_to_group.username = ?)`
541 var exists bool
542 err := s.db.QueryRow(query, group, user).Scan(&exists)
543 if err != nil {
544 return false, err
545 }
546 return exists, nil
547}
548
549func (s *SQLiteStore) GetAllGroups() ([]Group, error) {
550 query := `SELECT name, description FROM groups`
551 return s.queryGroups(query)
552}
553
Davit Tabidze75d57c32024-07-19 19:17:55 +0400554func (s *SQLiteStore) AddSSHKeyForUser(username, sshKey string) error {
555 _, err := s.db.Exec(`INSERT INTO user_ssh_keys (username, ssh_key) VALUES (?, ?)`, username, sshKey)
556 if err != nil {
557 sqliteErr, ok := err.(*sqlite3.Error)
558 if ok && sqliteErr.ExtendedCode() == ErrorUniqueConstraintViolation {
559 return fmt.Errorf("%s such SSH public key already exists", sshKey)
560 }
561 return err
562 }
563 return nil
564}
565
566func (s *SQLiteStore) RemoveSSHKeyForUser(username, sshKey string) error {
567 _, err := s.db.Exec(`DELETE FROM user_ssh_keys WHERE username = ? AND ssh_key = ?`, username, sshKey)
568 if err != nil {
569 return err
570 }
571 return nil
572}
573
Davit Tabidzef867f2d2024-07-24 18:06:25 +0400574func (s *SQLiteStore) GetUsers(usernames []string) ([]User, error) {
575 var rows *sql.Rows
576 var err error
577 query := `
Davit Tabidze75d57c32024-07-19 19:17:55 +0400578 SELECT users.username, users.email, GROUP_CONCAT(user_ssh_keys.ssh_key, ',')
579 FROM users
Davit Tabidzef867f2d2024-07-24 18:06:25 +0400580 LEFT JOIN user_ssh_keys ON users.username = user_ssh_keys.username`
581 var args []interface{}
582 if usernames != nil {
583 if len(usernames) == 0 {
584 return []User{}, nil
585 }
586 query += " WHERE users.username IN ("
587 placeholders := strings.Repeat("?,", len(usernames)-1) + "?"
588 query += placeholders + ") "
589 for _, username := range usernames {
590 args = append(args, username)
591 }
592 }
593 query += " GROUP BY users.username"
594 rows, err = s.db.Query(query, args...)
Davit Tabidze75d57c32024-07-19 19:17:55 +0400595 if err != nil {
596 return nil, err
597 }
598 defer rows.Close()
599 var userInfos []User
600 for rows.Next() {
601 var username, email string
602 var sshKeys sql.NullString
603 if err := rows.Scan(&username, &email, &sshKeys); err != nil {
604 return nil, err
605 }
606 user := User{
607 Username: username,
608 Email: email,
609 }
610 if sshKeys.Valid {
611 user.SSHPublicKeys = strings.Split(sshKeys.String, ",")
612 }
613 userInfos = append(userInfos, user)
614 }
615 if err := rows.Err(); err != nil {
616 return nil, err
617 }
618 return userInfos, nil
619}
620
621func (s *SQLiteStore) GetUser(username string) (User, error) {
622 var user User
623 user.Username = username
624 query := `
625 SELECT users.email, GROUP_CONCAT(user_ssh_keys.ssh_key, ',')
626 FROM users
627 LEFT JOIN user_ssh_keys ON users.username = user_ssh_keys.username
628 WHERE users.username = ?
629 GROUP BY users.username
630 `
631 row := s.db.QueryRow(query, username)
632 var sshKeys sql.NullString
633 err := row.Scan(&user.Email, &sshKeys)
634 if err != nil {
635 if err == sql.ErrNoRows {
636 return User{}, fmt.Errorf("no user found with username %s", username)
637 }
638 return User{}, err
639 }
640 if sshKeys.Valid {
641 user.SSHPublicKeys = strings.Split(sshKeys.String, ",")
642 }
643 return user, nil
644}
645
646func (s *SQLiteStore) CreateUser(user, email string) error {
647 _, err := s.db.Exec(`INSERT INTO users (username, email) VALUES (?, ?)`, user, email)
648 if err != nil {
649 sqliteErr, ok := err.(*sqlite3.Error)
650 if ok {
651 if sqliteErr.ExtendedCode() == ErrorUniqueConstraintViolation {
652 if strings.Contains(err.Error(), "UNIQUE constraint failed: users.username") {
653 return fmt.Errorf("username %s already exists", user)
654 }
655 if strings.Contains(err.Error(), "UNIQUE constraint failed: users.email") {
656 return fmt.Errorf("email %s already exists", email)
657 }
658 }
659 }
660 return err
661 }
662 return nil
663}
664
DTabidze0d802592024-03-19 17:42:45 +0400665func getLoggedInUser(r *http.Request) (string, error) {
giodd213152024-09-27 11:26:59 +0200666 if user := r.Header.Get("X-Forwarded-User"); user != "" {
DTabidzec0b4d8f2024-03-22 17:25:10 +0400667 return user, nil
668 } else {
669 return "", fmt.Errorf("unauthenticated")
670 }
Davit Tabidze75d57c32024-07-19 19:17:55 +0400671 // return "tabo", nil
DTabidze0d802592024-03-19 17:42:45 +0400672}
673
674type Status int
675
676const (
677 Owner Status = iota
678 Member
679)
680
Giorgi Lekveishvili329af572024-03-25 20:14:41 +0400681func (s *Server) Start() error {
682 e := make(chan error)
683 go func() {
684 r := mux.NewRouter()
gio4a297752025-07-15 13:24:57 +0400685 r.PathPrefix("/static/").Handler(cachingHandler{http.FileServer(http.FS(staticResources))})
Davit Tabidze75d57c32024-07-19 19:17:55 +0400686 r.HandleFunc("/group/{group-name}/add-user/", s.addUserToGroupHandler).Methods(http.MethodPost)
687 r.HandleFunc("/group/{parent-group}/add-child-group", s.addChildGroupHandler).Methods(http.MethodPost)
688 r.HandleFunc("/group/{owned-group}/add-owner-group", s.addOwnerGroupHandler).Methods(http.MethodPost)
689 r.HandleFunc("/group/{parent-group}/remove-child-group/{child-group}", s.removeChildGroupHandler).Methods(http.MethodPost)
690 r.HandleFunc("/group/{group-name}/remove-owner/{username}", s.removeOwnerFromGroupHandler).Methods(http.MethodPost)
691 r.HandleFunc("/group/{group-name}/remove-member/{username}", s.removeMemberFromGroupHandler).Methods(http.MethodPost)
Giorgi Lekveishvili329af572024-03-25 20:14:41 +0400692 r.HandleFunc("/group/{group-name}", s.groupHandler)
Davit Tabidze75d57c32024-07-19 19:17:55 +0400693 r.HandleFunc("/user/{username}/ssh-key", s.addSSHKeyForUserHandler).Methods(http.MethodPost)
694 r.HandleFunc("/user/{username}/remove-ssh-key", s.removeSSHKeyForUserHandler).Methods(http.MethodPost)
DTabidze5d735e32024-03-26 16:01:06 +0400695 r.HandleFunc("/user/{username}", s.userHandler)
Davit Tabidze75d57c32024-07-19 19:17:55 +0400696 r.HandleFunc("/create-group", s.createGroupHandler).Methods(http.MethodPost)
Giorgi Lekveishvili329af572024-03-25 20:14:41 +0400697 r.HandleFunc("/", s.homePageHandler)
698 e <- http.ListenAndServe(fmt.Sprintf(":%d", *port), r)
699 }()
700 go func() {
701 r := mux.NewRouter()
702 r.HandleFunc("/api/init", s.apiInitHandler)
gio7fbd4ad2024-08-27 10:06:39 +0400703 // TODO(gio): change to /api/users/{username}
704 r.HandleFunc("/api/users/{username}/keys", s.apiAddUserKey).Methods(http.MethodPost)
Giorgi Lekveishvili329af572024-03-25 20:14:41 +0400705 r.HandleFunc("/api/user/{username}", s.apiMemberOfHandler)
Davit Tabidze75d57c32024-07-19 19:17:55 +0400706 r.HandleFunc("/api/users", s.apiGetAllUsers).Methods(http.MethodGet)
707 r.HandleFunc("/api/users", s.apiCreateUser).Methods(http.MethodPost)
Giorgi Lekveishvili329af572024-03-25 20:14:41 +0400708 e <- http.ListenAndServe(fmt.Sprintf(":%d", *apiPort), r)
709 }()
710 return <-e
DTabidze0d802592024-03-19 17:42:45 +0400711}
712
713type GroupData struct {
714 Group Group
715 Membership string
716}
717
Davit Tabidzec0d2bf52024-04-03 15:39:33 +0400718func (s *Server) checkIsOwner(w http.ResponseWriter, user, group string) error {
DTabidze0d802592024-03-19 17:42:45 +0400719 isOwner, err := s.store.IsGroupOwner(user, group)
720 if err != nil {
Davit Tabidzec0d2bf52024-04-03 15:39:33 +0400721 return err
DTabidze0d802592024-03-19 17:42:45 +0400722 }
Davit Tabidzec0d2bf52024-04-03 15:39:33 +0400723 if isOwner {
724 return nil
DTabidze0d802592024-03-19 17:42:45 +0400725 }
Davit Tabidzec0d2bf52024-04-03 15:39:33 +0400726 // TODO(dtabidze): right now this only checks if user is member of just one lvl upper group. should add transitive group check.
727 isMemberOfOwnerGroup, err := s.store.IsMemberOfOwnerGroup(user, group)
728 if err != nil {
729 return err
730 }
731 if !isMemberOfOwnerGroup {
Davit Tabidze5f1a2c62024-07-17 17:57:27 +0400732 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 +0400733 }
734 return nil
DTabidze0d802592024-03-19 17:42:45 +0400735}
736
DTabidze4b44ff42024-04-02 03:16:26 +0400737type templates struct {
738 group *template.Template
739 user *template.Template
740}
741
742func parseTemplates(fs embed.FS) (templates, error) {
743 base, err := template.ParseFS(fs, "memberships-tmpl/base.html")
744 if err != nil {
745 return templates{}, err
746 }
747 parse := func(path string) (*template.Template, error) {
748 if b, err := base.Clone(); err != nil {
749 return nil, err
750 } else {
751 return b.ParseFS(fs, path)
752 }
753 }
754 user, err := parse("memberships-tmpl/user.html")
755 if err != nil {
756 return templates{}, err
757 }
758 group, err := parse("memberships-tmpl/group.html")
759 if err != nil {
760 return templates{}, err
761 }
762 return templates{group, user}, nil
763}
764
DTabidze0d802592024-03-19 17:42:45 +0400765func (s *Server) homePageHandler(w http.ResponseWriter, r *http.Request) {
766 loggedInUser, err := getLoggedInUser(r)
767 if err != nil {
768 http.Error(w, "User Not Logged In", http.StatusUnauthorized)
769 return
770 }
DTabidze5d735e32024-03-26 16:01:06 +0400771 http.Redirect(w, r, "/user/"+loggedInUser, http.StatusSeeOther)
772}
773
Davit Tabidze75d57c32024-07-19 19:17:55 +0400774type UserPageData struct {
775 OwnerGroups []Group
776 MembershipGroups []Group
777 TransitiveGroups []Group
778 LoggedInUserPage bool
779 CurrentUser string
780 SSHPublicKeys []string
781 Email string
782 ErrorMessage string
783}
784
DTabidze5d735e32024-03-26 16:01:06 +0400785func (s *Server) userHandler(w http.ResponseWriter, r *http.Request) {
786 loggedInUser, err := getLoggedInUser(r)
787 if err != nil {
788 http.Error(w, "User Not Logged In", http.StatusUnauthorized)
789 return
790 }
DTabidze4b44ff42024-04-02 03:16:26 +0400791 errorMsg := r.URL.Query().Get("errorMessage")
DTabidze5d735e32024-03-26 16:01:06 +0400792 vars := mux.Vars(r)
793 user := strings.ToLower(vars["username"])
794 // TODO(dtabidze): should check if username exists or not.
795 loggedInUserPage := loggedInUser == user
796 ownerGroups, err := s.store.GetGroupsOwnedBy(user)
DTabidze0d802592024-03-19 17:42:45 +0400797 if err != nil {
798 http.Error(w, err.Error(), http.StatusInternalServerError)
799 return
800 }
DTabidze5d735e32024-03-26 16:01:06 +0400801 membershipGroups, err := s.store.GetGroupsUserBelongsTo(user)
DTabidze0d802592024-03-19 17:42:45 +0400802 if err != nil {
803 http.Error(w, err.Error(), http.StatusInternalServerError)
804 return
805 }
DTabidze5d735e32024-03-26 16:01:06 +0400806 transitiveGroups, err := s.store.GetAllTransitiveGroupsForUser(user)
DTabidzec0b4d8f2024-03-22 17:25:10 +0400807 if err != nil {
808 http.Error(w, err.Error(), http.StatusInternalServerError)
809 return
810 }
Davit Tabidze75d57c32024-07-19 19:17:55 +0400811 userInfo, err := s.store.GetUser(user)
812 if err != nil {
813 http.Error(w, err.Error(), http.StatusInternalServerError)
814 return
815 }
816 data := UserPageData{
DTabidze0d802592024-03-19 17:42:45 +0400817 OwnerGroups: ownerGroups,
818 MembershipGroups: membershipGroups,
DTabidzec0b4d8f2024-03-22 17:25:10 +0400819 TransitiveGroups: transitiveGroups,
DTabidze5d735e32024-03-26 16:01:06 +0400820 LoggedInUserPage: loggedInUserPage,
821 CurrentUser: user,
Davit Tabidze75d57c32024-07-19 19:17:55 +0400822 SSHPublicKeys: userInfo.SSHPublicKeys,
823 Email: userInfo.Email,
DTabidze4b44ff42024-04-02 03:16:26 +0400824 ErrorMessage: errorMsg,
DTabidze0d802592024-03-19 17:42:45 +0400825 }
DTabidze4b44ff42024-04-02 03:16:26 +0400826 templates, err := parseTemplates(tmpls)
827 if err != nil {
828 http.Error(w, err.Error(), http.StatusInternalServerError)
829 return
830 }
831 if err := templates.user.Execute(w, data); err != nil {
DTabidze0d802592024-03-19 17:42:45 +0400832 http.Error(w, err.Error(), http.StatusInternalServerError)
833 return
834 }
835}
836
837func (s *Server) createGroupHandler(w http.ResponseWriter, r *http.Request) {
838 loggedInUser, err := getLoggedInUser(r)
839 if err != nil {
840 http.Error(w, "User Not Logged In", http.StatusUnauthorized)
841 return
842 }
DTabidze0d802592024-03-19 17:42:45 +0400843 if err := r.ParseForm(); err != nil {
844 http.Error(w, err.Error(), http.StatusInternalServerError)
845 return
846 }
847 var group Group
848 group.Name = r.PostFormValue("group-name")
DTabidze908bb852024-03-25 20:07:57 +0400849 if err := isValidGroupName(group.Name); err != nil {
DTabidze4b44ff42024-04-02 03:16:26 +0400850 // http.Error(w, err.Error(), http.StatusBadRequest)
851 redirectURL := fmt.Sprintf("/user/%s?errorMessage=%s", loggedInUser, url.QueryEscape(err.Error()))
852 http.Redirect(w, r, redirectURL, http.StatusFound)
DTabidze908bb852024-03-25 20:07:57 +0400853 return
854 }
DTabidze0d802592024-03-19 17:42:45 +0400855 group.Description = r.PostFormValue("description")
856 if err := s.store.CreateGroup(loggedInUser, group); err != nil {
DTabidze4b44ff42024-04-02 03:16:26 +0400857 // http.Error(w, err.Error(), http.StatusInternalServerError)
858 redirectURL := fmt.Sprintf("/user/%s?errorMessage=%s", loggedInUser, url.QueryEscape(err.Error()))
859 http.Redirect(w, r, redirectURL, http.StatusFound)
DTabidze0d802592024-03-19 17:42:45 +0400860 return
861 }
862 http.Redirect(w, r, "/", http.StatusSeeOther)
863}
864
Davit Tabidze75d57c32024-07-19 19:17:55 +0400865type GroupPageData struct {
866 GroupName string
867 Description string
868 Owners []string
869 Members []string
870 AllGroups []Group
871 TransitiveGroups []Group
872 ChildGroups []Group
873 OwnerGroups []Group
874 ErrorMessage string
875}
876
DTabidze0d802592024-03-19 17:42:45 +0400877func (s *Server) groupHandler(w http.ResponseWriter, r *http.Request) {
DTabidzec0b4d8f2024-03-22 17:25:10 +0400878 _, err := getLoggedInUser(r)
879 if err != nil {
880 http.Error(w, "User Not Logged In", http.StatusUnauthorized)
881 return
882 }
DTabidze4b44ff42024-04-02 03:16:26 +0400883 errorMsg := r.URL.Query().Get("errorMessage")
DTabidzed7744a62024-03-20 14:09:15 +0400884 vars := mux.Vars(r)
885 groupName := vars["group-name"]
DTabidze908bb852024-03-25 20:07:57 +0400886 exists, err := s.store.DoesGroupExist(groupName)
887 if err != nil {
888 http.Error(w, err.Error(), http.StatusInternalServerError)
889 return
890 }
891 if !exists {
DTabidze4b44ff42024-04-02 03:16:26 +0400892 errorMsg = fmt.Sprintf("group with the name '%s' not found", groupName)
DTabidze908bb852024-03-25 20:07:57 +0400893 http.Error(w, errorMsg, http.StatusNotFound)
894 return
895 }
DTabidze0d802592024-03-19 17:42:45 +0400896 if err != nil {
897 http.Error(w, err.Error(), http.StatusInternalServerError)
898 return
899 }
900 owners, err := s.store.GetGroupOwners(groupName)
901 if err != nil {
902 http.Error(w, err.Error(), http.StatusInternalServerError)
903 return
904 }
905 members, err := s.store.GetGroupMembers(groupName)
906 if err != nil {
907 http.Error(w, err.Error(), http.StatusInternalServerError)
908 return
909 }
910 description, err := s.store.GetGroupDescription(groupName)
911 if err != nil {
912 http.Error(w, err.Error(), http.StatusInternalServerError)
913 return
914 }
Davit Tabidzec0d2bf52024-04-03 15:39:33 +0400915 allGroups, err := s.store.GetAllGroups()
DTabidze0d802592024-03-19 17:42:45 +0400916 if err != nil {
917 http.Error(w, err.Error(), http.StatusInternalServerError)
918 return
919 }
DTabidzec0b4d8f2024-03-22 17:25:10 +0400920 transitiveGroups, err := s.store.GetAllTransitiveGroupsForGroup(groupName)
921 if err != nil {
922 http.Error(w, err.Error(), http.StatusInternalServerError)
923 return
924 }
925 childGroups, err := s.store.GetDirectChildrenGroups(groupName)
926 if err != nil {
927 http.Error(w, err.Error(), http.StatusInternalServerError)
928 return
929 }
Davit Tabidzec0d2bf52024-04-03 15:39:33 +0400930 ownerGroups, err := s.store.GetGroupOwnerGroups(groupName)
931 if err != nil {
932 http.Error(w, err.Error(), http.StatusInternalServerError)
933 return
934 }
Davit Tabidze75d57c32024-07-19 19:17:55 +0400935 data := GroupPageData{
DTabidzec0b4d8f2024-03-22 17:25:10 +0400936 GroupName: groupName,
937 Description: description,
938 Owners: owners,
939 Members: members,
Davit Tabidzec0d2bf52024-04-03 15:39:33 +0400940 AllGroups: allGroups,
DTabidzec0b4d8f2024-03-22 17:25:10 +0400941 TransitiveGroups: transitiveGroups,
942 ChildGroups: childGroups,
Davit Tabidzec0d2bf52024-04-03 15:39:33 +0400943 OwnerGroups: ownerGroups,
DTabidze4b44ff42024-04-02 03:16:26 +0400944 ErrorMessage: errorMsg,
DTabidze0d802592024-03-19 17:42:45 +0400945 }
DTabidze4b44ff42024-04-02 03:16:26 +0400946 templates, err := parseTemplates(tmpls)
947 if err != nil {
948 http.Error(w, err.Error(), http.StatusInternalServerError)
949 return
950 }
951 if err := templates.group.Execute(w, data); err != nil {
DTabidze0d802592024-03-19 17:42:45 +0400952 http.Error(w, err.Error(), http.StatusInternalServerError)
953 return
954 }
955}
956
DTabidze2b224bf2024-03-27 13:25:49 +0400957func (s *Server) removeChildGroupHandler(w http.ResponseWriter, r *http.Request) {
958 loggedInUser, err := getLoggedInUser(r)
959 if err != nil {
960 http.Error(w, "User Not Logged In", http.StatusUnauthorized)
961 return
962 }
Davit Tabidze75d57c32024-07-19 19:17:55 +0400963 vars := mux.Vars(r)
964 parentGroup := vars["parent-group"]
965 childGroup := vars["child-group"]
966 if err := isValidGroupName(parentGroup); err != nil {
967 http.Error(w, err.Error(), http.StatusBadRequest)
968 return
DTabidze2b224bf2024-03-27 13:25:49 +0400969 }
Davit Tabidze75d57c32024-07-19 19:17:55 +0400970 if err := isValidGroupName(childGroup); err != nil {
971 http.Error(w, err.Error(), http.StatusBadRequest)
972 return
973 }
974 if err := s.checkIsOwner(w, loggedInUser, parentGroup); err != nil {
975 redirectURL := fmt.Sprintf("/group/%s?errorMessage=%s", parentGroup, url.QueryEscape(err.Error()))
976 http.Redirect(w, r, redirectURL, http.StatusSeeOther)
977 return
978 }
979 err = s.store.RemoveFromGroupToGroup(parentGroup, childGroup)
980 if err != nil {
981 redirectURL := fmt.Sprintf("/group/%s?errorMessage=%s", parentGroup, url.QueryEscape(err.Error()))
982 http.Redirect(w, r, redirectURL, http.StatusFound)
983 return
984 }
985 http.Redirect(w, r, "/group/"+parentGroup, http.StatusSeeOther)
DTabidze2b224bf2024-03-27 13:25:49 +0400986}
987
DTabidze078385f2024-03-27 14:49:05 +0400988func (s *Server) removeOwnerFromGroupHandler(w http.ResponseWriter, r *http.Request) {
DTabidze2b224bf2024-03-27 13:25:49 +0400989 loggedInUser, err := getLoggedInUser(r)
990 if err != nil {
991 http.Error(w, "User Not Logged In", http.StatusUnauthorized)
992 return
993 }
Davit Tabidze75d57c32024-07-19 19:17:55 +0400994 vars := mux.Vars(r)
995 username := vars["username"]
996 groupName := vars["group-name"]
997 tableName := "owners"
998 if err := isValidGroupName(groupName); err != nil {
999 http.Error(w, err.Error(), http.StatusBadRequest)
1000 return
DTabidze2b224bf2024-03-27 13:25:49 +04001001 }
Davit Tabidze75d57c32024-07-19 19:17:55 +04001002 if err := s.checkIsOwner(w, loggedInUser, groupName); err != nil {
1003 redirectURL := fmt.Sprintf("/group/%s?errorMessage=%s", groupName, url.QueryEscape(err.Error()))
1004 http.Redirect(w, r, redirectURL, http.StatusSeeOther)
1005 return
1006 }
1007 err = s.store.RemoveUserFromTable(username, groupName, tableName)
1008 if err != nil {
1009 redirectURL := fmt.Sprintf("/group/%s?errorMessage=%s", groupName, url.QueryEscape(err.Error()))
1010 http.Redirect(w, r, redirectURL, http.StatusFound)
1011 return
1012 }
1013 http.Redirect(w, r, "/group/"+groupName, http.StatusSeeOther)
DTabidze2b224bf2024-03-27 13:25:49 +04001014}
1015
DTabidze078385f2024-03-27 14:49:05 +04001016func (s *Server) removeMemberFromGroupHandler(w http.ResponseWriter, r *http.Request) {
1017 loggedInUser, err := getLoggedInUser(r)
1018 if err != nil {
1019 http.Error(w, "User Not Logged In", http.StatusUnauthorized)
1020 return
1021 }
Davit Tabidze75d57c32024-07-19 19:17:55 +04001022 vars := mux.Vars(r)
1023 username := vars["username"]
1024 groupName := vars["group-name"]
1025 tableName := "user_to_group"
1026 if err := isValidGroupName(groupName); err != nil {
1027 http.Error(w, err.Error(), http.StatusBadRequest)
1028 return
DTabidze078385f2024-03-27 14:49:05 +04001029 }
Davit Tabidze75d57c32024-07-19 19:17:55 +04001030 if err := s.checkIsOwner(w, loggedInUser, groupName); err != nil {
1031 redirectURL := fmt.Sprintf("/group/%s?errorMessage=%s", groupName, url.QueryEscape(err.Error()))
1032 http.Redirect(w, r, redirectURL, http.StatusSeeOther)
1033 return
1034 }
1035 err = s.store.RemoveUserFromTable(username, groupName, tableName)
1036 if err != nil {
1037 redirectURL := fmt.Sprintf("/group/%s?errorMessage=%s", groupName, url.QueryEscape(err.Error()))
1038 http.Redirect(w, r, redirectURL, http.StatusFound)
1039 return
1040 }
1041 http.Redirect(w, r, "/group/"+groupName, http.StatusSeeOther)
DTabidze078385f2024-03-27 14:49:05 +04001042}
1043
1044func (s *Server) addUserToGroupHandler(w http.ResponseWriter, r *http.Request) {
DTabidze0d802592024-03-19 17:42:45 +04001045 loggedInUser, err := getLoggedInUser(r)
1046 if err != nil {
1047 http.Error(w, "User Not Logged In", http.StatusUnauthorized)
1048 return
1049 }
DTabidze078385f2024-03-27 14:49:05 +04001050 vars := mux.Vars(r)
1051 groupName := vars["group-name"]
DTabidze908bb852024-03-25 20:07:57 +04001052 if err := isValidGroupName(groupName); err != nil {
1053 http.Error(w, err.Error(), http.StatusBadRequest)
1054 return
1055 }
1056 username := strings.ToLower(r.FormValue("username"))
1057 if username == "" {
1058 http.Error(w, "Username parameter is required", http.StatusBadRequest)
1059 return
1060 }
DTabidze0d802592024-03-19 17:42:45 +04001061 status, err := convertStatus(r.FormValue("status"))
1062 if err != nil {
1063 http.Error(w, err.Error(), http.StatusBadRequest)
1064 return
1065 }
Davit Tabidzec0d2bf52024-04-03 15:39:33 +04001066 if err := s.checkIsOwner(w, loggedInUser, groupName); err != nil {
1067 redirectURL := fmt.Sprintf("/group/%s?errorMessage=%s", groupName, url.QueryEscape(err.Error()))
1068 http.Redirect(w, r, redirectURL, http.StatusSeeOther)
DTabidze0d802592024-03-19 17:42:45 +04001069 return
1070 }
1071 switch status {
1072 case Owner:
1073 err = s.store.AddGroupOwner(username, groupName)
1074 case Member:
1075 err = s.store.AddGroupMember(username, groupName)
1076 default:
1077 http.Error(w, "Invalid status", http.StatusBadRequest)
1078 return
1079 }
1080 if err != nil {
DTabidze4b44ff42024-04-02 03:16:26 +04001081 redirectURL := fmt.Sprintf("/group/%s?errorMessage=%s", groupName, url.QueryEscape(err.Error()))
1082 http.Redirect(w, r, redirectURL, http.StatusFound)
DTabidze0d802592024-03-19 17:42:45 +04001083 return
1084 }
1085 http.Redirect(w, r, "/group/"+groupName, http.StatusSeeOther)
1086}
1087
1088func (s *Server) addChildGroupHandler(w http.ResponseWriter, r *http.Request) {
1089 // 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 +04001090 loggedInUser, err := getLoggedInUser(r)
1091 if err != nil {
1092 http.Error(w, "User Not Logged In", http.StatusUnauthorized)
1093 return
1094 }
DTabidze078385f2024-03-27 14:49:05 +04001095 vars := mux.Vars(r)
1096 parentGroup := vars["parent-group"]
DTabidze908bb852024-03-25 20:07:57 +04001097 if err := isValidGroupName(parentGroup); err != nil {
1098 http.Error(w, err.Error(), http.StatusBadRequest)
1099 return
1100 }
DTabidze0d802592024-03-19 17:42:45 +04001101 childGroup := r.FormValue("child-group")
DTabidze908bb852024-03-25 20:07:57 +04001102 if err := isValidGroupName(childGroup); err != nil {
1103 http.Error(w, err.Error(), http.StatusBadRequest)
1104 return
1105 }
Davit Tabidzec0d2bf52024-04-03 15:39:33 +04001106 if err := s.checkIsOwner(w, loggedInUser, parentGroup); err != nil {
1107 redirectURL := fmt.Sprintf("/group/%s?errorMessage=%s", parentGroup, url.QueryEscape(err.Error()))
1108 http.Redirect(w, r, redirectURL, http.StatusSeeOther)
DTabidze0d802592024-03-19 17:42:45 +04001109 return
1110 }
1111 if err := s.store.AddChildGroup(parentGroup, childGroup); err != nil {
DTabidze4b44ff42024-04-02 03:16:26 +04001112 redirectURL := fmt.Sprintf("/group/%s?errorMessage=%s", parentGroup, url.QueryEscape(err.Error()))
1113 http.Redirect(w, r, redirectURL, http.StatusFound)
DTabidze0d802592024-03-19 17:42:45 +04001114 return
1115 }
1116 http.Redirect(w, r, "/group/"+parentGroup, http.StatusSeeOther)
1117}
1118
Davit Tabidzec0d2bf52024-04-03 15:39:33 +04001119func (s *Server) addOwnerGroupHandler(w http.ResponseWriter, r *http.Request) {
Davit Tabidzec0d2bf52024-04-03 15:39:33 +04001120 loggedInUser, err := getLoggedInUser(r)
1121 if err != nil {
1122 http.Error(w, "User Not Logged In", http.StatusUnauthorized)
1123 return
1124 }
1125 vars := mux.Vars(r)
1126 ownedGroup := vars["owned-group"]
1127 if err := isValidGroupName(ownedGroup); err != nil {
1128 http.Error(w, err.Error(), http.StatusBadRequest)
1129 return
1130 }
1131 ownerGroup := r.FormValue("owner-group")
1132 if err := isValidGroupName(ownerGroup); err != nil {
1133 http.Error(w, err.Error(), http.StatusBadRequest)
1134 return
1135 }
1136 if err := s.checkIsOwner(w, loggedInUser, ownedGroup); err != nil {
1137 redirectURL := fmt.Sprintf("/group/%s?errorMessage=%s", ownedGroup, url.QueryEscape(err.Error()))
1138 http.Redirect(w, r, redirectURL, http.StatusSeeOther)
1139 return
1140 }
1141 if err := s.store.AddOwnerGroup(ownerGroup, ownedGroup); err != nil {
1142 redirectURL := fmt.Sprintf("/group/%s?errorMessage=%s", ownedGroup, url.QueryEscape(err.Error()))
1143 http.Redirect(w, r, redirectURL, http.StatusFound)
1144 return
1145 }
1146 http.Redirect(w, r, "/group/"+ownedGroup, http.StatusSeeOther)
1147}
1148
Davit Tabidze75d57c32024-07-19 19:17:55 +04001149func (s *Server) addSSHKeyForUserHandler(w http.ResponseWriter, r *http.Request) {
1150 defer s.pingAllSyncAddresses()
1151 loggedInUser, err := getLoggedInUser(r)
1152 if err != nil {
1153 http.Error(w, "User Not Logged In", http.StatusUnauthorized)
1154 return
1155 }
1156 vars := mux.Vars(r)
1157 username := vars["username"]
1158 if loggedInUser != username {
1159 http.Error(w, "You are not allowed to add SSH key for someone else", http.StatusUnauthorized)
1160 return
1161 }
1162 sshKey := r.FormValue("ssh-key")
1163 if sshKey == "" {
1164 http.Error(w, "SSH key not present", http.StatusBadRequest)
1165 return
1166 }
gio7fbd4ad2024-08-27 10:06:39 +04001167 if err := s.store.AddSSHKeyForUser(strings.ToLower(username), sshKey); err != nil {
Davit Tabidze75d57c32024-07-19 19:17:55 +04001168 redirectURL := fmt.Sprintf("/user/%s?errorMessage=%s", loggedInUser, url.QueryEscape(err.Error()))
1169 http.Redirect(w, r, redirectURL, http.StatusFound)
1170 return
1171 }
1172 http.Redirect(w, r, "/user/"+loggedInUser, http.StatusSeeOther)
1173}
1174
1175func (s *Server) removeSSHKeyForUserHandler(w http.ResponseWriter, r *http.Request) {
1176 defer s.pingAllSyncAddresses()
1177 loggedInUser, err := getLoggedInUser(r)
1178 if err != nil {
1179 http.Error(w, "User Not Logged In", http.StatusUnauthorized)
1180 return
1181 }
1182 vars := mux.Vars(r)
1183 username := vars["username"]
1184 if loggedInUser != username {
1185 http.Error(w, "You are not allowed to remove SSH key for someone else", http.StatusUnauthorized)
1186 return
1187 }
1188 if err := r.ParseForm(); err != nil {
1189 http.Error(w, "Invalid request body", http.StatusBadRequest)
1190 return
1191 }
1192 sshKey := r.FormValue("ssh-key")
1193 if sshKey == "" {
1194 http.Error(w, "SSH key not present", http.StatusBadRequest)
1195 return
1196 }
1197 if err := s.store.RemoveSSHKeyForUser(username, sshKey); err != nil {
1198 redirectURL := fmt.Sprintf("/user/%s?errorMessage=%s", loggedInUser, url.QueryEscape(err.Error()))
1199 http.Redirect(w, r, redirectURL, http.StatusFound)
1200 return
1201 }
1202 http.Redirect(w, r, "/user/"+loggedInUser, http.StatusSeeOther)
1203}
1204
Giorgi Lekveishvili942c7612024-03-22 19:27:48 +04001205type initRequest struct {
gio2728e402024-08-01 18:14:21 +04001206 User string `json:"user"`
1207 Email string `json:"email"`
Giorgi Lekveishvili942c7612024-03-22 19:27:48 +04001208 Groups []string `json:"groups"`
1209}
1210
1211func (s *Server) apiInitHandler(w http.ResponseWriter, r *http.Request) {
1212 var req initRequest
1213 if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
1214 http.Error(w, err.Error(), http.StatusBadRequest)
1215 return
1216 }
gio2728e402024-08-01 18:14:21 +04001217 if err := s.store.Init(req.User, req.Email, req.Groups); err != nil {
Giorgi Lekveishvili942c7612024-03-22 19:27:48 +04001218 http.Error(w, err.Error(), http.StatusInternalServerError)
1219 return
1220 }
1221}
1222
1223type userInfo struct {
DTabidzed7744a62024-03-20 14:09:15 +04001224 MemberOf []string `json:"memberOf"`
1225}
1226
1227func (s *Server) apiMemberOfHandler(w http.ResponseWriter, r *http.Request) {
1228 vars := mux.Vars(r)
1229 user, ok := vars["username"]
DTabidze908bb852024-03-25 20:07:57 +04001230 if !ok || user == "" {
DTabidzed7744a62024-03-20 14:09:15 +04001231 http.Error(w, "Username parameter is required", http.StatusBadRequest)
1232 return
1233 }
DTabidze908bb852024-03-25 20:07:57 +04001234 user = strings.ToLower(user)
DTabidzed7744a62024-03-20 14:09:15 +04001235 transitiveGroups, err := s.store.GetAllTransitiveGroupsForUser(user)
1236 if err != nil {
1237 http.Error(w, err.Error(), http.StatusInternalServerError)
1238 return
1239 }
DTabidzec0b4d8f2024-03-22 17:25:10 +04001240 var groupNames []string
1241 for _, group := range transitiveGroups {
1242 groupNames = append(groupNames, group.Name)
1243 }
DTabidzed7744a62024-03-20 14:09:15 +04001244 w.Header().Set("Content-Type", "application/json")
Giorgi Lekveishvili942c7612024-03-22 19:27:48 +04001245 if err := json.NewEncoder(w).Encode(userInfo{groupNames}); err != nil {
DTabidzed7744a62024-03-20 14:09:15 +04001246 http.Error(w, err.Error(), http.StatusInternalServerError)
1247 return
1248 }
1249}
1250
Davit Tabidze75d57c32024-07-19 19:17:55 +04001251func (s *Server) apiGetAllUsers(w http.ResponseWriter, r *http.Request) {
gio7fbd4ad2024-08-27 10:06:39 +04001252 s.addSyncAddress(r.FormValue("selfAddress"))
Davit Tabidzef867f2d2024-07-24 18:06:25 +04001253 var users []User
1254 var err error
1255 groups := r.FormValue("groups")
1256 if groups == "" {
1257 users, err = s.store.GetUsers(nil)
1258 } else {
1259 uniqueUsers := make(map[string]struct{})
1260 g := strings.Split(groups, ",")
1261 uniqueTG := make(map[string]struct{})
1262 for _, group := range g {
1263 uniqueTG[group] = struct{}{}
1264 trGroups, err := s.store.GetAllTransitiveGroupsForGroup(group)
1265 if err != nil {
1266 http.Error(w, err.Error(), http.StatusInternalServerError)
1267 return
1268 }
1269 for _, tg := range trGroups {
1270 uniqueTG[tg.Name] = struct{}{}
1271 }
1272 }
1273 for group := range uniqueTG {
1274 u, err := s.store.GetGroupMembers(group)
1275 if err != nil {
1276 http.Error(w, err.Error(), http.StatusInternalServerError)
1277 return
1278 }
1279 for _, user := range u {
1280 uniqueUsers[user] = struct{}{}
1281 }
1282 }
1283 usernames := make([]string, 0, len(uniqueUsers))
1284 for username := range uniqueUsers {
1285 usernames = append(usernames, username)
1286 }
1287 users, err = s.store.GetUsers(usernames)
1288 }
Davit Tabidze75d57c32024-07-19 19:17:55 +04001289 if err != nil {
Davit Tabidzef867f2d2024-07-24 18:06:25 +04001290 http.Error(w, "Failed to retrieve user infos", http.StatusInternalServerError)
Davit Tabidze75d57c32024-07-19 19:17:55 +04001291 return
1292 }
1293 w.Header().Set("Content-Type", "application/json")
1294 if err := json.NewEncoder(w).Encode(users); err != nil {
1295 http.Error(w, err.Error(), http.StatusInternalServerError)
1296 return
1297 }
1298}
1299
gio2728e402024-08-01 18:14:21 +04001300type createUserRequest struct {
1301 User string `json:"user"`
1302 Email string `json:"email"`
1303}
1304
Davit Tabidze75d57c32024-07-19 19:17:55 +04001305func (s *Server) apiCreateUser(w http.ResponseWriter, r *http.Request) {
1306 defer s.pingAllSyncAddresses()
gio2728e402024-08-01 18:14:21 +04001307 var req createUserRequest
1308 if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
Davit Tabidze75d57c32024-07-19 19:17:55 +04001309 http.Error(w, "Invalid request body", http.StatusBadRequest)
1310 return
1311 }
gio2728e402024-08-01 18:14:21 +04001312 if req.User == "" {
Davit Tabidze75d57c32024-07-19 19:17:55 +04001313 http.Error(w, "Username cannot be empty", http.StatusBadRequest)
1314 return
1315 }
gio2728e402024-08-01 18:14:21 +04001316 if req.Email == "" {
Davit Tabidze75d57c32024-07-19 19:17:55 +04001317 http.Error(w, "Email cannot be empty", http.StatusBadRequest)
1318 return
1319 }
gio2728e402024-08-01 18:14:21 +04001320 if err := s.store.CreateUser(strings.ToLower(req.User), strings.ToLower(req.Email)); err != nil {
Davit Tabidze75d57c32024-07-19 19:17:55 +04001321 http.Error(w, err.Error(), http.StatusInternalServerError)
1322 return
1323 }
Davit Tabidze75d57c32024-07-19 19:17:55 +04001324}
1325
gio7fbd4ad2024-08-27 10:06:39 +04001326type addUserKeyRequest struct {
1327 User string `json:"user"`
1328 PublicKey string `json:"publicKey"`
1329}
1330
1331func (s *Server) apiAddUserKey(w http.ResponseWriter, r *http.Request) {
1332 var req addUserKeyRequest
1333 if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
1334 http.Error(w, "Invalid request body", http.StatusBadRequest)
1335 return
1336 }
1337 if req.User == "" {
1338 http.Error(w, "Username cannot be empty", http.StatusBadRequest)
1339 return
1340 }
1341 if req.PublicKey == "" {
1342 http.Error(w, "PublicKey cannot be empty", http.StatusBadRequest)
1343 return
1344 }
1345 if err := s.store.AddSSHKeyForUser(strings.ToLower(req.User), req.PublicKey); err != nil {
1346 http.Error(w, err.Error(), http.StatusInternalServerError)
1347 return
1348 }
1349}
1350
1351// TODO(gio): enque sync event instead of directly reaching out to clients.
1352// This will allow to deduplicate sync events and save resources.
Davit Tabidze75d57c32024-07-19 19:17:55 +04001353func (s *Server) pingAllSyncAddresses() {
1354 s.mu.Lock()
1355 defer s.mu.Unlock()
1356 for address := range s.syncAddresses {
gio7fbd4ad2024-08-27 10:06:39 +04001357 go func(address string) {
1358 log.Printf("Pinging %s", address)
1359 resp, err := http.Get(address)
1360 if err != nil {
1361 // TODO(gio): remove sync address after N number of failures.
1362 log.Printf("Failed to ping %s: %v", address, err)
1363 return
1364 }
1365 defer resp.Body.Close()
1366 if resp.StatusCode != http.StatusOK {
1367 log.Printf("Ping to %s returned status %d", address, resp.StatusCode)
1368 }
1369 }(address)
Davit Tabidze75d57c32024-07-19 19:17:55 +04001370 }
1371}
1372
1373func (s *Server) addSyncAddress(address string) {
gio7fbd4ad2024-08-27 10:06:39 +04001374 if address == "" {
1375 return
1376 }
1377 fmt.Printf("Adding sync address: %s\n", address)
Davit Tabidze75d57c32024-07-19 19:17:55 +04001378 s.mu.Lock()
1379 defer s.mu.Unlock()
1380 s.syncAddresses[address] = struct{}{}
1381}
1382
DTabidze908bb852024-03-25 20:07:57 +04001383func convertStatus(status string) (Status, error) {
1384 switch status {
1385 case "Owner":
1386 return Owner, nil
1387 case "Member":
1388 return Member, nil
1389 default:
1390 return Owner, fmt.Errorf("invalid status: %s", status)
1391 }
1392}
1393
1394func isValidGroupName(group string) error {
1395 if strings.TrimSpace(group) == "" {
Davit Tabidze5f1a2c62024-07-17 17:57:27 +04001396 return fmt.Errorf("Group name can't be empty or contain only whitespaces")
DTabidze908bb852024-03-25 20:07:57 +04001397 }
1398 validGroupName := regexp.MustCompile(`^[a-z0-9\-_:.\/ ]+$`)
1399 if !validGroupName.MatchString(group) {
Davit Tabidze5f1a2c62024-07-17 17:57:27 +04001400 return fmt.Errorf("Group name should contain only lowercase letters, digits, -, _, :, ., /")
DTabidze908bb852024-03-25 20:07:57 +04001401 }
1402 return nil
1403}
1404
DTabidze0d802592024-03-19 17:42:45 +04001405func main() {
1406 flag.Parse()
DTabidzec0b4d8f2024-03-22 17:25:10 +04001407 db, err := sql.Open("sqlite3", *dbPath)
DTabidze0d802592024-03-19 17:42:45 +04001408 if err != nil {
1409 panic(err)
1410 }
DTabidzec0b4d8f2024-03-22 17:25:10 +04001411 store, err := NewSQLiteStore(db)
1412 if err != nil {
1413 panic(err)
1414 }
Davit Tabidze75d57c32024-07-19 19:17:55 +04001415 s := Server{
1416 store: store,
1417 syncAddresses: make(map[string]struct{}),
1418 mu: sync.Mutex{},
1419 }
Giorgi Lekveishvili329af572024-03-25 20:14:41 +04001420 log.Fatal(s.Start())
DTabidze0d802592024-03-19 17:42:45 +04001421}