2024-04-09 16:53:03 +02:00
|
|
|
package webserver
|
|
|
|
|
|
|
|
import (
|
|
|
|
"errors"
|
|
|
|
"fmt"
|
|
|
|
"io"
|
|
|
|
"os"
|
|
|
|
"strings"
|
|
|
|
"sync"
|
2024-05-01 22:12:48 +02:00
|
|
|
"time"
|
2024-04-09 16:53:03 +02:00
|
|
|
|
|
|
|
"encoding/json"
|
|
|
|
"net/http"
|
|
|
|
"path/filepath"
|
|
|
|
"testing"
|
|
|
|
|
|
|
|
"github.com/jech/galene/group"
|
2024-05-01 22:12:48 +02:00
|
|
|
"github.com/jech/galene/token"
|
2024-04-09 16:53:03 +02:00
|
|
|
)
|
|
|
|
|
|
|
|
var setupOnce = sync.OnceFunc(func() {
|
|
|
|
Insecure = true
|
2024-04-14 02:12:46 +02:00
|
|
|
err := Serve("localhost:1234", "")
|
|
|
|
if err != nil {
|
|
|
|
panic("could not start server")
|
|
|
|
}
|
2024-04-09 16:53:03 +02:00
|
|
|
})
|
|
|
|
|
|
|
|
func setupTest(dir, datadir string) error {
|
|
|
|
setupOnce()
|
|
|
|
|
|
|
|
group.Directory = dir
|
|
|
|
group.DataDirectory = datadir
|
|
|
|
config := `{
|
|
|
|
"writableGroups": true,
|
|
|
|
"users": {
|
|
|
|
"root": {
|
|
|
|
"password": "pw",
|
|
|
|
"permissions": "admin"
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}`
|
|
|
|
f, err := os.Create(filepath.Join(group.DataDirectory, "config.json"))
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
defer f.Close()
|
|
|
|
_, err = f.WriteString(config)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
2024-05-01 22:12:48 +02:00
|
|
|
|
|
|
|
token.SetStatefulFilename(filepath.Join(datadir, "tokens.jsonl"))
|
2024-04-09 16:53:03 +02:00
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2024-05-01 22:12:48 +02:00
|
|
|
func marshalToString(v any) string {
|
|
|
|
buf, err := json.Marshal(v)
|
|
|
|
if err != nil {
|
|
|
|
return ""
|
|
|
|
}
|
|
|
|
return string(buf)
|
|
|
|
}
|
|
|
|
|
2024-04-09 16:53:03 +02:00
|
|
|
func TestApi(t *testing.T) {
|
|
|
|
err := setupTest(t.TempDir(), t.TempDir())
|
|
|
|
if err != nil {
|
|
|
|
t.Fatal(err)
|
|
|
|
}
|
|
|
|
|
|
|
|
client := http.Client{}
|
|
|
|
|
|
|
|
do := func(method, path, ctype, im, inm, body string) (*http.Response, error) {
|
|
|
|
req, err := http.NewRequest(method,
|
|
|
|
"http://localhost:1234"+path,
|
|
|
|
strings.NewReader(body),
|
|
|
|
)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
if ctype != "" {
|
|
|
|
req.Header.Set("Content-Type", ctype)
|
|
|
|
}
|
|
|
|
if im != "" {
|
|
|
|
req.Header.Set("If-Match", im)
|
|
|
|
}
|
|
|
|
if inm != "" {
|
|
|
|
req.Header.Set("If-None-Match", inm)
|
|
|
|
}
|
|
|
|
req.SetBasicAuth("root", "pw")
|
|
|
|
return client.Do(req)
|
|
|
|
}
|
|
|
|
|
|
|
|
getString := func(path string) (string, error) {
|
|
|
|
resp, err := do("GET", path, "", "", "", "")
|
|
|
|
if err != nil {
|
|
|
|
return "", err
|
|
|
|
}
|
|
|
|
defer resp.Body.Close()
|
|
|
|
if resp.StatusCode != http.StatusOK {
|
|
|
|
return "", fmt.Errorf("Status is %v", resp.StatusCode)
|
|
|
|
}
|
|
|
|
ctype := parseContentType(resp.Header.Get("Content-Type"))
|
|
|
|
if !strings.EqualFold(ctype, "text/plain") {
|
|
|
|
return "", errors.New("Unexpected Content-Type")
|
|
|
|
}
|
|
|
|
b, err := io.ReadAll(resp.Body)
|
|
|
|
return string(b), err
|
|
|
|
}
|
|
|
|
|
|
|
|
getJSON := func(path string, value any) error {
|
|
|
|
resp, err := do("GET", path, "", "", "", "")
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
defer resp.Body.Close()
|
|
|
|
if resp.StatusCode != http.StatusOK {
|
|
|
|
return fmt.Errorf("Status is %v", resp.StatusCode)
|
|
|
|
}
|
|
|
|
ctype := parseContentType(resp.Header.Get("Content-Type"))
|
|
|
|
if !strings.EqualFold(ctype, "application/json") {
|
|
|
|
return errors.New("Unexpected")
|
|
|
|
}
|
|
|
|
d := json.NewDecoder(resp.Body)
|
|
|
|
return d.Decode(value)
|
|
|
|
}
|
|
|
|
|
|
|
|
s, err := getString("/galene-api/0/.groups/")
|
|
|
|
if err != nil || s != "" {
|
|
|
|
t.Errorf("Get groups: %v", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
resp, err := do("PUT", "/galene-api/0/.groups/test/",
|
|
|
|
"application/json", "\"foo\"", "",
|
|
|
|
"{}")
|
|
|
|
if err != nil || resp.StatusCode != http.StatusPreconditionFailed {
|
|
|
|
t.Errorf("Create group (bad ETag): %v %v", err, resp.StatusCode)
|
|
|
|
}
|
|
|
|
|
|
|
|
resp, err = do("PUT", "/galene-api/0/.groups/test/",
|
|
|
|
"text/plain", "", "",
|
|
|
|
"Hello, world!")
|
|
|
|
if err != nil || resp.StatusCode != http.StatusUnsupportedMediaType {
|
|
|
|
t.Errorf("Create group (bad content-type): %v %v",
|
|
|
|
err, resp.StatusCode)
|
|
|
|
}
|
|
|
|
|
|
|
|
resp, err = do("PUT", "/galene-api/0/.groups/test/",
|
|
|
|
"application/json", "", "*",
|
|
|
|
"{}")
|
|
|
|
if err != nil || resp.StatusCode != http.StatusCreated {
|
|
|
|
t.Errorf("Create group: %v %v", err, resp.StatusCode)
|
|
|
|
}
|
|
|
|
|
|
|
|
var desc *group.Description
|
|
|
|
err = getJSON("/galene-api/0/.groups/test/", &desc)
|
|
|
|
if err != nil || len(desc.Users) != 0 {
|
|
|
|
t.Errorf("Get group: %v", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
resp, err = do("PUT", "/galene-api/0/.groups/test/",
|
|
|
|
"application/json", "", "*",
|
|
|
|
"{}")
|
|
|
|
if err != nil || resp.StatusCode != http.StatusPreconditionFailed {
|
|
|
|
t.Errorf("Create group (bad ETag): %v %v", err, resp.StatusCode)
|
|
|
|
}
|
|
|
|
|
|
|
|
resp, err = do("DELETE", "/galene-api/0/.groups/test/",
|
|
|
|
"", "", "*", "")
|
|
|
|
if err != nil || resp.StatusCode != http.StatusPreconditionFailed {
|
|
|
|
t.Errorf("Delete group (bad ETag): %v %v", err, resp.StatusCode)
|
|
|
|
}
|
|
|
|
|
|
|
|
s, err = getString("/galene-api/0/.groups/")
|
|
|
|
if err != nil || s != "test\n" {
|
|
|
|
t.Errorf("Get groups: %v %#v", err, s)
|
|
|
|
}
|
|
|
|
|
2024-04-11 13:45:30 +02:00
|
|
|
resp, err = do("PUT", "/galene-api/0/.groups/test/.fallback-users",
|
|
|
|
"application/json", "", "",
|
|
|
|
`[{"password": "topsecret"}]`)
|
|
|
|
if err != nil || resp.StatusCode != http.StatusNoContent {
|
|
|
|
t.Errorf("Set fallback users: %v %v", err, resp.StatusCode)
|
|
|
|
}
|
|
|
|
|
2024-04-11 13:25:59 +02:00
|
|
|
resp, err = do("PUT", "/galene-api/0/.groups/test/.keys",
|
|
|
|
"application/jwk-set+json", "", "",
|
|
|
|
`{"keys": [{
|
|
|
|
"kty": "oct", "alg": "HS256",
|
|
|
|
"k": "4S9YZLHK1traIaXQooCnPfBw_yR8j9VEPaAMWAog_YQ"
|
|
|
|
}]}`)
|
|
|
|
if err != nil || resp.StatusCode != http.StatusNoContent {
|
|
|
|
t.Errorf("Set key: %v %v", err, resp.StatusCode)
|
|
|
|
}
|
|
|
|
|
2024-04-09 16:53:03 +02:00
|
|
|
s, err = getString("/galene-api/0/.groups/test/.users/")
|
|
|
|
if err != nil || s != "" {
|
|
|
|
t.Errorf("Get users: %v", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
resp, err = do("PUT", "/galene-api/0/.groups/test/.users/jch",
|
|
|
|
"text/plain", "", "*",
|
|
|
|
`hello, world!`)
|
|
|
|
if err != nil || resp.StatusCode != http.StatusUnsupportedMediaType {
|
|
|
|
t.Errorf("Create user (bad content-type): %v %v",
|
|
|
|
err, resp.StatusCode)
|
|
|
|
}
|
|
|
|
|
|
|
|
resp, err = do("PUT", "/galene-api/0/.groups/test/.users/jch",
|
|
|
|
"application/json", "", "*",
|
|
|
|
`{"permissions": "present"}`)
|
|
|
|
if err != nil || resp.StatusCode != http.StatusCreated {
|
|
|
|
t.Errorf("Create user: %v %v", err, resp.StatusCode)
|
|
|
|
}
|
|
|
|
|
|
|
|
s, err = getString("/galene-api/0/.groups/test/.users/")
|
|
|
|
if err != nil || s != "jch\n" {
|
|
|
|
t.Errorf("Get users: %v", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
resp, err = do("PUT", "/galene-api/0/.groups/test/.users/jch",
|
|
|
|
"application/json", "", "*",
|
|
|
|
`{"permissions": "present"}`)
|
|
|
|
if err != nil || resp.StatusCode != http.StatusPreconditionFailed {
|
|
|
|
t.Errorf("Create user (bad ETag): %v %v", err, resp.StatusCode)
|
|
|
|
}
|
|
|
|
|
|
|
|
resp, err = do("PUT", "/galene-api/0/.groups/test/.users/jch/.password",
|
|
|
|
"application/json", "", "",
|
|
|
|
`"toto"`)
|
|
|
|
if err != nil || resp.StatusCode != http.StatusNoContent {
|
|
|
|
t.Errorf("Set password (PUT): %v %v", err, resp.StatusCode)
|
|
|
|
}
|
|
|
|
|
|
|
|
resp, err = do("POST", "/galene-api/0/.groups/test/.users/jch/.password",
|
|
|
|
"text/plain", "", "",
|
|
|
|
`toto`)
|
|
|
|
if err != nil || resp.StatusCode != http.StatusNoContent {
|
|
|
|
t.Errorf("Set password (POST): %v %v", err, resp.StatusCode)
|
|
|
|
}
|
|
|
|
|
|
|
|
var user group.UserDescription
|
|
|
|
err = getJSON("/galene-api/0/.groups/test/.users/jch", &user)
|
|
|
|
if err != nil {
|
|
|
|
t.Errorf("Get user: %v", err)
|
|
|
|
}
|
2024-04-12 01:05:48 +02:00
|
|
|
if user.Password.Type != "" && user.Password.Key != nil {
|
2024-04-09 16:53:03 +02:00
|
|
|
t.Errorf("User not sanitised properly")
|
|
|
|
}
|
|
|
|
|
|
|
|
desc, err = group.GetDescription("test")
|
|
|
|
if err != nil {
|
|
|
|
t.Errorf("GetDescription: %v", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
if len(desc.Users) != 1 {
|
|
|
|
t.Errorf("Users: %#v", desc.Users)
|
|
|
|
}
|
|
|
|
|
|
|
|
if desc.Users["jch"].Password.Type != "pbkdf2" {
|
|
|
|
t.Errorf("Password.Type: %v", desc.Users["jch"].Password.Type)
|
|
|
|
}
|
|
|
|
|
|
|
|
resp, err = do("DELETE", "/galene-api/0/.groups/test/.users/jch",
|
|
|
|
"", "", "", "")
|
|
|
|
if err != nil || resp.StatusCode != http.StatusNoContent {
|
|
|
|
t.Errorf("Delete group: %v %v", err, resp.StatusCode)
|
|
|
|
}
|
|
|
|
|
|
|
|
desc, err = group.GetDescription("test")
|
|
|
|
if err != nil {
|
|
|
|
t.Errorf("GetDescription: %v", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
if len(desc.Users) != 0 {
|
|
|
|
t.Errorf("Users (after delete): %#v", desc.Users)
|
|
|
|
}
|
|
|
|
|
2024-04-11 13:45:30 +02:00
|
|
|
if len(desc.FallbackUsers) != 1 {
|
|
|
|
t.Errorf("Keys: %v", len(desc.AuthKeys))
|
|
|
|
}
|
|
|
|
|
2024-04-11 13:25:59 +02:00
|
|
|
if len(desc.AuthKeys) != 1 {
|
|
|
|
t.Errorf("Keys: %v", len(desc.AuthKeys))
|
|
|
|
}
|
|
|
|
|
2024-05-01 22:12:48 +02:00
|
|
|
resp, err = do("POST", "/galene-api/0/.groups/test/.tokens/",
|
|
|
|
"application/json", "", "", `{"group":"bad"}`)
|
|
|
|
if err != nil || resp.StatusCode != http.StatusBadRequest {
|
|
|
|
t.Errorf("Create token (bad group): %v %v", err, resp.StatusCode)
|
|
|
|
}
|
|
|
|
|
|
|
|
resp, err = do("POST", "/galene-api/0/.groups/test/.tokens/",
|
|
|
|
"application/json", "", "", "{}")
|
|
|
|
if err != nil || resp.StatusCode != http.StatusCreated {
|
|
|
|
t.Errorf("Create token: %v %v", err, resp.StatusCode)
|
|
|
|
}
|
|
|
|
|
|
|
|
tokname, err := getString("/galene-api/0/.groups/test/.tokens/")
|
|
|
|
if err != nil {
|
|
|
|
t.Errorf("Get tokens: %v", err)
|
|
|
|
}
|
|
|
|
tokname = tokname[:len(tokname)-1]
|
|
|
|
|
|
|
|
tokens, etag, err := token.List("test")
|
|
|
|
if err != nil || len(tokens) != 1 || tokens[0].Token != tokname {
|
|
|
|
t.Errorf("token.List: %v %v", tokens, err)
|
|
|
|
}
|
|
|
|
|
|
|
|
tokenpath := "/galene-api/0/.groups/test/.tokens/" + tokname
|
|
|
|
resp, err = do("GET", tokenpath,
|
|
|
|
"", "", "", "")
|
|
|
|
if err != nil || resp.StatusCode != http.StatusOK {
|
|
|
|
t.Errorf("Get token: %v %v", err, resp.StatusCode)
|
|
|
|
}
|
|
|
|
|
|
|
|
tok := tokens[0].Clone()
|
|
|
|
e := time.Now().Add(time.Hour)
|
|
|
|
tok.Expires = &e
|
|
|
|
resp, err = do("PUT", tokenpath,
|
|
|
|
"application/json", etag, "", marshalToString(tok))
|
|
|
|
if err != nil || resp.StatusCode != http.StatusNoContent {
|
|
|
|
t.Errorf("Update token: %v %v", err, resp.StatusCode)
|
|
|
|
}
|
|
|
|
|
|
|
|
tok.Group = "bad"
|
|
|
|
resp, err = do("PUT", tokenpath,
|
|
|
|
"application/json", "", "", marshalToString(tok))
|
|
|
|
if err != nil || resp.StatusCode != http.StatusBadRequest {
|
|
|
|
t.Errorf("Update token (bad group): %v %v", err, resp.StatusCode)
|
|
|
|
}
|
|
|
|
|
|
|
|
tokens, etag, err = token.List("test")
|
|
|
|
if err != nil || len(tokens) != 1 {
|
|
|
|
t.Errorf("Token list: %v %v", tokens, err)
|
|
|
|
}
|
|
|
|
if !tokens[0].Expires.Equal(e) {
|
|
|
|
t.Errorf("Got %v, expected %v", tokens[0].Expires, e)
|
|
|
|
}
|
|
|
|
|
|
|
|
resp, err = do("GET", tokenpath, "", "", "", "")
|
|
|
|
if err != nil || resp.StatusCode != http.StatusOK {
|
|
|
|
t.Errorf("Get token: %v %v", err, resp.StatusCode)
|
|
|
|
}
|
|
|
|
|
|
|
|
resp, err = do("DELETE", tokenpath, "", "", "", "")
|
|
|
|
if err != nil || resp.StatusCode != http.StatusNoContent {
|
|
|
|
t.Errorf("Update token: %v %v", err, resp.StatusCode)
|
|
|
|
}
|
|
|
|
tokens, etag, err = token.List("test")
|
|
|
|
if err != nil || len(tokens) != 0 {
|
|
|
|
t.Errorf("Token list: %v %v", tokens, err)
|
|
|
|
}
|
|
|
|
|
2024-04-11 13:45:30 +02:00
|
|
|
resp, err = do("DELETE", "/galene-api/0/.groups/test/.fallback-users",
|
|
|
|
"", "", "", "")
|
|
|
|
if err != nil || resp.StatusCode != http.StatusNoContent {
|
|
|
|
t.Errorf("Delete fallback users: %v %v", err, resp.StatusCode)
|
|
|
|
}
|
|
|
|
|
2024-04-11 13:25:59 +02:00
|
|
|
resp, err = do("DELETE", "/galene-api/0/.groups/test/.keys",
|
|
|
|
"", "", "", "")
|
|
|
|
if err != nil || resp.StatusCode != http.StatusNoContent {
|
|
|
|
t.Errorf("Delete keys: %v %v", err, resp.StatusCode)
|
|
|
|
}
|
|
|
|
|
2024-04-09 16:53:03 +02:00
|
|
|
resp, err = do("DELETE", "/galene-api/0/.groups/test/",
|
|
|
|
"", "", "", "")
|
|
|
|
if err != nil || resp.StatusCode != http.StatusNoContent {
|
|
|
|
t.Errorf("Delete group: %v %v", err, resp.StatusCode)
|
|
|
|
}
|
|
|
|
|
|
|
|
_, err = group.GetDescription("test")
|
2024-04-14 13:33:52 +02:00
|
|
|
if !errors.Is(err, os.ErrNotExist) {
|
2024-04-09 16:53:03 +02:00
|
|
|
t.Errorf("Group exists after delete")
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func TestApiBadAuth(t *testing.T) {
|
|
|
|
err := setupTest(t.TempDir(), t.TempDir())
|
|
|
|
if err != nil {
|
|
|
|
t.Fatal(err)
|
|
|
|
}
|
|
|
|
|
|
|
|
client := http.Client{}
|
|
|
|
|
|
|
|
do := func(method, path string) {
|
|
|
|
req, err := http.NewRequest(method,
|
|
|
|
"http://localhost:1234"+path,
|
|
|
|
nil)
|
|
|
|
if err != nil {
|
|
|
|
t.Errorf("New request: %v", err)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
req.SetBasicAuth("root", "badpw")
|
|
|
|
resp, err := client.Do(req)
|
|
|
|
if err != nil {
|
|
|
|
t.Errorf("%v %v: %v", method, path, err)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
if resp.StatusCode != http.StatusUnauthorized {
|
|
|
|
t.Errorf("%v %v: %v", method, path, resp.StatusCode)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
do("GET", "/galene-api/0/.stats")
|
|
|
|
do("GET", "/galene-api/0/.groups/")
|
|
|
|
do("PUT", "/galene-api/0/.groups/test/")
|
|
|
|
|
|
|
|
f, err := os.Create(filepath.Join(group.Directory, "test.json"))
|
|
|
|
if err != nil {
|
|
|
|
t.Fatalf("Create(test.json): %v", err)
|
|
|
|
}
|
|
|
|
f.WriteString(`{
|
|
|
|
"users": {"jch": {"permissions": "present", "password": "pw"}}
|
|
|
|
}\n`)
|
|
|
|
f.Close()
|
|
|
|
|
|
|
|
do("PUT", "/galene-api/0/.groups/test/")
|
|
|
|
do("DELETE", "/galene-api/0/.groups/test/")
|
|
|
|
do("GET", "/galene-api/0/.groups/test/.users/")
|
|
|
|
do("GET", "/galene-api/0/.groups/test/.users/jch")
|
|
|
|
do("GET", "/galene-api/0/.groups/test/.users/jch")
|
|
|
|
do("PUT", "/galene-api/0/.groups/test/.users/jch")
|
|
|
|
do("DELETE", "/galene-api/0/.groups/test/.users/jch")
|
|
|
|
do("GET", "/galene-api/0/.groups/test/.users/not-jch")
|
|
|
|
do("PUT", "/galene-api/0/.groups/test/.users/not-jch")
|
|
|
|
do("PUT", "/galene-api/0/.groups/test/.users/jch/.password")
|
|
|
|
do("POST", "/galene-api/0/.groups/test/.users/jch/.password")
|
2024-05-01 22:12:48 +02:00
|
|
|
do("GET", "/galene-api/0/.groups/test/.tokens/")
|
|
|
|
do("POST", "/galene-api/0/.groups/test/.tokens/")
|
|
|
|
do("GET", "/galene-api/0/.groups/test/.tokens/token")
|
|
|
|
do("PUT", "/galene-api/0/.groups/test/.tokens/token")
|
|
|
|
do("DELETE", "/galene-api/0/.groups/test/.tokens/token")
|
2024-04-09 16:53:03 +02:00
|
|
|
}
|