| package main |
| |
| import ( |
| "database/sql" |
| "embed" |
| "encoding/json" |
| "flag" |
| "fmt" |
| "html/template" |
| "log" |
| "net/http" |
| "net/url" |
| "regexp" |
| "strings" |
| "sync" |
| |
| "github.com/ncruces/go-sqlite3" |
| _ "github.com/ncruces/go-sqlite3/driver" |
| _ "github.com/ncruces/go-sqlite3/embed" |
| |
| "github.com/gorilla/mux" |
| ) |
| |
| var port = flag.Int("port", 8080, "Port to listen on") |
| var apiPort = flag.Int("api-port", 8081, "Port to listen on for API requests") |
| var dbPath = flag.String("db-path", "memberships.db", "Path to SQLite file") |
| |
| //go:embed memberships-tmpl/* |
| var tmpls embed.FS |
| |
| //go:embed stat |
| var staticResources embed.FS |
| |
| type Store interface { |
| // Initializes store with admin user and their groups. |
| Init(user, email string, groups []string) error |
| CreateGroup(owner string, group Group) error |
| AddChildGroup(parent, child string) error |
| AddOwnerGroup(owned_group, owner_group string) error |
| DoesGroupExist(group string) (bool, error) |
| GetGroupsOwnedBy(user string) ([]Group, error) |
| GetGroupsUserBelongsTo(user string) ([]Group, error) |
| IsGroupOwner(user, group string) (bool, error) |
| IsMemberOfOwnerGroup(user, group string) (bool, error) |
| AddGroupMember(user, group string) error |
| AddGroupOwner(user, group string) error |
| GetGroupOwners(group string) ([]string, error) |
| GetGroupOwnerGroups(group string) ([]Group, error) |
| GetGroupMembers(group string) ([]string, error) |
| GetGroupDescription(group string) (string, error) |
| GetAllTransitiveGroupsForUser(user string) ([]Group, error) |
| GetGroupsGroupBelongsTo(group string) ([]Group, error) |
| GetDirectChildrenGroups(group string) ([]Group, error) |
| GetAllTransitiveGroupsForGroup(group string) ([]Group, error) |
| RemoveFromGroupToGroup(parent, child string) error |
| RemoveUserFromTable(username, groupName, tableName string) error |
| GetAllGroups() ([]Group, error) |
| GetUsers(username []string) ([]User, error) |
| GetUser(username string) (User, error) |
| AddSSHKeyForUser(username, sshKey string) error |
| RemoveSSHKeyForUser(username, sshKey string) error |
| CreateUser(user, email string) error |
| } |
| |
| type Server struct { |
| store Store |
| syncAddresses map[string]struct{} |
| mu sync.Mutex |
| } |
| |
| type Group struct { |
| Name string |
| Description string |
| } |
| |
| type User struct { |
| Username string `json:"username"` |
| Email string `json:"email"` |
| SSHPublicKeys []string `json:"sshPublicKeys,omitempty"` |
| } |
| |
| type SQLiteStore struct { |
| db *sql.DB |
| } |
| |
| const ( |
| ErrorUniqueConstraintViolation = 2067 |
| ErrorConstraintPrimaryKeyViolation = 1555 |
| ) |
| |
| func NewSQLiteStore(db *sql.DB) (*SQLiteStore, error) { |
| _, err := db.Exec(` |
| CREATE TABLE IF NOT EXISTS groups ( |
| name TEXT PRIMARY KEY, |
| description TEXT |
| ); |
| CREATE TABLE IF NOT EXISTS owners ( |
| username TEXT, |
| group_name TEXT, |
| FOREIGN KEY(group_name) REFERENCES groups(name), |
| UNIQUE (username, group_name) |
| ); |
| CREATE TABLE IF NOT EXISTS owner_groups ( |
| owner_group TEXT, |
| owned_group TEXT, |
| FOREIGN KEY(owner_group) REFERENCES groups(name), |
| FOREIGN KEY(owned_group) REFERENCES groups(name), |
| UNIQUE (owner_group, owned_group) |
| ); |
| CREATE TABLE IF NOT EXISTS group_to_group ( |
| parent_group TEXT, |
| child_group TEXT, |
| FOREIGN KEY(parent_group) REFERENCES groups(name), |
| FOREIGN KEY(child_group) REFERENCES groups(name), |
| UNIQUE (parent_group, child_group) |
| ); |
| CREATE TABLE IF NOT EXISTS user_to_group ( |
| username TEXT, |
| group_name TEXT, |
| FOREIGN KEY(group_name) REFERENCES groups(name), |
| UNIQUE (username, group_name) |
| ); |
| CREATE TABLE IF NOT EXISTS users ( |
| username TEXT PRIMARY KEY, |
| email TEXT, |
| UNIQUE (email) |
| ); |
| CREATE TABLE IF NOT EXISTS user_ssh_keys ( |
| username TEXT, |
| ssh_key TEXT, |
| UNIQUE (ssh_key), |
| FOREIGN KEY(username) REFERENCES users(username) |
| );`) |
| if err != nil { |
| return nil, err |
| } |
| return &SQLiteStore{db: db}, nil |
| } |
| |
| func (s *SQLiteStore) Init(user, email string, groups []string) error { |
| tx, err := s.db.Begin() |
| if err != nil { |
| return err |
| } |
| defer tx.Rollback() |
| row := tx.QueryRow("SELECT COUNT(*) FROM groups") |
| var count int |
| if err := row.Scan(&count); err != nil { |
| return err |
| } |
| if count != 0 { |
| return fmt.Errorf("Store already initialised") |
| } |
| query := `INSERT INTO users (username, email) VALUES (?, ?)` |
| if _, err := tx.Exec(query, user, email); err != nil { |
| return err |
| } |
| for _, g := range groups { |
| query = `INSERT INTO groups (name, description) VALUES (?, '')` |
| if _, err := tx.Exec(query, g); err != nil { |
| return err |
| } |
| query = `INSERT INTO owners (username, group_name) VALUES (?, ?)` |
| if _, err := tx.Exec(query, user, g); err != nil { |
| return err |
| } |
| query = `INSERT INTO user_to_group (username, group_name) VALUES (?, ?)` |
| if _, err := tx.Exec(query, user, g); err != nil { |
| return err |
| } |
| } |
| return tx.Commit() |
| } |
| |
| func (s *SQLiteStore) queryGroups(query string, args ...interface{}) ([]Group, error) { |
| groups := make([]Group, 0) |
| rows, err := s.db.Query(query, args...) |
| if err != nil { |
| return nil, err |
| } |
| defer rows.Close() |
| for rows.Next() { |
| var group Group |
| if err := rows.Scan(&group.Name, &group.Description); err != nil { |
| return nil, err |
| } |
| groups = append(groups, group) |
| } |
| if err := rows.Err(); err != nil { |
| return nil, err |
| } |
| return groups, nil |
| } |
| |
| func (s *SQLiteStore) GetGroupsOwnedBy(user string) ([]Group, error) { |
| query := ` |
| SELECT groups.name, groups.description |
| FROM groups |
| JOIN owners ON groups.name = owners.group_name |
| WHERE owners.username = ?` |
| return s.queryGroups(query, user) |
| } |
| |
| func (s *SQLiteStore) GetGroupsUserBelongsTo(user string) ([]Group, error) { |
| query := ` |
| SELECT groups.name, groups.description |
| FROM groups |
| JOIN user_to_group ON groups.name = user_to_group.group_name |
| WHERE user_to_group.username = ?` |
| return s.queryGroups(query, user) |
| } |
| |
| func (s *SQLiteStore) CreateGroup(owner string, group Group) error { |
| tx, err := s.db.Begin() |
| if err != nil { |
| return err |
| } |
| defer tx.Rollback() |
| query := `INSERT INTO groups (name, description) VALUES (?, ?)` |
| if _, err := tx.Exec(query, group.Name, group.Description); err != nil { |
| sqliteErr, ok := err.(*sqlite3.Error) |
| if ok && sqliteErr.ExtendedCode() == ErrorConstraintPrimaryKeyViolation { |
| return fmt.Errorf("Group with the name %s already exists", group.Name) |
| } |
| return err |
| } |
| query = `INSERT INTO owners (username, group_name) VALUES (?, ?)` |
| if _, err := tx.Exec(query, owner, group.Name); err != nil { |
| return err |
| } |
| return tx.Commit() |
| } |
| |
| func (s *SQLiteStore) IsGroupOwner(user, group string) (bool, error) { |
| query := ` |
| SELECT EXISTS ( |
| SELECT 1 |
| FROM owners |
| WHERE username = ? AND group_name = ? |
| )` |
| var exists bool |
| if err := s.db.QueryRow(query, user, group).Scan(&exists); err != nil { |
| return false, err |
| } |
| return exists, nil |
| } |
| |
| func (s *SQLiteStore) AddGroupMember(user, group string) error { |
| _, err := s.db.Exec(`INSERT INTO user_to_group (username, group_name) VALUES (?, ?)`, user, group) |
| if err != nil { |
| sqliteErr, ok := err.(*sqlite3.Error) |
| if ok && sqliteErr.ExtendedCode() == ErrorUniqueConstraintViolation { |
| return fmt.Errorf("%s is already a member of group %s", user, group) |
| } |
| return err |
| } |
| return nil |
| } |
| |
| func (s *SQLiteStore) AddGroupOwner(user, group string) error { |
| _, err := s.db.Exec(`INSERT INTO owners (username, group_name) VALUES (?, ?)`, user, group) |
| if err != nil { |
| sqliteErr, ok := err.(*sqlite3.Error) |
| if ok && sqliteErr.ExtendedCode() == ErrorUniqueConstraintViolation { |
| return fmt.Errorf("%s is already an owner of group %s", user, group) |
| } |
| return err |
| } |
| return nil |
| } |
| |
| func (s *SQLiteStore) getUsersByGroup(table, group string) ([]string, error) { |
| query := fmt.Sprintf("SELECT username FROM %s WHERE group_name = ?", table) |
| rows, err := s.db.Query(query, group) |
| if err != nil { |
| return nil, err |
| } |
| defer rows.Close() |
| var users []string |
| for rows.Next() { |
| var username string |
| if err := rows.Scan(&username); err != nil { |
| return nil, err |
| } |
| users = append(users, username) |
| } |
| if err := rows.Err(); err != nil { |
| return nil, err |
| } |
| return users, nil |
| } |
| |
| func (s *SQLiteStore) GetGroupOwners(group string) ([]string, error) { |
| return s.getUsersByGroup("owners", group) |
| } |
| |
| func (s *SQLiteStore) GetGroupMembers(group string) ([]string, error) { |
| return s.getUsersByGroup("user_to_group", group) |
| } |
| |
| func (s *SQLiteStore) GetGroupDescription(group string) (string, error) { |
| var description string |
| query := `SELECT description FROM groups WHERE name = ?` |
| if err := s.db.QueryRow(query, group).Scan(&description); err != nil { |
| return "", err |
| } |
| return description, nil |
| } |
| |
| func (s *SQLiteStore) DoesGroupExist(group string) (bool, error) { |
| query := `SELECT EXISTS (SELECT 1 FROM groups WHERE name = ?)` |
| var exists bool |
| if err := s.db.QueryRow(query, group).Scan(&exists); err != nil { |
| return false, err |
| } |
| return exists, nil |
| } |
| |
| func (s *SQLiteStore) AddChildGroup(parent, child string) error { |
| if parent == child { |
| return fmt.Errorf("Parent and child groups can not have same name") |
| } |
| exists, err := s.DoesGroupExist(parent) |
| if err != nil { |
| return fmt.Errorf("Error checking parent group existence: %v", err) |
| } |
| if !exists { |
| return fmt.Errorf("Parent group with name %s does not exist", parent) |
| } |
| exists, err = s.DoesGroupExist(child) |
| if err != nil { |
| return fmt.Errorf("Error checking child group existence: %v", err) |
| } |
| if !exists { |
| return fmt.Errorf("Child group with name %s does not exist", child) |
| } |
| parentGroups, err := s.GetAllTransitiveGroupsForGroup(parent) |
| if err != nil { |
| return err |
| } |
| for _, group := range parentGroups { |
| if group.Name == child { |
| return fmt.Errorf("Circular reference detected: group %s is already a parent of group %s", child, parent) |
| } |
| } |
| _, err = s.db.Exec(`INSERT INTO group_to_group (parent_group, child_group) VALUES (?, ?)`, parent, child) |
| if err != nil { |
| sqliteErr, ok := err.(*sqlite3.Error) |
| if ok && sqliteErr.ExtendedCode() == ErrorUniqueConstraintViolation { |
| return fmt.Errorf("Child group name %s already exists in group %s", child, parent) |
| } |
| return err |
| } |
| return nil |
| } |
| |
| func (s *SQLiteStore) GetAllTransitiveGroupsForUser(user string) ([]Group, error) { |
| if groups, err := s.GetGroupsUserBelongsTo(user); err != nil { |
| return nil, err |
| } else { |
| visited := map[string]struct{}{} |
| return s.getAllParentGroupsRecursive(groups, visited) |
| } |
| } |
| |
| func (s *SQLiteStore) GetAllTransitiveGroupsForGroup(group string) ([]Group, error) { |
| if p, err := s.GetGroupsGroupBelongsTo(group); err != nil { |
| return nil, err |
| } else { |
| // Mark initial group as visited |
| visited := map[string]struct{}{ |
| group: struct{}{}, |
| } |
| return s.getAllParentGroupsRecursive(p, visited) |
| } |
| } |
| |
| func (s *SQLiteStore) getAllParentGroupsRecursive(groups []Group, visited map[string]struct{}) ([]Group, error) { |
| var ret []Group |
| for _, g := range groups { |
| if _, ok := visited[g.Name]; ok { |
| continue |
| } |
| visited[g.Name] = struct{}{} |
| ret = append(ret, g) |
| if p, err := s.GetGroupsGroupBelongsTo(g.Name); err != nil { |
| return nil, err |
| } else if res, err := s.getAllParentGroupsRecursive(p, visited); err != nil { |
| return nil, err |
| } else { |
| ret = append(ret, res...) |
| } |
| } |
| return ret, nil |
| } |
| |
| func (s *SQLiteStore) GetGroupsGroupBelongsTo(group string) ([]Group, error) { |
| query := ` |
| SELECT groups.name, groups.description |
| FROM groups |
| JOIN group_to_group ON groups.name = group_to_group.parent_group |
| WHERE group_to_group.child_group = ?` |
| rows, err := s.db.Query(query, group) |
| if err != nil { |
| return nil, err |
| } |
| defer rows.Close() |
| var parentGroups []Group |
| for rows.Next() { |
| var parentGroup Group |
| if err := rows.Scan(&parentGroup.Name, &parentGroup.Description); err != nil { |
| return nil, err |
| } |
| parentGroups = append(parentGroups, parentGroup) |
| } |
| if err := rows.Err(); err != nil { |
| return nil, err |
| } |
| return parentGroups, nil |
| } |
| |
| func (s *SQLiteStore) GetDirectChildrenGroups(group string) ([]Group, error) { |
| query := ` |
| SELECT groups.name, groups.description |
| FROM groups |
| JOIN group_to_group ON groups.name = group_to_group.child_group |
| WHERE group_to_group.parent_group = ?` |
| rows, err := s.db.Query(query, group) |
| if err != nil { |
| return nil, err |
| } |
| defer rows.Close() |
| var childrenGroups []Group |
| for rows.Next() { |
| var childGroup Group |
| if err := rows.Scan(&childGroup.Name, &childGroup.Description); err != nil { |
| return nil, err |
| } |
| childrenGroups = append(childrenGroups, childGroup) |
| } |
| if err := rows.Err(); err != nil { |
| return nil, err |
| } |
| return childrenGroups, nil |
| } |
| |
| func (s *SQLiteStore) RemoveFromGroupToGroup(parent, child string) error { |
| query := `DELETE FROM group_to_group WHERE parent_group = ? AND child_group = ?` |
| rowDeleted, err := s.db.Exec(query, parent, child) |
| if err != nil { |
| return err |
| } |
| rowDeletedNumber, err := rowDeleted.RowsAffected() |
| if err != nil { |
| return err |
| } |
| if rowDeletedNumber == 0 { |
| return fmt.Errorf("Pair of parent '%s' and child '%s' groups not found", parent, child) |
| } |
| return nil |
| } |
| |
| func (s *SQLiteStore) RemoveUserFromTable(username, groupName, tableName string) error { |
| if tableName == "owners" { |
| owners, err := s.GetGroupOwners(groupName) |
| if err != nil { |
| return err |
| } |
| if len(owners) == 1 { |
| return fmt.Errorf("Cannot remove the last owner of the group") |
| } |
| } |
| query := fmt.Sprintf("DELETE FROM %s WHERE username = ? AND group_name = ?", tableName) |
| rowDeleted, err := s.db.Exec(query, username, groupName) |
| if err != nil { |
| return err |
| } |
| rowDeletedNumber, err := rowDeleted.RowsAffected() |
| if err != nil { |
| return err |
| } |
| if rowDeletedNumber == 0 { |
| return fmt.Errorf("Pair of group '%s' and user '%s' not found", groupName, username) |
| } |
| return nil |
| } |
| |
| func (s *SQLiteStore) AddOwnerGroup(owner_group, owned_group string) error { |
| if owned_group == owner_group { |
| return fmt.Errorf("Group can not own itself") |
| } |
| exists, err := s.DoesGroupExist(owned_group) |
| if err != nil { |
| return fmt.Errorf("Error checking owned group existence: %v", err) |
| } |
| if !exists { |
| return fmt.Errorf("Owned group with name %s does not exist", owned_group) |
| } |
| exists, err = s.DoesGroupExist(owner_group) |
| if err != nil { |
| return fmt.Errorf("Error checking owner group existence: %v", err) |
| } |
| if !exists { |
| return fmt.Errorf("Owner group with name %s does not exist", owner_group) |
| } |
| _, err = s.db.Exec(`INSERT INTO owner_groups (owner_group, owned_group) VALUES (?, ?)`, owner_group, owned_group) |
| if err != nil { |
| sqliteErr, ok := err.(*sqlite3.Error) |
| if ok && sqliteErr.ExtendedCode() == ErrorUniqueConstraintViolation { |
| return fmt.Errorf("Group named %s is already owner of a group %s", owner_group, owned_group) |
| } |
| return err |
| } |
| return nil |
| } |
| |
| func (s *SQLiteStore) GetGroupOwnerGroups(group string) ([]Group, error) { |
| query := ` |
| SELECT groups.name, groups.description |
| FROM groups |
| JOIN owner_groups ON groups.name = owner_groups.owner_group |
| WHERE owner_groups.owned_group = ?` |
| return s.queryGroups(query, group) |
| } |
| |
| func (s *SQLiteStore) IsMemberOfOwnerGroup(user, group string) (bool, error) { |
| query := ` |
| SELECT EXISTS ( |
| SELECT 1 FROM owner_groups |
| INNER JOIN user_to_group ON owner_groups.owner_group = user_to_group.group_name |
| WHERE owner_groups.owned_group = ? AND user_to_group.username = ?)` |
| var exists bool |
| err := s.db.QueryRow(query, group, user).Scan(&exists) |
| if err != nil { |
| return false, err |
| } |
| return exists, nil |
| } |
| |
| func (s *SQLiteStore) GetAllGroups() ([]Group, error) { |
| query := `SELECT name, description FROM groups` |
| return s.queryGroups(query) |
| } |
| |
| func (s *SQLiteStore) AddSSHKeyForUser(username, sshKey string) error { |
| _, err := s.db.Exec(`INSERT INTO user_ssh_keys (username, ssh_key) VALUES (?, ?)`, username, sshKey) |
| if err != nil { |
| sqliteErr, ok := err.(*sqlite3.Error) |
| if ok && sqliteErr.ExtendedCode() == ErrorUniqueConstraintViolation { |
| return fmt.Errorf("%s such SSH public key already exists", sshKey) |
| } |
| return err |
| } |
| return nil |
| } |
| |
| func (s *SQLiteStore) RemoveSSHKeyForUser(username, sshKey string) error { |
| _, err := s.db.Exec(`DELETE FROM user_ssh_keys WHERE username = ? AND ssh_key = ?`, username, sshKey) |
| if err != nil { |
| return err |
| } |
| return nil |
| } |
| |
| func (s *SQLiteStore) GetUsers(usernames []string) ([]User, error) { |
| var rows *sql.Rows |
| var err error |
| query := ` |
| SELECT users.username, users.email, GROUP_CONCAT(user_ssh_keys.ssh_key, ',') |
| FROM users |
| LEFT JOIN user_ssh_keys ON users.username = user_ssh_keys.username` |
| var args []interface{} |
| if usernames != nil { |
| if len(usernames) == 0 { |
| return []User{}, nil |
| } |
| query += " WHERE users.username IN (" |
| placeholders := strings.Repeat("?,", len(usernames)-1) + "?" |
| query += placeholders + ") " |
| for _, username := range usernames { |
| args = append(args, username) |
| } |
| } |
| query += " GROUP BY users.username" |
| rows, err = s.db.Query(query, args...) |
| if err != nil { |
| return nil, err |
| } |
| defer rows.Close() |
| var userInfos []User |
| for rows.Next() { |
| var username, email string |
| var sshKeys sql.NullString |
| if err := rows.Scan(&username, &email, &sshKeys); err != nil { |
| return nil, err |
| } |
| user := User{ |
| Username: username, |
| Email: email, |
| } |
| if sshKeys.Valid { |
| user.SSHPublicKeys = strings.Split(sshKeys.String, ",") |
| } |
| userInfos = append(userInfos, user) |
| } |
| if err := rows.Err(); err != nil { |
| return nil, err |
| } |
| return userInfos, nil |
| } |
| |
| func (s *SQLiteStore) GetUser(username string) (User, error) { |
| var user User |
| user.Username = username |
| query := ` |
| SELECT users.email, GROUP_CONCAT(user_ssh_keys.ssh_key, ',') |
| FROM users |
| LEFT JOIN user_ssh_keys ON users.username = user_ssh_keys.username |
| WHERE users.username = ? |
| GROUP BY users.username |
| ` |
| row := s.db.QueryRow(query, username) |
| var sshKeys sql.NullString |
| err := row.Scan(&user.Email, &sshKeys) |
| if err != nil { |
| if err == sql.ErrNoRows { |
| return User{}, fmt.Errorf("no user found with username %s", username) |
| } |
| return User{}, err |
| } |
| if sshKeys.Valid { |
| user.SSHPublicKeys = strings.Split(sshKeys.String, ",") |
| } |
| return user, nil |
| } |
| |
| func (s *SQLiteStore) CreateUser(user, email string) error { |
| _, err := s.db.Exec(`INSERT INTO users (username, email) VALUES (?, ?)`, user, email) |
| if err != nil { |
| sqliteErr, ok := err.(*sqlite3.Error) |
| if ok { |
| if sqliteErr.ExtendedCode() == ErrorUniqueConstraintViolation { |
| if strings.Contains(err.Error(), "UNIQUE constraint failed: users.username") { |
| return fmt.Errorf("username %s already exists", user) |
| } |
| if strings.Contains(err.Error(), "UNIQUE constraint failed: users.email") { |
| return fmt.Errorf("email %s already exists", email) |
| } |
| } |
| } |
| return err |
| } |
| return nil |
| } |
| |
| func getLoggedInUser(r *http.Request) (string, error) { |
| if user := r.Header.Get("X-Forwarded-User"); user != "" { |
| return user, nil |
| } else { |
| return "", fmt.Errorf("unauthenticated") |
| } |
| // return "tabo", nil |
| } |
| |
| type Status int |
| |
| const ( |
| Owner Status = iota |
| Member |
| ) |
| |
| func (s *Server) Start() error { |
| e := make(chan error) |
| go func() { |
| r := mux.NewRouter() |
| r.PathPrefix("/static/").Handler(http.FileServer(http.FS(staticResources))) |
| r.HandleFunc("/group/{group-name}/add-user/", s.addUserToGroupHandler).Methods(http.MethodPost) |
| r.HandleFunc("/group/{parent-group}/add-child-group", s.addChildGroupHandler).Methods(http.MethodPost) |
| r.HandleFunc("/group/{owned-group}/add-owner-group", s.addOwnerGroupHandler).Methods(http.MethodPost) |
| r.HandleFunc("/group/{parent-group}/remove-child-group/{child-group}", s.removeChildGroupHandler).Methods(http.MethodPost) |
| r.HandleFunc("/group/{group-name}/remove-owner/{username}", s.removeOwnerFromGroupHandler).Methods(http.MethodPost) |
| r.HandleFunc("/group/{group-name}/remove-member/{username}", s.removeMemberFromGroupHandler).Methods(http.MethodPost) |
| r.HandleFunc("/group/{group-name}", s.groupHandler) |
| r.HandleFunc("/user/{username}/ssh-key", s.addSSHKeyForUserHandler).Methods(http.MethodPost) |
| r.HandleFunc("/user/{username}/remove-ssh-key", s.removeSSHKeyForUserHandler).Methods(http.MethodPost) |
| r.HandleFunc("/user/{username}", s.userHandler) |
| r.HandleFunc("/create-group", s.createGroupHandler).Methods(http.MethodPost) |
| r.HandleFunc("/", s.homePageHandler) |
| e <- http.ListenAndServe(fmt.Sprintf(":%d", *port), r) |
| }() |
| go func() { |
| r := mux.NewRouter() |
| r.HandleFunc("/api/init", s.apiInitHandler) |
| // TODO(gio): change to /api/users/{username} |
| r.HandleFunc("/api/users/{username}/keys", s.apiAddUserKey).Methods(http.MethodPost) |
| r.HandleFunc("/api/user/{username}", s.apiMemberOfHandler) |
| r.HandleFunc("/api/users", s.apiGetAllUsers).Methods(http.MethodGet) |
| r.HandleFunc("/api/users", s.apiCreateUser).Methods(http.MethodPost) |
| e <- http.ListenAndServe(fmt.Sprintf(":%d", *apiPort), r) |
| }() |
| return <-e |
| } |
| |
| type GroupData struct { |
| Group Group |
| Membership string |
| } |
| |
| func (s *Server) checkIsOwner(w http.ResponseWriter, user, group string) error { |
| isOwner, err := s.store.IsGroupOwner(user, group) |
| if err != nil { |
| return err |
| } |
| if isOwner { |
| return nil |
| } |
| // TODO(dtabidze): right now this only checks if user is member of just one lvl upper group. should add transitive group check. |
| isMemberOfOwnerGroup, err := s.store.IsMemberOfOwnerGroup(user, group) |
| if err != nil { |
| return err |
| } |
| if !isMemberOfOwnerGroup { |
| return fmt.Errorf("You are not the owner or a member of any owner group of the group %s", group) |
| } |
| return nil |
| } |
| |
| type templates struct { |
| group *template.Template |
| user *template.Template |
| } |
| |
| func parseTemplates(fs embed.FS) (templates, error) { |
| base, err := template.ParseFS(fs, "memberships-tmpl/base.html") |
| if err != nil { |
| return templates{}, err |
| } |
| parse := func(path string) (*template.Template, error) { |
| if b, err := base.Clone(); err != nil { |
| return nil, err |
| } else { |
| return b.ParseFS(fs, path) |
| } |
| } |
| user, err := parse("memberships-tmpl/user.html") |
| if err != nil { |
| return templates{}, err |
| } |
| group, err := parse("memberships-tmpl/group.html") |
| if err != nil { |
| return templates{}, err |
| } |
| return templates{group, user}, nil |
| } |
| |
| func (s *Server) homePageHandler(w http.ResponseWriter, r *http.Request) { |
| loggedInUser, err := getLoggedInUser(r) |
| if err != nil { |
| http.Error(w, "User Not Logged In", http.StatusUnauthorized) |
| return |
| } |
| http.Redirect(w, r, "/user/"+loggedInUser, http.StatusSeeOther) |
| } |
| |
| type UserPageData struct { |
| OwnerGroups []Group |
| MembershipGroups []Group |
| TransitiveGroups []Group |
| LoggedInUserPage bool |
| CurrentUser string |
| SSHPublicKeys []string |
| Email string |
| ErrorMessage string |
| } |
| |
| func (s *Server) userHandler(w http.ResponseWriter, r *http.Request) { |
| loggedInUser, err := getLoggedInUser(r) |
| if err != nil { |
| http.Error(w, "User Not Logged In", http.StatusUnauthorized) |
| return |
| } |
| errorMsg := r.URL.Query().Get("errorMessage") |
| vars := mux.Vars(r) |
| user := strings.ToLower(vars["username"]) |
| // TODO(dtabidze): should check if username exists or not. |
| loggedInUserPage := loggedInUser == user |
| ownerGroups, err := s.store.GetGroupsOwnedBy(user) |
| if err != nil { |
| http.Error(w, err.Error(), http.StatusInternalServerError) |
| return |
| } |
| membershipGroups, err := s.store.GetGroupsUserBelongsTo(user) |
| if err != nil { |
| http.Error(w, err.Error(), http.StatusInternalServerError) |
| return |
| } |
| transitiveGroups, err := s.store.GetAllTransitiveGroupsForUser(user) |
| if err != nil { |
| http.Error(w, err.Error(), http.StatusInternalServerError) |
| return |
| } |
| userInfo, err := s.store.GetUser(user) |
| if err != nil { |
| http.Error(w, err.Error(), http.StatusInternalServerError) |
| return |
| } |
| data := UserPageData{ |
| OwnerGroups: ownerGroups, |
| MembershipGroups: membershipGroups, |
| TransitiveGroups: transitiveGroups, |
| LoggedInUserPage: loggedInUserPage, |
| CurrentUser: user, |
| SSHPublicKeys: userInfo.SSHPublicKeys, |
| Email: userInfo.Email, |
| ErrorMessage: errorMsg, |
| } |
| templates, err := parseTemplates(tmpls) |
| if err != nil { |
| http.Error(w, err.Error(), http.StatusInternalServerError) |
| return |
| } |
| if err := templates.user.Execute(w, data); err != nil { |
| http.Error(w, err.Error(), http.StatusInternalServerError) |
| return |
| } |
| } |
| |
| func (s *Server) createGroupHandler(w http.ResponseWriter, r *http.Request) { |
| loggedInUser, err := getLoggedInUser(r) |
| if err != nil { |
| http.Error(w, "User Not Logged In", http.StatusUnauthorized) |
| return |
| } |
| if err := r.ParseForm(); err != nil { |
| http.Error(w, err.Error(), http.StatusInternalServerError) |
| return |
| } |
| var group Group |
| group.Name = r.PostFormValue("group-name") |
| if err := isValidGroupName(group.Name); err != nil { |
| // http.Error(w, err.Error(), http.StatusBadRequest) |
| redirectURL := fmt.Sprintf("/user/%s?errorMessage=%s", loggedInUser, url.QueryEscape(err.Error())) |
| http.Redirect(w, r, redirectURL, http.StatusFound) |
| return |
| } |
| group.Description = r.PostFormValue("description") |
| if err := s.store.CreateGroup(loggedInUser, group); err != nil { |
| // http.Error(w, err.Error(), http.StatusInternalServerError) |
| redirectURL := fmt.Sprintf("/user/%s?errorMessage=%s", loggedInUser, url.QueryEscape(err.Error())) |
| http.Redirect(w, r, redirectURL, http.StatusFound) |
| return |
| } |
| http.Redirect(w, r, "/", http.StatusSeeOther) |
| } |
| |
| type GroupPageData struct { |
| GroupName string |
| Description string |
| Owners []string |
| Members []string |
| AllGroups []Group |
| TransitiveGroups []Group |
| ChildGroups []Group |
| OwnerGroups []Group |
| ErrorMessage string |
| } |
| |
| func (s *Server) groupHandler(w http.ResponseWriter, r *http.Request) { |
| _, err := getLoggedInUser(r) |
| if err != nil { |
| http.Error(w, "User Not Logged In", http.StatusUnauthorized) |
| return |
| } |
| errorMsg := r.URL.Query().Get("errorMessage") |
| vars := mux.Vars(r) |
| groupName := vars["group-name"] |
| exists, err := s.store.DoesGroupExist(groupName) |
| if err != nil { |
| http.Error(w, err.Error(), http.StatusInternalServerError) |
| return |
| } |
| if !exists { |
| errorMsg = fmt.Sprintf("group with the name '%s' not found", groupName) |
| http.Error(w, errorMsg, http.StatusNotFound) |
| return |
| } |
| if err != nil { |
| http.Error(w, err.Error(), http.StatusInternalServerError) |
| return |
| } |
| owners, err := s.store.GetGroupOwners(groupName) |
| if err != nil { |
| http.Error(w, err.Error(), http.StatusInternalServerError) |
| return |
| } |
| members, err := s.store.GetGroupMembers(groupName) |
| if err != nil { |
| http.Error(w, err.Error(), http.StatusInternalServerError) |
| return |
| } |
| description, err := s.store.GetGroupDescription(groupName) |
| if err != nil { |
| http.Error(w, err.Error(), http.StatusInternalServerError) |
| return |
| } |
| allGroups, err := s.store.GetAllGroups() |
| if err != nil { |
| http.Error(w, err.Error(), http.StatusInternalServerError) |
| return |
| } |
| transitiveGroups, err := s.store.GetAllTransitiveGroupsForGroup(groupName) |
| if err != nil { |
| http.Error(w, err.Error(), http.StatusInternalServerError) |
| return |
| } |
| childGroups, err := s.store.GetDirectChildrenGroups(groupName) |
| if err != nil { |
| http.Error(w, err.Error(), http.StatusInternalServerError) |
| return |
| } |
| ownerGroups, err := s.store.GetGroupOwnerGroups(groupName) |
| if err != nil { |
| http.Error(w, err.Error(), http.StatusInternalServerError) |
| return |
| } |
| data := GroupPageData{ |
| GroupName: groupName, |
| Description: description, |
| Owners: owners, |
| Members: members, |
| AllGroups: allGroups, |
| TransitiveGroups: transitiveGroups, |
| ChildGroups: childGroups, |
| OwnerGroups: ownerGroups, |
| ErrorMessage: errorMsg, |
| } |
| templates, err := parseTemplates(tmpls) |
| if err != nil { |
| http.Error(w, err.Error(), http.StatusInternalServerError) |
| return |
| } |
| if err := templates.group.Execute(w, data); err != nil { |
| http.Error(w, err.Error(), http.StatusInternalServerError) |
| return |
| } |
| } |
| |
| func (s *Server) removeChildGroupHandler(w http.ResponseWriter, r *http.Request) { |
| loggedInUser, err := getLoggedInUser(r) |
| if err != nil { |
| http.Error(w, "User Not Logged In", http.StatusUnauthorized) |
| return |
| } |
| vars := mux.Vars(r) |
| parentGroup := vars["parent-group"] |
| childGroup := vars["child-group"] |
| if err := isValidGroupName(parentGroup); err != nil { |
| http.Error(w, err.Error(), http.StatusBadRequest) |
| return |
| } |
| if err := isValidGroupName(childGroup); err != nil { |
| http.Error(w, err.Error(), http.StatusBadRequest) |
| return |
| } |
| if err := s.checkIsOwner(w, loggedInUser, parentGroup); err != nil { |
| redirectURL := fmt.Sprintf("/group/%s?errorMessage=%s", parentGroup, url.QueryEscape(err.Error())) |
| http.Redirect(w, r, redirectURL, http.StatusSeeOther) |
| return |
| } |
| err = s.store.RemoveFromGroupToGroup(parentGroup, childGroup) |
| if err != nil { |
| redirectURL := fmt.Sprintf("/group/%s?errorMessage=%s", parentGroup, url.QueryEscape(err.Error())) |
| http.Redirect(w, r, redirectURL, http.StatusFound) |
| return |
| } |
| http.Redirect(w, r, "/group/"+parentGroup, http.StatusSeeOther) |
| } |
| |
| func (s *Server) removeOwnerFromGroupHandler(w http.ResponseWriter, r *http.Request) { |
| loggedInUser, err := getLoggedInUser(r) |
| if err != nil { |
| http.Error(w, "User Not Logged In", http.StatusUnauthorized) |
| return |
| } |
| vars := mux.Vars(r) |
| username := vars["username"] |
| groupName := vars["group-name"] |
| tableName := "owners" |
| if err := isValidGroupName(groupName); err != nil { |
| http.Error(w, err.Error(), http.StatusBadRequest) |
| return |
| } |
| if err := s.checkIsOwner(w, loggedInUser, groupName); err != nil { |
| redirectURL := fmt.Sprintf("/group/%s?errorMessage=%s", groupName, url.QueryEscape(err.Error())) |
| http.Redirect(w, r, redirectURL, http.StatusSeeOther) |
| return |
| } |
| err = s.store.RemoveUserFromTable(username, groupName, tableName) |
| if err != nil { |
| redirectURL := fmt.Sprintf("/group/%s?errorMessage=%s", groupName, url.QueryEscape(err.Error())) |
| http.Redirect(w, r, redirectURL, http.StatusFound) |
| return |
| } |
| http.Redirect(w, r, "/group/"+groupName, http.StatusSeeOther) |
| } |
| |
| func (s *Server) removeMemberFromGroupHandler(w http.ResponseWriter, r *http.Request) { |
| loggedInUser, err := getLoggedInUser(r) |
| if err != nil { |
| http.Error(w, "User Not Logged In", http.StatusUnauthorized) |
| return |
| } |
| vars := mux.Vars(r) |
| username := vars["username"] |
| groupName := vars["group-name"] |
| tableName := "user_to_group" |
| if err := isValidGroupName(groupName); err != nil { |
| http.Error(w, err.Error(), http.StatusBadRequest) |
| return |
| } |
| if err := s.checkIsOwner(w, loggedInUser, groupName); err != nil { |
| redirectURL := fmt.Sprintf("/group/%s?errorMessage=%s", groupName, url.QueryEscape(err.Error())) |
| http.Redirect(w, r, redirectURL, http.StatusSeeOther) |
| return |
| } |
| err = s.store.RemoveUserFromTable(username, groupName, tableName) |
| if err != nil { |
| redirectURL := fmt.Sprintf("/group/%s?errorMessage=%s", groupName, url.QueryEscape(err.Error())) |
| http.Redirect(w, r, redirectURL, http.StatusFound) |
| return |
| } |
| http.Redirect(w, r, "/group/"+groupName, http.StatusSeeOther) |
| } |
| |
| func (s *Server) addUserToGroupHandler(w http.ResponseWriter, r *http.Request) { |
| loggedInUser, err := getLoggedInUser(r) |
| if err != nil { |
| http.Error(w, "User Not Logged In", http.StatusUnauthorized) |
| return |
| } |
| vars := mux.Vars(r) |
| groupName := vars["group-name"] |
| if err := isValidGroupName(groupName); err != nil { |
| http.Error(w, err.Error(), http.StatusBadRequest) |
| return |
| } |
| username := strings.ToLower(r.FormValue("username")) |
| if username == "" { |
| http.Error(w, "Username parameter is required", http.StatusBadRequest) |
| return |
| } |
| status, err := convertStatus(r.FormValue("status")) |
| if err != nil { |
| http.Error(w, err.Error(), http.StatusBadRequest) |
| return |
| } |
| if err := s.checkIsOwner(w, loggedInUser, groupName); err != nil { |
| redirectURL := fmt.Sprintf("/group/%s?errorMessage=%s", groupName, url.QueryEscape(err.Error())) |
| http.Redirect(w, r, redirectURL, http.StatusSeeOther) |
| return |
| } |
| switch status { |
| case Owner: |
| err = s.store.AddGroupOwner(username, groupName) |
| case Member: |
| err = s.store.AddGroupMember(username, groupName) |
| default: |
| http.Error(w, "Invalid status", http.StatusBadRequest) |
| return |
| } |
| if err != nil { |
| redirectURL := fmt.Sprintf("/group/%s?errorMessage=%s", groupName, url.QueryEscape(err.Error())) |
| http.Redirect(w, r, redirectURL, http.StatusFound) |
| return |
| } |
| http.Redirect(w, r, "/group/"+groupName, http.StatusSeeOther) |
| } |
| |
| func (s *Server) addChildGroupHandler(w http.ResponseWriter, r *http.Request) { |
| // TODO(dtabidze): In future we might need to make one group OWNER of another and not just a member. |
| loggedInUser, err := getLoggedInUser(r) |
| if err != nil { |
| http.Error(w, "User Not Logged In", http.StatusUnauthorized) |
| return |
| } |
| vars := mux.Vars(r) |
| parentGroup := vars["parent-group"] |
| if err := isValidGroupName(parentGroup); err != nil { |
| http.Error(w, err.Error(), http.StatusBadRequest) |
| return |
| } |
| childGroup := r.FormValue("child-group") |
| if err := isValidGroupName(childGroup); err != nil { |
| http.Error(w, err.Error(), http.StatusBadRequest) |
| return |
| } |
| if err := s.checkIsOwner(w, loggedInUser, parentGroup); err != nil { |
| redirectURL := fmt.Sprintf("/group/%s?errorMessage=%s", parentGroup, url.QueryEscape(err.Error())) |
| http.Redirect(w, r, redirectURL, http.StatusSeeOther) |
| return |
| } |
| if err := s.store.AddChildGroup(parentGroup, childGroup); err != nil { |
| redirectURL := fmt.Sprintf("/group/%s?errorMessage=%s", parentGroup, url.QueryEscape(err.Error())) |
| http.Redirect(w, r, redirectURL, http.StatusFound) |
| return |
| } |
| http.Redirect(w, r, "/group/"+parentGroup, http.StatusSeeOther) |
| } |
| |
| func (s *Server) addOwnerGroupHandler(w http.ResponseWriter, r *http.Request) { |
| loggedInUser, err := getLoggedInUser(r) |
| if err != nil { |
| http.Error(w, "User Not Logged In", http.StatusUnauthorized) |
| return |
| } |
| vars := mux.Vars(r) |
| ownedGroup := vars["owned-group"] |
| if err := isValidGroupName(ownedGroup); err != nil { |
| http.Error(w, err.Error(), http.StatusBadRequest) |
| return |
| } |
| ownerGroup := r.FormValue("owner-group") |
| if err := isValidGroupName(ownerGroup); err != nil { |
| http.Error(w, err.Error(), http.StatusBadRequest) |
| return |
| } |
| if err := s.checkIsOwner(w, loggedInUser, ownedGroup); err != nil { |
| redirectURL := fmt.Sprintf("/group/%s?errorMessage=%s", ownedGroup, url.QueryEscape(err.Error())) |
| http.Redirect(w, r, redirectURL, http.StatusSeeOther) |
| return |
| } |
| if err := s.store.AddOwnerGroup(ownerGroup, ownedGroup); err != nil { |
| redirectURL := fmt.Sprintf("/group/%s?errorMessage=%s", ownedGroup, url.QueryEscape(err.Error())) |
| http.Redirect(w, r, redirectURL, http.StatusFound) |
| return |
| } |
| http.Redirect(w, r, "/group/"+ownedGroup, http.StatusSeeOther) |
| } |
| |
| func (s *Server) addSSHKeyForUserHandler(w http.ResponseWriter, r *http.Request) { |
| defer s.pingAllSyncAddresses() |
| loggedInUser, err := getLoggedInUser(r) |
| if err != nil { |
| http.Error(w, "User Not Logged In", http.StatusUnauthorized) |
| return |
| } |
| vars := mux.Vars(r) |
| username := vars["username"] |
| if loggedInUser != username { |
| http.Error(w, "You are not allowed to add SSH key for someone else", http.StatusUnauthorized) |
| return |
| } |
| sshKey := r.FormValue("ssh-key") |
| if sshKey == "" { |
| http.Error(w, "SSH key not present", http.StatusBadRequest) |
| return |
| } |
| if err := s.store.AddSSHKeyForUser(strings.ToLower(username), sshKey); err != nil { |
| redirectURL := fmt.Sprintf("/user/%s?errorMessage=%s", loggedInUser, url.QueryEscape(err.Error())) |
| http.Redirect(w, r, redirectURL, http.StatusFound) |
| return |
| } |
| http.Redirect(w, r, "/user/"+loggedInUser, http.StatusSeeOther) |
| } |
| |
| func (s *Server) removeSSHKeyForUserHandler(w http.ResponseWriter, r *http.Request) { |
| defer s.pingAllSyncAddresses() |
| loggedInUser, err := getLoggedInUser(r) |
| if err != nil { |
| http.Error(w, "User Not Logged In", http.StatusUnauthorized) |
| return |
| } |
| vars := mux.Vars(r) |
| username := vars["username"] |
| if loggedInUser != username { |
| http.Error(w, "You are not allowed to remove SSH key for someone else", http.StatusUnauthorized) |
| return |
| } |
| if err := r.ParseForm(); err != nil { |
| http.Error(w, "Invalid request body", http.StatusBadRequest) |
| return |
| } |
| sshKey := r.FormValue("ssh-key") |
| if sshKey == "" { |
| http.Error(w, "SSH key not present", http.StatusBadRequest) |
| return |
| } |
| if err := s.store.RemoveSSHKeyForUser(username, sshKey); err != nil { |
| redirectURL := fmt.Sprintf("/user/%s?errorMessage=%s", loggedInUser, url.QueryEscape(err.Error())) |
| http.Redirect(w, r, redirectURL, http.StatusFound) |
| return |
| } |
| http.Redirect(w, r, "/user/"+loggedInUser, http.StatusSeeOther) |
| } |
| |
| type initRequest struct { |
| User string `json:"user"` |
| Email string `json:"email"` |
| Groups []string `json:"groups"` |
| } |
| |
| func (s *Server) apiInitHandler(w http.ResponseWriter, r *http.Request) { |
| var req initRequest |
| if err := json.NewDecoder(r.Body).Decode(&req); err != nil { |
| http.Error(w, err.Error(), http.StatusBadRequest) |
| return |
| } |
| if err := s.store.Init(req.User, req.Email, req.Groups); err != nil { |
| http.Error(w, err.Error(), http.StatusInternalServerError) |
| return |
| } |
| } |
| |
| type userInfo struct { |
| MemberOf []string `json:"memberOf"` |
| } |
| |
| func (s *Server) apiMemberOfHandler(w http.ResponseWriter, r *http.Request) { |
| vars := mux.Vars(r) |
| user, ok := vars["username"] |
| if !ok || user == "" { |
| http.Error(w, "Username parameter is required", http.StatusBadRequest) |
| return |
| } |
| user = strings.ToLower(user) |
| transitiveGroups, err := s.store.GetAllTransitiveGroupsForUser(user) |
| if err != nil { |
| http.Error(w, err.Error(), http.StatusInternalServerError) |
| return |
| } |
| var groupNames []string |
| for _, group := range transitiveGroups { |
| groupNames = append(groupNames, group.Name) |
| } |
| w.Header().Set("Content-Type", "application/json") |
| if err := json.NewEncoder(w).Encode(userInfo{groupNames}); err != nil { |
| http.Error(w, err.Error(), http.StatusInternalServerError) |
| return |
| } |
| } |
| |
| func (s *Server) apiGetAllUsers(w http.ResponseWriter, r *http.Request) { |
| s.addSyncAddress(r.FormValue("selfAddress")) |
| var users []User |
| var err error |
| groups := r.FormValue("groups") |
| if groups == "" { |
| users, err = s.store.GetUsers(nil) |
| } else { |
| uniqueUsers := make(map[string]struct{}) |
| g := strings.Split(groups, ",") |
| uniqueTG := make(map[string]struct{}) |
| for _, group := range g { |
| uniqueTG[group] = struct{}{} |
| trGroups, err := s.store.GetAllTransitiveGroupsForGroup(group) |
| if err != nil { |
| http.Error(w, err.Error(), http.StatusInternalServerError) |
| return |
| } |
| for _, tg := range trGroups { |
| uniqueTG[tg.Name] = struct{}{} |
| } |
| } |
| for group := range uniqueTG { |
| u, err := s.store.GetGroupMembers(group) |
| if err != nil { |
| http.Error(w, err.Error(), http.StatusInternalServerError) |
| return |
| } |
| for _, user := range u { |
| uniqueUsers[user] = struct{}{} |
| } |
| } |
| usernames := make([]string, 0, len(uniqueUsers)) |
| for username := range uniqueUsers { |
| usernames = append(usernames, username) |
| } |
| users, err = s.store.GetUsers(usernames) |
| } |
| if err != nil { |
| http.Error(w, "Failed to retrieve user infos", http.StatusInternalServerError) |
| return |
| } |
| w.Header().Set("Content-Type", "application/json") |
| if err := json.NewEncoder(w).Encode(users); err != nil { |
| http.Error(w, err.Error(), http.StatusInternalServerError) |
| return |
| } |
| } |
| |
| type createUserRequest struct { |
| User string `json:"user"` |
| Email string `json:"email"` |
| } |
| |
| func (s *Server) apiCreateUser(w http.ResponseWriter, r *http.Request) { |
| defer s.pingAllSyncAddresses() |
| var req createUserRequest |
| if err := json.NewDecoder(r.Body).Decode(&req); err != nil { |
| http.Error(w, "Invalid request body", http.StatusBadRequest) |
| return |
| } |
| if req.User == "" { |
| http.Error(w, "Username cannot be empty", http.StatusBadRequest) |
| return |
| } |
| if req.Email == "" { |
| http.Error(w, "Email cannot be empty", http.StatusBadRequest) |
| return |
| } |
| if err := s.store.CreateUser(strings.ToLower(req.User), strings.ToLower(req.Email)); err != nil { |
| http.Error(w, err.Error(), http.StatusInternalServerError) |
| return |
| } |
| } |
| |
| type addUserKeyRequest struct { |
| User string `json:"user"` |
| PublicKey string `json:"publicKey"` |
| } |
| |
| func (s *Server) apiAddUserKey(w http.ResponseWriter, r *http.Request) { |
| var req addUserKeyRequest |
| if err := json.NewDecoder(r.Body).Decode(&req); err != nil { |
| http.Error(w, "Invalid request body", http.StatusBadRequest) |
| return |
| } |
| if req.User == "" { |
| http.Error(w, "Username cannot be empty", http.StatusBadRequest) |
| return |
| } |
| if req.PublicKey == "" { |
| http.Error(w, "PublicKey cannot be empty", http.StatusBadRequest) |
| return |
| } |
| if err := s.store.AddSSHKeyForUser(strings.ToLower(req.User), req.PublicKey); err != nil { |
| http.Error(w, err.Error(), http.StatusInternalServerError) |
| return |
| } |
| } |
| |
| // TODO(gio): enque sync event instead of directly reaching out to clients. |
| // This will allow to deduplicate sync events and save resources. |
| func (s *Server) pingAllSyncAddresses() { |
| s.mu.Lock() |
| defer s.mu.Unlock() |
| for address := range s.syncAddresses { |
| go func(address string) { |
| log.Printf("Pinging %s", address) |
| resp, err := http.Get(address) |
| if err != nil { |
| // TODO(gio): remove sync address after N number of failures. |
| log.Printf("Failed to ping %s: %v", address, err) |
| return |
| } |
| defer resp.Body.Close() |
| if resp.StatusCode != http.StatusOK { |
| log.Printf("Ping to %s returned status %d", address, resp.StatusCode) |
| } |
| }(address) |
| } |
| } |
| |
| func (s *Server) addSyncAddress(address string) { |
| if address == "" { |
| return |
| } |
| fmt.Printf("Adding sync address: %s\n", address) |
| s.mu.Lock() |
| defer s.mu.Unlock() |
| s.syncAddresses[address] = struct{}{} |
| } |
| |
| func convertStatus(status string) (Status, error) { |
| switch status { |
| case "Owner": |
| return Owner, nil |
| case "Member": |
| return Member, nil |
| default: |
| return Owner, fmt.Errorf("invalid status: %s", status) |
| } |
| } |
| |
| func isValidGroupName(group string) error { |
| if strings.TrimSpace(group) == "" { |
| return fmt.Errorf("Group name can't be empty or contain only whitespaces") |
| } |
| validGroupName := regexp.MustCompile(`^[a-z0-9\-_:.\/ ]+$`) |
| if !validGroupName.MatchString(group) { |
| return fmt.Errorf("Group name should contain only lowercase letters, digits, -, _, :, ., /") |
| } |
| return nil |
| } |
| |
| func main() { |
| flag.Parse() |
| db, err := sql.Open("sqlite3", *dbPath) |
| if err != nil { |
| panic(err) |
| } |
| store, err := NewSQLiteStore(db) |
| if err != nil { |
| panic(err) |
| } |
| s := Server{ |
| store: store, |
| syncAddresses: make(map[string]struct{}), |
| mu: sync.Mutex{}, |
| } |
| log.Fatal(s.Start()) |
| } |