Test auth and add scope

master
breel 2020-08-08 09:42:02 -06:00
parent 46bd1bbdfc
commit 1a06d9634b
7 changed files with 294 additions and 17 deletions

View File

@ -4,4 +4,5 @@ const (
AuthKey = "DnDex-Auth"
UserKey = "DnDex-User"
NewAuthKey = "New-" + AuthKey
ScopeKey = "Scope"
)

View File

@ -9,19 +9,35 @@ import (
"github.com/google/uuid"
)
func Generate(g storage.RateLimitedGraph, r *http.Request, salt string) (string, error) {
namespaceRequested := readRequestedNamespace(r)
key, err := getKeyForNamespace(r.Context(), g, namespaceRequested)
func GeneratePlain(g storage.RateLimitedGraph, r *http.Request) (string, error) {
token, _, err := generateToken(g, r)
if err != nil {
return "", err
}
token, err := makeTokenForNamespace(r.Context(), g, namespaceRequested)
return token.Obfuscate()
}
func Generate(g storage.RateLimitedGraph, r *http.Request, salt string) (string, error) {
token, key, err := generateToken(g, r)
if err != nil {
return "", err
}
return token.Encode(salt + key)
}
func generateToken(g storage.RateLimitedGraph, r *http.Request) (Token, string, error) {
namespaceRequested := readRequestedNamespace(r)
key, err := getKeyForNamespace(r.Context(), g, namespaceRequested)
if err != nil {
return Token{}, "", err
}
token, err := makeTokenForNamespace(r.Context(), g, namespaceRequested)
if err != nil {
return Token{}, "", err
}
return token, key, nil
}
func readRequestedNamespace(r *http.Request) string {
return readRequested(r, UserKey)
}

View File

@ -2,6 +2,7 @@ package auth
import (
"context"
"io"
"local/dndex/storage"
"local/dndex/storage/entity"
"net/http"
@ -48,4 +49,49 @@ func TestGenerate(t *testing.T) {
t.Fatal(err)
}
})
t.Run("ok plain", func(t *testing.T) {
g, r, _ := fresh()
obf, err := GeneratePlain(g, r)
if err != nil {
t.Fatal(err)
}
var token Token
if err := token.Deobfuscate(obf); err != nil {
t.Fatal(err)
}
})
t.Run("404", func(t *testing.T) {
g, r, _ := fresh()
r.Body = struct {
io.Reader
io.Closer
}{
Reader: strings.NewReader(UserKey + "=" + uuid.New().String()),
Closer: r.Body,
}
r.ParseForm()
salt := uuid.New().String()
_, err := Generate(g, r, salt)
if err == nil {
t.Fatal(err)
}
})
t.Run("404 plain", func(t *testing.T) {
g, r, _ := fresh()
r.Body = struct {
io.Reader
io.Closer
}{
Reader: strings.NewReader(UserKey + "=" + uuid.New().String()),
Closer: r.Body,
}
r.ParseForm()
_, err := GeneratePlain(g, r)
if err == nil {
t.Fatal(err)
}
})
}

75
server/auth/scope.go Normal file
View File

@ -0,0 +1,75 @@
package auth
import (
"context"
"local/dndex/storage"
"net/http"
"strings"
)
type Scope struct {
Namespace string
EntityName string
EntityID string
}
func GetScope(r *http.Request, gs ...storage.RateLimitedGraph) Scope {
scope := Scope{}
if ok := scope.fromCtx(r.Context()); ok {
return scope
}
scope.fromToken(r)
scope.fromPath(r.URL.Path)
for _, g := range gs {
scope.fromGraph(r.Context(), g)
}
reqWithContextValue(r, ScopeKey, scope)
return scope
}
func reqWithContextValue(r *http.Request, k string, v interface{}) {
*r = *(r.WithContext(context.WithValue(r.Context(), k, v)))
}
func (s *Scope) fromCtx(ctx context.Context) bool {
var ok bool
*s, ok = ctx.Value(ScopeKey).(Scope)
return ok
}
func (s *Scope) fromToken(r *http.Request) bool {
token, ok := getToken(r)
if ok {
s.Namespace = token.Namespace
}
return ok
}
func (s *Scope) fromPath(path string) bool {
if !strings.HasPrefix(path, "/entity/") {
return false
}
paths := strings.Split(path, "/")
if len(paths) < 2 {
return false
}
path = paths[2]
path = strings.Split(path, "#")[0]
if len(path) == 0 {
return false
}
s.EntityID = path
return true
}
func (s *Scope) fromGraph(ctx context.Context, g storage.RateLimitedGraph) bool {
if s.EntityID == "" {
return false
}
one, err := g.Get(ctx, s.Namespace, s.EntityID)
ok := err == nil && one.Name != ""
if ok {
s.EntityName = one.Name
}
return ok
}

146
server/auth/scope_test.go Normal file
View File

