Test auth and add scope
parent
46bd1bbdfc
commit
1a06d9634b
|
|
@ -4,4 +4,5 @@ const (
|
||||||
AuthKey = "DnDex-Auth"
|
AuthKey = "DnDex-Auth"
|
||||||
UserKey = "DnDex-User"
|
UserKey = "DnDex-User"
|
||||||
NewAuthKey = "New-" + AuthKey
|
NewAuthKey = "New-" + AuthKey
|
||||||
|
ScopeKey = "Scope"
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -9,19 +9,35 @@ import (
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
)
|
)
|
||||||
|
|
||||||
func Generate(g storage.RateLimitedGraph, r *http.Request, salt string) (string, error) {
|
func GeneratePlain(g storage.RateLimitedGraph, r *http.Request) (string, error) {
|
||||||
namespaceRequested := readRequestedNamespace(r)
|
token, _, err := generateToken(g, r)
|
||||||
key, err := getKeyForNamespace(r.Context(), g, namespaceRequested)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
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 {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
return token.Encode(salt + key)
|
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 {
|
func readRequestedNamespace(r *http.Request) string {
|
||||||
return readRequested(r, UserKey)
|
return readRequested(r, UserKey)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ package auth
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"io"
|
||||||
"local/dndex/storage"
|
"local/dndex/storage"
|
||||||
"local/dndex/storage/entity"
|
"local/dndex/storage/entity"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
@ -48,4 +49,49 @@ func TestGenerate(t *testing.T) {
|
||||||
t.Fatal(err)
|
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)
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
@ -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)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -3,6 +3,7 @@ package server
|
||||||
import (
|
import (
|
||||||
"io"
|
"io"
|
||||||
"local/dndex/config"
|
"local/dndex/config"
|
||||||
|
"local/dndex/server/auth"
|
||||||
"local/gziphttp"
|
"local/gziphttp"
|
||||||
"net/http"
|
"net/http"
|
||||||
"time"
|
"time"
|
||||||
|
|
@ -39,7 +40,8 @@ func (rest *REST) defend(foo http.HandlerFunc) http.HandlerFunc {
|
||||||
|
|
||||||
func (rest *REST) auth(foo http.HandlerFunc) http.HandlerFunc {
|
func (rest *REST) auth(foo http.HandlerFunc) http.HandlerFunc {
|
||||||
return func(w http.ResponseWriter, r *http.Request) {
|
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
|
return
|
||||||
}
|
}
|
||||||
foo(w, r)
|
foo(w, r)
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ package server
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"local/dndex/config"
|
"local/dndex/config"
|
||||||
|
"local/dndex/server/auth"
|
||||||
"local/dndex/storage"
|
"local/dndex/storage"
|
||||||
"local/router"
|
"local/router"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
@ -14,15 +15,6 @@ type REST struct {
|
||||||
g storage.RateLimitedGraph
|
g storage.RateLimitedGraph
|
||||||
}
|
}
|
||||||
|
|
||||||
type RESTScope struct {
|
|
||||||
entity scope
|
|
||||||
user scope
|
|
||||||
}
|
|
||||||
type scope struct {
|
|
||||||
name string
|
|
||||||
id string
|
|
||||||
}
|
|
||||||
|
|
||||||
func Listen(g storage.RateLimitedGraph) error {
|
func Listen(g storage.RateLimitedGraph) error {
|
||||||
rest, err := NewREST(g)
|
rest, err := NewREST(g)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
@ -62,9 +54,8 @@ func NewREST(g storage.RateLimitedGraph) (*REST, error) {
|
||||||
return rest, nil
|
return rest, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (rest *REST) scope(r *http.Request) RESTScope {
|
func (rest *REST) scope(r *http.Request) auth.Scope {
|
||||||
value, _ := r.Context().Value(AuthKey).(RESTScope)
|
return auth.GetScope(r)
|
||||||
return value
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (rest *REST) files(w http.ResponseWriter, _ *http.Request) {
|
func (rest *REST) files(w http.ResponseWriter, _ *http.Request) {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue