From 4657dd9505fd6af75e059a2d40c74bba17f33830 Mon Sep 17 00:00:00 2001 From: Bel LaPointe Date: Fri, 18 Feb 2022 10:53:45 -0700 Subject: [PATCH] i cant uncache chrome fuckit --- server/authenticate.go | 133 ++++++++++----- server/authenticate_test.go | 170 +++++++++++++++----- server/main.go | 2 + server/public/ui/render.go | 12 +- server/public/ui/templates/_namespace.ctmpl | 28 ++-- server/server.go | 62 ++++--- server/testdata/users.yaml | 6 + 7 files changed, 289 insertions(+), 124 deletions(-) create mode 100644 server/testdata/users.yaml diff --git a/server/authenticate.go b/server/authenticate.go index e7a1621..8a8059f 100644 --- a/server/authenticate.go +++ b/server/authenticate.go @@ -1,10 +1,12 @@ package main import ( + "context" "encoding/base64" "encoding/json" "errors" "hash/crc32" + "log" "net/http" "os" "time" @@ -16,9 +18,21 @@ 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 @@ -27,21 +41,30 @@ type Cookie struct { 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 } - if ok, err := needsLogin(r); err != nil { - return nil, false, err - } else if ok { - promptLogin(w) - return nil, true, nil - } - user, _ := loginCookie(r) - namespace, _ := namespaceCookie(r) - return server.WithLoggedIn(user.User, namespace, user.Groups), false, nil + return server.WithUser(user.User, user.Group, user.Groups), false, nil } func promptLogin(w http.ResponseWriter) { @@ -54,6 +77,7 @@ func (server *Server) parseLogin(w http.ResponseWriter, r *http.Request) (bool, if !ok { return false, nil } + ok, err := server.auth.Login(username, password) if err != nil { return false, err @@ -62,6 +86,7 @@ func (server *Server) parseLogin(w http.ResponseWriter, r *http.Request) (bool, promptLogin(w) return true, nil } + groups, err := server.auth.Groups(username) if err != nil { return false, err @@ -69,11 +94,47 @@ func (server *Server) parseLogin(w http.ResponseWriter, r *http.Request) (bool, if len(groups) == 0 { return false, errors.New("user has no groups") } - setLoginCookie(w, r, User{ + + user := User{ User: username, Groups: groups, - }) - setNamespaceCookie(w, r, groups[0]) + 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 } @@ -82,12 +143,8 @@ func needsLogin(r *http.Request) (bool, error) { if !ok { return true, nil } - group, ok := namespaceCookie(r) - if !ok { - return true, nil - } for i := range user.Groups { - if group == user.Groups[i] { + if user.Group == user.Groups[i] { return false, nil } } @@ -99,41 +156,39 @@ func setLoginCookie(w http.ResponseWriter, r *http.Request, user User) { Name: "login", Value: encodeUserCookie(user), Expires: time.Now().Add(time.Hour * 24), + Path: "/", } - w.Header().Add("Set-Cookie", cookie.String()) - r.AddCookie(cookie) + 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 setNamespaceCookie(w http.ResponseWriter, r *http.Request, s string) { - cookie := &http.Cookie{ - Name: "namespace", - Value: s, - Expires: time.Now().Add(time.Hour * 24), - } - w.Header().Add("Set-Cookie", cookie.String()) - r.AddCookie(cookie) -} - -func namespaceCookie(r *http.Request) (string, bool) { - return getCookie("namespace", r) -} - func getCookie(key string, r *http.Request) (string, bool) { - cookies := r.Cookies() - for i := range cookies { - if cookies[i].Name == key && (cookies[i].Expires.IsZero() || time.Now().Before(cookies[i].Expires)) { - return cookies[i].Value, true - } + cookie, err := r.Cookie(key) + if err != nil { + log.Printf("err getting cookie %s: %v: %+v", key, err, r.Cookies()) + return "", false } - return "", false + return cookie.Value, cookie.Expires.IsZero() || time.Now().Before(cookie.Expires) } func decodeUserCookie(raw string) (User, bool) { diff --git a/server/authenticate_test.go b/server/authenticate_test.go index ae72a64..7519d55 100644 --- a/server/authenticate_test.go +++ b/server/authenticate_test.go @@ -65,9 +65,6 @@ func TestGetSetLoginCookie(t *testing.T) { if w.Header().Get("Set-Cookie") == "" { t.Error(w.Header()) } - if r.Cookies()[0].Name != "login" { - t.Error(r.Cookies()) - } got, ok := loginCookie(r) if !ok { @@ -78,9 +75,110 @@ func TestGetSetLoginCookie(t *testing.T) { } } +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"}} + 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) @@ -93,7 +191,9 @@ func TestNeedsLogin(t *testing.T) { t.Run("no namespace provided", func(t *testing.T) { r := httptest.NewRequest(http.MethodGet, "/", nil) - setLoginCookie(w, r, user) + u2 := user + u2.Group = "" + setLoginCookie(w, r, u2) if ok, err := needsLogin(r); err != nil { t.Fatal(err) } else if !ok { @@ -104,7 +204,6 @@ func TestNeedsLogin(t *testing.T) { t.Run("cookie tampered", func(t *testing.T) { r := httptest.NewRequest(http.MethodGet, "/", nil) setLoginCookie(w, r, user) - setNamespaceCookie(w, r, user.Groups[0]) cookieSecret += "modified" if ok, err := needsLogin(r); err != nil { t.Fatal(err) @@ -115,8 +214,9 @@ func TestNeedsLogin(t *testing.T) { t.Run("bad namespace", func(t *testing.T) { r := httptest.NewRequest(http.MethodGet, "/", nil) - setLoginCookie(w, r, user) - setNamespaceCookie(w, r, "teehee") + u2 := user + u2.Group = "teehee" + setLoginCookie(w, r, u2) if ok, err := needsLogin(r); err != nil { t.Fatal(err) } else if !ok { @@ -127,7 +227,6 @@ func TestNeedsLogin(t *testing.T) { t.Run("ok", func(t *testing.T) { r := httptest.NewRequest(http.MethodGet, "/", nil) setLoginCookie(w, r, user) - setNamespaceCookie(w, r, user.Groups[0]) if ok, err := needsLogin(r); err != nil { t.Fatal(err) } else if ok { @@ -172,15 +271,9 @@ func TestServerParseLogin(t *testing.T) { if w.Code == http.StatusUnauthorized { t.Error(w.Code) } - if len(w.Header()["Set-Cookie"]) != 2 { + if len(w.Header()["Set-Cookie"]) != 1 { t.Error(w.Header()) } - if len(r.Cookies()) != 2 { - t.Error(r.Cookies()) - } - if v, ok := namespaceCookie(r); !ok || v != "group" { - t.Error(r.Cookies()) - } if user, ok := loginCookie(r); !ok || user.User != "user" || user.Groups[0] != "group" || user.Groups[1] != "othergroup" { t.Error(user) } @@ -192,8 +285,7 @@ func TestServerAuthenticate(t *testing.T) { t.Run("ok: already logged in", func(t *testing.T) { r := httptest.NewRequest(http.MethodGet, "/", nil) - setLoginCookie(httptest.NewRecorder(), r, User{User: "user", Groups: []string{"group", "othergroup"}}) - setNamespaceCookie(httptest.NewRecorder(), r, "othergroup") + 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) @@ -204,20 +296,20 @@ func TestServerAuthenticate(t *testing.T) { if server == s2 { t.Error(done) } - if server.loggedIn != nil { - t.Error(server.loggedIn) + if server.user != nil { + t.Error(server.user) } - if s2.loggedIn == nil { - t.Error(s2.loggedIn) + if s2.user == nil { + t.Error(s2.user) } - if s2.loggedIn.user != "user" { - t.Error(s2.loggedIn) + if s2.user.User != "user" { + t.Error(s2.user) } - if s2.loggedIn.group != "othergroup" { - t.Error(s2.loggedIn) + if s2.user.Group != "othergroup" { + t.Error(s2.user) } - if fmt.Sprint(s2.loggedIn.groups) != fmt.Sprint([]string{"group", "othergroup"}) { - t.Error(s2.loggedIn) + if fmt.Sprint(s2.user.Groups) != fmt.Sprint([]string{"group", "othergroup"}) { + t.Error(s2.user) } }) @@ -235,25 +327,25 @@ func TestServerAuthenticate(t *testing.T) { if server == s2 { t.Error(done) } - if server.loggedIn != nil { - t.Error(server.loggedIn) + if server.user != nil { + t.Error(server.user) } - if s2.loggedIn == nil { - t.Error(s2.loggedIn) + if s2.user == nil { + t.Error(s2.user) } - if s2.loggedIn.user != "user" { - t.Error(s2.loggedIn) + if s2.user.User != "user" { + t.Error(s2.user) } - if s2.loggedIn.group != "group" { - t.Error(s2.loggedIn) + if s2.user.Group != "group" { + t.Error(s2.user) } - if fmt.Sprint(s2.loggedIn.groups) != fmt.Sprint([]string{"group", "othergroup"}) { - t.Error(s2.loggedIn) + 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"]) != 2 { + if len(w.Header()["Set-Cookie"]) != 1 { t.Error(w.Header()) } }) diff --git a/server/main.go b/server/main.go index b40663c..271ede1 100644 --- a/server/main.go +++ b/server/main.go @@ -3,6 +3,7 @@ package main import ( "errors" "local/args" + "log" "net/http" "os" "path" @@ -26,6 +27,7 @@ func main() { if err := s.Routes(); err != nil { 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 { panic(err) } diff --git a/server/public/ui/render.go b/server/public/ui/render.go index 57fcdb5..4482782 100644 --- a/server/public/ui/render.go +++ b/server/public/ui/render.go @@ -36,13 +36,13 @@ func main() { return oneT } data := map[string]interface{}{ + "Namespaces": []string{"datastore", "dp-orchestration"}, "This": map[string]interface{}{ - "Namespaces": []string{"datastore", "dp-orchestration"}, - "ID": "id00/id11", - "Title": "title id11", - "ReadOnly": false, - "PID": "id00", - "PTitle": "title id00", + "ID": "id00/id11", + "Title": "title id11", + "ReadOnly": false, + "PID": "id00", + "PTitle": "title id00", "Content": `# hello ## world diff --git a/server/public/ui/templates/_namespace.ctmpl b/server/public/ui/templates/_namespace.ctmpl index 3b17f96..4cf1ccb 100644 --- a/server/public/ui/templates/_namespace.ctmpl +++ b/server/public/ui/templates/_namespace.ctmpl @@ -1,16 +1,16 @@ {{ define "_namespace" }} - -{{ if .This.Namespaces }} - -{{ end }} + + {{ $cur := .Namespace }} + {{ if .Namespaces }} + + {{ end }} {{ end }} diff --git a/server/server.go b/server/server.go index 04ad180..0ef9a50 100644 --- a/server/server.go +++ b/server/server.go @@ -26,37 +26,31 @@ import ( ) type Server struct { - router *router.Router - root string - auth auth - loggedIn *loggedIn -} - -type loggedIn struct { - user string - group string - groups []string + router *router.Router + root string + auth auth + user *User } func NewServer(root string, auth auth) *Server { return &Server{ - router: router.New(), - root: root, - auth: auth, + root: root, + auth: auth, } } -func (server *Server) WithLoggedIn(user, group string, groups []string) *Server { +func (server *Server) WithUser(user, group string, groups []string) *Server { s2 := *server - s2.loggedIn = &loggedIn{ - user: user, - group: group, - groups: groups, + s2.user = &User{ + User: user, + Group: group, + Groups: groups, } return &s2 } func (server *Server) Routes() error { + server.router = router.New() wildcard := func(s string) string { return strings.TrimSuffix(s, "/") + "/" + router.Wildcard } @@ -75,7 +69,6 @@ func (server *Server) Routes() error { "/ui/search": server.uiSearchHandler, wildcards("/ui/files"): server.uiFilesHandler, } { - log.Printf("listening for %s", path) if err := server.router.Add(path, server.tryCatchHttpHandler(handler)); err != nil { return err } @@ -85,15 +78,21 @@ func (server *Server) Routes() error { func (server *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) { if server.auth != nil { - if s2, done, err := server.authenticate(w, r); err != nil { + s2, done, err := server.authenticate(w, r) + if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return - } else if done { + } + if done { return - } else if s2 != nil { + } + if s2 != nil { server = s2 } } + if err := server.Routes(); err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + } server.router.ServeHTTP(w, r) } @@ -264,11 +263,20 @@ func (server *Server) uiSearchHandler(w http.ResponseWriter, r *http.Request) er return err } return t.Lookup("search").Execute(w, map[string]interface{}{ - "Results": data, - "Tree": string(branchesJSON), + "Results": data, + "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 { id := NewID(strings.TrimPrefix(r.URL.Path, "/ui/files")) t, err := server.uiSubTemplates() @@ -318,7 +326,9 @@ func (server *Server) uiFilesHandler(w http.ResponseWriter, r *http.Request) err "PID": id.Pop().String(), "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) } @@ -356,7 +366,7 @@ func (server *Server) rootHandler(w http.ResponseWriter, r *http.Request) error } 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 { diff --git a/server/testdata/users.yaml b/server/testdata/users.yaml new file mode 100644 index 0000000..f0231a1 --- /dev/null +++ b/server/testdata/users.yaml @@ -0,0 +1,6 @@ +users: + bel: + password: bel + groups: + - g1 + - g2