diff --git a/.dockerignore b/.dockerignore index bc42722..1a37e48 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,2 +1,3 @@ bin go +test diff --git a/Makefile b/Makefile index c85d501..a409f1c 100644 --- a/Makefile +++ b/Makefile @@ -1,12 +1,7 @@ NAMESPACE ?= hashbangctl -## Primary Targets - .PHONY: build -build: fetch docker-build - -.PHONY: build-native -build-native: +build: GOBIN=$(PWD)/bin \ GOPATH=$(PWD)/go \ CGO_ENABLED=0 \ @@ -15,12 +10,9 @@ build-native: go install ./... .PHONY: serve -serve: docker-start docker-logs docker-stop - -.PHONY: serve-native -serve-native: +serve: build API_URL="https://userdb.hashbang.sh/v1" \ - API_TOKEN="eyJhbGciOiJIUzI1NiJ9.eyJyb2xlIjoiYXBpLXVzZXItY3JlYXRlIn0.iOcRzRAjPsT9DOhu5OSeRuQ38D3KL5NppsfyuZYiDeI" \ + API_TOKEN="eyJhbGciOiJIUzI1NiJ9.eyJyb2xlIjoiYXBpLXVzZXItbWFuYWdlIn0.gEKj0rWfh5_cnhfAk3U-oGL1_WN6ZrbywfIwxXx_wDA" \ HOST_KEY_SEED="This is an insecure seed" \ bin/server @@ -34,90 +26,80 @@ connect: -p2222 localhost .PHONY: test -test: \ - docker-build-test \ - docker-restart \ - docker-test \ - docker-stop +test: docker-build docker-build-test docker-stop docker-start + docker run \ + -it \ + --rm \ + --hostname=$(NAMESPACE)-test \ + --name $(NAMESPACE)-test \ + --network=userdb \ + --env UID=$(shell id -u) \ + --env GID=$(shell id -g) \ + --env CONTAINER=$(NAMESPACE) \ + --env PGPASSWORD=test_password \ + --env PGHOST=userdb-postgres \ + --env PGDATABASE=postgres \ + --env PGUSER=postgres \ + --volume $(PWD)/test:/home/test \ + local/$(NAMESPACE)-test \ + scripts/docker-shell bats test.bats .PHONY: test-shell -test-shell: \ - docker-build-test \ - docker-restart \ - docker-test-shell \ - docker-stop +test-shell: docker-build docker-build-test docker-stop docker-start + docker run \ + -it \ + --rm \ + --hostname=$(NAMESPACE)-test \ + --name $(NAMESPACE)-test \ + --network=userdb \ + --env UID=$(shell id -u) \ + --env GID=$(shell id -g) \ + --env CONTAINER=$(NAMESPACE) \ + --env PGPASSWORD=test_password \ + --env PGHOST=userdb-postgres \ + --env PGDATABASE=postgres \ + --env PGUSER=postgres \ + --volume $(PWD)/test:/home/test \ + local/$(NAMESPACE)-test \ + scripts/docker-shell bash .PHONY: clean clean: docker-clean rm -rf ./go ./bin -.PHONY: fetch -fetch: - git submodule update --init --recursive +.PHONY: initdb +initdb: + docker exec --user postgres -it "userdb-postgres" \ + psql -c "delete from passwd; delete from hosts;"; + docker exec --user postgres -it "userdb-postgres" \ + psql -c "insert into hosts (name,maxusers) values ('local1.hashbang.sh','500');"; + docker exec --user postgres -it "userdb-postgres" \ + psql -c "insert into hosts (name,maxusers) values ('local2.hashbang.sh','500');"; -.PHONY: fetch-latest -fetch-latest: - git submodule foreach 'git checkout master && git pull' -## Secondary Targets +.PHONY: docker-logs +docker-logs: + scripts/docker-logs $(NAMESPACE) userdb-postgres userdb-postgrest .PHONY: docker-build docker-build: docker build -t local/$(NAMESPACE) . - docker build -t local/$(NAMESPACE)-userdb test/modules/userdb/ - docker build \ - --build-arg=POSTGREST_VERSION=v6.0.2 \ - -t local/$(NAMESPACE)-postgrest \ - test/postgrest/ -.PHONY: docker-restart -docker-restart: docker-stop docker-start +.PHONY: docker-build-test +docker-build-test: + docker build -t local/$(NAMESPACE)-test test/ .PHONY: docker-start -docker-start: - docker network inspect $(NAMESPACE) \ - || docker network create $(NAMESPACE) +docker-start: docker-build + $(MAKE) -C test/modules/userdb docker-start docker inspect -f '{{.State.Running}}' $(NAMESPACE) 2>/dev/null \ || docker run \ --detach=true \ --name $(NAMESPACE) \ - --network=$(NAMESPACE) \ - --env API_URL="http://hashbangctl-postgrest:3000" \ - --env API_TOKEN="eyJhbGciOiJIUzI1NiJ9.eyJyb2xlIjoiYXBpLXVzZXItY3JlYXRlIn0.iOcRzRAjPsT9DOhu5OSeRuQ38D3KL5NppsfyuZYiDeI" \ - --env HOST_KEY_SEED="Replace me with something actually random" \ - --expose="2222" \ - -p "2222:2222" \ - local/$(NAMESPACE) - docker inspect -f '{{.State.Running}}' $(NAMESPACE)-userdb 2>/dev/null \ - || docker run \ - --rm \ - --detach=true \ - --name $(NAMESPACE)-userdb \ - --network=$(NAMESPACE) \ - --volume $(PWD)/test/test_data.sql:/docker-entrypoint-initdb.d/99-init.sql \ - local/$(NAMESPACE)-userdb - docker inspect -f '{{.State.Running}}' $(NAMESPACE)-postgrest 2>/dev/null \ - || docker run \ - --rm \ - --detach=true \ - --name $(NAMESPACE)-postgrest \ - --network=$(NAMESPACE) \ - --env PGRST_DB_URI="postgres://postgres@$(NAMESPACE)-userdb/userdb" \ - --env PGRST_JWT_SECRET="a_test_only_postgrest_jwt_secret" \ - --env PGRST_DB_ANON_ROLE="api-anon" \ - --env PGRST_DB_SCHEMA="v1" \ - local/$(NAMESPACE)-postgrest - -.PHONY: docker-start-prod -docker-start-prod: - docker network inspect $(NAMESPACE) \ - || docker network create $(NAMESPACE) - docker inspect -f '{{.State.Running}}' $(NAMESPACE) 2>/dev/null \ - || docker run \ - --detach=true \ - --name $(NAMESPACE) \ - --network=$(NAMESPACE) \ - --env API_URL="https://userdb.hashbang.sh/v1" \ + --network=userdb \ + --env API_URL="http://userdb-postgrest:3000" \ + --env API_TOKEN="eyJhbGciOiJIUzI1NiJ9.eyJyb2xlIjoiYXBpLXVzZXItbWFuYWdlIn0.gEKj0rWfh5_cnhfAk3U-oGL1_WN6ZrbywfIwxXx_wDA" \ + --env HOST_KEY_SEED="This is an insecure seed" \ --expose="2222" \ -p "2222:2222" \ local/$(NAMESPACE) @@ -126,54 +108,4 @@ docker-start-prod: docker-stop: docker inspect -f '{{.State.Running}}' $(NAMESPACE) 2>/dev/null \ && docker rm -f $(NAMESPACE) || true - docker inspect -f '{{.State.Running}}' $(NAMESPACE)-userdb 2>/dev/null \ - && docker rm -f $(NAMESPACE)-userdb || true - docker inspect -f '{{.State.Running}}' $(NAMESPACE)-postgrest 2>/dev/null \ - && docker rm -f $(NAMESPACE)-postgrest || true - -.PHONY: docker-logs -docker-logs: - scripts/docker-logs $(NAMESPACE) $(NAMESPACE)-userdb $(NAMESPACE)-postgrest - -.PHONY: docker-clean -docker-clean: docker-stop - docker image rm local/$(NAMESPACE) - docker image rm local/$(NAMESPACE)-test - docker image rm local/$(NAMESPACE)-postgrest - docker image rm local/$(NAMESPACE)-userdb - -.PHONY: docker-test -docker-test: - docker run \ - -it \ - --rm \ - --hostname=$(NAMESPACE)-test \ - --name $(NAMESPACE)-test \ - --network=$(NAMESPACE) \ - --env CONTAINER=$(NAMESPACE) \ - --env PGPASSWORD=test_password \ - --env PGHOST=$(NAMESPACE)-userdb \ - --env PGDATABASE=userdb \ - --env PGUSER=postgres \ - local/$(NAMESPACE)-test - -.PHONY: docker-test-shell -docker-test-shell: \ - docker-build docker-stop docker-start docker-build-test docker-stop - docker run \ - --rm \ - -it \ - --hostname=$(NAMESPACE)-test \ - --name $(NAMESPACE)-test \ - --network=$(NAMESPACE) \ - --env CONTAINER=$(NAMESPACE) \ - --env PGPASSWORD=test_password \ - --env PGHOST=$(NAMESPACE)-userdb \ - --env PGDATABASE=userdb \ - --env PGUSER=postgres \ - local/$(NAMESPACE)-test \ - bash - -.PHONY: docker-build-test -docker-build-test: - docker build -t local/$(NAMESPACE)-test test/ + $(MAKE) -C test/modules/userdb docker-stop diff --git a/cmd/client/api.go b/cmd/client/api.go new file mode 100644 index 0000000..2091f48 --- /dev/null +++ b/cmd/client/api.go @@ -0,0 +1,207 @@ +package main + +import ( + "bytes" + "encoding/json" + "encoding/base64" + "crypto/sha256" + "errors" + "fmt" + "io/ioutil" + "log" + "math/rand" + "net/http" + "os" + "time" + "strings" +) + +type RequestBody struct { + Name string `json:"name"` + Host string `json:"host"` + Shell string `json:"shell"` + Keys []string `json:"keys"` +} + +type ResponseBody struct { + Hint string `json:"hint"` + Details string `json:"details"` + Message string `json:"message"` + Code string `json:"code"` + Request RequestBody `json:"request"` +} + +type SshPublicKey struct { + Fingerprint string `json:"fingerprint"` + Base64Fingerprint string `json:"base64_fingerprint"` + Type string `json:"type"` + Key string `json:"key"` + Comment string `json:"comment"` + Uid int `json:"uid"` +} + +type User struct { + Uid int `json:"uid"` + Name string `json:"name"` + Host string `json:"host"` + Type string `json:"type"` + Key string `json:"key"` + Comment string `json:"comment"` + Shell string `json:"shell"` +} + +type Host struct { + Name string `json:"name"` + MaxUsers int `json:"maxusers"` +} + +func getHosts() ([]string, error) { + + var hostResponse []Host + hosts := []string{} + + apiUrl := fmt.Sprintf("%s/hosts", os.Getenv("API_URL")) + apiToken := os.Getenv("API_TOKEN") + req, _ := http.NewRequest("GET", apiUrl, nil) + req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", apiToken)) + req.Header.Add("Content-Type", "application/json") + client := &http.Client{} + res, err := client.Do(req) + if err != nil { + return hosts, err + } + body, err := ioutil.ReadAll(res.Body) + defer res.Body.Close() + if err != nil { + return hosts, err + } + err = json.Unmarshal(body, &hostResponse) + if err != nil { + return hosts, err + } + + r := rand.New(rand.NewSource(time.Now().Unix())) + for _, i := range r.Perm(len(hostResponse)) { + hosts = append(hosts, hostResponse[i].Name) + } + return hosts, nil +} + +func getKeys( + key string, +) ([]SshPublicKey, error) { + var sshPublicKeys []SshPublicKey + keyStripped := strings.Fields(key)[1] + keyDecoded, err := base64.StdEncoding.DecodeString(keyStripped) + if err != nil { + return sshPublicKeys, err + } + fingerprint := sha256.Sum256(keyDecoded) + apiUrl := fmt.Sprintf( + "%s/ssh_public_key?fingerprint=ilike.%x", + os.Getenv("API_URL"), + fingerprint, + ) + apiToken := os.Getenv("API_TOKEN") + req, _ := http.NewRequest("GET", apiUrl, nil) + req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", apiToken)) + req.Header.Add("Content-Type", "application/json") + client := &http.Client{} + res, err := client.Do(req) + if err != nil { + return sshPublicKeys, err + } + body, err := ioutil.ReadAll(res.Body) + defer res.Body.Close() + if err != nil { + return sshPublicKeys, err + } + err = json.Unmarshal([]byte(body), &sshPublicKeys) + if err != nil { + return sshPublicKeys, err + } + return sshPublicKeys, nil +} + +func getUsersById( + uid int, +) ([]User, error) { + var users []User + apiUrl := fmt.Sprintf( + "%s/passwd?uid=eq.%d", + os.Getenv("API_URL"), + uid, + ) + apiToken := os.Getenv("API_TOKEN") + req, _ := http.NewRequest("GET", apiUrl, nil) + req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", apiToken)) + req.Header.Add("Content-Type", "application/json") + client := &http.Client{} + res, err := client.Do(req) + if err != nil { + return users, err + } + body, err := ioutil.ReadAll(res.Body) + defer res.Body.Close() + if err != nil { + return users, err + } + err = json.Unmarshal([]byte(body), &users) + if err != nil { + return users, err + } + return users, nil +} + +func createUser( + logger *log.Logger, + host string, + name string, + key string, +) error { + apiUrl := fmt.Sprintf("%s/signup", os.Getenv("API_URL")) + apiToken := os.Getenv("API_TOKEN") + requestBody := RequestBody{ + Name: name, + Host: host, + Shell: "/bin/bash", + Keys: []string{key}, + } + jsonData, err := json.Marshal(requestBody) + logger.Println("[client] ??", string(jsonData)) + req, _ := http.NewRequest("POST", apiUrl, bytes.NewBuffer(jsonData)) + req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", apiToken)) + req.Header.Add("Content-Type", "application/json") + client := &http.Client{} + res, err := client.Do(req) + if err != nil { + return err + } + if res.StatusCode == 201 { + logger.Println("[client] ++", string(jsonData)) + return nil + } + body, err := ioutil.ReadAll(res.Body) + defer res.Body.Close() + if err != nil { + return err + } + var responseBody ResponseBody + err = json.Unmarshal(body, &responseBody) + if err != nil { + return err + } + responseBody.Request = requestBody + jsonError, err := json.Marshal(responseBody) + logger.Println("[client] !!", string(jsonError)) + return errors.New(responseBody.Message) +} + +func editUser( + logger *log.Logger, + user User, + sshPublicKeys []SshPublicKey, +) error { + logger.Println("User: %s",user) + return errors.New("placeholder") +} diff --git a/cmd/client/forms.go b/cmd/client/forms.go new file mode 100644 index 0000000..f40d6f2 --- /dev/null +++ b/cmd/client/forms.go @@ -0,0 +1,181 @@ +package main + +import ( + "fmt" + "github.com/gdamore/tcell" + "github.com/rivo/tview" + "log" + "os" +) + +var logoText = ` + █████ █████ █████ + █████ █████ █████ + █████ █████ █████ +███████████████████████ █████ +███████████████████████ █████ + █████ █████ █████ + █████ █████ █████ +███████████████████████ █████ +███████████████████████ █████ + █████ █████       + █████ █████ █████ + █████ █████ █████ + +` + +func createForm( + logger *log.Logger, + hosts []string, +) { + app := tview.NewApplication() + logo := tview.NewTextView() + logo.SetTextAlign(1) + logo.SetText(logoText) + frame := tview.NewFrame(func() tview.Primitive { + form := tview.NewForm() + form.SetLabelColor(tcell.ColorWhite) + form.SetItemPadding(2) + form.SetFieldTextColor(tcell.ColorGray) + form.SetButtonTextColor(tcell.ColorGray) + form.SetFieldBackgroundColor(tcell.ColorWhite) + form.SetButtonBackgroundColor(tcell.ColorWhite) + form.SetBorder(false) + form.SetButtonsAlign(1) + form.AddDropDown("Server", hosts, 0, nil) + // TODO: check username is available. Append numbers if needed + form.AddInputField("User Name", + os.Getenv("USER"), 33, tview.InputFieldMaxLength(30), nil, + ) + form.AddInputField("Public Key", + os.Getenv("KEY"), 33, tview.InputFieldMaxLength(800), nil, + ) + form.AddButton("Create", func() { + server_dropdown := form.GetFormItem(0).(*tview.DropDown) + _, server := server_dropdown.GetCurrentOption() + user := form.GetFormItem(1).(*tview.InputField).GetText() + key := form.GetFormItem(2).(*tview.InputField).GetText() + err := createUser(logger, server, user, key) + if err != nil { + app.Stop() + fmt.Fprintln( + os.Stderr, + "\nError: Account creation failed\n", + fmt.Errorf("\n%v\n", err), + ) + os.Exit(1) + } + app.Stop() + fmt.Fprintln( + os.Stdout, + "\nAccount creation successful!\n", + "\nYou can now connect to your account via:\n", + fmt.Sprintf("\n$ ssh %s@%s\n", user, server), + ) + os.Exit(1) + }) + form.AddButton("Exit", app.Stop) + return form + }()) + frame.SetBorder(false) + flex := tview.NewFlex() + flex.AddItem(tview.NewBox(), 0, 1, false) + flex.AddItem( + tview.NewFlex().SetDirection(tview.FlexRow). + AddItem(tview.NewBox(), 0, 1, false). + AddItem(logo, 14, 1, false). + AddItem(frame, 14, 1, true). + AddItem(tview.NewBox(), 0, 1, false), + 50, 2, true) + flex.AddItem(tview.NewBox(), 0, 1, false) + if err := app.SetRoot(flex, true).Run(); err != nil { + panic(err) + } +} + +func editForm( + logger *log.Logger, + hosts []string, + user User, + sshPublicKeys []SshPublicKey, +) { + app := tview.NewApplication() + logo := tview.NewTextView() + logo.SetTextAlign(1) + logo.SetText(logoText) + title := tview.NewTextView() + title.SetTextAlign(1) + title.SetText(fmt.Sprintf("Editing User: \"%s\"",user.Name)) + frame := tview.NewFrame(func() tview.Primitive { + form := tview.NewForm() + form.SetLabelColor(tcell.ColorWhite) + form.SetItemPadding(2) + form.SetFieldTextColor(tcell.ColorGray) + form.SetButtonTextColor(tcell.ColorGray) + form.SetFieldBackgroundColor(tcell.ColorWhite) + form.SetButtonBackgroundColor(tcell.ColorWhite) + form.SetBorder(false) + form.SetButtonsAlign(1) + form.AddDropDown("Server", hosts, 0, nil) + // TODO: check username is available. Append numbers if needed + form.AddInputField("Shell", + user.Shell, 33, tview.InputFieldMaxLength(800), nil, + ) + keyNum := len(sshPublicKeys) + for i:=0; i < len(sshPublicKeys); i++{ + key := sshPublicKeys[i] + keyString := fmt.Sprintf("%s %s",key.Type, key.Key) + form.AddInputField(fmt.Sprintf("Public Key %d",i+1), + keyString, 33, tview.InputFieldMaxLength(800), nil, + ) + } + form.AddButton("Add Key", func(){ + keyNum = keyNum + 1 + form.AddInputField(fmt.Sprintf("Public Key %d",keyNum), + "", 33, tview.InputFieldMaxLength(800), nil, + ) + }) + form.AddButton("Update", func() { + //server_dropdown := form.GetFormItem(0).(*tview.DropDown) + //_, server := server_dropdown.GetCurrentOption() + //shell := form.GetFormItem(1).(*tview.InputField).GetText() + err := editUser(logger, user, sshPublicKeys) + if err != nil { + app.Stop() + fmt.Fprintln( + os.Stderr, + "\nError: Account update failed\n", + fmt.Errorf("\n%v\n", err), + ) + //TODO: update User and sshPublicKeys structs based on input + fmt.Fprintln(os.Stdout,"\nUser: ",user, sshPublicKeys) + os.Exit(1) + } + app.Stop() + fmt.Fprintln( + os.Stdout, + "\nAccount update successful!\n", + "\nYou can connect to your account via:\n", + fmt.Sprintf("\n$ ssh %s@%s\n", user.Name, user.Host), + ) + os.Exit(1) + }) + form.AddButton("Exit", app.Stop) + return form + }()) + frame.SetBorder(false) + flex := tview.NewFlex() + flex.AddItem(tview.NewBox(), 0, 1, false) + flex.AddItem( + tview.NewFlex().SetDirection(tview.FlexRow). + AddItem(tview.NewBox(), 0, 1, false). + AddItem(logo, 14, 1, false). + AddItem(title, 1, 1, false). + AddItem(frame, 14, 1, true). + AddItem(tview.NewBox(), 0, 1, false), + 50, 2, true) + flex.AddItem(tview.NewBox(), 0, 1, false) + if err := app.SetRoot(flex, true).Run(); err != nil { + panic(err) + } +} diff --git a/cmd/client/main.go b/cmd/client/main.go index 351f18a..7d50fdd 100644 --- a/cmd/client/main.go +++ b/cmd/client/main.go @@ -1,143 +1,18 @@ package main import ( - "bytes" - "encoding/json" - "errors" "fmt" - "github.com/gdamore/tcell" - "github.com/rivo/tview" - "io/ioutil" "log" - "math/rand" - "net/http" "os" - "time" ) -type RequestBody struct { - Name string `json:"name"` - Host string `json:"host"` - Shell string `json:"shell"` - Keys []string `json:"keys"` -} - -type ResponseBody struct { - Hint string `json:"hint"` - Details string `json:"details"` - Message string `json:"message"` - Code string `json:"code"` - Request RequestBody `json:"request"` -} - -type Host struct { - Name string `json:"name"` - MaxUsers int `json:"maxusers"` -} - -func getExisting() { - // TODO: - // hash public key - // lookup pubkey via userdb/postgrest computed column - // kick user out if their key is already in use -} - -func getUsername() { - // TODO: - // curl -i "https://userdb.hashbang.sh/passwd?name=ilike.lrvick*&select=name&limit10" - // Modify input username to be unix compatible - // if result is available, return - // If not, append random 4 digit number then return - //return true -} - -func getHosts() ([]string, error) { - - var hostResponse []Host - hosts := []string{} - - apiUrl := fmt.Sprintf("%s/hosts", os.Getenv("API_URL")) - //TODO: implement API tokens on prod - // apiToken := os.Getenv("API_TOKEN") - req, _ := http.NewRequest("GET", apiUrl, nil) - //TODO: implement API tokens on prod - // req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", apiToken)) - req.Header.Add("Content-Type", "application/json") - client := &http.Client{} - res, err := client.Do(req) - if err != nil { - return hosts, err - } - body, err := ioutil.ReadAll(res.Body) - defer res.Body.Close() - if err != nil { - return hosts, err - } - err = json.Unmarshal(body, &hostResponse) - if err != nil { - return hosts, err - } - - r := rand.New(rand.NewSource(time.Now().Unix())) - for _, i := range r.Perm(len(hostResponse)) { - hosts = append(hosts, hostResponse[i].Name) - } - return hosts, nil -} - -func createAccount( - logger *log.Logger, - host string, - name string, - key string, -) error { - apiUrl := fmt.Sprintf("%s/signup", os.Getenv("API_URL")) - //TODO: implement API tokens on prod - //apiToken := os.Getenv("API_TOKEN") - requestBody := RequestBody{ - Name: name, - Host: host, - Shell: "/bin/bash", - Keys: []string{key}, - } - jsonData, err := json.Marshal(requestBody) - logger.Println("[client] ??", string(jsonData)) - req, _ := http.NewRequest("POST", apiUrl, bytes.NewBuffer(jsonData)) - //TODO: implement API tokens on prod - //req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", apiToken)) - req.Header.Add("Content-Type", "application/json") - client := &http.Client{} - res, err := client.Do(req) - if err != nil { - return err - } - if res.StatusCode == 201 { - logger.Println("[client] ++", string(jsonData)) - return nil - } - body, err := ioutil.ReadAll(res.Body) - defer res.Body.Close() - if err != nil { - return err - } - var responseBody ResponseBody - err = json.Unmarshal(body, &responseBody) - if err != nil { - return err - } - responseBody.Request = requestBody - jsonError, err := json.Marshal(responseBody) - logger.Println("[client] !!", string(jsonError)) - return errors.New(responseBody.Message) -} - func main() { fd := os.NewFile(3, "/proc/self/fd/3") defer fd.Close() logger := log.New(fd, "", log.Ldate|log.Ltime) - - if os.Getenv("KEY") == "none" { + key := os.Getenv("KEY") + if key == "none" { fmt.Fprintln( os.Stderr, "\nError: Public key authentication required\n", @@ -146,94 +21,26 @@ func main() { ) os.Exit(1) } - - hosts, err := getHosts() + keys, err := getKeys(key) if err != nil { - fmt.Fprintln(os.Stderr, "\nError: Unable to get host list") - fmt.Fprintln(os.Stderr, err) - os.Exit(1) - } - - app := tview.NewApplication() - - logo := tview.NewTextView() - logo.SetTextAlign(1) - logo.SetText(` - █████ █████ █████ - █████ █████ █████ - █████ █████ █████ -███████████████████████ █████ -███████████████████████ █████ - █████ █████ █████ - █████ █████ █████ -███████████████████████ █████ -███████████████████████ █████ - █████ █████       - █████ █████ █████ - █████ █████ █████ - -`) - - frame := tview.NewFrame(func() tview.Primitive { - form := tview.NewForm() - form.SetLabelColor(tcell.ColorWhite) - form.SetItemPadding(2) - form.SetFieldTextColor(tcell.ColorGray) - form.SetButtonTextColor(tcell.ColorGray) - form.SetFieldBackgroundColor(tcell.ColorWhite) - form.SetButtonBackgroundColor(tcell.ColorWhite) - form.SetBorder(false) - form.SetButtonsAlign(1) - form.AddDropDown("Server", hosts, 0, nil) - // TODO: check username is available. Append numbers if needed - form.AddInputField("User Name", - os.Getenv("USER"), 33, tview.InputFieldMaxLength(30), nil, - ) - form.AddInputField("Public Key", - os.Getenv("KEY"), 33, tview.InputFieldMaxLength(800), nil, - ) - form.AddButton("Create", func() { - server_dropdown := form.GetFormItem(0).(*tview.DropDown) - _, server := server_dropdown.GetCurrentOption() - user := form.GetFormItem(1).(*tview.InputField).GetText() - key := form.GetFormItem(2).(*tview.InputField).GetText() - err := createAccount(logger, server, user, key) - if err != nil { - app.Stop() - fmt.Fprintln( - os.Stderr, - "\nError: Account creation failed\n", - fmt.Errorf("\n%v\n", err), - ) - os.Exit(1) - } - app.Stop() - fmt.Fprintln( - os.Stdout, - "\nAccount creation successful!\n", - "\nYou can now connect to your account via:\n", - fmt.Sprintf("\n$ ssh %s@%s\n", user, server), - ) - os.Exit(1) - }) - form.AddButton("Exit", app.Stop) - return form - }()) - frame.SetBorder(false) - - flex := tview.NewFlex() - flex.AddItem(tview.NewBox(), 0, 1, false) - flex.AddItem( - tview.NewFlex().SetDirection(tview.FlexRow). - AddItem(tview.NewBox(), 0, 1, false). - AddItem(logo, 14, 1, false). - AddItem(frame, 14, 1, true). - AddItem(tview.NewBox(), 0, 1, false), - 50, 2, true) - flex.AddItem(tview.NewBox(), 0, 1, false) - - if err := app.SetRoot(flex, true).Run(); err != nil { - panic(err) - } - + fmt.Fprintln(os.Stderr, "\nError: Unable to get keys list") + fmt.Fprintln(os.Stderr, err) + } + hosts, err := getHosts() + if err != nil { + fmt.Fprintln(os.Stderr, "\nError: Unable to get host list") + fmt.Fprintln(os.Stderr, err) + os.Exit(1) + } + if len(keys) == 0 { + createForm(logger, hosts) + } else { + users, err := getUsersById(keys[0].Uid) + if err != nil { + fmt.Fprintln(os.Stderr, "\nError: Unable to get user list") + fmt.Fprintln(os.Stderr, err) + os.Exit(1) + } + editForm(logger, hosts, users[0], keys) + } } diff --git a/cmd/server/main.go b/cmd/server/main.go index 5cc41dc..881533c 100644 --- a/cmd/server/main.go +++ b/cmd/server/main.go @@ -18,6 +18,7 @@ import ( "path/filepath" "sync" "time" + "regexp" ) var limiter = NewIPRateLimiter(rate.Every(time.Minute), 10) @@ -87,6 +88,28 @@ func handleConnection(nConn net.Conn, sshConfig *ssh.ServerConfig) { for req := range in { switch req.Type { + case "exec": + re := regexp.MustCompile(`[^a-zA-Z0-9]+`) + command := re.ReplaceAllString(string(req.Payload), "") + log.Println("[server] <-", string(jsonLoginData)) + switch command { + case "debug": + channel.Write([]byte(fmt.Sprintf( + "%s\n\r",jsonLoginData, + ))) + channel.SendRequest( + "exit-status", false, []byte{0, 0, 0, 0}, + ) + channel.Close() + return + default: + channel.Write([]byte(fmt.Sprintf( + "\n\rUnknown command: \"%s\"\n\n\r", + command, + ))) + channel.Close() + return + } case "shell": runDir, _ := filepath.Abs(filepath.Dir(os.Args[0])) pr, pw, _ := os.Pipe() diff --git a/test/Dockerfile b/test/Dockerfile index 5be613a..9984096 100644 --- a/test/Dockerfile +++ b/test/Dockerfile @@ -8,25 +8,16 @@ RUN adduser admin && \ sudo \ postgresql-client \ curl \ + tmux \ + jq \ ssh &&\ rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* -ADD . /home/admin/ - -RUN \ - printf "source /home/admin/test_helper.bash\nsetup" \ - >> /home/admin/.bashrc && \ - chown -R admin:admin /home/admin && \ - echo '%admin ALL=(ALL) NOPASSWD:ALL' >> /etc/sudoers - -USER admin - -WORKDIR /home/admin +WORKDIR /home/test ENV LANG=C.UTF-8 \ TZ=UTC \ TERM=xterm-256color \ - USER="admin" \ - HOME="/home/admin" + HOME="/home/test" CMD ["/usr/bin/bats", "/test/test.bats"] diff --git a/test/keys/id_rsa b/test/keys/id_rsa new file mode 100644 index 0000000..006ce42 --- /dev/null +++ b/test/keys/id_rsa @@ -0,0 +1,38 @@ +-----BEGIN OPENSSH PRIVATE KEY----- +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 +j8gbIXHwCjWpsAAAAKbHJ2aWNrQGRldgE= +-----END OPENSSH PRIVATE KEY----- diff --git a/test/keys/id_rsa.pub b/test/keys/id_rsa.pub new file mode 100644 index 0000000..74db3af --- /dev/null +++ b/test/keys/id_rsa.pub @@ -0,0 +1 @@ +ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQCzPBNpvP8hlNGw2/8bxb6S8pYNR8v0wmv5pBx7qHX4S6Z3jDKI/+yl3/nBrrHTbrULy2Nvr420y8e1QCqRQQRNmZeXvLbyHQfltWo1Mgt3IxhV3NxWLfF6vgJ/903Qw0EpcG2A3SES5o0kNhsWnEFGGW5ZKNsvq0Kab1rX7Il4tcZtju+tLmhFErrCdyq2hVuQOXjeqc3R+7Tpv3OBjp7ye8HjwztWmxz/di9sFj7rG00GkMQwwc6om+UAxg4pdccATvaCh2z2e7hhK6RFyDc/1QRLcp7n3gdmJ0Ssgg1d7rNGR5j7KwonqTiXPacDiYqQHNTnX7yRdjSOVDOCHw4SClBiXwEKap+p+l+zqxNk9bCpIvM1ANKVc4Q/AGWOIxLu49aWCju3+1molLUmCA/vKsj/hjy42sqcOkpNfT82hGXLqG6th5jj0VgTbbN7Vw0hBBqP+G6fxr68I36NDTtF7Rm9PjjDfyMEO+LzOExHFxJYpwoHq76c7iiYisB0mIk= lrvick@dev diff --git a/test/modules/userdb b/test/modules/userdb index bed8d00..329c8bf 160000 --- a/test/modules/userdb +++ b/test/modules/userdb @@ -1 +1 @@ -Subproject commit bed8d007aefaaf9fff7660baa359649a25d5e9bc +Subproject commit 329c8bf3d20d1b7d14a91ffb79701f6b268a06b3 diff --git a/test/postgrest/Dockerfile b/test/postgrest/Dockerfile deleted file mode 100644 index c3b1d51..0000000 --- a/test/postgrest/Dockerfile +++ /dev/null @@ -1,53 +0,0 @@ -FROM debian:stretch-slim - -ARG POSTGREST_VERSION - -# Install libpq5 -RUN apt-get -qq update && \ - apt-get -qq install -y --no-install-recommends libpq5 && \ - apt-get -qq clean && \ - rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* - -# Install postgrest -RUN BUILD_DEPS="curl ca-certificates xz-utils" && \ - apt-get -qq update && \ - apt-get -qq install -y --no-install-recommends $BUILD_DEPS && \ - cd /tmp && \ - curl -SLO https://github.com/PostgREST/postgrest/releases/download/${POSTGREST_VERSION}/postgrest-${POSTGREST_VERSION}-ubuntu.tar.xz && \ - tar -xJvf postgrest-${POSTGREST_VERSION}-ubuntu.tar.xz && \ - mv postgrest /usr/local/bin/postgrest && \ - cd / && \ - apt-get -qq purge --auto-remove -y $BUILD_DEPS && \ - apt-get -qq clean && \ - rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* - -COPY postgrest.conf /etc/postgrest.conf - -ENV PGRST_DB_URI= \ - PGRST_DB_SCHEMA=public \ - PGRST_DB_ANON_ROLE= \ - PGRST_DB_POOL=100 \ - PGRST_DB_EXTRA_SEARCH_PATH=public \ - PGRST_SERVER_HOST=*4 \ - PGRST_SERVER_PORT=3000 \ - PGRST_SERVER_PROXY_URI= \ - PGRST_JWT_SECRET= \ - PGRST_SECRET_IS_BASE64=false \ - PGRST_JWT_AUD= \ - PGRST_MAX_ROWS= \ - PGRST_PRE_REQUEST= \ - PGRST_ROLE_CLAIM_KEY=".role" \ - PGRST_ROOT_SPEC= \ - PGRST_RAW_MEDIA_TYPES= - -RUN groupadd -g 1000 postgrest && \ - useradd -r -u 1000 -g postgrest postgrest && \ - chown postgrest:postgrest /etc/postgrest.conf - -USER 1000 - -# PostgREST reads /etc/postgrest.conf so map the configuration -# file in when you run this container -CMD exec postgrest /etc/postgrest.conf - -EXPOSE 3000 diff --git a/test/postgrest/postgrest.conf b/test/postgrest/postgrest.conf deleted file mode 100644 index faf8af5..0000000 --- a/test/postgrest/postgrest.conf +++ /dev/null @@ -1,19 +0,0 @@ -db-uri = "$(PGRST_DB_URI)" -db-schema = "$(PGRST_DB_SCHEMA)" -db-anon-role = "$(PGRST_DB_ANON_ROLE)" -db-pool = "$(PGRST_DB_POOL)" -db-extra-search-path = "$(PGRST_DB_EXTRA_SEARCH_PATH)" - -server-host = "$(PGRST_SERVER_HOST)" -server-port = "$(PGRST_SERVER_PORT)" - -server-proxy-uri = "$(PGRST_SERVER_PROXY_URI)" -jwt-secret = "$(PGRST_JWT_SECRET)" -secret-is-base64 = "$(PGRST_SECRET_IS_BASE64)" -jwt-aud = "$(PGRST_JWT_AUD)" -role-claim-key = "$(PGRST_ROLE_CLAIM_KEY)" - -max-rows = "$(PGRST_MAX_ROWS)" -pre-request = "$(PGRST_PRE_REQUEST)" -root-spec = "$(PGRST_ROOT_SPEC)" -raw-media-types = "$(PGRST_RAW_MEDIA_TYPES)" diff --git a/scripts/docker-logs b/test/scripts/docker-logs similarity index 87% rename from scripts/docker-logs rename to test/scripts/docker-logs index cacee73..bb48cc9 100755 --- a/scripts/docker-logs +++ b/test/scripts/docker-logs @@ -7,7 +7,7 @@ fi pids=() cleanup(){ - kill "${pids[@]}" + kill "${pids[@]}" >/dev/null 2>&1 } trap cleanup EXIT diff --git a/test/scripts/docker-shell b/test/scripts/docker-shell new file mode 100755 index 0000000..ecf26ad --- /dev/null +++ b/test/scripts/docker-shell @@ -0,0 +1,9 @@ +#!/bin/bash +set -e + +groupadd -g ${GID} test +useradd -u ${UID} -g ${GID} -d /home/test test + +echo '%test ALL=(ALL) NOPASSWD:ALL' >> /etc/sudoers + +setpriv --reuid=test --regid=test --init-groups $@ diff --git a/test/test.bats b/test/test.bats index dbd71e9..702072d 100644 --- a/test/test.bats +++ b/test/test.bats @@ -1,64 +1,30 @@ load test_helper -@test "Can connect to userdb PostgreSQL" { - sleep 5 - run pg_isready -U postgres -h hashbangctl-userdb; - [ "$status" -eq 0 ] - echo "$output" | grep "accepting connections" +@test "Cannot login without an ssh key" { + run ssh_command + [ "$status" -eq 255 ] } -@test "Can connect to userdb PostgREST" { - run curl http://hashbangctl-postgrest:3000 +@test "can login with an ed25519 ssh key" { + run ssh_command "ed25519" "jdoe" "debug" [ "$status" -eq 0 ] - echo "$output" | grep "swagger" + [[ "$output" == *"ssh-ed25519"* ]] } -@test "Cannot create user anonymously via PostgREST" { - run curl http://hashbangctl-postgrest:3000/passwd \ - -H "Content-Type:application/json" \ - -X POST \ - --data-binary @- <<-EOF - { - "name": "testuser", - "host": "test.hashbang.sh", - "data": { - "shell": "/bin/bash", - "ssh_keys": ["$(cat keys/id_ed25519.pub)"] - } - } - EOF +@test "can login with an rsa ssh key" { + run ssh_command "rsa" "jdoe" "debug" [ "$status" -eq 0 ] - echo "$output" | grep "permission denied" + [[ "$output" == *"rsa"* ]] } -@test "Can create user with a valid host and valid auth via PostgREST" { - - run curl http://hashbangctl-postgrest:3000/passwd \ - -H "Content-Type: application/json" \ - -H "Authorization: Bearer $(jwt_token 'api-user-create')" \ - -X POST \ - --data-binary @- <<-EOF - { - "name": "testuser42", - "host": "te1.hashbang.sh", - "data": { - "shell": "/bin/bash", - "ssh_keys": ["$(cat keys/id_ed25519.pub)"] - } - } - EOF - [ "$status" -eq 0 ] - - run curl http://hashbangctl-postgrest:3000/passwd?name=eq.testuser42 - echo "$output" | grep "testuser42" -} - -@test "Cannot login without an ssh key" { - run ssh_command +@test "Cannot run an invalid command" { + run ssh_command "ed25519" "jdoe" "invalid" [ "$status" -eq 255 ] + [[ "$output" == *"Unknown command"* ]] } -#@test "Can login with an ed25519 ssh key" { -# run ssh_command "ed25519" -# [ "$status" -eq 0 ] -#} +@test "Can create user with an ed25519 ssh key" { + run tmux_command "ssh_command ed25519" + run tmux_keys TAB TAB TAB ENTER + [ "$status" -eq 0 ] +} diff --git a/test/test_helper.bash b/test/test_helper.bash index 4ec1aa0..2b6fbd7 100644 --- a/test/test_helper.bash +++ b/test/test_helper.bash @@ -2,11 +2,33 @@ setup(){ echo "Settting up test" + psql -c "insert into hosts (name,maxusers) values ('test.hashbang.sh','500');"; + psql -c "insert into hosts (name,maxusers) values ('test2.hashbang.sh','500');"; + tmux new -d -y 28 -x 45 -s hashbangctl-test bash + tmux send-keys -t hashbangctl-test "source test_helper.bash" ENTER } teardown(){ echo "Tearing down test" - psql -c "delete from passwd;"; + psql -c "delete from passwd;" + psql -c "delete from hosts;" + tmux kill-session -t hashbangctl-test +} + +tmux_command(){ + cmd="${1:-}" + tmux send-keys -t hashbangctl-test "$cmd" ENTER + sleep 0.1 +} + +tmux_keys(){ + cmd="${1:-}" + tmux send-keys -t hashbangctl-test $@ + sleep 0.1 +} + +tmux_debug(){ + tmux capture-pane -t hashbangctl-test -pS - >&3 } ssh_command(){