mirror of
https://github.com/jech/galene.git
synced 2024-11-09 18:25:58 +01:00
Implement administrative API.
This commit is contained in:
parent
5753c46910
commit
cc38c53075
3 changed files with 706 additions and 19 deletions
|
@ -25,7 +25,7 @@ async function listStats() {
|
||||||
|
|
||||||
let l;
|
let l;
|
||||||
try {
|
try {
|
||||||
let r = await fetch('/galene-api/.stats');
|
let r = await fetch('/galene-api/0/.stats');
|
||||||
if(!r.ok)
|
if(!r.ok)
|
||||||
throw new Error(`${r.status} ${r.statusText}`);
|
throw new Error(`${r.status} ${r.statusText}`);
|
||||||
l = await r.json();
|
l = await r.json();
|
||||||
|
|
406
webserver/api.go
406
webserver/api.go
|
@ -1,11 +1,19 @@
|
||||||
package webserver
|
package webserver
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"crypto/rand"
|
||||||
|
"crypto/sha256"
|
||||||
|
"encoding/hex"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"log"
|
"fmt"
|
||||||
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"golang.org/x/crypto/pbkdf2"
|
||||||
|
|
||||||
|
"github.com/jech/galene/group"
|
||||||
"github.com/jech/galene/stats"
|
"github.com/jech/galene/stats"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -13,36 +21,60 @@ func parseContentType(ctype string) string {
|
||||||
return strings.Trim(strings.Split(ctype, ";")[0], " ")
|
return strings.Trim(strings.Split(ctype, ";")[0], " ")
|
||||||
}
|
}
|
||||||
|
|
||||||
func apiHandler(w http.ResponseWriter, r *http.Request) {
|
// checkAdmin checks whether the client authentifies as an administrator
|
||||||
|
func checkAdmin(w http.ResponseWriter, r *http.Request) bool {
|
||||||
username, password, ok := r.BasicAuth()
|
username, password, ok := r.BasicAuth()
|
||||||
|
if ok {
|
||||||
|
ok, _ = adminMatch(username, password)
|
||||||
|
}
|
||||||
if !ok {
|
if !ok {
|
||||||
failAuthentication(w, "galene-api")
|
failAuthentication(w, "/galene-api/")
|
||||||
return
|
return false
|
||||||
}
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
if ok, err := adminMatch(username, password); !ok {
|
// checkPasswordAdmin checks whether the client authentifies as either an
|
||||||
if err != nil {
|
// administrator or the given user. It is used to check whether the
|
||||||
log.Printf("Administrator password: %v", err)
|
// client has the right to change user's password.
|
||||||
|
func checkPasswordAdmin(w http.ResponseWriter, r *http.Request, groupname, user string) bool {
|
||||||
|
username, password, ok := r.BasicAuth()
|
||||||
|
if ok {
|
||||||
|
ok, _ := adminMatch(username, password)
|
||||||
|
if ok {
|
||||||
|
return true
|
||||||
}
|
}
|
||||||
failAuthentication(w, "galene-api")
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
if ok && username == user {
|
||||||
|
desc, err := group.GetDescription(groupname)
|
||||||
|
if err == nil && desc.Users != nil {
|
||||||
|
u, ok := desc.Users[user]
|
||||||
|
if ok {
|
||||||
|
ok, _ := u.Password.Match(password)
|
||||||
|
if ok {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
failAuthentication(w, "/galene-api/")
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func apiHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
if !strings.HasPrefix(r.URL.Path, "/galene-api/") {
|
if !strings.HasPrefix(r.URL.Path, "/galene-api/") {
|
||||||
http.NotFound(w, r)
|
http.NotFound(w, r)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
first, kind, rest := splitPath(r.URL.Path[len("/galene/api"):])
|
first, kind, rest := splitPath(r.URL.Path[len("/galene-api"):])
|
||||||
if first != "" {
|
if first == "/0" && kind == ".stats" && rest == "" {
|
||||||
http.NotFound(w, r)
|
if !checkAdmin(w, r) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if kind == ".stats" && rest == "" {
|
|
||||||
if r.Method != "HEAD" && r.Method != "GET" {
|
if r.Method != "HEAD" && r.Method != "GET" {
|
||||||
http.Error(w, "method not allowed",
|
methodNotAllowed(w, "HEAD", "GET")
|
||||||
http.StatusMethodNotAllowed)
|
return
|
||||||
}
|
}
|
||||||
w.Header().Set("content-type", "application/json")
|
w.Header().Set("content-type", "application/json")
|
||||||
w.Header().Set("cache-control", "no-cache")
|
w.Header().Set("cache-control", "no-cache")
|
||||||
|
@ -54,8 +86,346 @@ func apiHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
e := json.NewEncoder(w)
|
e := json.NewEncoder(w)
|
||||||
e.Encode(ss)
|
e.Encode(ss)
|
||||||
return
|
return
|
||||||
|
} else if first == "/0" && kind == ".groups" {
|
||||||
|
apiGroupHandler(w, r, rest)
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
http.NotFound(w, r)
|
http.NotFound(w, r)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func apiGroupHandler(w http.ResponseWriter, r *http.Request, pth string) {
|
||||||
|
first, kind, rest := splitPath(pth)
|
||||||
|
if first == "" {
|
||||||
|
notFound(w)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
g := first[1:]
|
||||||
|
if g == "" && kind == "" {
|
||||||
|
if !checkAdmin(w, r) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if r.Method != "HEAD" && r.Method != "GET" {
|
||||||
|
methodNotAllowed(w, "HEAD", "GET")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
groups, err := group.GetDescriptionNames()
|
||||||
|
if err != nil {
|
||||||
|
httpError(w, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
w.Header().Set("content-type", "text/plain; charset=utf-8")
|
||||||
|
if r.Method == "HEAD" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
for _, g := range groups {
|
||||||
|
fmt.Fprintln(w, g)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if kind == ".users" {
|
||||||
|
apiUserHandler(w, r, g, rest)
|
||||||
|
return
|
||||||
|
} else if kind != "" {
|
||||||
|
if !checkAdmin(w, r) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
notFound(w)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if !checkAdmin(w, r) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if r.Method == "HEAD" || r.Method == "GET" {
|
||||||
|
desc, etag, err := group.GetSanitisedDescription(g)
|
||||||
|
if err != nil {
|
||||||
|
httpError(w, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
w.Header().Set("etag", etag)
|
||||||
|
|
||||||
|
done := checkPreconditions(w, r, etag)
|
||||||
|
if done {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
w.Header().Set("content-type", "application/json")
|
||||||
|
if r.Method == "HEAD" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
e := json.NewEncoder(w)
|
||||||
|
e.Encode(desc)
|
||||||
|
return
|
||||||
|
} else if r.Method == "PUT" {
|
||||||
|
etag, err := group.GetDescriptionTag(g)
|
||||||
|
if os.IsNotExist(err) {
|
||||||
|
err = nil
|
||||||
|
etag = ""
|
||||||
|
} else if err != nil {
|
||||||
|
httpError(w, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
done := checkPreconditions(w, r, etag)
|
||||||
|
if done {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ctype := parseContentType(
|
||||||
|
r.Header.Get("Content-Type"),
|
||||||
|
)
|
||||||
|
if !strings.EqualFold(ctype, "application/json") {
|
||||||
|
http.Error(w, "unsupported content type",
|
||||||
|
http.StatusUnsupportedMediaType)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
d := json.NewDecoder(r.Body)
|
||||||
|
var newdesc group.Description
|
||||||
|
err = d.Decode(&newdesc)
|
||||||
|
if err != nil {
|
||||||
|
httpError(w, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
err = group.UpdateDescription(g, etag, &newdesc)
|
||||||
|
if err != nil {
|
||||||
|
httpError(w, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if etag == "" {
|
||||||
|
w.WriteHeader(http.StatusCreated)
|
||||||
|
} else {
|
||||||
|
w.WriteHeader(http.StatusNoContent)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
} else if r.Method == "DELETE" {
|
||||||
|
etag, err := group.GetDescriptionTag(g)
|
||||||
|
if err != nil {
|
||||||
|
httpError(w, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
done := checkPreconditions(w, r, etag)
|
||||||
|
if done {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
err = group.DeleteDescription(g, etag)
|
||||||
|
if err != nil {
|
||||||
|
httpError(w, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
w.WriteHeader(http.StatusNoContent)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
methodNotAllowed(w, "HEAD", "GET", "PUT", "DELETE")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func apiUserHandler(w http.ResponseWriter, r *http.Request, g, pth string) {
|
||||||
|
if pth == "/" {
|
||||||
|
if !checkAdmin(w, r) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if r.Method != "HEAD" && r.Method != "GET" {
|
||||||
|
http.Error(w, "method not allowed",
|
||||||
|
http.StatusMethodNotAllowed)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
users, etag, err := group.GetUsers(g)
|
||||||
|
if err != nil {
|
||||||
|
httpError(w, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
w.Header().Set("content-type", "text/plain; charset=utf-8")
|
||||||
|
w.Header().Set("etag", etag)
|
||||||
|
done := checkPreconditions(w, r, etag)
|
||||||
|
if done {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if r.Method == "HEAD" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
for _, u := range users {
|
||||||
|
fmt.Fprintln(w, u)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
first2, kind2, rest2 := splitPath(pth)
|
||||||
|
if kind2 == ".password" && first2 != "" && rest2 == "" {
|
||||||
|
passwordHandler(w, r, g, first2[1:])
|
||||||
|
return
|
||||||
|
} else if kind2 != "" || first2 == "" {
|
||||||
|
if !checkAdmin(w, r) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
notFound(w)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if !checkAdmin(w, r) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
username := first2[1:]
|
||||||
|
if r.Method == "HEAD" || r.Method == "GET" {
|
||||||
|
w.Header().Set("content-type", "application/json")
|
||||||
|
user, etag, err := group.GetSanitisedUser(g, username)
|
||||||
|
if err != nil {
|
||||||
|
httpError(w, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
w.Header().Set("content-type", "application/json")
|
||||||
|
w.Header().Set("etag", etag)
|
||||||
|
done := checkPreconditions(w, r, etag)
|
||||||
|
if done {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if r.Method == "HEAD" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
e := json.NewEncoder(w)
|
||||||
|
e.Encode(user)
|
||||||
|
return
|
||||||
|
} else if r.Method == "PUT" {
|
||||||
|
etag, err := group.GetUserTag(g, username)
|
||||||
|
if os.IsNotExist(err) {
|
||||||
|
etag = ""
|
||||||
|
err = nil
|
||||||
|
} else if err != nil {
|
||||||
|
httpError(w, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
done := checkPreconditions(w, r, etag)
|
||||||
|
if done {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ctype := parseContentType(r.Header.Get("Content-Type"))
|
||||||
|
if !strings.EqualFold(ctype, "application/json") {
|
||||||
|
http.Error(w, "unsupported content type",
|
||||||
|
http.StatusUnsupportedMediaType)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
d := json.NewDecoder(r.Body)
|
||||||
|
var newdesc group.UserDescription
|
||||||
|
err = d.Decode(&newdesc)
|
||||||
|
if err != nil {
|
||||||
|
httpError(w, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
err = group.UpdateUser(g, username, etag, &newdesc)
|
||||||
|
if err != nil {
|
||||||
|
httpError(w, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if etag == "" {
|
||||||
|
w.WriteHeader(http.StatusCreated)
|
||||||
|
} else {
|
||||||
|
w.WriteHeader(http.StatusNoContent)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
} else if r.Method == "DELETE" {
|
||||||
|
etag, err := group.GetUserTag(g, username)
|
||||||
|
if err != nil {
|
||||||
|
httpError(w, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
done := checkPreconditions(w, r, etag)
|
||||||
|
if done {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
err = group.DeleteUser(g, username, etag)
|
||||||
|
if err != nil {
|
||||||
|
httpError(w, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
w.WriteHeader(http.StatusNoContent)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
methodNotAllowed(w, "HEAD", "GET", "PUT", "DELETE")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func passwordHandler(w http.ResponseWriter, r *http.Request, g, user string) {
|
||||||
|
if !checkPasswordAdmin(w, r, g, user) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if r.Method == "PUT" {
|
||||||
|
ctype := parseContentType(r.Header.Get("Content-Type"))
|
||||||
|
if !strings.EqualFold(ctype, "application/json") {
|
||||||
|
http.Error(w, "unsupported content type",
|
||||||
|
http.StatusUnsupportedMediaType)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
d := json.NewDecoder(r.Body)
|
||||||
|
var pw group.Password
|
||||||
|
err := d.Decode(&pw)
|
||||||
|
if err != nil {
|
||||||
|
httpError(w, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
err = group.SetUserPassword(g, user, pw)
|
||||||
|
if err != nil {
|
||||||
|
httpError(w, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
w.WriteHeader(http.StatusNoContent)
|
||||||
|
return
|
||||||
|
} else if r.Method == "POST" {
|
||||||
|
ctype := parseContentType(r.Header.Get("Content-Type"))
|
||||||
|
if !strings.EqualFold(ctype, "text/plain") {
|
||||||
|
http.Error(w, "unsupported content type",
|
||||||
|
http.StatusUnsupportedMediaType)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
body, err := io.ReadAll(http.MaxBytesReader(w, r.Body, 4096))
|
||||||
|
if err != nil {
|
||||||
|
httpError(w, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
salt := make([]byte, 8)
|
||||||
|
_, err = rand.Read(salt)
|
||||||
|
if err != nil {
|
||||||
|
httpError(w, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
iterations := 4096
|
||||||
|
key := pbkdf2.Key(body, salt, iterations, 32, sha256.New)
|
||||||
|
pw := group.Password{
|
||||||
|
Type: "pbkdf2",
|
||||||
|
Hash: "sha-256",
|
||||||
|
Key: hex.EncodeToString(key),
|
||||||
|
Salt: hex.EncodeToString(salt),
|
||||||
|
Iterations: iterations,
|
||||||
|
}
|
||||||
|
err = group.SetUserPassword(g, user, pw)
|
||||||
|
if err != nil {
|
||||||
|
httpError(w, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
w.WriteHeader(http.StatusNoContent)
|
||||||
|
return
|
||||||
|
} else if r.Method == "DELETE" {
|
||||||
|
err := group.SetUserPassword(g, user, group.Password{})
|
||||||
|
if err != nil {
|
||||||
|
httpError(w, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
w.WriteHeader(http.StatusNoContent)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
methodNotAllowed(w, "PUT", "POST", "DELETE")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
317
webserver/api_test.go
Normal file
317
webserver/api_test.go
Normal file
|
@ -0,0 +1,317 @@
|
||||||
|
package webserver
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"encoding/json"
|
||||||
|
"net/http"
|
||||||
|
"path/filepath"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/jech/galene/group"
|
||||||
|
)
|
||||||
|
|
||||||
|
var setupOnce = sync.OnceFunc(func() {
|
||||||
|
Insecure = true
|
||||||
|
go func() {
|
||||||
|
err := Serve("localhost:1234", "")
|
||||||
|
if err != nil {
|
||||||
|
panic("could not start server")
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
time.Sleep(100 * time.Millisecond)
|
||||||
|
})
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
if user.Password.Type != "" && user.Password.Key != "" {
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
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")
|
||||||
|
if !os.IsNotExist(err) {
|
||||||
|
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")
|
||||||
|
}
|
Loading…
Reference in a new issue