Compare commits
8 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
99f88d2fb8 | ||
|
|
4657dd9505 | ||
|
|
aeb5781ec9 | ||
|
|
b951e057c4 | ||
|
|
09c06a4a0c | ||
|
|
44d548c603 | ||
|
|
08dfb715d3 | ||
|
|
fa499c200e |
61
server/auth.go
Normal file
61
server/auth.go
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"io/ioutil"
|
||||||
|
|
||||||
|
yaml "gopkg.in/yaml.v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
type auth interface {
|
||||||
|
Login(string, string) (bool, error)
|
||||||
|
Groups(string) ([]string, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
type FileAuth struct {
|
||||||
|
path string
|
||||||
|
}
|
||||||
|
|
||||||
|
type fileAuthContent struct {
|
||||||
|
Users map[string]struct {
|
||||||
|
Password string
|
||||||
|
Groups []string
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewFileAuth(path string) FileAuth {
|
||||||
|
return FileAuth{path: path}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (fileAuth FileAuth) Login(u, p string) (bool, error) {
|
||||||
|
content, err := fileAuth.load()
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
entry, ok := content.Users[u]
|
||||||
|
return ok && entry.Password == p, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (fileAuth FileAuth) Groups(u string) ([]string, error) {
|
||||||
|
content, err := fileAuth.load()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
entry, ok := content.Users[u]
|
||||||
|
if !ok {
|
||||||
|
return nil, errors.New("invalid user")
|
||||||
|
}
|
||||||
|
return entry.Groups, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (fileAuth FileAuth) load() (fileAuthContent, error) {
|
||||||
|
var fileAuthContent fileAuthContent
|
||||||
|
b, err := ioutil.ReadFile(fileAuth.path)
|
||||||
|
if err != nil {
|
||||||
|
return fileAuthContent, err
|
||||||
|
}
|
||||||
|
if err := yaml.Unmarshal(b, &fileAuthContent); err != nil {
|
||||||
|
return fileAuthContent, err
|
||||||
|
}
|
||||||
|
return fileAuthContent, nil
|
||||||
|
}
|
||||||
118
server/auth_test.go
Normal file
118
server/auth_test.go
Normal file
@@ -0,0 +1,118 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"io/ioutil"
|
||||||
|
"os"
|
||||||
|
"path"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestFileAuth(t *testing.T) {
|
||||||
|
user := "username"
|
||||||
|
passw := "password"
|
||||||
|
g := "group"
|
||||||
|
emptyp := func() string {
|
||||||
|
d := t.TempDir()
|
||||||
|
f, err := ioutil.TempFile(d, "login.yaml.*")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
f.Close()
|
||||||
|
return path.Join(d, f.Name())
|
||||||
|
}
|
||||||
|
goodp := func() string {
|
||||||
|
p := emptyp()
|
||||||
|
if err := ensureAndWrite(p, []byte(fmt.Sprintf(`{
|
||||||
|
"users": {
|
||||||
|
%q: {
|
||||||
|
"password": %q,
|
||||||
|
"groups": [%q]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}`, user, passw, g))); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
return p
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Run("no file", func(t *testing.T) {
|
||||||
|
p := emptyp()
|
||||||
|
os.Remove(p)
|
||||||
|
fa := NewFileAuth(p)
|
||||||
|
if _, err := fa.Login(user, passw); err == nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("bad file", func(t *testing.T) {
|
||||||
|
p := emptyp()
|
||||||
|
if err := ensureAndWrite(p, []byte(`{"hello:}`)); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
fa := NewFileAuth(p)
|
||||||
|
if _, err := fa.Login(user, passw); err == nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("bad user", func(t *testing.T) {
|
||||||
|
p := goodp()
|
||||||
|
fa := NewFileAuth(p)
|
||||||
|
if ok, err := fa.Login("bad"+user, passw); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
} else if ok {
|
||||||
|
t.Fatal(ok)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("bad pass", func(t *testing.T) {
|
||||||
|
p := goodp()
|
||||||
|
fa := NewFileAuth(p)
|
||||||
|
if ok, err := fa.Login(user, "bad"+passw); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
} else if ok {
|
||||||
|
t.Fatal(ok)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("good load", func(t *testing.T) {
|
||||||
|
p := goodp()
|
||||||
|
fa := NewFileAuth(p)
|
||||||
|
got, err := fa.load()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if len(got.Users) != 1 {
|
||||||
|
t.Error(got.Users)
|
||||||
|
}
|
||||||
|
if entry, ok := got.Users[user]; !ok {
|
||||||
|
t.Error(ok)
|
||||||
|
} else if entry.Password != passw {
|
||||||
|
t.Error(entry)
|
||||||
|
} else if len(entry.Groups) != 1 {
|
||||||
|
t.Error(entry.Groups)
|
||||||
|
} else if entry.Groups[0] != g {
|
||||||
|
t.Error(entry.Groups)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("good", func(t *testing.T) {
|
||||||
|
p := goodp()
|
||||||
|
b, _ := ioutil.ReadFile(p)
|
||||||
|
t.Logf("goodp: %s: %s", p, b)
|
||||||
|
fa := NewFileAuth(p)
|
||||||
|
if ok, err := fa.Login(user, passw); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
} else if !ok {
|
||||||
|
t.Fatal(ok)
|
||||||
|
}
|
||||||
|
if groups, err := fa.Groups(user); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
} else if len(groups) != 1 {
|
||||||
|
t.Fatal(groups)
|
||||||
|
} else if groups[0] != g {
|
||||||
|
t.Fatal(groups)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
251
server/authenticate.go
Normal file
251
server/authenticate.go
Normal file
@@ -0,0 +1,251 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/base64"
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"hash/crc32"
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
)
|
||||||
|
|
||||||
|
var cookieSecret = os.Getenv("COOKIE_SECRET")
|
||||||
|
|
||||||
|
type User struct {
|
||||||
|
User string
|
||||||
|
Group string
|
||||||
|
Groups []string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (user User) Is(other User) bool {
|
||||||
|
for i := range user.Groups {
|
||||||
|
if i >= len(other.Groups) || user.Groups[i] != other.Groups[i] {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return user.User == other.User &&
|
||||||
|
user.Group == other.Group &&
|
||||||
|
len(user.Groups) == len(other.Groups)
|
||||||
|
}
|
||||||
|
|
||||||
|
type Cookie struct {
|
||||||
|
Hash string
|
||||||
|
Salt string
|
||||||
|
Value string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (server *Server) authenticate(w http.ResponseWriter, r *http.Request) (*Server, bool, error) {
|
||||||
|
if done, err := server.parseLogin(w, r); err != nil {
|
||||||
|
log.Printf("error parsing login: %v", err)
|
||||||
|
return nil, false, err
|
||||||
|
} else if done {
|
||||||
|
log.Printf("login rendered body")
|
||||||
|
return nil, true, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if ok, err := needsLogin(r); err != nil {
|
||||||
|
log.Printf("error checking if login needed: %v", err)
|
||||||
|
return nil, false, err
|
||||||
|
} else if ok {
|
||||||
|
log.Printf("needs login")
|
||||||
|
promptLogin(w)
|
||||||
|
return nil, true, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if done, err := changeNamespace(w, r); err != nil {
|
||||||
|
return nil, false, err
|
||||||
|
} else if done {
|
||||||
|
return nil, true, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
user, _ := loginCookie(r)
|
||||||
|
return server.WithUser(user.User, user.Group, user.Groups), false, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func promptLogin(w http.ResponseWriter) {
|
||||||
|
w.Header().Set("WWW-Authenticate", "Basic")
|
||||||
|
w.WriteHeader(http.StatusUnauthorized)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (server *Server) parseLogin(w http.ResponseWriter, r *http.Request) (bool, error) {
|
||||||
|
username, password, ok := r.BasicAuth()
|
||||||
|
if !ok {
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
ok, err := server.auth.Login(username, password)
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
if !ok {
|
||||||
|
promptLogin(w)
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
groups, err := server.auth.Groups(username)
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
if len(groups) == 0 {
|
||||||
|
return false, errors.New("user has no groups")
|
||||||
|
}
|
||||||
|
|
||||||
|
user := User{
|
||||||
|
User: username,
|
||||||
|
Groups: groups,
|
||||||
|
Group: groups[0],
|
||||||
|
}
|
||||||
|
|
||||||
|
olduser, _ := loginCookie(r)
|
||||||
|
for i := range groups {
|
||||||
|
if groups[i] == olduser.Group {
|
||||||
|
user.Group = olduser.Group
|
||||||
|
}
|
||||||
|
}
|
||||||
|
log.Printf("%+v => %+v", olduser, user)
|
||||||
|
|
||||||
|
setLoginCookie(w, r, user)
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func changeNamespace(w http.ResponseWriter, r *http.Request) (bool, error) {
|
||||||
|
want := r.URL.Query().Get("namespace")
|
||||||
|
if want == "" {
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
user, ok := loginCookie(r)
|
||||||
|
if !ok {
|
||||||
|
promptLogin(w)
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if user.Group == want {
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
for i := range user.Groups {
|
||||||
|
if want == user.Groups[i] {
|
||||||
|
user.Group = want
|
||||||
|
setLoginCookie(w, r, user)
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func needsLogin(r *http.Request) (bool, error) {
|
||||||
|
user, ok := loginCookie(r)
|
||||||
|
if !ok {
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
for i := range user.Groups {
|
||||||
|
if user.Group == user.Groups[i] {
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func setLoginCookie(w http.ResponseWriter, r *http.Request, user User) {
|
||||||
|
cookie := &http.Cookie{
|
||||||
|
Name: "login",
|
||||||
|
Value: encodeUserCookie(user),
|
||||||
|
Expires: time.Now().Add(time.Hour * 24),
|
||||||
|
Path: "/",
|
||||||
|
}
|
||||||
|
if was, ok := requestLoginCookie(r); !ok || !was.Is(user) {
|
||||||
|
w.Header().Set("Set-Cookie", cookie.String())
|
||||||
|
}
|
||||||
|
log.Printf("setting login cookie: %+v", user)
|
||||||
|
*r = *r.WithContext(context.WithValue(r.Context(), "LOGIN_COOKIE", cookie.Value))
|
||||||
|
}
|
||||||
|
|
||||||
|
func loginCookie(r *http.Request) (User, bool) {
|
||||||
|
if v := r.Context().Value("LOGIN_COOKIE"); v != nil {
|
||||||
|
log.Printf("login cookie from ctx")
|
||||||
|
return decodeUserCookie(v.(string))
|
||||||
|
}
|
||||||
|
return requestLoginCookie(r)
|
||||||
|
}
|
||||||
|
|
||||||
|
func requestLoginCookie(r *http.Request) (User, bool) {
|
||||||
|
c, ok := getCookie("login", r)
|
||||||
|
log.Printf("request login cookie: %v, %v", c, ok)
|
||||||
|
if !ok {
|
||||||
|
return User{}, false
|
||||||
|
}
|
||||||
|
return decodeUserCookie(c)
|
||||||
|
}
|
||||||
|
|
||||||
|
func getCookie(key string, r *http.Request) (string, bool) {
|
||||||
|
var cookie *http.Cookie
|
||||||
|
cookies := r.Cookies()
|
||||||
|
for i := range cookies {
|
||||||
|
if cookies[i].Name == key && (cookies[i].Expires.IsZero() || time.Now().Before(cookies[i].Expires)) {
|
||||||
|
cookie = cookies[i]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if cookie == nil {
|
||||||
|
return "", false
|
||||||
|
}
|
||||||
|
return cookie.Value, cookie.Expires.IsZero() || time.Now().Before(cookie.Expires)
|
||||||
|
}
|
||||||
|
|
||||||
|
func decodeUserCookie(raw string) (User, bool) {
|
||||||
|
decoded, ok := decodeCookie(raw)
|
||||||
|
if !ok {
|
||||||
|
return User{}, ok
|
||||||
|
}
|
||||||
|
var user User
|
||||||
|
err := json.Unmarshal([]byte(decoded), &user)
|
||||||
|
return user, err == nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func encodeUserCookie(user User) string {
|
||||||
|
b, err := json.Marshal(user)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
return encodeCookie(string(b))
|
||||||
|
}
|
||||||
|
|
||||||
|
func encodeCookie(s string) string {
|
||||||
|
cookie := Cookie{
|
||||||
|
Salt: uuid.New().String(),
|
||||||
|
Value: s,
|
||||||
|
}
|
||||||
|
hash := crc32.NewIEEE()
|
||||||
|
hash.Write([]byte(cookieSecret))
|
||||||
|
hash.Write([]byte(cookie.Salt))
|
||||||
|
hash.Write([]byte(cookie.Value))
|
||||||
|
cookie.Hash = base64.StdEncoding.EncodeToString(hash.Sum(nil))
|
||||||
|
b, err := json.Marshal(cookie)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
return base64.StdEncoding.EncodeToString(b)
|
||||||
|
}
|
||||||
|
|
||||||
|
func decodeCookie(s string) (string, bool) {
|
||||||
|
b, err := base64.StdEncoding.DecodeString(s)
|
||||||
|
if err != nil {
|
||||||
|
return "", false
|
||||||
|
}
|
||||||
|
var cookie Cookie
|
||||||
|
if err := json.Unmarshal(b, &cookie); err != nil {
|
||||||
|
return "", false
|
||||||
|
}
|
||||||
|
hash := crc32.NewIEEE()
|
||||||
|
hash.Write([]byte(cookieSecret))
|
||||||
|
hash.Write([]byte(cookie.Salt))
|
||||||
|
hash.Write([]byte(cookie.Value))
|
||||||
|
if got := base64.StdEncoding.EncodeToString(hash.Sum(nil)); cookie.Hash != got {
|
||||||
|
return "", false
|
||||||
|
}
|
||||||
|
return cookie.Value, true
|
||||||
|
}
|
||||||
361
server/authenticate_test.go
Normal file
361
server/authenticate_test.go
Normal file
@@ -0,0 +1,361 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"path"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestEncodeDecodeCookie(t *testing.T) {
|
||||||
|
newTestServer(t)
|
||||||
|
|
||||||
|
for i := 0; i < 5; i++ {
|
||||||
|
value := uuid.New().String()
|
||||||
|
encoded := encodeCookie(value)
|
||||||
|
for j := 0; j < 5; j++ {
|
||||||
|
decoded, ok := decodeCookie(encoded)
|
||||||
|
if !ok || decoded != value {
|
||||||
|
t.Errorf("value=%s, encoded=%s, decoded=%s", value, encoded, decoded)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEncodeDecodeUserCookie(t *testing.T) {
|
||||||
|
newTestServer(t)
|
||||||
|
|
||||||
|
user := User{
|
||||||
|
User: "abc",
|
||||||
|
Groups: []string{"def", "ghi"},
|
||||||
|
}
|
||||||
|
encoded := encodeUserCookie(user)
|
||||||
|
decoded, ok := decodeUserCookie(encoded)
|
||||||
|
if !ok {
|
||||||
|
t.Fatal(ok)
|
||||||
|
}
|
||||||
|
if fmt.Sprint(user) != fmt.Sprint(decoded) {
|
||||||
|
t.Fatal(user, decoded)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetCookie(t *testing.T) {
|
||||||
|
r := httptest.NewRequest(http.MethodGet, "/", nil)
|
||||||
|
r.AddCookie(&http.Cookie{
|
||||||
|
Name: "abc",
|
||||||
|
Value: "def",
|
||||||
|
Expires: time.Now().Add(time.Hour),
|
||||||
|
})
|
||||||
|
got, _ := getCookie("abc", r)
|
||||||
|
if got != "def" {
|
||||||
|
t.Fatal(r.Cookies(), got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetSetLoginCookie(t *testing.T) {
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
r := httptest.NewRequest(http.MethodGet, "/", nil)
|
||||||
|
user := User{User: "a", Groups: []string{"g"}}
|
||||||
|
|
||||||
|
setLoginCookie(w, r, user)
|
||||||
|
if w.Header().Get("Set-Cookie") == "" {
|
||||||
|
t.Error(w.Header())
|
||||||
|
}
|
||||||
|
|
||||||
|
got, ok := loginCookie(r)
|
||||||
|
if !ok {
|
||||||
|
t.Error(ok)
|
||||||
|
}
|
||||||
|
if fmt.Sprint(user) != fmt.Sprint(got) {
|
||||||
|
t.Error(user, got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestChangeNamespace(t *testing.T) {
|
||||||
|
newTestServer(t)
|
||||||
|
user := User{
|
||||||
|
User: "user",
|
||||||
|
Groups: []string{"group", "othergroup"},
|
||||||
|
Group: "group",
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Run("noop", func(t *testing.T) {
|
||||||
|
r := httptest.NewRequest(http.MethodGet, "/", nil)
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
done, err := changeNamespace(w, r)
|
||||||
|
if err != nil {
|
||||||
|
t.Error(err)
|
||||||
|
}
|
||||||
|
if done {
|
||||||
|
t.Error(done)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("change to ``", func(t *testing.T) {
|
||||||
|
r := httptest.NewRequest(http.MethodGet, "/?namespace=", nil)
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
done, err := changeNamespace(w, r)
|
||||||
|
if err != nil {
|
||||||
|
t.Error(err)
|
||||||
|
}
|
||||||
|
if done {
|
||||||
|
t.Error(done)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("change to bad", func(t *testing.T) {
|
||||||
|
r := httptest.NewRequest(http.MethodGet, "/?namespace=never", nil)
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
setLoginCookie(w, r, user)
|
||||||
|
done, err := changeNamespace(w, r)
|
||||||
|
if err != nil {
|
||||||
|
t.Error(err)
|
||||||
|
}
|
||||||
|
if done {
|
||||||
|
t.Error(done)
|
||||||
|
}
|
||||||
|
user, ok := loginCookie(r)
|
||||||
|
if !ok {
|
||||||
|
t.Error(ok)
|
||||||
|
}
|
||||||
|
if user.Group == "never" {
|
||||||
|
t.Error("change namespace acknowledged bad change")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("change without login", func(t *testing.T) {
|
||||||
|
r := httptest.NewRequest(http.MethodGet, "/?namespace="+user.Group, nil)
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
done, err := changeNamespace(w, r)
|
||||||
|
if err != nil {
|
||||||
|
t.Error(err)
|
||||||
|
}
|
||||||
|
if !done {
|
||||||
|
t.Error(done)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("change to same", func(t *testing.T) {
|
||||||
|
r := httptest.NewRequest(http.MethodGet, "/?namespace="+user.Group, nil)
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
setLoginCookie(w, r, user)
|
||||||
|
done, err := changeNamespace(w, r)
|
||||||
|
if err != nil {
|
||||||
|
t.Error(err)
|
||||||
|
}
|
||||||
|
if done {
|
||||||
|
t.Error(done)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("change to ok", func(t *testing.T) {
|
||||||
|
r := httptest.NewRequest(http.MethodGet, "/?namespace="+user.Groups[1], nil)
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
setLoginCookie(w, r, user)
|
||||||
|
done, err := changeNamespace(w, r)
|
||||||
|
if err != nil {
|
||||||
|
t.Error(err)
|
||||||
|
}
|
||||||
|
if done {
|
||||||
|
t.Error(done)
|
||||||
|
}
|
||||||
|
user, ok := loginCookie(r)
|
||||||
|
if !ok {
|
||||||
|
t.Error(ok)
|
||||||
|
}
|
||||||
|
if user.Group != user.Groups[1] {
|
||||||
|
t.Error(user.Group)
|
||||||
|
}
|
||||||
|
if w.Header().Get("Set-Cookie") == "" {
|
||||||
|
t.Error(w.Header())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNeedsLogin(t *testing.T) {
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
user := User{User: "user", Groups: []string{"group0", "group1"}, Group: "group0"}
|
||||||
|
|
||||||
|
t.Run("no login provided", func(t *testing.T) {
|
||||||
|
r := httptest.NewRequest(http.MethodGet, "/", nil)
|
||||||
|
if ok, err := needsLogin(r); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
} else if !ok {
|
||||||
|
t.Fatal(ok)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("no namespace provided", func(t *testing.T) {
|
||||||
|
r := httptest.NewRequest(http.MethodGet, "/", nil)
|
||||||
|
u2 := user
|
||||||
|
u2.Group = ""
|
||||||
|
setLoginCookie(w, r, u2)
|
||||||
|
if ok, err := needsLogin(r); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
} else if !ok {
|
||||||
|
t.Fatal(ok)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("cookie tampered", func(t *testing.T) {
|
||||||
|
r := httptest.NewRequest(http.MethodGet, "/", nil)
|
||||||
|
setLoginCookie(w, r, user)
|
||||||
|
cookieSecret += "modified"
|
||||||
|
if ok, err := needsLogin(r); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
} else if !ok {
|
||||||
|
t.Fatal(ok)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("bad namespace", func(t *testing.T) {
|
||||||
|
r := httptest.NewRequest(http.MethodGet, "/", nil)
|
||||||
|
u2 := user
|
||||||
|
u2.Group = "teehee"
|
||||||
|
setLoginCookie(w, r, u2)
|
||||||
|
if ok, err := needsLogin(r); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
} else if !ok {
|
||||||
|
t.Fatal(ok)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("ok", func(t *testing.T) {
|
||||||
|
r := httptest.NewRequest(http.MethodGet, "/", nil)
|
||||||
|
setLoginCookie(w, r, user)
|
||||||
|
if ok, err := needsLogin(r); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
} else if ok {
|
||||||
|
t.Fatal(ok)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestServerParseLogin(t *testing.T) {
|
||||||
|
server := newTestServer(t)
|
||||||
|
|
||||||
|
t.Run("no basic auth", func(t *testing.T) {
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
r := httptest.NewRequest(http.MethodGet, "/", nil)
|
||||||
|
if done, err := server.parseLogin(w, r); done || err != nil {
|
||||||
|
t.Fatal(done, err)
|
||||||
|
}
|
||||||
|
if w.Code == http.StatusUnauthorized {
|
||||||
|
t.Error(w.Code)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("bad basic auth", func(t *testing.T) {
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
r := httptest.NewRequest(http.MethodGet, "/", nil)
|
||||||
|
r.SetBasicAuth("junk", "junk")
|
||||||
|
if done, err := server.parseLogin(w, r); !done || err != nil {
|
||||||
|
t.Fatal(done, err)
|
||||||
|
}
|
||||||
|
if w.Code != http.StatusUnauthorized {
|
||||||
|
t.Error(w.Code)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("ok", func(t *testing.T) {
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
r := httptest.NewRequest(http.MethodGet, "/", nil)
|
||||||
|
r.SetBasicAuth("user", "passw")
|
||||||
|
if done, err := server.parseLogin(w, r); done || err != nil {
|
||||||
|
t.Fatal(done, err)
|
||||||
|
}
|
||||||
|
if w.Code == http.StatusUnauthorized {
|
||||||
|
t.Error(w.Code)
|
||||||
|
}
|
||||||
|
if len(w.Header()["Set-Cookie"]) != 1 {
|
||||||
|
t.Error(w.Header())
|
||||||
|
}
|
||||||
|
if user, ok := loginCookie(r); !ok || user.User != "user" || user.Groups[0] != "group" || user.Groups[1] != "othergroup" {
|
||||||
|
t.Error(user)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestServerAuthenticate(t *testing.T) {
|
||||||
|
server := newTestServer(t)
|
||||||
|
|
||||||
|
t.Run("ok: already logged in", func(t *testing.T) {
|
||||||
|
r := httptest.NewRequest(http.MethodGet, "/", nil)
|
||||||
|
setLoginCookie(httptest.NewRecorder(), r, User{User: "user", Group: "othergroup", Groups: []string{"group", "othergroup"}})
|
||||||
|
s2, done, err := server.authenticate(nil, r)
|
||||||
|
if err != nil {
|
||||||
|
t.Error(err)
|
||||||
|
}
|
||||||
|
if done {
|
||||||
|
t.Error(done)
|
||||||
|
}
|
||||||
|
if server == s2 {
|
||||||
|
t.Error(done)
|
||||||
|
}
|
||||||
|
if server.user != nil {
|
||||||
|
t.Error(server.user)
|
||||||
|
}
|
||||||
|
if s2.user == nil {
|
||||||
|
t.Error(s2.user)
|
||||||
|
}
|
||||||
|
if s2.user.User != "user" {
|
||||||
|
t.Error(s2.user)
|
||||||
|
}
|
||||||
|
if s2.user.Group != "othergroup" {
|
||||||
|
t.Error(s2.user)
|
||||||
|
}
|
||||||
|
if fmt.Sprint(s2.user.Groups) != fmt.Sprint([]string{"group", "othergroup"}) {
|
||||||
|
t.Error(s2.user)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("ok: basic auth", func(t *testing.T) {
|
||||||
|
r := httptest.NewRequest(http.MethodGet, "/", nil)
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
r.SetBasicAuth("user", "passw")
|
||||||
|
s2, done, err := server.authenticate(w, r)
|
||||||
|
if err != nil {
|
||||||
|
t.Error(err)
|
||||||
|
}
|
||||||
|
if done {
|
||||||
|
t.Error(done)
|
||||||
|
}
|
||||||
|
if server == s2 {
|
||||||
|
t.Error(done)
|
||||||
|
}
|
||||||
|
if server.user != nil {
|
||||||
|
t.Error(server.user)
|
||||||
|
}
|
||||||
|
if s2.user == nil {
|
||||||
|
t.Error(s2.user)
|
||||||
|
}
|
||||||
|
if s2.user.User != "user" {
|
||||||
|
t.Error(s2.user)
|
||||||
|
}
|
||||||
|
if s2.user.Group != "group" {
|
||||||
|
t.Error(s2.user)
|
||||||
|
}
|
||||||
|
if fmt.Sprint(s2.user.Groups) != fmt.Sprint([]string{"group", "othergroup"}) {
|
||||||
|
t.Error(s2.user)
|
||||||
|
}
|
||||||
|
if w.Code != http.StatusOK {
|
||||||
|
t.Error(w.Code)
|
||||||
|
}
|
||||||
|
if len(w.Header()["Set-Cookie"]) != 1 {
|
||||||
|
t.Error(w.Header())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func newTestServer(t *testing.T) *Server {
|
||||||
|
cookieSecret = uuid.New().String()
|
||||||
|
p := path.Join(t.TempDir(), "auth.yaml")
|
||||||
|
ensureAndWrite(p, []byte(`{"users":{"user":{"password":"passw", "groups":["group", "othergroup"]}}}`))
|
||||||
|
return &Server{
|
||||||
|
auth: NewFileAuth(p),
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,23 +1,52 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"errors"
|
||||||
"local/args"
|
"local/args"
|
||||||
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"path"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
as := args.NewArgSet()
|
as := args.NewArgSet()
|
||||||
as.Append(args.INT, "p", "port to listen on", 3004)
|
as.Append(args.INT, "p", "port to listen on", 3004)
|
||||||
as.Append(args.STRING, "d", "root dir with /index.html and /media and /files", "./public")
|
as.Append(args.STRING, "d", "root dir with /index.html and /media and /files", "./public")
|
||||||
|
as.Append(args.STRING, "auth", "auth mode [none, path/to/some.yaml, ldap", "none")
|
||||||
if err := as.Parse(); err != nil {
|
if err := as.Parse(); err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
s := NewServer(as.GetString("d"))
|
auth, err := authFactory(as.GetString("auth"))
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
s := NewServer(as.GetString("d"), auth)
|
||||||
if err := s.Routes(); err != nil {
|
if err := s.Routes(); err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
|
log.Printf("listening on %v with %s", as.GetInt("p"), as.GetString("auth"))
|
||||||
if err := http.ListenAndServe(":"+strconv.Itoa(as.GetInt("p")), s); err != nil {
|
if err := http.ListenAndServe(":"+strconv.Itoa(as.GetInt("p")), s); err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func authFactory(key string) (auth, error) {
|
||||||
|
switch path.Base(strings.ToLower(key)) {
|
||||||
|
case "none", "":
|
||||||
|
return nil, nil
|
||||||
|
case "ldap":
|
||||||
|
return nil, errors.New("not impl ldap auth")
|
||||||
|
}
|
||||||
|
stat, err := os.Stat(key)
|
||||||
|
if os.IsNotExist(err) {
|
||||||
|
return nil, errors.New("looks like auth path does not exist")
|
||||||
|
} else if err != nil {
|
||||||
|
return nil, err
|
||||||
|
} else if stat.IsDir() {
|
||||||
|
return nil, errors.New("looks like auth path is a dir")
|
||||||
|
}
|
||||||
|
return NewFileAuth(key), nil
|
||||||
|
}
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
</header>
|
</header>
|
||||||
<body class="fullscreen tb_fullscreen lr_fullscreen" style="position: absolute">
|
<body class="fullscreen tb_fullscreen lr_fullscreen" style="position: absolute">
|
||||||
<div class="rows" style="height: inherit;">
|
<div class="rows" style="height: inherit;">
|
||||||
{{ template "_searchbar" }}
|
{{ template "_topbar" . }}
|
||||||
<div class="columns thic_flex tb_buffer" style="height: calc(100% - 4rem);">
|
<div class="columns thic_flex tb_buffer" style="height: calc(100% - 4rem);">
|
||||||
{{ template "_filetree" . }}
|
{{ template "_filetree" . }}
|
||||||
<div class="thic_flex lr_fullscreen" style="margin-left: 1em; width: 5px;">
|
<div class="thic_flex lr_fullscreen" style="margin-left: 1em; width: 5px;">
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ func main() {
|
|||||||
return oneT
|
return oneT
|
||||||
}
|
}
|
||||||
data := map[string]interface{}{
|
data := map[string]interface{}{
|
||||||
|
"Namespaces": []string{"datastore", "dp-orchestration"},
|
||||||
"This": map[string]interface{}{
|
"This": map[string]interface{}{
|
||||||
"ID": "id00/id11",
|
"ID": "id00/id11",
|
||||||
"Title": "title id11",
|
"Title": "title id11",
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
</header>
|
</header>
|
||||||
<body class="fullscreen tb_fullscreen lr_fullscreen" style="position: absolute">
|
<body class="fullscreen tb_fullscreen lr_fullscreen" style="position: absolute">
|
||||||
<div class="rows" style="height: inherit;">
|
<div class="rows" style="height: inherit;">
|
||||||
{{ template "_searchbar" }}
|
{{ template "_topbar" . }}
|
||||||
<div class="columns thic_flex tb_buffer" style="height: calc(100% - 4rem);">
|
<div class="columns thic_flex tb_buffer" style="height: calc(100% - 4rem);">
|
||||||
{{ template "_filetree" . }}
|
{{ template "_filetree" . }}
|
||||||
<div class="thic_flex lr_fullscreen" style="margin-left: 1em; width: 5px;">
|
<div class="thic_flex lr_fullscreen" style="margin-left: 1em; width: 5px;">
|
||||||
|
|||||||
16
server/public/ui/templates/_namespace.ctmpl
Normal file
16
server/public/ui/templates/_namespace.ctmpl
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
{{ define "_namespace" }}
|
||||||
|
<script>
|
||||||
|
function setNamespace() {
|
||||||
|
document.getElementById("namespace").disabled = true
|
||||||
|
window.location.href = `${window.location.protocol}`+"//"+`${window.location.host}/ui/files?namespace=${document.getElementById("namespace").value}`
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
{{ $cur := .Namespace }}
|
||||||
|
{{ if .Namespaces }}
|
||||||
|
<select id="namespace" onload="markNamespace()" onchange="setNamespace()" style="max-width: 7rem;">
|
||||||
|
{{ range .Namespaces }}
|
||||||
|
<option {{ if eq $cur . }}selected{{ end }}>{{ . }}</option>
|
||||||
|
{{ end }}
|
||||||
|
</select>
|
||||||
|
{{ end }}
|
||||||
|
{{ end }}
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
{{ define "_searchbar" }}
|
{{ define "_searchbar" }}
|
||||||
<form class="columns" action="/ui/search" method="GET">
|
<form class="columns thic_flex" action="/ui/search" method="GET">
|
||||||
<input class="thic_flex" type="text" name="q" placeholder="space delimited search regexp"/>
|
<input class="thic_flex" type="text" name="q" placeholder="space delimited search regexp"/>
|
||||||
<input class="info lil_btn" type="submit" value="search"/>
|
<input class="info lil_btn" type="submit" value="search"/>
|
||||||
</form>
|
</form>
|
||||||
|
|||||||
6
server/public/ui/templates/_topbar.ctmpl
Normal file
6
server/public/ui/templates/_topbar.ctmpl
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
{{ define "_topbar" }}
|
||||||
|
<div class="columns lr_fullscreen">
|
||||||
|
{{ template "_namespace" . }}
|
||||||
|
{{ template "_searchbar" . }}
|
||||||
|
</div>
|
||||||
|
{{ end }}
|
||||||
@@ -28,16 +28,29 @@ import (
|
|||||||
type Server struct {
|
type Server struct {
|
||||||
router *router.Router
|
router *router.Router
|
||||||
root string
|
root string
|
||||||
|
auth auth
|
||||||
|
user *User
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewServer(root string) *Server {
|
func NewServer(root string, auth auth) *Server {
|
||||||
return &Server{
|
return &Server{
|
||||||
router: router.New(),
|
|
||||||
root: root,
|
root: root,
|
||||||
|
auth: auth,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (server *Server) WithUser(user, group string, groups []string) *Server {
|
||||||
|
s2 := *server
|
||||||
|
s2.user = &User{
|
||||||
|
User: user,
|
||||||
|
Group: group,
|
||||||
|
Groups: groups,
|
||||||
|
}
|
||||||
|
return &s2
|
||||||
|
}
|
||||||
|
|
||||||
func (server *Server) Routes() error {
|
func (server *Server) Routes() error {
|
||||||
|
server.router = router.New()
|
||||||
wildcard := func(s string) string {
|
wildcard := func(s string) string {
|
||||||
return strings.TrimSuffix(s, "/") + "/" + router.Wildcard
|
return strings.TrimSuffix(s, "/") + "/" + router.Wildcard
|
||||||
}
|
}
|
||||||
@@ -56,7 +69,6 @@ func (server *Server) Routes() error {
|
|||||||
"/ui/search": server.uiSearchHandler,
|
"/ui/search": server.uiSearchHandler,
|
||||||
wildcards("/ui/files"): server.uiFilesHandler,
|
wildcards("/ui/files"): server.uiFilesHandler,
|
||||||
} {
|
} {
|
||||||
log.Printf("listening for %s", path)
|
|
||||||
if err := server.router.Add(path, server.tryCatchHttpHandler(handler)); err != nil {
|
if err := server.router.Add(path, server.tryCatchHttpHandler(handler)); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -65,6 +77,22 @@ func (server *Server) Routes() error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (server *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
func (server *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if server.auth != nil {
|
||||||
|
s2, done, err := server.authenticate(w, r)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if done {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if s2 != nil {
|
||||||
|
server = s2
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err := server.Routes(); err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
}
|
||||||
server.router.ServeHTTP(w, r)
|
server.router.ServeHTTP(w, r)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -237,9 +265,18 @@ func (server *Server) uiSearchHandler(w http.ResponseWriter, r *http.Request) er
|
|||||||
return t.Lookup("search").Execute(w, map[string]interface{}{
|
return t.Lookup("search").Execute(w, map[string]interface{}{
|
||||||
"Results": data,
|
"Results": data,
|
||||||
"Tree": string(branchesJSON),
|
"Tree": string(branchesJSON),
|
||||||
|
"Namespaces": server.getUser().Groups,
|
||||||
|
"Namespace": server.getUser().Group,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (server *Server) getUser() User {
|
||||||
|
if server.user != nil {
|
||||||
|
return *server.user
|
||||||
|
}
|
||||||
|
return User{}
|
||||||
|
}
|
||||||
|
|
||||||
func (server *Server) uiFilesHandler(w http.ResponseWriter, r *http.Request) error {
|
func (server *Server) uiFilesHandler(w http.ResponseWriter, r *http.Request) error {
|
||||||
id := NewID(strings.TrimPrefix(r.URL.Path, "/ui/files"))
|
id := NewID(strings.TrimPrefix(r.URL.Path, "/ui/files"))
|
||||||
t, err := server.uiSubTemplates()
|
t, err := server.uiSubTemplates()
|
||||||
@@ -290,6 +327,8 @@ func (server *Server) uiFilesHandler(w http.ResponseWriter, r *http.Request) err
|
|||||||
"PTitle": parent.Meta.Title,
|
"PTitle": parent.Meta.Title,
|
||||||
},
|
},
|
||||||
"Tree": string(branchesJSON),
|
"Tree": string(branchesJSON),
|
||||||
|
"Namespaces": server.getUser().Groups,
|
||||||
|
"Namespace": server.getUser().Group,
|
||||||
}
|
}
|
||||||
return t.Lookup("files").Execute(w, data)
|
return t.Lookup("files").Execute(w, data)
|
||||||
}
|
}
|
||||||
@@ -327,7 +366,7 @@ func (server *Server) rootHandler(w http.ResponseWriter, r *http.Request) error
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (server *Server) tree() Tree {
|
func (server *Server) tree() Tree {
|
||||||
return NewTree(path.Join(server.root, "files"))
|
return NewTree(path.Join(server.root, "files", server.getUser().Group))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (server *Server) diskMediaPath(id string) string {
|
func (server *Server) diskMediaPath(id string) string {
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func TestServerRoutes(t *testing.T) {
|
func TestServerRoutes(t *testing.T) {
|
||||||
server := NewServer(t.TempDir())
|
server := NewServer(t.TempDir(), nil)
|
||||||
if err := server.Routes(); err != nil {
|
if err := server.Routes(); err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
@@ -153,7 +153,7 @@ func TestServerRoutes(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestServerPutTreeGetFile(t *testing.T) {
|
func TestServerPutTreeGetFile(t *testing.T) {
|
||||||
server := NewServer(t.TempDir())
|
server := NewServer(t.TempDir(), nil)
|
||||||
if err := server.Routes(); err != nil {
|
if err := server.Routes(); err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|||||||
6
server/testdata/users.yaml
vendored
Normal file
6
server/testdata/users.yaml
vendored
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
users:
|
||||||
|
bel:
|
||||||
|
password: bel
|
||||||
|
groups:
|
||||||
|
- g1
|
||||||
|
- g2
|
||||||
@@ -1,7 +1,14 @@
|
|||||||
todo:
|
todo:
|
||||||
|
- logout
|
||||||
|
- encrypt files at docker build time, put decrypt key in vault
|
||||||
|
- create fileauth login file
|
||||||
|
- secret for cookie encrypt+decrypt
|
||||||
|
- secrets
|
||||||
|
- team-specific deployment;; prob grab a VM
|
||||||
- mark generated via meta so other files in the dir can be created, deleted, replaced safely
|
- mark generated via meta so other files in the dir can be created, deleted, replaced safely
|
||||||
- links like `/Smoktests` in user-files home wiki don't rewrite
|
- links like `/Smoktests` in user-files home wiki don't rewrite
|
||||||
- map fullURLScraped->internalURL for relative links sometimes
|
- map fullURLScraped->internalURL for relative links sometimes
|
||||||
|
- LDAP login
|
||||||
- scrape odo
|
- scrape odo
|
||||||
- rewrite links if available to local
|
- rewrite links if available to local
|
||||||
- anchor per line
|
- anchor per line
|
||||||
|
|||||||
Reference in New Issue
Block a user