blob: 8ef084833092bd2c0412f7a88300d256a978726f [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 Tabidzef867f2d2024-07-24 18:06:25 +040058 GetUsers(username []string) ([]User, error)
Davit Tabidze75d57c32024-07-19 19:17:55 +040059 GetUser(username string) (User, error)
60 AddSSHKeyForUser(username, sshKey string) error
61 RemoveSSHKeyForUser(username, sshKey string) error
62 CreateUser(user, email string) error
DTabidze0d802592024-03-19 17:42:45 +040063}
64
65type Server struct {
Davit Tabidze75d57c32024-07-19 19:17:55 +040066 store Store
67 syncAddresses map[string]struct{}
68 mu sync.Mutex
DTabidze0d802592024-03-19 17:42:45 +040069}
70
71type Group struct {
72 Name string
73 Description string
74}
75
Davit Tabidze75d57c32024-07-19 19:17:55 +040076type User struct {
77 Username string `json:"username"`
78 Email string `json:"email"`
79 SSHPublicKeys []string `json:"sshPublicKeys,omitempty"`
80}
81
DTabidze0d802592024-03-19 17:42:45 +040082type SQLiteStore struct {
83 db *sql.DB
84}
85
Davit Tabidzec0d2bf52024-04-03 15:39:33 +040086const (
87 ErrorUniqueConstraintViolation = 2067
88 ErrorConstraintPrimaryKeyViolation = 1555
89)
90
DTabidzec0b4d8f2024-03-22 17:25:10 +040091func NewSQLiteStore(db *sql.DB) (*SQLiteStore, error) {
92 _, err := db.Exec(`
Davit Tabidzec0d2bf52024-04-03 15:39:33 +040093 CREATE TABLE IF NOT EXISTS groups (
94 name TEXT PRIMARY KEY,
95 description TEXT
96 );
97 CREATE TABLE IF NOT EXISTS owners (
98 username TEXT,
99 group_name TEXT,
100 FOREIGN KEY(group_name) REFERENCES groups(name),
101 UNIQUE (username, group_name)
102 );
103 CREATE TABLE IF NOT EXISTS owner_groups (
104 owner_group TEXT,
105 owned_group TEXT,
106 FOREIGN KEY(owner_group) REFERENCES groups(name),
107 FOREIGN KEY(owned_group) REFERENCES groups(name),
108 UNIQUE (owner_group, owned_group)
109 );
110 CREATE TABLE IF NOT EXISTS group_to_group (
111 parent_group TEXT,
112 child_group TEXT,
113 FOREIGN KEY(parent_group) REFERENCES groups(name),
114 FOREIGN KEY(child_group) REFERENCES groups(name),
115 UNIQUE (parent_group, child_group)
116 );
117 CREATE TABLE IF NOT EXISTS user_to_group (
118 username TEXT,
119 group_name TEXT,
120 FOREIGN KEY(group_name) REFERENCES groups(name),
121 UNIQUE (username, group_name)
Davit Tabidze75d57c32024-07-19 19:17:55 +0400122 );
123 CREATE TABLE IF NOT EXISTS users (
124 username TEXT PRIMARY KEY,
125 email TEXT,
126 UNIQUE (email)
127 );
128 CREATE TABLE IF NOT EXISTS user_ssh_keys (
129 username TEXT,
130 ssh_key TEXT,
131 UNIQUE (ssh_key),
132 FOREIGN KEY(username) REFERENCES users(username)
Davit Tabidzec0d2bf52024-04-03 15:39:33 +0400133 );`)
DTabidze0d802592024-03-19 17:42:45 +0400134 if err != nil {
135 return nil, err
136 }
137 return &SQLiteStore{db: db}, nil
138}
139
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
Davit Tabidzef867f2d2024-07-24 18:06:25 +0400561func (s *SQLiteStore) GetUsers(usernames []string) ([]User, error) {
562 var rows *sql.Rows
563 var err error
564 query := `
Davit Tabidze75d57c32024-07-19 19:17:55 +0400565 SELECT users.username, users.email, GROUP_CONCAT(user_ssh_keys.ssh_key, ',')
566 FROM users
Davit Tabidzef867f2d2024-07-24 18:06:25 +0400567 LEFT JOIN user_ssh_keys ON users.username = user_ssh_keys.username`
568 var args []interface{}
569 if usernames != nil {
570 if len(usernames) == 0 {
571 return []User{}, nil
572 }
573 query += " WHERE users.username IN ("
574 placeholders := strings.Repeat("?,", len(usernames)-1) + "?"
575 query += placeholders + ") "
576 for _, username := range usernames {
577 args = append(args, username)
578 }
579 }
580 query += " GROUP BY users.username"
581 rows, err = s.db.Query(query, args...)
Davit Tabidze75d57c32024-07-19 19:17:55 +0400582 if err != nil {
583 return nil, err
584 }
585 defer rows.Close()
586 var userInfos []User
587 for rows.Next() {
588 var username, email string
589 var sshKeys sql.NullString
590 if err := rows.Scan(&username, &email, &sshKeys); err != nil {
591 return nil, err
592 }
593 user := User{
594 Username: username,
595 Email: email,
596 }
597 if sshKeys.Valid {
598 user.SSHPublicKeys = strings.Split(sshKeys.String, ",")
599 }
600 userInfos = append(userInfos, user)
601 }
602 if err := rows.Err(); err != nil {
603 return nil, err
604 }
605 return userInfos, nil
606}
607
608func (s *SQLiteStore) GetUser(username string) (User, error) {
609 var user User
610 user.Username = username
611 query := `
612 SELECT users.email, GROUP_CONCAT(user_ssh_keys.ssh_key, ',')
613 FROM users
614 LEFT JOIN user_ssh_keys ON users.username = user_ssh_keys.username
615 WHERE users.username = ?
616 GROUP BY users.username
617 `
618 row := s.db.QueryRow(query, username)
619 var sshKeys sql.NullString
620 err := row.Scan(&user.Email, &sshKeys)
621 if err != nil {
622 if err == sql.ErrNoRows {
623 return User{}, fmt.Errorf("no user found with username %s", username)
624 }
625 return User{}, err
626 }
627 if sshKeys.Valid {
628 user.SSHPublicKeys = strings.Split(sshKeys.String, ",")
629 }
630 return user, nil
631}
632
633func (s *SQLiteStore) CreateUser(user, email string) error {
634 _, err := s.db.Exec(`INSERT INTO users (username, email) VALUES (?, ?)`, user, email)
635 if err != nil {
636 sqliteErr, ok := err.(*sqlite3.Error)
637 if ok {
638 if sqliteErr.ExtendedCode() == ErrorUniqueConstraintViolation {
639 if strings.Contains(err.Error(), "UNIQUE constraint failed: users.username") {
640 return fmt.Errorf("username %s already exists", user)
641 }
642 if strings.Contains(err.Error(), "UNIQUE constraint failed: users.email") {
643 return fmt.Errorf("email %s already exists", email)
644 }
645 }
646 }
647 return err
648 }
649 return nil
650}
651
DTabidze0d802592024-03-19 17:42:45 +0400652func getLoggedInUser(r *http.Request) (string, error) {
DTabidzec0b4d8f2024-03-22 17:25:10 +0400653 if user := r.Header.Get("X-User"); user != "" {
654 return user, nil
655 } else {
656 return "", fmt.Errorf("unauthenticated")
657 }
Davit Tabidze75d57c32024-07-19 19:17:55 +0400658 // return "tabo", nil
DTabidze0d802592024-03-19 17:42:45 +0400659}
660
661type Status int
662
663const (
664 Owner Status = iota
665 Member
666)
667
Giorgi Lekveishvili329af572024-03-25 20:14:41 +0400668func (s *Server) Start() error {
669 e := make(chan error)
670 go func() {
671 r := mux.NewRouter()
672 r.PathPrefix("/static/").Handler(http.FileServer(http.FS(staticResources)))
Davit Tabidze75d57c32024-07-19 19:17:55 +0400673 r.HandleFunc("/group/{group-name}/add-user/", s.addUserToGroupHandler).Methods(http.MethodPost)
674 r.HandleFunc("/group/{parent-group}/add-child-group", s.addChildGroupHandler).Methods(http.MethodPost)
675 r.HandleFunc("/group/{owned-group}/add-owner-group", s.addOwnerGroupHandler).Methods(http.MethodPost)
676 r.HandleFunc("/group/{parent-group}/remove-child-group/{child-group}", s.removeChildGroupHandler).Methods(http.MethodPost)
677 r.HandleFunc("/group/{group-name}/remove-owner/{username}", s.removeOwnerFromGroupHandler).Methods(http.MethodPost)
678 r.HandleFunc("/group/{group-name}/remove-member/{username}", s.removeMemberFromGroupHandler).Methods(http.MethodPost)
Giorgi Lekveishvili329af572024-03-25 20:14:41 +0400679 r.HandleFunc("/group/{group-name}", s.groupHandler)
Davit Tabidze75d57c32024-07-19 19:17:55 +0400680 r.HandleFunc("/user/{username}/ssh-key", s.addSSHKeyForUserHandler).Methods(http.MethodPost)
681 r.HandleFunc("/user/{username}/remove-ssh-key", s.removeSSHKeyForUserHandler).Methods(http.MethodPost)
DTabidze5d735e32024-03-26 16:01:06 +0400682 r.HandleFunc("/user/{username}", s.userHandler)
Davit Tabidze75d57c32024-07-19 19:17:55 +0400683 r.HandleFunc("/create-group", s.createGroupHandler).Methods(http.MethodPost)
Giorgi Lekveishvili329af572024-03-25 20:14:41 +0400684 r.HandleFunc("/", s.homePageHandler)
685 e <- http.ListenAndServe(fmt.Sprintf(":%d", *port), r)
686 }()
687 go func() {
688 r := mux.NewRouter()
689 r.HandleFunc("/api/init", s.apiInitHandler)
690 r.HandleFunc("/api/user/{username}", s.apiMemberOfHandler)
Davit Tabidze75d57c32024-07-19 19:17:55 +0400691 r.HandleFunc("/api/users", s.apiGetAllUsers).Methods(http.MethodGet)
692 r.HandleFunc("/api/users", s.apiCreateUser).Methods(http.MethodPost)
Giorgi Lekveishvili329af572024-03-25 20:14:41 +0400693 e <- http.ListenAndServe(fmt.Sprintf(":%d", *apiPort), r)
694 }()
695 return <-e
DTabidze0d802592024-03-19 17:42:45 +0400696}
697
698type GroupData struct {
699 Group Group
700 Membership string
701}
702
Davit Tabidzec0d2bf52024-04-03 15:39:33 +0400703func (s *Server) checkIsOwner(w http.ResponseWriter, user, group string) error {
DTabidze0d802592024-03-19 17:42:45 +0400704 isOwner, err := s.store.IsGroupOwner(user, group)
705 if err != nil {
Davit Tabidzec0d2bf52024-04-03 15:39:33 +0400706 return err
DTabidze0d802592024-03-19 17:42:45 +0400707 }
Davit Tabidzec0d2bf52024-04-03 15:39:33 +0400708 if isOwner {
709 return nil
DTabidze0d802592024-03-19 17:42:45 +0400710 }
Davit Tabidzec0d2bf52024-04-03 15:39:33 +0400711 // TODO(dtabidze): right now this only checks if user is member of just one lvl upper group. should add transitive group check.
712 isMemberOfOwnerGroup, err := s.store.IsMemberOfOwnerGroup(user, group)
713 if err != nil {
714 return err
715 }
716 if !isMemberOfOwnerGroup {
Davit Tabidze5f1a2c62024-07-17 17:57:27 +0400717 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 +0400718 }
719 return nil
DTabidze0d802592024-03-19 17:42:45 +0400720}
721
DTabidze4b44ff42024-04-02 03:16:26 +0400722type templates struct {
723 group *template.Template
724 user *template.Template
725}
726
727func parseTemplates(fs embed.FS) (templates, error) {
728 base, err := template.ParseFS(fs, "memberships-tmpl/base.html")
729 if err != nil {
730 return templates{}, err
731 }
732 parse := func(path string) (*template.Template, error) {
733 if b, err := base.Clone(); err != nil {
734 return nil, err
735 } else {
736 return b.ParseFS(fs, path)
737 }
738 }
739 user, err := parse("memberships-tmpl/user.html")
740 if err != nil {
741 return templates{}, err
742 }
743 group, err := parse("memberships-tmpl/group.html")
744 if err != nil {
745 return templates{}, err
746 }
747 return templates{group, user}, nil
748}
749
DTabidze0d802592024-03-19 17:42:45 +0400750func (s *Server) homePageHandler(w http.ResponseWriter, r *http.Request) {
751 loggedInUser, err := getLoggedInUser(r)
752 if err != nil {
753 http.Error(w, "User Not Logged In", http.StatusUnauthorized)
754 return
755 }
DTabidze5d735e32024-03-26 16:01:06 +0400756 http.Redirect(w, r, "/user/"+loggedInUser, http.StatusSeeOther)
757}
758
Davit Tabidze75d57c32024-07-19 19:17:55 +0400759type UserPageData struct {
760 OwnerGroups []Group
761 MembershipGroups []Group
762 TransitiveGroups []Group
763 LoggedInUserPage bool
764 CurrentUser string
765 SSHPublicKeys []string
766 Email string
767 ErrorMessage string
768}
769
DTabidze5d735e32024-03-26 16:01:06 +0400770func (s *Server) userHandler(w http.ResponseWriter, r *http.Request) {
771 loggedInUser, err := getLoggedInUser(r)
772 if err != nil {
773 http.Error(w, "User Not Logged In", http.StatusUnauthorized)
774 return
775 }
DTabidze4b44ff42024-04-02 03:16:26 +0400776 errorMsg := r.URL.Query().Get("errorMessage")
DTabidze5d735e32024-03-26 16:01:06 +0400777 vars := mux.Vars(r)
778 user := strings.ToLower(vars["username"])
779 // TODO(dtabidze): should check if username exists or not.
780 loggedInUserPage := loggedInUser == user
781 ownerGroups, err := s.store.GetGroupsOwnedBy(user)
DTabidze0d802592024-03-19 17:42:45 +0400782 if err != nil {
783 http.Error(w, err.Error(), http.StatusInternalServerError)
784 return
785 }
DTabidze5d735e32024-03-26 16:01:06 +0400786 membershipGroups, err := s.store.GetGroupsUserBelongsTo(user)
DTabidze0d802592024-03-19 17:42:45 +0400787 if err != nil {
788 http.Error(w, err.Error(), http.StatusInternalServerError)
789 return
790 }
DTabidze5d735e32024-03-26 16:01:06 +0400791 transitiveGroups, err := s.store.GetAllTransitiveGroupsForUser(user)
DTabidzec0b4d8f2024-03-22 17:25:10 +0400792 if err != nil {
793 http.Error(w, err.Error(), http.StatusInternalServerError)
794 return
795 }
Davit Tabidze75d57c32024-07-19 19:17:55 +0400796 userInfo, err := s.store.GetUser(user)
797 if err != nil {
798 http.Error(w, err.Error(), http.StatusInternalServerError)
799 return
800 }
801 data := UserPageData{
DTabidze0d802592024-03-19 17:42:45 +0400802 OwnerGroups: ownerGroups,
803 MembershipGroups: membershipGroups,
DTabidzec0b4d8f2024-03-22 17:25:10 +0400804 TransitiveGroups: transitiveGroups,
DTabidze5d735e32024-03-26 16:01:06 +0400805 LoggedInUserPage: loggedInUserPage,
806 CurrentUser: user,
Davit Tabidze75d57c32024-07-19 19:17:55 +0400807 SSHPublicKeys: userInfo.SSHPublicKeys,
808 Email: userInfo.Email,
DTabidze4b44ff42024-04-02 03:16:26 +0400809 ErrorMessage: errorMsg,
DTabidze0d802592024-03-19 17:42:45 +0400810 }
DTabidze4b44ff42024-04-02 03:16:26 +0400811 templates, err := parseTemplates(tmpls)
812 if err != nil {
813 http.Error(w, err.Error(), http.StatusInternalServerError)
814 return
815 }
816 if err := templates.user.Execute(w, data); err != nil {
DTabidze0d802592024-03-19 17:42:45 +0400817 http.Error(w, err.Error(), http.StatusInternalServerError)
818 return
819 }
820}
821
822func (s *Server) createGroupHandler(w http.ResponseWriter, r *http.Request) {
823 loggedInUser, err := getLoggedInUser(r)
824 if err != nil {
825 http.Error(w, "User Not Logged In", http.StatusUnauthorized)
826 return
827 }
DTabidze0d802592024-03-19 17:42:45 +0400828 if err := r.ParseForm(); err != nil {
829 http.Error(w, err.Error(), http.StatusInternalServerError)
830 return
831 }
832 var group Group
833 group.Name = r.PostFormValue("group-name")
DTabidze908bb852024-03-25 20:07:57 +0400834 if err := isValidGroupName(group.Name); err != nil {
DTabidze4b44ff42024-04-02 03:16:26 +0400835 // http.Error(w, err.Error(), http.StatusBadRequest)
836 redirectURL := fmt.Sprintf("/user/%s?errorMessage=%s", loggedInUser, url.QueryEscape(err.Error()))
837 http.Redirect(w, r, redirectURL, http.StatusFound)
DTabidze908bb852024-03-25 20:07:57 +0400838 return
839 }
DTabidze0d802592024-03-19 17:42:45 +0400840 group.Description = r.PostFormValue("description")
841 if err := s.store.CreateGroup(loggedInUser, group); err != nil {
DTabidze4b44ff42024-04-02 03:16:26 +0400842 // http.Error(w, err.Error(), http.StatusInternalServerError)
843 redirectURL := fmt.Sprintf("/user/%s?errorMessage=%s", loggedInUser, url.QueryEscape(err.Error()))
844 http.Redirect(w, r, redirectURL, http.StatusFound)
DTabidze0d802592024-03-19 17:42:45 +0400845 return
846 }
847 http.Redirect(w, r, "/", http.StatusSeeOther)
848}
849
Davit Tabidze75d57c32024-07-19 19:17:55 +0400850type GroupPageData struct {
851 GroupName string
852 Description string
853 Owners []string
854 Members []string
855 AllGroups []Group
856 TransitiveGroups []Group
857 ChildGroups []Group
858 OwnerGroups []Group
859 ErrorMessage string
860}
861
DTabidze0d802592024-03-19 17:42:45 +0400862func (s *Server) groupHandler(w http.ResponseWriter, r *http.Request) {
DTabidzec0b4d8f2024-03-22 17:25:10 +0400863 _, err := getLoggedInUser(r)
864 if err != nil {
865 http.Error(w, "User Not Logged In", http.StatusUnauthorized)
866 return
867 }
DTabidze4b44ff42024-04-02 03:16:26 +0400868 errorMsg := r.URL.Query().Get("errorMessage")
DTabidzed7744a62024-03-20 14:09:15 +0400869 vars := mux.Vars(r)
870 groupName := vars["group-name"]
DTabidze908bb852024-03-25 20:07:57 +0400871 exists, err := s.store.DoesGroupExist(groupName)
872 if err != nil {
873 http.Error(w, err.Error(), http.StatusInternalServerError)
874 return
875 }
876 if !exists {
DTabidze4b44ff42024-04-02 03:16:26 +0400877 errorMsg = fmt.Sprintf("group with the name '%s' not found", groupName)
DTabidze908bb852024-03-25 20:07:57 +0400878 http.Error(w, errorMsg, http.StatusNotFound)
879 return
880 }
DTabidze0d802592024-03-19 17:42:45 +0400881 if err != nil {
882 http.Error(w, err.Error(), http.StatusInternalServerError)
883 return
884 }
885 owners, err := s.store.GetGroupOwners(groupName)
886 if err != nil {
887 http.Error(w, err.Error(), http.StatusInternalServerError)
888 return
889 }
890 members, err := s.store.GetGroupMembers(groupName)
891 if err != nil {
892 http.Error(w, err.Error(), http.StatusInternalServerError)
893 return
894 }
895 description, err := s.store.GetGroupDescription(groupName)
896 if err != nil {
897 http.Error(w, err.Error(), http.StatusInternalServerError)
898 return
899 }
Davit Tabidzec0d2bf52024-04-03 15:39:33 +0400900 allGroups, err := s.store.GetAllGroups()
DTabidze0d802592024-03-19 17:42:45 +0400901 if err != nil {
902 http.Error(w, err.Error(), http.StatusInternalServerError)
903 return
904 }
DTabidzec0b4d8f2024-03-22 17:25:10 +0400905 transitiveGroups, err := s.store.GetAllTransitiveGroupsForGroup(groupName)
906 if err != nil {
907 http.Error(w, err.Error(), http.StatusInternalServerError)
908 return
909 }
910 childGroups, err := s.store.GetDirectChildrenGroups(groupName)
911 if err != nil {
912 http.Error(w, err.Error(), http.StatusInternalServerError)
913 return
914 }
Davit Tabidzec0d2bf52024-04-03 15:39:33 +0400915 ownerGroups, err := s.store.GetGroupOwnerGroups(groupName)
916 if err != nil {
917 http.Error(w, err.Error(), http.StatusInternalServerError)
918 return
919 }
Davit Tabidze75d57c32024-07-19 19:17:55 +0400920 data := GroupPageData{
DTabidzec0b4d8f2024-03-22 17:25:10 +0400921 GroupName: groupName,
922 Description: description,
923 Owners: owners,
924 Members: members,
Davit Tabidzec0d2bf52024-04-03 15:39:33 +0400925 AllGroups: allGroups,
DTabidzec0b4d8f2024-03-22 17:25:10 +0400926 TransitiveGroups: transitiveGroups,
927 ChildGroups: childGroups,
Davit Tabidzec0d2bf52024-04-03 15:39:33 +0400928 OwnerGroups: ownerGroups,
DTabidze4b44ff42024-04-02 03:16:26 +0400929 ErrorMessage: errorMsg,
DTabidze0d802592024-03-19 17:42:45 +0400930 }
DTabidze4b44ff42024-04-02 03:16:26 +0400931 templates, err := parseTemplates(tmpls)
932 if err != nil {
933 http.Error(w, err.Error(), http.StatusInternalServerError)
934 return
935 }
936 if err := templates.group.Execute(w, data); err != nil {
DTabidze0d802592024-03-19 17:42:45 +0400937 http.Error(w, err.Error(), http.StatusInternalServerError)
938 return
939 }
940}
941
DTabidze2b224bf2024-03-27 13:25:49 +0400942func (s *Server) removeChildGroupHandler(w http.ResponseWriter, r *http.Request) {
943 loggedInUser, err := getLoggedInUser(r)
944 if err != nil {
945 http.Error(w, "User Not Logged In", http.StatusUnauthorized)
946 return
947 }
Davit Tabidze75d57c32024-07-19 19:17:55 +0400948 vars := mux.Vars(r)
949 parentGroup := vars["parent-group"]
950 childGroup := vars["child-group"]
951 if err := isValidGroupName(parentGroup); err != nil {
952 http.Error(w, err.Error(), http.StatusBadRequest)
953 return
DTabidze2b224bf2024-03-27 13:25:49 +0400954 }
Davit Tabidze75d57c32024-07-19 19:17:55 +0400955 if err := isValidGroupName(childGroup); err != nil {
956 http.Error(w, err.Error(), http.StatusBadRequest)
957 return
958 }
959 if err := s.checkIsOwner(w, loggedInUser, parentGroup); err != nil {
960 redirectURL := fmt.Sprintf("/group/%s?errorMessage=%s", parentGroup, url.QueryEscape(err.Error()))
961 http.Redirect(w, r, redirectURL, http.StatusSeeOther)
962 return
963 }
964 err = s.store.RemoveFromGroupToGroup(parentGroup, childGroup)
965 if err != nil {
966 redirectURL := fmt.Sprintf("/group/%s?errorMessage=%s", parentGroup, url.QueryEscape(err.Error()))
967 http.Redirect(w, r, redirectURL, http.StatusFound)
968 return
969 }
970 http.Redirect(w, r, "/group/"+parentGroup, http.StatusSeeOther)
DTabidze2b224bf2024-03-27 13:25:49 +0400971}
972
DTabidze078385f2024-03-27 14:49:05 +0400973func (s *Server) removeOwnerFromGroupHandler(w http.ResponseWriter, r *http.Request) {
DTabidze2b224bf2024-03-27 13:25:49 +0400974 loggedInUser, err := getLoggedInUser(r)
975 if err != nil {
976 http.Error(w, "User Not Logged In", http.StatusUnauthorized)
977 return
978 }
Davit Tabidze75d57c32024-07-19 19:17:55 +0400979 vars := mux.Vars(r)
980 username := vars["username"]
981 groupName := vars["group-name"]
982 tableName := "owners"
983 if err := isValidGroupName(groupName); err != nil {
984 http.Error(w, err.Error(), http.StatusBadRequest)
985 return
DTabidze2b224bf2024-03-27 13:25:49 +0400986 }
Davit Tabidze75d57c32024-07-19 19:17:55 +0400987 if err := s.checkIsOwner(w, loggedInUser, groupName); err != nil {
988 redirectURL := fmt.Sprintf("/group/%s?errorMessage=%s", groupName, url.QueryEscape(err.Error()))
989 http.Redirect(w, r, redirectURL, http.StatusSeeOther)
990 return
991 }
992 err = s.store.RemoveUserFromTable(username, groupName, tableName)
993 if err != nil {
994 redirectURL := fmt.Sprintf("/group/%s?errorMessage=%s", groupName, url.QueryEscape(err.Error()))
995 http.Redirect(w, r, redirectURL, http.StatusFound)
996 return
997 }
998 http.Redirect(w, r, "/group/"+groupName, http.StatusSeeOther)
DTabidze2b224bf2024-03-27 13:25:49 +0400999}
1000
DTabidze078385f2024-03-27 14:49:05 +04001001func (s *Server) removeMemberFromGroupHandler(w http.ResponseWriter, r *http.Request) {
1002 loggedInUser, err := getLoggedInUser(r)
1003 if err != nil {
1004 http.Error(w, "User Not Logged In", http.StatusUnauthorized)
1005 return
1006 }
Davit Tabidze75d57c32024-07-19 19:17:55 +04001007 vars := mux.Vars(r)
1008 username := vars["username"]
1009 groupName := vars["group-name"]
1010 tableName := "user_to_group"
1011 if err := isValidGroupName(groupName); err != nil {
1012 http.Error(w, err.Error(), http.StatusBadRequest)
1013 return
DTabidze078385f2024-03-27 14:49:05 +04001014 }
Davit Tabidze75d57c32024-07-19 19:17:55 +04001015 if err := s.checkIsOwner(w, loggedInUser, groupName); err != nil {
1016 redirectURL := fmt.Sprintf("/group/%s?errorMessage=%s", groupName, url.QueryEscape(err.Error()))
1017 http.Redirect(w, r, redirectURL, http.StatusSeeOther)
1018 return
1019 }
1020 err = s.store.RemoveUserFromTable(username, groupName, tableName)
1021 if err != nil {
1022 redirectURL := fmt.Sprintf("/group/%s?errorMessage=%s", groupName, url.QueryEscape(err.Error()))
1023 http.Redirect(w, r, redirectURL, http.StatusFound)
1024 return
1025 }
1026 http.Redirect(w, r, "/group/"+groupName, http.StatusSeeOther)
DTabidze078385f2024-03-27 14:49:05 +04001027}
1028
1029func (s *Server) addUserToGroupHandler(w http.ResponseWriter, r *http.Request) {
DTabidze0d802592024-03-19 17:42:45 +04001030 loggedInUser, err := getLoggedInUser(r)
1031 if err != nil {
1032 http.Error(w, "User Not Logged In", http.StatusUnauthorized)
1033 return
1034 }
DTabidze078385f2024-03-27 14:49:05 +04001035 vars := mux.Vars(r)
1036 groupName := vars["group-name"]
DTabidze908bb852024-03-25 20:07:57 +04001037 if err := isValidGroupName(groupName); err != nil {
1038 http.Error(w, err.Error(), http.StatusBadRequest)
1039 return
1040 }
1041 username := strings.ToLower(r.FormValue("username"))
1042 if username == "" {
1043 http.Error(w, "Username parameter is required", http.StatusBadRequest)
1044 return
1045 }
DTabidze0d802592024-03-19 17:42:45 +04001046 status, err := convertStatus(r.FormValue("status"))
1047 if err != nil {
1048 http.Error(w, err.Error(), http.StatusBadRequest)
1049 return
1050 }
Davit Tabidzec0d2bf52024-04-03 15:39:33 +04001051 if err := s.checkIsOwner(w, loggedInUser, groupName); err != nil {
1052 redirectURL := fmt.Sprintf("/group/%s?errorMessage=%s", groupName, url.QueryEscape(err.Error()))
1053 http.Redirect(w, r, redirectURL, http.StatusSeeOther)
DTabidze0d802592024-03-19 17:42:45 +04001054 return
1055 }
1056 switch status {
1057 case Owner:
1058 err = s.store.AddGroupOwner(username, groupName)
1059 case Member:
1060 err = s.store.AddGroupMember(username, groupName)
1061 default:
1062 http.Error(w, "Invalid status", http.StatusBadRequest)
1063 return
1064 }
1065 if err != nil {
DTabidze4b44ff42024-04-02 03:16:26 +04001066 redirectURL := fmt.Sprintf("/group/%s?errorMessage=%s", groupName, url.QueryEscape(err.Error()))
1067 http.Redirect(w, r, redirectURL, http.StatusFound)
DTabidze0d802592024-03-19 17:42:45 +04001068 return
1069 }
1070 http.Redirect(w, r, "/group/"+groupName, http.StatusSeeOther)
1071}
1072
1073func (s *Server) addChildGroupHandler(w http.ResponseWriter, r *http.Request) {
1074 // 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 +04001075 loggedInUser, err := getLoggedInUser(r)
1076 if err != nil {
1077 http.Error(w, "User Not Logged In", http.StatusUnauthorized)
1078 return
1079 }
DTabidze078385f2024-03-27 14:49:05 +04001080 vars := mux.Vars(r)
1081 parentGroup := vars["parent-group"]
DTabidze908bb852024-03-25 20:07:57 +04001082 if err := isValidGroupName(parentGroup); err != nil {
1083 http.Error(w, err.Error(), http.StatusBadRequest)
1084 return
1085 }
DTabidze0d802592024-03-19 17:42:45 +04001086 childGroup := r.FormValue("child-group")
DTabidze908bb852024-03-25 20:07:57 +04001087 if err := isValidGroupName(childGroup); err != nil {
1088 http.Error(w, err.Error(), http.StatusBadRequest)
1089 return
1090 }
Davit Tabidzec0d2bf52024-04-03 15:39:33 +04001091 if err := s.checkIsOwner(w, loggedInUser, parentGroup); err != nil {
1092 redirectURL := fmt.Sprintf("/group/%s?errorMessage=%s", parentGroup, url.QueryEscape(err.Error()))
1093 http.Redirect(w, r, redirectURL, http.StatusSeeOther)
DTabidze0d802592024-03-19 17:42:45 +04001094 return
1095 }
1096 if err := s.store.AddChildGroup(parentGroup, childGroup); err != nil {
DTabidze4b44ff42024-04-02 03:16:26 +04001097 redirectURL := fmt.Sprintf("/group/%s?errorMessage=%s", parentGroup, url.QueryEscape(err.Error()))
1098 http.Redirect(w, r, redirectURL, http.StatusFound)
DTabidze0d802592024-03-19 17:42:45 +04001099 return
1100 }
1101 http.Redirect(w, r, "/group/"+parentGroup, http.StatusSeeOther)
1102}
1103
Davit Tabidzec0d2bf52024-04-03 15:39:33 +04001104func (s *Server) addOwnerGroupHandler(w http.ResponseWriter, r *http.Request) {
Davit Tabidzec0d2bf52024-04-03 15:39:33 +04001105 loggedInUser, err := getLoggedInUser(r)
1106 if err != nil {
1107 http.Error(w, "User Not Logged In", http.StatusUnauthorized)
1108 return
1109 }
1110 vars := mux.Vars(r)
1111 ownedGroup := vars["owned-group"]
1112 if err := isValidGroupName(ownedGroup); err != nil {
1113 http.Error(w, err.Error(), http.StatusBadRequest)
1114 return
1115 }
1116 ownerGroup := r.FormValue("owner-group")
1117 if err := isValidGroupName(ownerGroup); err != nil {
1118 http.Error(w, err.Error(), http.StatusBadRequest)
1119 return
1120 }
1121 if err := s.checkIsOwner(w, loggedInUser, ownedGroup); err != nil {
1122 redirectURL := fmt.Sprintf("/group/%s?errorMessage=%s", ownedGroup, url.QueryEscape(err.Error()))
1123 http.Redirect(w, r, redirectURL, http.StatusSeeOther)
1124 return
1125 }
1126 if err := s.store.AddOwnerGroup(ownerGroup, ownedGroup); err != nil {
1127 redirectURL := fmt.Sprintf("/group/%s?errorMessage=%s", ownedGroup, url.QueryEscape(err.Error()))
1128 http.Redirect(w, r, redirectURL, http.StatusFound)
1129 return
1130 }
1131 http.Redirect(w, r, "/group/"+ownedGroup, http.StatusSeeOther)
1132}
1133
Davit Tabidze75d57c32024-07-19 19:17:55 +04001134func (s *Server) addSSHKeyForUserHandler(w http.ResponseWriter, r *http.Request) {
1135 defer s.pingAllSyncAddresses()
1136 loggedInUser, err := getLoggedInUser(r)
1137 if err != nil {
1138 http.Error(w, "User Not Logged In", http.StatusUnauthorized)
1139 return
1140 }
1141 vars := mux.Vars(r)
1142 username := vars["username"]
1143 if loggedInUser != username {
1144 http.Error(w, "You are not allowed to add SSH key for someone else", http.StatusUnauthorized)
1145 return
1146 }
1147 sshKey := r.FormValue("ssh-key")
1148 if sshKey == "" {
1149 http.Error(w, "SSH key not present", http.StatusBadRequest)
1150 return
1151 }
1152 if err := s.store.AddSSHKeyForUser(username, sshKey); err != nil {
1153 redirectURL := fmt.Sprintf("/user/%s?errorMessage=%s", loggedInUser, url.QueryEscape(err.Error()))
1154 http.Redirect(w, r, redirectURL, http.StatusFound)
1155 return
1156 }
1157 http.Redirect(w, r, "/user/"+loggedInUser, http.StatusSeeOther)
1158}
1159
1160func (s *Server) removeSSHKeyForUserHandler(w http.ResponseWriter, r *http.Request) {
1161 defer s.pingAllSyncAddresses()
1162 loggedInUser, err := getLoggedInUser(r)
1163 if err != nil {
1164 http.Error(w, "User Not Logged In", http.StatusUnauthorized)
1165 return
1166 }
1167 vars := mux.Vars(r)
1168 username := vars["username"]
1169 if loggedInUser != username {
1170 http.Error(w, "You are not allowed to remove SSH key for someone else", http.StatusUnauthorized)
1171 return
1172 }
1173 if err := r.ParseForm(); err != nil {
1174 http.Error(w, "Invalid request body", http.StatusBadRequest)
1175 return
1176 }
1177 sshKey := r.FormValue("ssh-key")
1178 if sshKey == "" {
1179 http.Error(w, "SSH key not present", http.StatusBadRequest)
1180 return
1181 }
1182 if err := s.store.RemoveSSHKeyForUser(username, sshKey); err != nil {
1183 redirectURL := fmt.Sprintf("/user/%s?errorMessage=%s", loggedInUser, url.QueryEscape(err.Error()))
1184 http.Redirect(w, r, redirectURL, http.StatusFound)
1185 return
1186 }
1187 http.Redirect(w, r, "/user/"+loggedInUser, http.StatusSeeOther)
1188}
1189
Giorgi Lekveishvili942c7612024-03-22 19:27:48 +04001190type initRequest struct {
1191 Owner string `json:"owner"`
1192 Groups []string `json:"groups"`
1193}
1194
1195func (s *Server) apiInitHandler(w http.ResponseWriter, r *http.Request) {
1196 var req initRequest
1197 if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
1198 http.Error(w, err.Error(), http.StatusBadRequest)
1199 return
1200 }
1201 if err := s.store.Init(req.Owner, req.Groups); err != nil {
1202 http.Error(w, err.Error(), http.StatusInternalServerError)
1203 return
1204 }
1205}
1206
1207type userInfo struct {
DTabidzed7744a62024-03-20 14:09:15 +04001208 MemberOf []string `json:"memberOf"`
1209}
1210
1211func (s *Server) apiMemberOfHandler(w http.ResponseWriter, r *http.Request) {
1212 vars := mux.Vars(r)
1213 user, ok := vars["username"]
DTabidze908bb852024-03-25 20:07:57 +04001214 if !ok || user == "" {
DTabidzed7744a62024-03-20 14:09:15 +04001215 http.Error(w, "Username parameter is required", http.StatusBadRequest)
1216 return
1217 }
DTabidze908bb852024-03-25 20:07:57 +04001218 user = strings.ToLower(user)
DTabidzed7744a62024-03-20 14:09:15 +04001219 transitiveGroups, err := s.store.GetAllTransitiveGroupsForUser(user)
1220 if err != nil {
1221 http.Error(w, err.Error(), http.StatusInternalServerError)
1222 return
1223 }
DTabidzec0b4d8f2024-03-22 17:25:10 +04001224 var groupNames []string
1225 for _, group := range transitiveGroups {
1226 groupNames = append(groupNames, group.Name)
1227 }
DTabidzed7744a62024-03-20 14:09:15 +04001228 w.Header().Set("Content-Type", "application/json")
Giorgi Lekveishvili942c7612024-03-22 19:27:48 +04001229 if err := json.NewEncoder(w).Encode(userInfo{groupNames}); err != nil {
DTabidzed7744a62024-03-20 14:09:15 +04001230 http.Error(w, err.Error(), http.StatusInternalServerError)
1231 return
1232 }
1233}
1234
Davit Tabidze75d57c32024-07-19 19:17:55 +04001235func (s *Server) apiGetAllUsers(w http.ResponseWriter, r *http.Request) {
1236 defer s.pingAllSyncAddresses()
1237 selfAddress := r.FormValue("selfAddress")
1238 if selfAddress != "" {
1239 s.addSyncAddress(selfAddress)
1240 }
Davit Tabidzef867f2d2024-07-24 18:06:25 +04001241 var users []User
1242 var err error
1243 groups := r.FormValue("groups")
1244 if groups == "" {
1245 users, err = s.store.GetUsers(nil)
1246 } else {
1247 uniqueUsers := make(map[string]struct{})
1248 g := strings.Split(groups, ",")
1249 uniqueTG := make(map[string]struct{})
1250 for _, group := range g {
1251 uniqueTG[group] = struct{}{}
1252 trGroups, err := s.store.GetAllTransitiveGroupsForGroup(group)
1253 if err != nil {
1254 http.Error(w, err.Error(), http.StatusInternalServerError)
1255 return
1256 }
1257 for _, tg := range trGroups {
1258 uniqueTG[tg.Name] = struct{}{}
1259 }
1260 }
1261 for group := range uniqueTG {
1262 u, err := s.store.GetGroupMembers(group)
1263 if err != nil {
1264 http.Error(w, err.Error(), http.StatusInternalServerError)
1265 return
1266 }
1267 for _, user := range u {
1268 uniqueUsers[user] = struct{}{}
1269 }
1270 }
1271 usernames := make([]string, 0, len(uniqueUsers))
1272 for username := range uniqueUsers {
1273 usernames = append(usernames, username)
1274 }
1275 users, err = s.store.GetUsers(usernames)
1276 }
Davit Tabidze75d57c32024-07-19 19:17:55 +04001277 if err != nil {
Davit Tabidzef867f2d2024-07-24 18:06:25 +04001278 http.Error(w, "Failed to retrieve user infos", http.StatusInternalServerError)
Davit Tabidze75d57c32024-07-19 19:17:55 +04001279 return
1280 }
1281 w.Header().Set("Content-Type", "application/json")
1282 if err := json.NewEncoder(w).Encode(users); err != nil {
1283 http.Error(w, err.Error(), http.StatusInternalServerError)
1284 return
1285 }
1286}
1287
1288func (s *Server) apiCreateUser(w http.ResponseWriter, r *http.Request) {
1289 defer s.pingAllSyncAddresses()
1290 selfAddress := r.FormValue("selfAddress")
1291 if selfAddress != "" {
1292 s.addSyncAddress(selfAddress)
1293 }
1294 if err := r.ParseForm(); err != nil {
1295 http.Error(w, "Invalid request body", http.StatusBadRequest)
1296 return
1297 }
1298 username := r.FormValue("username")
1299 email := r.FormValue("email")
1300 if username == "" {
1301 http.Error(w, "Username cannot be empty", http.StatusBadRequest)
1302 return
1303 }
1304 if email == "" {
1305 http.Error(w, "Email cannot be empty", http.StatusBadRequest)
1306 return
1307 }
1308 username = strings.ToLower(username)
1309 email = strings.ToLower(email)
1310 err := s.store.CreateUser(username, email)
1311 if err != nil {
1312 http.Error(w, err.Error(), http.StatusInternalServerError)
1313 return
1314 }
1315 w.WriteHeader(http.StatusOK)
1316}
1317
1318func (s *Server) pingAllSyncAddresses() {
1319 s.mu.Lock()
1320 defer s.mu.Unlock()
1321 for address := range s.syncAddresses {
1322 resp, err := http.Get(address)
1323 if err != nil {
1324 log.Printf("Failed to ping %s: %v", address, err)
1325 continue
1326 }
1327 resp.Body.Close()
1328 if resp.StatusCode != http.StatusOK {
1329 log.Printf("Ping to %s returned status %d", address, resp.StatusCode)
1330 }
1331 }
1332}
1333
1334func (s *Server) addSyncAddress(address string) {
1335 s.mu.Lock()
1336 defer s.mu.Unlock()
1337 s.syncAddresses[address] = struct{}{}
1338}
1339
DTabidze908bb852024-03-25 20:07:57 +04001340func convertStatus(status string) (Status, error) {
1341 switch status {
1342 case "Owner":
1343 return Owner, nil
1344 case "Member":
1345 return Member, nil
1346 default:
1347 return Owner, fmt.Errorf("invalid status: %s", status)
1348 }
1349}
1350
1351func isValidGroupName(group string) error {
1352 if strings.TrimSpace(group) == "" {
Davit Tabidze5f1a2c62024-07-17 17:57:27 +04001353 return fmt.Errorf("Group name can't be empty or contain only whitespaces")
DTabidze908bb852024-03-25 20:07:57 +04001354 }
1355 validGroupName := regexp.MustCompile(`^[a-z0-9\-_:.\/ ]+$`)
1356 if !validGroupName.MatchString(group) {
Davit Tabidze5f1a2c62024-07-17 17:57:27 +04001357 return fmt.Errorf("Group name should contain only lowercase letters, digits, -, _, :, ., /")
DTabidze908bb852024-03-25 20:07:57 +04001358 }
1359 return nil
1360}
1361
DTabidze0d802592024-03-19 17:42:45 +04001362func main() {
1363 flag.Parse()
DTabidzec0b4d8f2024-03-22 17:25:10 +04001364 db, err := sql.Open("sqlite3", *dbPath)
DTabidze0d802592024-03-19 17:42:45 +04001365 if err != nil {
1366 panic(err)
1367 }
DTabidzec0b4d8f2024-03-22 17:25:10 +04001368 store, err := NewSQLiteStore(db)
1369 if err != nil {
1370 panic(err)
1371 }
Davit Tabidze75d57c32024-07-19 19:17:55 +04001372 s := Server{
1373 store: store,
1374 syncAddresses: make(map[string]struct{}),
1375 mu: sync.Mutex{},
1376 }
Giorgi Lekveishvili329af572024-03-25 20:14:41 +04001377 log.Fatal(s.Start())
DTabidze0d802592024-03-19 17:42:45 +04001378}