@ -0,0 +1,146 @@
package auth
import (
"context"
"local/dndex/storage"
"local/dndex/storage/entity"
"net/http"
"net/http/httptest"
"testing"
"github.com/google/uuid"
)
func TestScopeFromCtx(t *testing.T) {
var scope Scope
if ok := scope.fromCtx(context.TODO()); ok {
t.Fatal(ok)
}
ctxScope := Scope{
Namespace: uuid.New().String(),
EntityName: uuid.New().String(),
EntityID: uuid.New().String(),
}
ctx := context.WithValue(context.TODO(), ScopeKey, ctxScope)
if ok := scope.fromCtx(ctx); !ok {
t.Fatal(ok)
} else if ctxScope != scope {
t.Fatal(scope)
}
}
func TestScopeFromToken(t *testing.T) {
var scope Scope
r := httptest.NewRequest(http.MethodGet, "/", nil)
if ok := scope.fromToken(r); ok {
t.Fatal(ok)
}
token := Token{
Namespace: uuid.New().String(),
}
obf, _ := token.Obfuscate()
r.AddCookie(&http.Cookie{
Name: AuthKey,
Value: obf,
})
if ok := scope.fromToken(r); !ok {
t.Fatal(ok)
}
}
func TestScopeFromPath(t *testing.T) {
cases := map[string]struct {
ok bool
id string
}{
"/": {},
"/hello": {},
"/hello/": {},
"/hello/entity": {},
"/entity": {},
"/entity/": {},
"/entity/id": {
ok: true,
id: "id",
},
"/entity/id/": {
ok: true,
id: "id",
},
"/entity/id/excess": {
ok: true,
id: "id",
},
"/entity/id#excess": {
ok: true,
id: "id",
},
"/entity/id?excess": {
ok: true,
id: "id",
},
}
for name, d := range cases {
c := d
path := name
t.Run(name, func(t *testing.T) {
r := httptest.NewRequest(http.MethodGet, path, nil)
var scope Scope
ok := scope.fromPath(r.URL.Path)
if ok != c.ok {
t.Fatal(c.ok, ok)
}
if scope.EntityID != c.id {
t.Fatalf("want %q, got %q", c.id, scope.EntityID)
}
})
}
}
// func (s *Scope) fromGraph(ctx context.Context, g storage.RateLimitedGraph) bool {
func TestScopeFromGraph(t *testing.T) {
namespace := uuid.New().String()
id := uuid.New().String()
name := uuid.New().String()
gEmpty := storage.NewRateLimitedGraph()
gOne := storage.NewRateLimitedGraph()
if err := gOne.Insert(context.TODO(), namespace, entity.One{ID: id, Name: name}); err != nil {
t.Fatal(err)
}
scope := Scope{
Namespace: namespace,
}
if ok := scope.fromGraph(context.TODO(), gEmpty); ok {
t.Fatal(ok)
} else if scope.EntityName != "" {
t.Fatal(scope)
}
if ok := scope.fromGraph(context.TODO(), gOne); ok {
t.Fatal(ok)
} else if scope.EntityName != "" {
t.Fatal(scope)
}
scope.EntityID = id
if ok := scope.fromGraph(context.TODO(), gEmpty); ok {
t.Fatal(ok)
} else if scope.EntityName != "" {
t.Fatal(scope)
}
if ok := scope.fromGraph(context.TODO(), gOne); !ok {
t.Fatal(ok)
} else if scope.EntityName != name {
t.Fatal(scope)
}
}

View File

@ -3,6 +3,7 @@ package server
import (
"io"
"local/dndex/config"
"local/dndex/server/auth"
"local/gziphttp"
"net/http"
"time"
@ -39,7 +40,8 @@ func (rest *REST) defend(foo http.HandlerFunc) http.HandlerFunc {
func (rest *REST) auth(foo http.HandlerFunc) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
if err := Auth(rest.g, w, r); err != nil {
if err := auth.Verify(rest.g, w, r); err != nil {
http.Error(w, err.Error(), http.StatusUnauthorized)
return
}
foo(w, r)

View File

@ -3,6 +3,7 @@ package server
import (
"fmt"
"local/dndex/config"
"local/dndex/server/auth"
"local/dndex/storage"
"local/router"
"net/http"
@ -14,15 +15,6 @@ type REST struct {
g storage.RateLimitedGraph
}
type RESTScope struct {
entity scope
user scope
}
type scope struct {
name string
id string
}
func Listen(g storage.RateLimitedGraph) error {
rest, err := NewREST(g)
if err != nil {
@ -62,9 +54,8 @@ func NewREST(g storage.RateLimitedGraph) (*REST, error) {
return rest, nil
}
func (rest *REST) scope(r *http.Request) RESTScope {
value, _ := r.Context().Value(AuthKey).(RESTScope)
return value
func (rest *REST) scope(r *http.Request) auth.Scope {
return auth.GetScope(r)
}
func (rest *REST) files(w http.ResponseWriter, _ *http.Request) {