blob: 88729a2bd0538bd1e43f49de4bc307efdba4ee31 [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
31//go:embed static
32var staticResources embed.FS
33
34type Store interface {
Giorgi Lekveishvili942c7612024-03-22 19:27:48 +040035 // Initializes store with admin user and their groups.
36 Init(owner 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 Tabidze75d57c32024-07-19 19:17:55 +040058 GetAllUsers() ([]User, error)
59 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
Giorgi Lekveishvili942c7612024-03-22 19:27:48 +0400140func (s *SQLiteStore) Init(owner string, groups []string) error {
141 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 }
154 for _, g := range groups {
155 query := `INSERT INTO groups (name, description) VALUES (?, '')`
156 if _, err := tx.Exec(query, g); err != nil {
157 return err
158 }
159 query = `INSERT INTO owners (username, group_name) VALUES (?, ?)`
160 if _, err := tx.Exec(query, owner, g); err != nil {
161 return err
162 }
Giorgi Lekveishvilid542b732024-03-25 18:17:39 +0400163 query = `INSERT INTO user_to_group (username, group_name) VALUES (?, ?)`
164 if _, err := tx.Exec(query, owner, g); err != nil {
165 return err
166 }
Giorgi Lekveishvili942c7612024-03-22 19:27:48 +0400167 }
168 return tx.Commit()
169}
170
DTabidze0d802592024-03-19 17:42:45 +0400171func (s *SQLiteStore) queryGroups(query string, args ...interface{}) ([]Group, error) {
172 groups := make([]Group, 0)
173 rows, err := s.db.Query(query, args...)
174 if err != nil {
175 return nil, err
176 }
177 defer rows.Close()
178 for rows.Next() {
179 var group Group
180 if err := rows.Scan(&group.Name, &group.Description); err != nil {
181 return nil, err
182 }
183 groups = append(groups, group)
184 }
185 if err := rows.Err(); err != nil {
186 return nil, err
187 }
188 return groups, nil
189}
190
191func (s *SQLiteStore) GetGroupsOwnedBy(user string) ([]Group, error) {
192 query := `
193 SELECT groups.name, groups.description
194 FROM groups
195 JOIN owners ON groups.name = owners.group_name
196 WHERE owners.username = ?`
197 return s.queryGroups(query, user)
198}
199
DTabidzed7744a62024-03-20 14:09:15 +0400200func (s *SQLiteStore) GetGroupsUserBelongsTo(user string) ([]Group, error) {
DTabidze0d802592024-03-19 17:42:45 +0400201 query := `
202 SELECT groups.name, groups.description
203 FROM groups
204 JOIN user_to_group ON groups.name = user_to_group.group_name
205 WHERE user_to_group.username = ?`
206 return s.queryGroups(query, user)
207}
208
209func (s *SQLiteStore) CreateGroup(owner string, group Group) error {
210 tx, err := s.db.Begin()
211 if err != nil {
212 return err
213 }
214 defer tx.Rollback()
215 query := `INSERT INTO groups (name, description) VALUES (?, ?)`
216 if _, err := tx.Exec(query, group.Name, group.Description); err != nil {
217 sqliteErr, ok := err.(*sqlite3.Error)
Davit Tabidzec0d2bf52024-04-03 15:39:33 +0400218 if ok && sqliteErr.ExtendedCode() == ErrorConstraintPrimaryKeyViolation {
DTabidze0d802592024-03-19 17:42:45 +0400219 return fmt.Errorf("Group with the name %s already exists", group.Name)
220 }
221 return err
222 }
223 query = `INSERT INTO owners (username, group_name) VALUES (?, ?)`
224 if _, err := tx.Exec(query, owner, group.Name); err != nil {
225 return err
226 }
Giorgi Lekveishvili942c7612024-03-22 19:27:48 +0400227 return tx.Commit()
DTabidze0d802592024-03-19 17:42:45 +0400228}
229
230func (s *SQLiteStore) IsGroupOwner(user, group string) (bool, error) {
231 query := `
232 SELECT EXISTS (
233 SELECT 1
234 FROM owners
235 WHERE username = ? AND group_name = ?
236 )`
237 var exists bool
238 if err := s.db.QueryRow(query, user, group).Scan(&exists); err != nil {
239 return false, err
240 }
241 return exists, nil
242}
243
DTabidze0d802592024-03-19 17:42:45 +0400244func (s *SQLiteStore) AddGroupMember(user, group string) error {
Davit Tabidzec0d2bf52024-04-03 15:39:33 +0400245 _, err := s.db.Exec(`INSERT INTO user_to_group (username, group_name) VALUES (?, ?)`, user, group)
DTabidze0d802592024-03-19 17:42:45 +0400246 if err != nil {
Davit Tabidzec0d2bf52024-04-03 15:39:33 +0400247 sqliteErr, ok := err.(*sqlite3.Error)
248 if ok && sqliteErr.ExtendedCode() == ErrorUniqueConstraintViolation {
249 return fmt.Errorf("%s is already a member of group %s", user, group)
250 }
DTabidze0d802592024-03-19 17:42:45 +0400251 return err
252 }
253 return nil
254}
255
256func (s *SQLiteStore) AddGroupOwner(user, group string) error {
Davit Tabidzec0d2bf52024-04-03 15:39:33 +0400257 _, err := s.db.Exec(`INSERT INTO owners (username, group_name) VALUES (?, ?)`, user, group)
DTabidze0d802592024-03-19 17:42:45 +0400258 if err != nil {
Davit Tabidzec0d2bf52024-04-03 15:39:33 +0400259 sqliteErr, ok := err.(*sqlite3.Error)
260 if ok && sqliteErr.ExtendedCode() == ErrorUniqueConstraintViolation {
261 return fmt.Errorf("%s is already an owner of group %s", user, group)
262 }
DTabidze0d802592024-03-19 17:42:45 +0400263 return err
264 }
265 return nil
266}
267
268func (s *SQLiteStore) getUsersByGroup(table, group string) ([]string, error) {
269 query := fmt.Sprintf("SELECT username FROM %s WHERE group_name = ?", table)
270 rows, err := s.db.Query(query, group)
271 if err != nil {
272 return nil, err
273 }
274 defer rows.Close()
275 var users []string
276 for rows.Next() {
277 var username string
278 if err := rows.Scan(&username); err != nil {
279 return nil, err
280 }
281 users = append(users, username)
282 }
283 if err := rows.Err(); err != nil {
284 return nil, err
285 }
286 return users, nil
287}
288
289func (s *SQLiteStore) GetGroupOwners(group string) ([]string, error) {
290 return s.getUsersByGroup("owners", group)
291}
292
293func (s *SQLiteStore) GetGroupMembers(group string) ([]string, error) {
294 return s.getUsersByGroup("user_to_group", group)
295}
296
297func (s *SQLiteStore) GetGroupDescription(group string) (string, error) {
298 var description string
299 query := `SELECT description FROM groups WHERE name = ?`
300 if err := s.db.QueryRow(query, group).Scan(&description); err != nil {
301 return "", err
302 }
303 return description, nil
304}
305
DTabidze908bb852024-03-25 20:07:57 +0400306func (s *SQLiteStore) DoesGroupExist(group string) (bool, error) {
307 query := `SELECT EXISTS (SELECT 1 FROM groups WHERE name = ?)`
308 var exists bool
309 if err := s.db.QueryRow(query, group).Scan(&exists); err != nil {
310 return false, err
311 }
312 return exists, nil
313}
314
DTabidze0d802592024-03-19 17:42:45 +0400315func (s *SQLiteStore) AddChildGroup(parent, child string) error {
DTabidze908bb852024-03-25 20:07:57 +0400316 if parent == child {
Davit Tabidze5f1a2c62024-07-17 17:57:27 +0400317 return fmt.Errorf("Parent and child groups can not have same name")
DTabidze908bb852024-03-25 20:07:57 +0400318 }
Davit Tabidzec0d2bf52024-04-03 15:39:33 +0400319 exists, err := s.DoesGroupExist(parent)
320 if err != nil {
Davit Tabidze5f1a2c62024-07-17 17:57:27 +0400321 return fmt.Errorf("Error checking parent group existence: %v", err)
DTabidze908bb852024-03-25 20:07:57 +0400322 }
Davit Tabidzec0d2bf52024-04-03 15:39:33 +0400323 if !exists {
Davit Tabidze5f1a2c62024-07-17 17:57:27 +0400324 return fmt.Errorf("Parent group with name %s does not exist", parent)
Davit Tabidzec0d2bf52024-04-03 15:39:33 +0400325 }
326 exists, err = s.DoesGroupExist(child)
327 if err != nil {
Davit Tabidze5f1a2c62024-07-17 17:57:27 +0400328 return fmt.Errorf("Error checking child group existence: %v", err)
Davit Tabidzec0d2bf52024-04-03 15:39:33 +0400329 }
330 if !exists {
Davit Tabidze5f1a2c62024-07-17 17:57:27 +0400331 return fmt.Errorf("Child group with name %s does not exist", child)
DTabidze908bb852024-03-25 20:07:57 +0400332 }
DTabidzec0b4d8f2024-03-22 17:25:10 +0400333 parentGroups, err := s.GetAllTransitiveGroupsForGroup(parent)
334 if err != nil {
335 return err
336 }
337 for _, group := range parentGroups {
338 if group.Name == child {
Davit Tabidze5f1a2c62024-07-17 17:57:27 +0400339 return fmt.Errorf("Circular reference detected: group %s is already a parent of group %s", child, parent)
DTabidzec0b4d8f2024-03-22 17:25:10 +0400340 }
341 }
Davit Tabidzec0d2bf52024-04-03 15:39:33 +0400342 _, err = s.db.Exec(`INSERT INTO group_to_group (parent_group, child_group) VALUES (?, ?)`, parent, child)
DTabidze0d802592024-03-19 17:42:45 +0400343 if err != nil {
Davit Tabidzec0d2bf52024-04-03 15:39:33 +0400344 sqliteErr, ok := err.(*sqlite3.Error)
345 if ok && sqliteErr.ExtendedCode() == ErrorUniqueConstraintViolation {
Davit Tabidze5f1a2c62024-07-17 17:57:27 +0400346 return fmt.Errorf("Child group name %s already exists in group %s", child, parent)
Davit Tabidzec0d2bf52024-04-03 15:39:33 +0400347 }
DTabidze0d802592024-03-19 17:42:45 +0400348 return err
349 }
350 return nil
351}
352
DTabidzec0b4d8f2024-03-22 17:25:10 +0400353func (s *SQLiteStore) GetAllTransitiveGroupsForUser(user string) ([]Group, error) {
354 if groups, err := s.GetGroupsUserBelongsTo(user); err != nil {
DTabidzed7744a62024-03-20 14:09:15 +0400355 return nil, err
DTabidzec0b4d8f2024-03-22 17:25:10 +0400356 } else {
357 visited := map[string]struct{}{}
358 return s.getAllParentGroupsRecursive(groups, visited)
DTabidzed7744a62024-03-20 14:09:15 +0400359 }
DTabidzec0b4d8f2024-03-22 17:25:10 +0400360}
361
362func (s *SQLiteStore) GetAllTransitiveGroupsForGroup(group string) ([]Group, error) {
363 if p, err := s.GetGroupsGroupBelongsTo(group); err != nil {
364 return nil, err
365 } else {
366 // Mark initial group as visited
367 visited := map[string]struct{}{
368 group: struct{}{},
369 }
370 return s.getAllParentGroupsRecursive(p, visited)
371 }
372}
373
374func (s *SQLiteStore) getAllParentGroupsRecursive(groups []Group, visited map[string]struct{}) ([]Group, error) {
375 var ret []Group
376 for _, g := range groups {
377 if _, ok := visited[g.Name]; ok {
378 continue
379 }
380 visited[g.Name] = struct{}{}
381 ret = append(ret, g)
382 if p, err := s.GetGroupsGroupBelongsTo(g.Name); err != nil {
DTabidzed7744a62024-03-20 14:09:15 +0400383 return nil, err
DTabidzec0b4d8f2024-03-22 17:25:10 +0400384 } else if res, err := s.getAllParentGroupsRecursive(p, visited); err != nil {
385 return nil, err
386 } else {
387 ret = append(ret, res...)
DTabidzed7744a62024-03-20 14:09:15 +0400388 }
389 }
DTabidzec0b4d8f2024-03-22 17:25:10 +0400390 return ret, nil
DTabidzed7744a62024-03-20 14:09:15 +0400391}
392
DTabidzec0b4d8f2024-03-22 17:25:10 +0400393func (s *SQLiteStore) GetGroupsGroupBelongsTo(group string) ([]Group, error) {
394 query := `
395 SELECT groups.name, groups.description
396 FROM groups
397 JOIN group_to_group ON groups.name = group_to_group.parent_group
398 WHERE group_to_group.child_group = ?`
DTabidzed7744a62024-03-20 14:09:15 +0400399 rows, err := s.db.Query(query, group)
400 if err != nil {
401 return nil, err
402 }
403 defer rows.Close()
DTabidzec0b4d8f2024-03-22 17:25:10 +0400404 var parentGroups []Group
DTabidzed7744a62024-03-20 14:09:15 +0400405 for rows.Next() {
DTabidzec0b4d8f2024-03-22 17:25:10 +0400406 var parentGroup Group
407 if err := rows.Scan(&parentGroup.Name, &parentGroup.Description); err != nil {
DTabidzed7744a62024-03-20 14:09:15 +0400408 return nil, err
409 }
410 parentGroups = append(parentGroups, parentGroup)
411 }
412 if err := rows.Err(); err != nil {
413 return nil, err
414 }
415 return parentGroups, nil
416}
417
DTabidzec0b4d8f2024-03-22 17:25:10 +0400418func (s *SQLiteStore) GetDirectChildrenGroups(group string) ([]Group, error) {
419 query := `
420 SELECT groups.name, groups.description
421 FROM groups
422 JOIN group_to_group ON groups.name = group_to_group.child_group
423 WHERE group_to_group.parent_group = ?`
424 rows, err := s.db.Query(query, group)
425 if err != nil {
426 return nil, err
427 }
428 defer rows.Close()
429 var childrenGroups []Group
430 for rows.Next() {
431 var childGroup Group
432 if err := rows.Scan(&childGroup.Name, &childGroup.Description); err != nil {
433 return nil, err
434 }
435 childrenGroups = append(childrenGroups, childGroup)
436 }
437 if err := rows.Err(); err != nil {
438 return nil, err
439 }
440 return childrenGroups, nil
441}
442
DTabidze2b224bf2024-03-27 13:25:49 +0400443func (s *SQLiteStore) RemoveFromGroupToGroup(parent, child string) error {
444 query := `DELETE FROM group_to_group WHERE parent_group = ? AND child_group = ?`
445 rowDeleted, err := s.db.Exec(query, parent, child)
446 if err != nil {
447 return err
448 }
449 rowDeletedNumber, err := rowDeleted.RowsAffected()
450 if err != nil {
451 return err
452 }
453 if rowDeletedNumber == 0 {
Davit Tabidze5f1a2c62024-07-17 17:57:27 +0400454 return fmt.Errorf("Pair of parent '%s' and child '%s' groups not found", parent, child)
DTabidze2b224bf2024-03-27 13:25:49 +0400455 }
456 return nil
457}
458
459func (s *SQLiteStore) RemoveUserFromTable(username, groupName, tableName string) error {
460 if tableName == "owners" {
461 owners, err := s.GetGroupOwners(groupName)
462 if err != nil {
463 return err
464 }
465 if len(owners) == 1 {
Davit Tabidze5f1a2c62024-07-17 17:57:27 +0400466 return fmt.Errorf("Cannot remove the last owner of the group")
DTabidze2b224bf2024-03-27 13:25:49 +0400467 }
468 }
469 query := fmt.Sprintf("DELETE FROM %s WHERE username = ? AND group_name = ?", tableName)
470 rowDeleted, err := s.db.Exec(query, username, groupName)
471 if err != nil {
472 return err
473 }
474 rowDeletedNumber, err := rowDeleted.RowsAffected()
475 if err != nil {
476 return err
477 }
478 if rowDeletedNumber == 0 {
Davit Tabidze5f1a2c62024-07-17 17:57:27 +0400479 return fmt.Errorf("Pair of group '%s' and user '%s' not found", groupName, username)
DTabidze2b224bf2024-03-27 13:25:49 +0400480 }
481 return nil
482}
483
Davit Tabidzec0d2bf52024-04-03 15:39:33 +0400484func (s *SQLiteStore) AddOwnerGroup(owner_group, owned_group string) error {
485 if owned_group == owner_group {
Davit Tabidze5f1a2c62024-07-17 17:57:27 +0400486 return fmt.Errorf("Group can not own itself")
Davit Tabidzec0d2bf52024-04-03 15:39:33 +0400487 }
488 exists, err := s.DoesGroupExist(owned_group)
489 if err != nil {
Davit Tabidze5f1a2c62024-07-17 17:57:27 +0400490 return fmt.Errorf("Error checking owned group existence: %v", err)
Davit Tabidzec0d2bf52024-04-03 15:39:33 +0400491 }
492 if !exists {
Davit Tabidze5f1a2c62024-07-17 17:57:27 +0400493 return fmt.Errorf("Owned group with name %s does not exist", owned_group)
Davit Tabidzec0d2bf52024-04-03 15:39:33 +0400494 }
495 exists, err = s.DoesGroupExist(owner_group)
496 if err != nil {
Davit Tabidze5f1a2c62024-07-17 17:57:27 +0400497 return fmt.Errorf("Error checking owner group existence: %v", err)
Davit Tabidzec0d2bf52024-04-03 15:39:33 +0400498 }
499 if !exists {
Davit Tabidze5f1a2c62024-07-17 17:57:27 +0400500 return fmt.Errorf("Owner group with name %s does not exist", owner_group)
Davit Tabidzec0d2bf52024-04-03 15:39:33 +0400501 }
502 _, err = s.db.Exec(`INSERT INTO owner_groups (owner_group, owned_group) VALUES (?, ?)`, owner_group, owned_group)
503 if err != nil {
504 sqliteErr, ok := err.(*sqlite3.Error)
505 if ok && sqliteErr.ExtendedCode() == ErrorUniqueConstraintViolation {
Davit Tabidze5f1a2c62024-07-17 17:57:27 +0400506 return fmt.Errorf("Group named %s is already owner of a group %s", owner_group, owned_group)
Davit Tabidzec0d2bf52024-04-03 15:39:33 +0400507 }
508 return err
509 }
510 return nil
511}
512
513func (s *SQLiteStore) GetGroupOwnerGroups(group string) ([]Group, error) {
514 query := `
515 SELECT groups.name, groups.description
516 FROM groups
517 JOIN owner_groups ON groups.name = owner_groups.owner_group
518 WHERE owner_groups.owned_group = ?`
519 return s.queryGroups(query, group)
520}
521
522func (s *SQLiteStore) IsMemberOfOwnerGroup(user, group string) (bool, error) {
523 query := `
524 SELECT EXISTS (
525 SELECT 1 FROM owner_groups
526 INNER JOIN user_to_group ON owner_groups.owner_group = user_to_group.group_name
527 WHERE owner_groups.owned_group = ? AND user_to_group.username = ?)`
528 var exists bool
529 err := s.db.QueryRow(query, group, user).Scan(&exists)
530 if err != nil {
531 return false, err
532 }
533 return exists, nil
534}
535
536func (s *SQLiteStore) GetAllGroups() ([]Group, error) {
537 query := `SELECT name, description FROM groups`
538 return s.queryGroups(query)
539}
540
Davit Tabidze75d57c32024-07-19 19:17:55 +0400541func (s *SQLiteStore) AddSSHKeyForUser(username, sshKey string) error {
542 _, err := s.db.Exec(`INSERT INTO user_ssh_keys (username, ssh_key) VALUES (?, ?)`, username, sshKey)
543 if err != nil {
544 sqliteErr, ok := err.(*sqlite3.Error)
545 if ok && sqliteErr.ExtendedCode() == ErrorUniqueConstraintViolation {
546 return fmt.Errorf("%s such SSH public key already exists", sshKey)
547 }
548 return err
549 }
550 return nil
551}
552
553func (s *SQLiteStore) RemoveSSHKeyForUser(username, sshKey string) error {
554 _, err := s.db.Exec(`DELETE FROM user_ssh_keys WHERE username = ? AND ssh_key = ?`, username, sshKey)
555 if err != nil {
556 return err
557 }
558 return nil
559}
560
561func (s *SQLiteStore) GetAllUsers() ([]User, error) {
562 rows, err := s.db.Query(`
563 SELECT users.username, users.email, GROUP_CONCAT(user_ssh_keys.ssh_key, ',')
564 FROM users
565 LEFT JOIN user_ssh_keys ON users.username = user_ssh_keys.username
566 GROUP BY users.username
567 `)
568 if err != nil {
569 return nil, err
570 }
571 defer rows.Close()
572 var userInfos []User
573 for rows.Next() {
574 var username, email string
575 var sshKeys sql.NullString
576 if err := rows.Scan(&username, &email, &sshKeys); err != nil {
577 return nil, err
578 }
579 user := User{
580 Username: username,
581 Email: email,
582 }
583 if sshKeys.Valid {
584 user.SSHPublicKeys = strings.Split(sshKeys.String, ",")
585 }
586 userInfos = append(userInfos, user)
587 }
588 if err := rows.Err(); err != nil {
589 return nil, err
590 }
591 return userInfos, nil
592}
593
594func (s *SQLiteStore) GetUser(username string) (User, error) {
595 var user User
596 user.Username = username
597 query := `
598 SELECT users.email, GROUP_CONCAT(user_ssh_keys.ssh_key, ',')
599 FROM users
600 LEFT JOIN user_ssh_keys ON users.username = user_ssh_keys.username
601 WHERE users.username = ?
602 GROUP BY users.username
603 `
604 row := s.db.QueryRow(query, username)
605 var sshKeys sql.NullString
606 err := row.Scan(&user.Email, &sshKeys)
607 if err != nil {
608 if err == sql.ErrNoRows {
609 return User{}, fmt.Errorf("no user found with username %s", username)
610 }
611 return User{}, err
612 }
613 if sshKeys.Valid {
614 user.SSHPublicKeys = strings.Split(sshKeys.String, ",")
615 }
616 return user, nil
617}
618
619func (s *SQLiteStore) CreateUser(user, email string) error {
620 _, err := s.db.Exec(`INSERT INTO users (username, email) VALUES (?, ?)`, user, email)
621 if err != nil {
622 sqliteErr, ok := err.(*sqlite3.Error)
623 if ok {
624 if sqliteErr.ExtendedCode() == ErrorUniqueConstraintViolation {
625 if strings.Contains(err.Error(), "UNIQUE constraint failed: users.username") {
626 return fmt.Errorf("username %s already exists", user)
627 }
628 if strings.Contains(err.Error(), "UNIQUE constraint failed: users.email") {
629 return fmt.Errorf("email %s already exists", email)
630 }
631 }
632 }
633 return err
634 }
635 return nil
636}
637
DTabidze0d802592024-03-19 17:42:45 +0400638func getLoggedInUser(r *http.Request) (string, error) {
DTabidzec0b4d8f2024-03-22 17:25:10 +0400639 if user := r.Header.Get("X-User"); user != "" {
640 return user, nil
641 } else {
642 return "", fmt.Errorf("unauthenticated")
643 }
Davit Tabidze75d57c32024-07-19 19:17:55 +0400644 // return "tabo", nil
DTabidze0d802592024-03-19 17:42:45 +0400645}
646
647type Status int
648
649const (
650 Owner Status = iota
651 Member
652)
653
Giorgi Lekveishvili329af572024-03-25 20:14:41 +0400654func (s *Server) Start() error {
655 e := make(chan error)
656 go func() {
657 r := mux.NewRouter()
658 r.PathPrefix("/static/").Handler(http.FileServer(http.FS(staticResources)))
Davit Tabidze75d57c32024-07-19 19:17:55 +0400659 r.HandleFunc("/group/{group-name}/add-user/", s.addUserToGroupHandler).Methods(http.MethodPost)
660 r.HandleFunc("/group/{parent-group}/add-child-group", s.addChildGroupHandler).Methods(http.MethodPost)
661 r.HandleFunc("/group/{owned-group}/add-owner-group", s.addOwnerGroupHandler).Methods(http.MethodPost)
662 r.HandleFunc("/group/{parent-group}/remove-child-group/{child-group}", s.removeChildGroupHandler).Methods(http.MethodPost)
663 r.HandleFunc("/group/{group-name}/remove-owner/{username}", s.removeOwnerFromGroupHandler).Methods(http.MethodPost)
664 r.HandleFunc("/group/{group-name}/remove-member/{username}", s.removeMemberFromGroupHandler).Methods(http.MethodPost)
Giorgi Lekveishvili329af572024-03-25 20:14:41 +0400665 r.HandleFunc("/group/{group-name}", s.groupHandler)
Davit Tabidze75d57c32024-07-19 19:17:55 +0400666 r.HandleFunc("/user/{username}/ssh-key", s.addSSHKeyForUserHandler).Methods(http.MethodPost)
667 r.HandleFunc("/user/{username}/remove-ssh-key", s.removeSSHKeyForUserHandler).Methods(http.MethodPost)
DTabidze5d735e32024-03-26 16:01:06 +0400668 r.HandleFunc("/user/{username}", s.userHandler)
Davit Tabidze75d57c32024-07-19 19:17:55 +0400669 r.HandleFunc("/create-group", s.createGroupHandler).Methods(http.MethodPost)
Giorgi Lekveishvili329af572024-03-25 20:14:41 +0400670 r.HandleFunc("/", s.homePageHandler)
671 e <- http.ListenAndServe(fmt.Sprintf(":%d", *port), r)
672 }()
673 go func() {
674 r := mux.NewRouter()
675 r.HandleFunc("/api/init", s.apiInitHandler)
676 r.HandleFunc("/api/user/{username}", s.apiMemberOfHandler)
Davit Tabidze75d57c32024-07-19 19:17:55 +0400677 r.HandleFunc("/api/users", s.apiGetAllUsers).Methods(http.MethodGet)
678 r.HandleFunc("/api/users", s.apiCreateUser).Methods(http.MethodPost)
Giorgi Lekveishvili329af572024-03-25 20:14:41 +0400679 e <- http.ListenAndServe(fmt.Sprintf(":%d", *apiPort), r)
680 }()
681 return <-e
DTabidze0d802592024-03-19 17:42:45 +0400682}
683
684type GroupData struct {
685 Group Group
686 Membership string
687}
688
Davit Tabidzec0d2bf52024-04-03 15:39:33 +0400689func (s *Server) checkIsOwner(w http.ResponseWriter, user, group string) error {
DTabidze0d802592024-03-19 17:42:45 +0400690 isOwner, err := s.store.IsGroupOwner(user, group)
691 if err != nil {
Davit Tabidzec0d2bf52024-04-03 15:39:33 +0400692 return err
DTabidze0d802592024-03-19 17:42:45 +0400693 }
Davit Tabidzec0d2bf52024-04-03 15:39:33 +0400694 if isOwner {
695 return nil
DTabidze0d802592024-03-19 17:42:45 +0400696 }
Davit Tabidzec0d2bf52024-04-03 15:39:33 +0400697 // TODO(dtabidze): right now this only checks if user is member of just one lvl upper group. should add transitive group check.
698 isMemberOfOwnerGroup, err := s.store.IsMemberOfOwnerGroup(user, group)
699 if err != nil {
700 return err
701 }
702 if !isMemberOfOwnerGroup {
Davit Tabidze5f1a2c62024-07-17 17:57:27 +0400703 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 +0400704 }
705 return nil
DTabidze0d802592024-03-19 17:42:45 +0400706}
707
DTabidze4b44ff42024-04-02 03:16:26 +0400708type templates struct {
709 group *template.Template
710 user *template.Template
711}
712
713func parseTemplates(fs embed.FS) (templates, error) {
714 base, err := template.ParseFS(fs, "memberships-tmpl/base.html")
715 if err != nil {
716 return templates{}, err
717 }
718 parse := func(path string) (*template.Template, error) {
719 if b, err := base.Clone(); err != nil {
720 return nil, err
721 } else {
722 return b.ParseFS(fs, path)
723 }
724 }
725 user, err := parse("memberships-tmpl/user.html")
726 if err != nil {
727 return templates{}, err
728 }
729 group, err := parse("memberships-tmpl/group.html")
730 if err != nil {
731 return templates{}, err
732 }
733 return templates{group, user}, nil
734}
735
DTabidze0d802592024-03-19 17:42:45 +0400736func (s *Server) homePageHandler(w http.ResponseWriter, r *http.Request) {
737 loggedInUser, err := getLoggedInUser(r)
738 if err != nil {
739 http.Error(w, "User Not Logged In", http.StatusUnauthorized)
740 return
741 }
DTabidze5d735e32024-03-26 16:01:06 +0400742 http.Redirect(w, r, "/user/"+loggedInUser, http.StatusSeeOther)
743}
744
Davit Tabidze75d57c32024-07-19 19:17:55 +0400745type UserPageData struct {
746 OwnerGroups []Group
747 MembershipGroups []Group
748 TransitiveGroups []Group
749 LoggedInUserPage bool
750 CurrentUser string
751 SSHPublicKeys []string
752 Email string
753 ErrorMessage string
754}
755
DTabidze5d735e32024-03-26 16:01:06 +0400756func (s *Server) userHandler(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 }
DTabidze4b44ff42024-04-02 03:16:26 +0400762 errorMsg := r.URL.Query().Get("errorMessage")
DTabidze5d735e32024-03-26 16:01:06 +0400763 vars := mux.Vars(r)
764 user := strings.ToLower(vars["username"])
765 // TODO(dtabidze): should check if username exists or not.
766 loggedInUserPage := loggedInUser == user
767 ownerGroups, err := s.store.GetGroupsOwnedBy(user)
DTabidze0d802592024-03-19 17:42:45 +0400768 if err != nil {
769 http.Error(w, err.Error(), http.StatusInternalServerError)
770 return
771 }
DTabidze5d735e32024-03-26 16:01:06 +0400772 membershipGroups, err := s.store.GetGroupsUserBelongsTo(user)
DTabidze0d802592024-03-19 17:42:45 +0400773 if err != nil {
774 http.Error(w, err.Error(), http.StatusInternalServerError)
775 return
776 }
DTabidze5d735e32024-03-26 16:01:06 +0400777 transitiveGroups, err := s.store.GetAllTransitiveGroupsForUser(user)
DTabidzec0b4d8f2024-03-22 17:25:10 +0400778 if err != nil {
779 http.Error(w, err.Error(), http.StatusInternalServerError)
780 return
781 }
Davit Tabidze75d57c32024-07-19 19:17:55 +0400782 userInfo, err := s.store.GetUser(user)
783 if err != nil {
784 http.Error(w, err.Error(), http.StatusInternalServerError)
785 return
786 }
787 data := UserPageData{
DTabidze0d802592024-03-19 17:42:45 +0400788 OwnerGroups: ownerGroups,
789 MembershipGroups: membershipGroups,
DTabidzec0b4d8f2024-03-22 17:25:10 +0400790 TransitiveGroups: transitiveGroups,
DTabidze5d735e32024-03-26 16:01:06 +0400791 LoggedInUserPage: loggedInUserPage,
792 CurrentUser: user,
Davit Tabidze75d57c32024-07-19 19:17:55 +0400793 SSHPublicKeys: userInfo.SSHPublicKeys,
794 Email: userInfo.Email,
DTabidze4b44ff42024-04-02 03:16:26 +0400795 ErrorMessage: errorMsg,
DTabidze0d802592024-03-19 17:42:45 +0400796 }
DTabidze4b44ff42024-04-02 03:16:26 +0400797 templates, err := parseTemplates(tmpls)
798 if err != nil {
799 http.Error(w, err.Error(), http.StatusInternalServerError)
800 return
801 }
802 if err := templates.user.Execute(w, data); err != nil {
DTabidze0d802592024-03-19 17:42:45 +0400803 http.Error(w, err.Error(), http.StatusInternalServerError)
804 return
805 }
806}
807
808func (s *Server) createGroupHandler(w http.ResponseWriter, r *http.Request) {
809 loggedInUser, err := getLoggedInUser(r)
810 if err != nil {
811 http.Error(w, "User Not Logged In", http.StatusUnauthorized)
812 return
813 }
DTabidze0d802592024-03-19 17:42:45 +0400814 if err := r.ParseForm(); err != nil {
815 http.Error(w, err.Error(), http.StatusInternalServerError)
816 return
817 }
818 var group Group
819 group.Name = r.PostFormValue("group-name")
DTabidze908bb852024-03-25 20:07:57 +0400820 if err := isValidGroupName(group.Name); err != nil {
DTabidze4b44ff42024-04-02 03:16:26 +0400821 // http.Error(w, err.Error(), http.StatusBadRequest)
822 redirectURL := fmt.Sprintf("/user/%s?errorMessage=%s", loggedInUser, url.QueryEscape(err.Error()))
823 http.Redirect(w, r, redirectURL, http.StatusFound)
DTabidze908bb852024-03-25 20:07:57 +0400824 return
825 }
DTabidze0d802592024-03-19 17:42:45 +0400826 group.Description = r.PostFormValue("description")
827 if err := s.store.CreateGroup(loggedInUser, group); err != nil {
DTabidze4b44ff42024-04-02 03:16:26 +0400828 // http.Error(w, err.Error(), http.StatusInternalServerError)
829 redirectURL := fmt.Sprintf("/user/%s?errorMessage=%s", loggedInUser, url.QueryEscape(err.Error()))
830 http.Redirect(w, r, redirectURL, http.StatusFound)
DTabidze0d802592024-03-19 17:42:45 +0400831 return
832 }
833 http.Redirect(w, r, "/", http.StatusSeeOther)
834}
835
Davit Tabidze75d57c32024-07-19 19:17:55 +0400836type GroupPageData struct {
837 GroupName string
838 Description string
839 Owners []string
840 Members []string
841 AllGroups []Group
842 TransitiveGroups []Group
843 ChildGroups []Group
844 OwnerGroups []Group
845 ErrorMessage string
846}
847
DTabidze0d802592024-03-19 17:42:45 +0400848func (s *Server) groupHandler(w http.ResponseWriter, r *http.Request) {
DTabidzec0b4d8f2024-03-22 17:25:10 +0400849 _, err := getLoggedInUser(r)
850 if err != nil {
851 http.Error(w, "User Not Logged In", http.StatusUnauthorized)
852 return
853 }
DTabidze4b44ff42024-04-02 03:16:26 +0400854 errorMsg := r.URL.Query().Get("errorMessage")
DTabidzed7744a62024-03-20 14:09:15 +0400855 vars := mux.Vars(r)
856 groupName := vars["group-name"]
DTabidze908bb852024-03-25 20:07:57 +0400857 exists, err := s.store.DoesGroupExist(groupName)
858 if err != nil {
859 http.Error(w, err.Error(), http.StatusInternalServerError)
860 return
861 }
862 if !exists {
DTabidze4b44ff42024-04-02 03:16:26 +0400863 errorMsg = fmt.Sprintf("group with the name '%s' not found", groupName)
DTabidze908bb852024-03-25 20:07:57 +0400864 http.Error(w, errorMsg, http.StatusNotFound)
865 return
866 }
DTabidze0d802592024-03-19 17:42:45 +0400867 if err != nil {
868 http.Error(w, err.Error(), http.StatusInternalServerError)
869 return
870 }
871 owners, err := s.store.GetGroupOwners(groupName)
872 if err != nil {
873 http.Error(w, err.Error(), http.StatusInternalServerError)
874 return
875 }
876 members, err := s.store.GetGroupMembers(groupName)
877 if err != nil {
878 http.Error(w, err.Error(), http.StatusInternalServerError)
879 return
880 }
881 description, err := s.store.GetGroupDescription(groupName)
882 if err != nil {
883 http.Error(w, err.Error(), http.StatusInternalServerError)
884 return
885 }
Davit Tabidzec0d2bf52024-04-03 15:39:33 +0400886 allGroups, err := s.store.GetAllGroups()
DTabidze0d802592024-03-19 17:42:45 +0400887 if err != nil {
888 http.Error(w, err.Error(), http.StatusInternalServerError)
889 return
890 }
DTabidzec0b4d8f2024-03-22 17:25:10 +0400891 transitiveGroups, err := s.store.GetAllTransitiveGroupsForGroup(groupName)
892 if err != nil {
893 http.Error(w, err.Error(), http.StatusInternalServerError)
894 return
895 }
896 childGroups, err := s.store.GetDirectChildrenGroups(groupName)
897 if err != nil {
898 http.Error(w, err.Error(), http.StatusInternalServerError)
899 return
900 }
Davit Tabidzec0d2bf52024-04-03 15:39:33 +0400901 ownerGroups, err := s.store.GetGroupOwnerGroups(groupName)
902 if err != nil {
903 http.Error(w, err.Error(), http.StatusInternalServerError)
904 return
905 }
Davit Tabidze75d57c32024-07-19 19:17:55 +0400906 data := GroupPageData{
DTabidzec0b4d8f2024-03-22 17:25:10 +0400907 GroupName: groupName,
908 Description: description,
909 Owners: owners,
910 Members: members,
Davit Tabidzec0d2bf52024-04-03 15:39:33 +0400911 AllGroups: allGroups,
DTabidzec0b4d8f2024-03-22 17:25:10 +0400912 TransitiveGroups: transitiveGroups,
913 ChildGroups: childGroups,
Davit Tabidzec0d2bf52024-04-03 15:39:33 +0400914 OwnerGroups: ownerGroups,
DTabidze4b44ff42024-04-02 03:16:26 +0400915 ErrorMessage: errorMsg,
DTabidze0d802592024-03-19 17:42:45 +0400916 }
DTabidze4b44ff42024-04-02 03:16:26 +0400917 templates, err := parseTemplates(tmpls)
918 if err != nil {
919 http.Error(w, err.Error(), http.StatusInternalServerError)
920 return
921 }
922 if err := templates.group.Execute(w, data); err != nil {
DTabidze0d802592024-03-19 17:42:45 +0400923 http.Error(w, err.Error(), http.StatusInternalServerError)
924 return
925 }
926}
927
DTabidze2b224bf2024-03-27 13:25:49 +0400928func (s *Server) removeChildGroupHandler(w http.ResponseWriter, r *http.Request) {
929 loggedInUser, err := getLoggedInUser(r)
930 if err != nil {
931 http.Error(w, "User Not Logged In", http.StatusUnauthorized)
932 return
933 }
Davit Tabidze75d57c32024-07-19 19:17:55 +0400934 vars := mux.Vars(r)
935 parentGroup := vars["parent-group"]
936 childGroup := vars["child-group"]
937 if err := isValidGroupName(parentGroup); err != nil {
938 http.Error(w, err.Error(), http.StatusBadRequest)
939 return
DTabidze2b224bf2024-03-27 13:25:49 +0400940 }
Davit Tabidze75d57c32024-07-19 19:17:55 +0400941 if err := isValidGroupName(childGroup); err != nil {
942 http.Error(w, err.Error(), http.StatusBadRequest)
943 return
944 }
945 if err := s.checkIsOwner(w, loggedInUser, parentGroup); err != nil {
946 redirectURL := fmt.Sprintf("/group/%s?errorMessage=%s", parentGroup, url.QueryEscape(err.Error()))
947 http.Redirect(w, r, redirectURL, http.StatusSeeOther)
948 return
949 }
950 err = s.store.RemoveFromGroupToGroup(parentGroup, childGroup)
951 if err != nil {
952 redirectURL := fmt.Sprintf("/group/%s?errorMessage=%s", parentGroup, url.QueryEscape(err.Error()))
953 http.Redirect(w, r, redirectURL, http.StatusFound)
954 return
955 }
956 http.Redirect(w, r, "/group/"+parentGroup, http.StatusSeeOther)
DTabidze2b224bf2024-03-27 13:25:49 +0400957}
958
DTabidze078385f2024-03-27 14:49:05 +0400959func (s *Server) removeOwnerFromGroupHandler(w http.ResponseWriter, r *http.Request) {
DTabidze2b224bf2024-03-27 13:25:49 +0400960 loggedInUser, err := getLoggedInUser(r)
961 if err != nil {
962 http.Error(w, "User Not Logged In", http.StatusUnauthorized)
963 return
964 }
Davit Tabidze75d57c32024-07-19 19:17:55 +0400965 vars := mux.Vars(r)
966 username := vars["username"]
967 groupName := vars["group-name"]
968 tableName := "owners"
969 if err := isValidGroupName(groupName); err != nil {
970 http.Error(w, err.Error(), http.StatusBadRequest)
971 return
DTabidze2b224bf2024-03-27 13:25:49 +0400972 }
Davit Tabidze75d57c32024-07-19 19:17:55 +0400973 if err := s.checkIsOwner(w, loggedInUser, groupName); err != nil {
974 redirectURL := fmt.Sprintf("/group/%s?errorMessage=%s", groupName, url.QueryEscape(err.Error()))
975 http.Redirect(w, r, redirectURL, http.StatusSeeOther)
976 return
977 }
978 err = s.store.RemoveUserFromTable(username, groupName, tableName)
979 if err != nil {
980 redirectURL := fmt.Sprintf("/group/%s?errorMessage=%s", groupName, url.QueryEscape(err.Error()))
981 http.Redirect(w, r, redirectURL, http.StatusFound)
982 return
983 }
984 http.Redirect(w, r, "/group/"+groupName, http.StatusSeeOther)
DTabidze2b224bf2024-03-27 13:25:49 +0400985}
986
DTabidze078385f2024-03-27 14:49:05 +0400987func (s *Server) removeMemberFromGroupHandler(w http.ResponseWriter, r *http.Request) {
988 loggedInUser, err := getLoggedInUser(r)
989 if err != nil {
990 http.Error(w, "User Not Logged In", http.StatusUnauthorized)
991 return
992 }
Davit Tabidze75d57c32024-07-19 19:17:55 +0400993 vars := mux.Vars(r)
994 username := vars["username"]
995 groupName := vars["group-name"]
996 tableName := "user_to_group"
997 if err := isValidGroupName(groupName); err != nil {
998 http.Error(w, err.Error(), http.StatusBadRequest)
999 return
DTabidze078385f2024-03-27 14:49:05 +04001000 }
Davit Tabidze75d57c32024-07-19 19:17:55 +04001001 if err := s.checkIsOwner(w, loggedInUser, groupName); err != nil {
1002 redirectURL := fmt.Sprintf("/group/%s?errorMessage=%s", groupName, url.QueryEscape(err.Error()))
1003 http.Redirect(w, r, redirectURL, http.StatusSeeOther)
1004 return
1005 }
1006 err = s.store.RemoveUserFromTable(username, groupName, tableName)
1007 if err != nil {
1008 redirectURL := fmt.Sprintf("/group/%s?errorMessage=%s", groupName, url.QueryEscape(err.Error()))
1009 http.Redirect(w, r, redirectURL, http.StatusFound)
1010 return
1011 }
1012 http.Redirect(w, r, "/group/"+groupName, http.StatusSeeOther)
DTabidze078385f2024-03-27 14:49:05 +04001013}
1014
1015func (s *Server) addUserToGroupHandler(w http.ResponseWriter, r *http.Request) {
DTabidze0d802592024-03-19 17:42:45 +04001016 loggedInUser, err := getLoggedInUser(r)
1017 if err != nil {
1018 http.Error(w, "User Not Logged In", http.StatusUnauthorized)
1019 return
1020 }
DTabidze078385f2024-03-27 14:49:05 +04001021 vars := mux.Vars(r)
1022 groupName := vars["group-name"]
DTabidze908bb852024-03-25 20:07:57 +04001023 if err := isValidGroupName(groupName); err != nil {
1024 http.Error(w, err.Error(), http.StatusBadRequest)
1025 return
1026 }
1027 username := strings.ToLower(r.FormValue("username"))
1028 if username == "" {
1029 http.Error(w, "Username parameter is required", http.StatusBadRequest)
1030 return
1031 }
DTabidze0d802592024-03-19 17:42:45 +04001032 status, err := convertStatus(r.FormValue("status"))
1033 if err != nil {
1034 http.Error(w, err.Error(), http.StatusBadRequest)
1035 return
1036 }
Davit Tabidzec0d2bf52024-04-03 15:39:33 +04001037 if err := s.checkIsOwner(w, loggedInUser, groupName); err != nil {
1038 redirectURL := fmt.Sprintf("/group/%s?errorMessage=%s", groupName, url.QueryEscape(err.Error()))
1039 http.Redirect(w, r, redirectURL, http.StatusSeeOther)
DTabidze0d802592024-03-19 17:42:45 +04001040 return
1041 }
1042 switch status {
1043 case Owner:
1044 err = s.store.AddGroupOwner(username, groupName)
1045 case Member:
1046 err = s.store.AddGroupMember(username, groupName)
1047 default:
1048 http.Error(w, "Invalid status", http.StatusBadRequest)
1049 return
1050 }
1051 if err != nil {
DTabidze4b44ff42024-04-02 03:16:26 +04001052 redirectURL := fmt.Sprintf("/group/%s?errorMessage=%s", groupName, url.QueryEscape(err.Error()))
1053 http.Redirect(w, r, redirectURL, http.StatusFound)
DTabidze0d802592024-03-19 17:42:45 +04001054 return
1055 }
1056 http.Redirect(w, r, "/group/"+groupName, http.StatusSeeOther)
1057}
1058
1059func (s *Server) addChildGroupHandler(w http.ResponseWriter, r *http.Request) {
1060 // 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 +04001061 loggedInUser, err := getLoggedInUser(r)
1062 if err != nil {
1063 http.Error(w, "User Not Logged In", http.StatusUnauthorized)
1064 return
1065 }
DTabidze078385f2024-03-27 14:49:05 +04001066 vars := mux.Vars(r)
1067 parentGroup := vars["parent-group"]
DTabidze908bb852024-03-25 20:07:57 +04001068 if err := isValidGroupName(parentGroup); err != nil {
1069 http.Error(w, err.Error(), http.StatusBadRequest)
1070 return
1071 }
DTabidze0d802592024-03-19 17:42:45 +04001072 childGroup := r.FormValue("child-group")
DTabidze908bb852024-03-25 20:07:57 +04001073 if err := isValidGroupName(childGroup); err != nil {
1074 http.Error(w, err.Error(), http.StatusBadRequest)
1075 return
1076 }
Davit Tabidzec0d2bf52024-04-03 15:39:33 +04001077 if err := s.checkIsOwner(w, loggedInUser, parentGroup); err != nil {
1078 redirectURL := fmt.Sprintf("/group/%s?errorMessage=%s", parentGroup, url.QueryEscape(err.Error()))
1079 http.Redirect(w, r, redirectURL, http.StatusSeeOther)
DTabidze0d802592024-03-19 17:42:45 +04001080 return
1081 }
1082 if err := s.store.AddChildGroup(parentGroup, childGroup); err != nil {
DTabidze4b44ff42024-04-02 03:16:26 +04001083 redirectURL := fmt.Sprintf("/group/%s?errorMessage=%s", parentGroup, url.QueryEscape(err.Error()))
1084 http.Redirect(w, r, redirectURL, http.StatusFound)
DTabidze0d802592024-03-19 17:42:45 +04001085 return
1086 }
1087 http.Redirect(w, r, "/group/"+parentGroup, http.StatusSeeOther)
1088}
1089
Davit Tabidzec0d2bf52024-04-03 15:39:33 +04001090func (s *Server) addOwnerGroupHandler(w http.ResponseWriter, r *http.Request) {
Davit Tabidzec0d2bf52024-04-03 15:39:33 +04001091 loggedInUser, err := getLoggedInUser(r)
1092 if err != nil {
1093 http.Error(w, "User Not Logged In", http.StatusUnauthorized)
1094 return
1095 }
1096 vars := mux.Vars(r)
1097 ownedGroup := vars["owned-group"]
1098 if err := isValidGroupName(ownedGroup); err != nil {
1099 http.Error(w, err.Error(), http.StatusBadRequest)
1100 return
1101 }
1102 ownerGroup := r.FormValue("owner-group")
1103 if err := isValidGroupName(ownerGroup); err != nil {
1104 http.Error(w, err.Error(), http.StatusBadRequest)
1105 return
1106 }
1107 if err := s.checkIsOwner(w, loggedInUser, ownedGroup); err != nil {
1108 redirectURL := fmt.Sprintf("/group/%s?errorMessage=%s", ownedGroup, url.QueryEscape(err.Error()))
1109 http.Redirect(w, r, redirectURL, http.StatusSeeOther)
1110 return
1111 }
1112 if err := s.store.AddOwnerGroup(ownerGroup, ownedGroup); err != nil {
1113 redirectURL := fmt.Sprintf("/group/%s?errorMessage=%s", ownedGroup, url.QueryEscape(err.Error()))
1114 http.Redirect(w, r, redirectURL, http.StatusFound)
1115 return
1116 }
1117 http.Redirect(w, r, "/group/"+ownedGroup, http.StatusSeeOther)
1118}
1119
Davit Tabidze75d57c32024-07-19 19:17:55 +04001120func (s *Server) addSSHKeyForUserHandler(w http.ResponseWriter, r *http.Request) {
1121 defer s.pingAllSyncAddresses()
1122 loggedInUser, err := getLoggedInUser(r)
1123 if err != nil {
1124 http.Error(w, "User Not Logged In", http.StatusUnauthorized)
1125 return
1126 }
1127 vars := mux.Vars(r)
1128 username := vars["username"]
1129 if loggedInUser != username {
1130 http.Error(w, "You are not allowed to add SSH key for someone else", http.StatusUnauthorized)
1131 return
1132 }
1133 sshKey := r.FormValue("ssh-key")
1134 if sshKey == "" {
1135 http.Error(w, "SSH key not present", http.StatusBadRequest)
1136 return
1137 }
1138 if err := s.store.AddSSHKeyForUser(username, sshKey); err != nil {
1139 redirectURL := fmt.Sprintf("/user/%s?errorMessage=%s", loggedInUser, url.QueryEscape(err.Error()))
1140 http.Redirect(w, r, redirectURL, http.StatusFound)
1141 return
1142 }
1143 http.Redirect(w, r, "/user/"+loggedInUser, http.StatusSeeOther)
1144}
1145
1146func (s *Server) removeSSHKeyForUserHandler(w http.ResponseWriter, r *http.Request) {
1147 defer s.pingAllSyncAddresses()
1148 loggedInUser, err := getLoggedInUser(r)
1149 if err != nil {
1150 http.Error(w, "User Not Logged In", http.StatusUnauthorized)
1151 return
1152 }
1153 vars := mux.Vars(r)
1154 username := vars["username"]
1155 if loggedInUser != username {
1156 http.Error(w, "You are not allowed to remove SSH key for someone else", http.StatusUnauthorized)
1157 return
1158 }
1159 if err := r.ParseForm(); err != nil {
1160 http.Error(w, "Invalid request body", http.StatusBadRequest)
1161 return
1162 }
1163 sshKey := r.FormValue("ssh-key")
1164 if sshKey == "" {
1165 http.Error(w, "SSH key not present", http.StatusBadRequest)
1166 return
1167 }
1168 if err := s.store.RemoveSSHKeyForUser(username, sshKey); err != nil {
1169 redirectURL := fmt.Sprintf("/user/%s?errorMessage=%s", loggedInUser, url.QueryEscape(err.Error()))
1170 http.Redirect(w, r, redirectURL, http.StatusFound)
1171 return
1172 }
1173 http.Redirect(w, r, "/user/"+loggedInUser, http.StatusSeeOther)
1174}
1175
Giorgi Lekveishvili942c7612024-03-22 19:27:48 +04001176type initRequest struct {
1177 Owner string `json:"owner"`
1178 Groups []string `json:"groups"`
1179}
1180
1181func (s *Server) apiInitHandler(w http.ResponseWriter, r *http.Request) {
1182 var req initRequest
1183 if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
1184 http.Error(w, err.Error(), http.StatusBadRequest)
1185 return
1186 }
1187 if err := s.store.Init(req.Owner, req.Groups); err != nil {
1188 http.Error(w, err.Error(), http.StatusInternalServerError)
1189 return
1190 }
1191}
1192
1193type userInfo struct {
DTabidzed7744a62024-03-20 14:09:15 +04001194 MemberOf []string `json:"memberOf"`
1195}
1196
1197func (s *Server) apiMemberOfHandler(w http.ResponseWriter, r *http.Request) {
1198 vars := mux.Vars(r)
1199 user, ok := vars["username"]
DTabidze908bb852024-03-25 20:07:57 +04001200 if !ok || user == "" {
DTabidzed7744a62024-03-20 14:09:15 +04001201 http.Error(w, "Username parameter is required", http.StatusBadRequest)
1202 return
1203 }
DTabidze908bb852024-03-25 20:07:57 +04001204 user = strings.ToLower(user)
DTabidzed7744a62024-03-20 14:09:15 +04001205 transitiveGroups, err := s.store.GetAllTransitiveGroupsForUser(user)
1206 if err != nil {
1207 http.Error(w, err.Error(), http.StatusInternalServerError)
1208 return
1209 }
DTabidzec0b4d8f2024-03-22 17:25:10 +04001210 var groupNames []string
1211 for _, group := range transitiveGroups {
1212 groupNames = append(groupNames, group.Name)
1213 }
DTabidzed7744a62024-03-20 14:09:15 +04001214 w.Header().Set("Content-Type", "application/json")
Giorgi Lekveishvili942c7612024-03-22 19:27:48 +04001215 if err := json.NewEncoder(w).Encode(userInfo{groupNames}); err != nil {
DTabidzed7744a62024-03-20 14:09:15 +04001216 http.Error(w, err.Error(), http.StatusInternalServerError)
1217 return
1218 }
1219}
1220
Davit Tabidze75d57c32024-07-19 19:17:55 +04001221func (s *Server) apiGetAllUsers(w http.ResponseWriter, r *http.Request) {
1222 defer s.pingAllSyncAddresses()
1223 selfAddress := r.FormValue("selfAddress")
1224 if selfAddress != "" {
1225 s.addSyncAddress(selfAddress)
1226 }
1227 users, err := s.store.GetAllUsers()
1228 if err != nil {
1229 http.Error(w, "Failed to retrieve SSH keys", http.StatusInternalServerError)
1230 return
1231 }
1232 w.Header().Set("Content-Type", "application/json")
1233 if err := json.NewEncoder(w).Encode(users); err != nil {
1234 http.Error(w, err.Error(), http.StatusInternalServerError)
1235 return
1236 }
1237}
1238
1239func (s *Server) apiCreateUser(w http.ResponseWriter, r *http.Request) {
1240 defer s.pingAllSyncAddresses()
1241 selfAddress := r.FormValue("selfAddress")
1242 if selfAddress != "" {
1243 s.addSyncAddress(selfAddress)
1244 }
1245 if err := r.ParseForm(); err != nil {
1246 http.Error(w, "Invalid request body", http.StatusBadRequest)
1247 return
1248 }
1249 username := r.FormValue("username")
1250 email := r.FormValue("email")
1251 if username == "" {
1252 http.Error(w, "Username cannot be empty", http.StatusBadRequest)
1253 return
1254 }
1255 if email == "" {
1256 http.Error(w, "Email cannot be empty", http.StatusBadRequest)
1257 return
1258 }
1259 username = strings.ToLower(username)
1260 email = strings.ToLower(email)
1261 err := s.store.CreateUser(username, email)
1262 if err != nil {
1263 http.Error(w, err.Error(), http.StatusInternalServerError)
1264 return
1265 }
1266 w.WriteHeader(http.StatusOK)
1267}
1268
1269func (s *Server) pingAllSyncAddresses() {
1270 s.mu.Lock()
1271 defer s.mu.Unlock()
1272 for address := range s.syncAddresses {
1273 resp, err := http.Get(address)
1274 if err != nil {
1275 log.Printf("Failed to ping %s: %v", address, err)
1276 continue
1277 }
1278 resp.Body.Close()
1279 if resp.StatusCode != http.StatusOK {
1280 log.Printf("Ping to %s returned status %d", address, resp.StatusCode)
1281 }
1282 }
1283}
1284
1285func (s *Server) addSyncAddress(address string) {
1286 s.mu.Lock()
1287 defer s.mu.Unlock()
1288 s.syncAddresses[address] = struct{}{}
1289}
1290
DTabidze908bb852024-03-25 20:07:57 +04001291func convertStatus(status string) (Status, error) {
1292 switch status {
1293 case "Owner":
1294 return Owner, nil
1295 case "Member":
1296 return Member, nil
1297 default:
1298 return Owner, fmt.Errorf("invalid status: %s", status)
1299 }
1300}
1301
1302func isValidGroupName(group string) error {
1303 if strings.TrimSpace(group) == "" {
Davit Tabidze5f1a2c62024-07-17 17:57:27 +04001304 return fmt.Errorf("Group name can't be empty or contain only whitespaces")
DTabidze908bb852024-03-25 20:07:57 +04001305 }
1306 validGroupName := regexp.MustCompile(`^[a-z0-9\-_:.\/ ]+$`)
1307 if !validGroupName.MatchString(group) {
Davit Tabidze5f1a2c62024-07-17 17:57:27 +04001308 return fmt.Errorf("Group name should contain only lowercase letters, digits, -, _, :, ., /")
DTabidze908bb852024-03-25 20:07:57 +04001309 }
1310 return nil
1311}
1312
DTabidze0d802592024-03-19 17:42:45 +04001313func main() {
1314 flag.Parse()
DTabidzec0b4d8f2024-03-22 17:25:10 +04001315 db, err := sql.Open("sqlite3", *dbPath)
DTabidze0d802592024-03-19 17:42:45 +04001316 if err != nil {
1317 panic(err)
1318 }
DTabidzec0b4d8f2024-03-22 17:25:10 +04001319 store, err := NewSQLiteStore(db)
1320 if err != nil {
1321 panic(err)
1322 }
Davit Tabidze75d57c32024-07-19 19:17:55 +04001323 s := Server{
1324 store: store,
1325 syncAddresses: make(map[string]struct{}),
1326 mu: sync.Mutex{},
1327 }
Giorgi Lekveishvili329af572024-03-25 20:14:41 +04001328 log.Fatal(s.Start())
DTabidze0d802592024-03-19 17:42:45 +04001329}