snapshot of https://github.com/traggo/server.git (master, 2026-09-16; blobs > 131072B omitted)
Some checks failed
build / build (push) Failing after 3s
Some checks failed
build / build (push) Failing after 3s
This commit is contained in:
29
auth/cleanup.go
Normal file
29
auth/cleanup.go
Normal file
@@ -0,0 +1,29 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/jinzhu/gorm"
|
||||
"github.com/jmattheis/go-timemath"
|
||||
"github.com/rs/zerolog/log"
|
||||
"github.com/traggo/server/model"
|
||||
)
|
||||
|
||||
var timeNow = time.Now
|
||||
|
||||
// CleanUp clean up expired devices
|
||||
func CleanUp(db *gorm.DB, interval time.Duration, close chan bool) {
|
||||
for {
|
||||
select {
|
||||
case <-time.After(interval):
|
||||
affected := db.Where("type = ? AND active_at < ?", model.TypeLongExpiry, timemath.Second.Subtract(timeNow(), model.TypeLongExpiry.Seconds())).
|
||||
Or("type = ? AND active_at < ?", model.TypeShortExpiry, timemath.Second.Subtract(timeNow(), model.TypeShortExpiry.Seconds())).
|
||||
Delete(new(model.Device)).RowsAffected
|
||||
if affected > 0 {
|
||||
log.Debug().Int64("amount", affected).Msg("removed devices")
|
||||
}
|
||||
case <-close:
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
138
auth/cleanup_test.go
Normal file
138
auth/cleanup_test.go
Normal file
@@ -0,0 +1,138 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/traggo/server/model"
|
||||
"github.com/traggo/server/test"
|
||||
)
|
||||
|
||||
func TestCleanUp_stops(t *testing.T) {
|
||||
db := test.InMemoryDB(t)
|
||||
defer db.Close()
|
||||
|
||||
methodDone := make(chan struct{})
|
||||
stopCleanUp := make(chan bool)
|
||||
|
||||
go func() {
|
||||
CleanUp(db.DB, time.Microsecond*10, stopCleanUp)
|
||||
methodDone <- struct{}{}
|
||||
}()
|
||||
|
||||
stopCleanUp <- true
|
||||
|
||||
select {
|
||||
case <-methodDone:
|
||||
// expected
|
||||
case <-time.After(time.Second):
|
||||
t.Fail()
|
||||
}
|
||||
}
|
||||
|
||||
func TestCleanUp_removeExpiredDevices(t *testing.T) {
|
||||
now := test.Time("2018-06-30T18:30:00Z")
|
||||
timeDispose := fakeTime(now)
|
||||
defer timeDispose()
|
||||
|
||||
db := test.InMemoryDB(t)
|
||||
defer db.Close()
|
||||
db.User(2)
|
||||
db.Create(&model.Device{
|
||||
ID: 1,
|
||||
Token: "abc",
|
||||
UserID: 2,
|
||||
Name: "android 1",
|
||||
Type: model.TypeShortExpiry,
|
||||
ActiveAt: test.Time("2018-06-30T17:30:00Z"),
|
||||
CreatedAt: test.Time("2009-06-30T18:30:00Z"),
|
||||
})
|
||||
db.Create(&model.Device{
|
||||
ID: 2,
|
||||
Token: "abc2",
|
||||
UserID: 2,
|
||||
Name: "android 2",
|
||||
Type: model.TypeShortExpiry,
|
||||
ActiveAt: test.Time("2018-06-30T17:29:59Z"),
|
||||
CreatedAt: test.Time("2009-06-30T18:30:00Z"),
|
||||
})
|
||||
db.Create(&model.Device{
|
||||
ID: 3,
|
||||
Token: "abc3",
|
||||
UserID: 2,
|
||||
Name: "android 3",
|
||||
Type: model.TypeLongExpiry,
|
||||
ActiveAt: test.Time("2018-05-31T18:30:00Z"),
|
||||
CreatedAt: test.Time("2009-06-30T18:30:00Z"),
|
||||
})
|
||||
db.Create(&model.Device{
|
||||
ID: 4,
|
||||
Token: "abc4",
|
||||
UserID: 2,
|
||||
Name: "android 4",
|
||||
Type: model.TypeLongExpiry,
|
||||
ActiveAt: test.Time("2018-05-31T18:29:59Z"),
|
||||
CreatedAt: test.Time("2009-06-30T18:30:00Z"),
|
||||
})
|
||||
db.Create(&model.Device{
|
||||
ID: 5,
|
||||
Token: "abc5",
|
||||
UserID: 2,
|
||||
Name: "android 5",
|
||||
Type: model.TypeNoExpiry,
|
||||
ActiveAt: test.Time("2002-06-30T18:30:00Z"),
|
||||
CreatedAt: test.Time("2000-06-30T18:30:00Z"),
|
||||
})
|
||||
|
||||
stopCleanUp := make(chan bool)
|
||||
|
||||
go CleanUp(db.DB, time.Microsecond*10, stopCleanUp)
|
||||
|
||||
<-time.After(time.Millisecond * 20)
|
||||
|
||||
expected := []model.Device{
|
||||
{
|
||||
ID: 1,
|
||||
Token: "abc",
|
||||
UserID: 2,
|
||||
Name: "android 1",
|
||||
Type: model.TypeShortExpiry,
|
||||
ActiveAt: test.Time("2018-06-30T17:30:00Z"),
|
||||
CreatedAt: test.Time("2009-06-30T18:30:00Z"),
|
||||
},
|
||||
{
|
||||
ID: 3,
|
||||
Token: "abc3",
|
||||
UserID: 2,
|
||||
Name: "android 3",
|
||||
Type: model.TypeLongExpiry,
|
||||
ActiveAt: test.Time("2018-05-31T18:30:00Z"),
|
||||
CreatedAt: test.Time("2009-06-30T18:30:00Z"),
|
||||
},
|
||||
{
|
||||
ID: 5,
|
||||
Token: "abc5",
|
||||
UserID: 2,
|
||||
Name: "android 5",
|
||||
Type: model.TypeNoExpiry,
|
||||
ActiveAt: test.Time("2002-06-30T18:30:00Z"),
|
||||
CreatedAt: test.Time("2000-06-30T18:30:00Z"),
|
||||
},
|
||||
}
|
||||
|
||||
var devices []model.Device
|
||||
db.Find(&devices)
|
||||
assert.Equal(t, expected, devices)
|
||||
stopCleanUp <- true
|
||||
}
|
||||
|
||||
func fakeTime(t time.Time) func() {
|
||||
old := timeNow
|
||||
timeNow = func() time.Time {
|
||||
return t
|
||||
}
|
||||
return func() {
|
||||
timeNow = old
|
||||
}
|
||||
}
|
||||
26
auth/hasrole.go
Normal file
26
auth/hasrole.go
Normal file
@@ -0,0 +1,26 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
|
||||
"github.com/99designs/gqlgen/graphql"
|
||||
"github.com/traggo/server/generated/gqlmodel"
|
||||
)
|
||||
|
||||
// HasRole checks if the current user has sufficient permissions.
|
||||
func HasRole() func(ctx context.Context, obj interface{}, next graphql.Resolver, role gqlmodel.Role) (res interface{}, err error) {
|
||||
return func(ctx context.Context, obj interface{}, next graphql.Resolver, role gqlmodel.Role) (interface{}, error) {
|
||||
user := GetUser(ctx)
|
||||
|
||||
if user == nil {
|
||||
return nil, errors.New("you need to login")
|
||||
}
|
||||
|
||||
if role == gqlmodel.RoleAdmin && !user.Admin {
|
||||
return nil, errors.New("permission denied")
|
||||
}
|
||||
|
||||
return next(ctx)
|
||||
}
|
||||
}
|
||||
55
auth/hasrole_test.go
Normal file
55
auth/hasrole_test.go
Normal file
@@ -0,0 +1,55 @@
|
||||
package auth_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/traggo/server/auth"
|
||||
"github.com/traggo/server/generated/gqlmodel"
|
||||
"github.com/traggo/server/test/fake"
|
||||
)
|
||||
|
||||
var (
|
||||
successResp = struct{}{}
|
||||
)
|
||||
|
||||
func TestHasRole_requiredUser_givenAdmin_succeeds(t *testing.T) {
|
||||
res, err := auth.HasRole()(fake.UserWithPerm(1, true), nil, noop, gqlmodel.RoleUser)
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, successResp, res)
|
||||
}
|
||||
|
||||
func TestHasRole_requiredAdmin_givenAdmin_succeeds(t *testing.T) {
|
||||
res, err := auth.HasRole()(fake.UserWithPerm(1, true), nil, noop, gqlmodel.RoleAdmin)
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, successResp, res)
|
||||
}
|
||||
|
||||
func TestHasRole_requiredUser_givenUser_succeeds(t *testing.T) {
|
||||
res, err := auth.HasRole()(fake.UserWithPerm(1, false), nil, noop, gqlmodel.RoleUser)
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, successResp, res)
|
||||
}
|
||||
|
||||
func TestHasRole_requiredAdmin_givenUser_fails(t *testing.T) {
|
||||
res, err := auth.HasRole()(fake.UserWithPerm(1, false), nil, noop, gqlmodel.RoleAdmin)
|
||||
assert.Nil(t, res)
|
||||
assert.EqualError(t, err, "permission denied")
|
||||
}
|
||||
|
||||
func TestHasRole_requiredAdmin_noUserGiven_fails(t *testing.T) {
|
||||
res, err := auth.HasRole()(context.Background(), nil, noop, gqlmodel.RoleAdmin)
|
||||
assert.Nil(t, res)
|
||||
assert.EqualError(t, err, "you need to login")
|
||||
}
|
||||
|
||||
func TestHasRole_requiredUser_noUserGiven_fails(t *testing.T) {
|
||||
res, err := auth.HasRole()(context.Background(), nil, noop, gqlmodel.RoleUser)
|
||||
assert.Nil(t, res)
|
||||
assert.EqualError(t, err, "you need to login")
|
||||
}
|
||||
|
||||
func noop(context.Context) (res interface{}, err error) {
|
||||
return successResp, nil
|
||||
}
|
||||
182
auth/middleware.go
Normal file
182
auth/middleware.go
Normal file
@@ -0,0 +1,182 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/jinzhu/gorm"
|
||||
"github.com/rs/zerolog/log"
|
||||
"github.com/traggo/server/model"
|
||||
)
|
||||
|
||||
type key string
|
||||
|
||||
var (
|
||||
traggoSession = "traggo"
|
||||
createSessionKey key = "createSession"
|
||||
destroySessionKey key = "destroySession"
|
||||
userKey key = "user"
|
||||
deviceKey key = "device"
|
||||
)
|
||||
|
||||
// Middleware is the auth middleware which sets user and device context parameters.
|
||||
func Middleware(db *gorm.DB) mux.MiddlewareFunc {
|
||||
return func(handler http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) {
|
||||
handler.ServeHTTP(writer, sessionCallbacks(reqisterUser(request, writer, db), writer))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func sessionCallbacks(request *http.Request, writer http.ResponseWriter) *http.Request {
|
||||
createSession := func(token string, maxAge int) {
|
||||
http.SetCookie(writer, &http.Cookie{
|
||||
Name: traggoSession,
|
||||
Value: token,
|
||||
MaxAge: maxAge,
|
||||
})
|
||||
}
|
||||
destroySession := func() {
|
||||
http.SetCookie(writer, &http.Cookie{
|
||||
Name: traggoSession,
|
||||
Value: "",
|
||||
MaxAge: -1,
|
||||
})
|
||||
}
|
||||
return request.WithContext(WithDestroySession(WithCreateSession(request.Context(), createSession), destroySession))
|
||||
}
|
||||
|
||||
func reqisterUser(request *http.Request, writer http.ResponseWriter, db *gorm.DB) *http.Request {
|
||||
token, err := getToken(request)
|
||||
if err != nil {
|
||||
return request
|
||||
}
|
||||
|
||||
device := &model.Device{}
|
||||
if db.Where("token = ?", token).Find(device).RecordNotFound() {
|
||||
log.Debug().Str("token", token).Msg("No device with token found")
|
||||
return request
|
||||
}
|
||||
|
||||
user := &model.User{}
|
||||
if db.Find(user, device.UserID).RecordNotFound() {
|
||||
log.Panic().Int("userID", device.UserID).Int("deviceID", device.ID).Msg("User not found")
|
||||
}
|
||||
log.Debug().Int("userid", device.UserID).Str("username", user.Name).Msg("User found")
|
||||
|
||||
impersonate := request.Header.Get("X-Traggo-Impersonate")
|
||||
if impersonate != "" {
|
||||
if !user.Admin {
|
||||
log.Info().Str("impersonate", impersonate).Msg("Trying to impersonate without being admin")
|
||||
return request
|
||||
}
|
||||
userID, err := strconv.Atoi(impersonate)
|
||||
if err != nil {
|
||||
log.Info().Str("impersonate", impersonate).Msg("Unable to parse impersonation header")
|
||||
return request
|
||||
}
|
||||
impersonateUser := &model.User{}
|
||||
if db.Find(impersonateUser, userID).RecordNotFound() {
|
||||
log.Info().Int("userid", userID).Msg("Impersonation user not found")
|
||||
return request
|
||||
}
|
||||
user = impersonateUser
|
||||
log.Info().Str("username", user.Name).Msg("Impersonation user")
|
||||
}
|
||||
|
||||
if device.ActiveAt.Before(time.Now().Add(5 * -time.Minute)) {
|
||||
log.Debug().Int("deviceId", device.ID).Str("deviceName", device.Name).Msg("update device activeAt")
|
||||
device.ActiveAt = timeNow()
|
||||
db.Save(device)
|
||||
|
||||
if cookie, err := request.Cookie(traggoSession); err == nil && cookie != nil {
|
||||
cookie.MaxAge = device.Type.Seconds()
|
||||
http.SetCookie(writer, cookie)
|
||||
}
|
||||
}
|
||||
|
||||
return request.WithContext(WithUser(WithDevice(request.Context(), device), user))
|
||||
}
|
||||
|
||||
func getToken(request *http.Request) (string, error) {
|
||||
if value := request.Header.Get("Authorization"); value != "" && strings.HasPrefix(value, "traggo ") {
|
||||
log.Debug().Msg("Using authorization header for authentication")
|
||||
key := strings.TrimPrefix(value, "traggo ")
|
||||
return key, nil
|
||||
}
|
||||
|
||||
if cookie, err := request.Cookie("traggo"); err == nil && cookie != nil && cookie.Value != "" {
|
||||
log.Debug().Msg("Using cookie for authentication")
|
||||
return cookie.Value, nil
|
||||
}
|
||||
|
||||
if token := request.FormValue("token"); token != "" {
|
||||
log.Debug().Msg("Using query parameter for authentication")
|
||||
return token, nil
|
||||
}
|
||||
|
||||
return "", errors.New("no token found")
|
||||
}
|
||||
|
||||
// WithDevice adds the authenticated device to the context.
|
||||
func WithDevice(ctx context.Context, device *model.Device) context.Context {
|
||||
return context.WithValue(
|
||||
ctx,
|
||||
deviceKey,
|
||||
device)
|
||||
}
|
||||
|
||||
// WithUser adds the authenticated user to the context.
|
||||
func WithUser(ctx context.Context, user *model.User) context.Context {
|
||||
return context.WithValue(
|
||||
ctx,
|
||||
userKey,
|
||||
user)
|
||||
}
|
||||
|
||||
// WithCreateSession adds the create session method
|
||||
func WithCreateSession(ctx context.Context, f func(token string, age int)) context.Context {
|
||||
return context.WithValue(ctx, createSessionKey, f)
|
||||
}
|
||||
|
||||
// WithDestroySession adds the destroy session method
|
||||
func WithDestroySession(ctx context.Context, f func()) context.Context {
|
||||
return context.WithValue(ctx, destroySessionKey, f)
|
||||
}
|
||||
|
||||
// GetUser returns the authenticated user or panics.
|
||||
func GetUser(ctx context.Context) *model.User {
|
||||
if user, ok := ctx.Value(userKey).(*model.User); ok {
|
||||
return user
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetDevice returns the authenticated device or nil.
|
||||
func GetDevice(ctx context.Context) *model.Device {
|
||||
if device, ok := ctx.Value(deviceKey).(*model.Device); ok {
|
||||
return device
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetCreateSession gets the create session callback
|
||||
func GetCreateSession(ctx context.Context) func(token string, age int) {
|
||||
if f, ok := ctx.Value(createSessionKey).(func(string, int)); ok {
|
||||
return f
|
||||
}
|
||||
panic("create session must exist")
|
||||
}
|
||||
|
||||
// GetDestroySession gets the destroy session callback
|
||||
func GetDestroySession(ctx context.Context) func() {
|
||||
if f, ok := ctx.Value(destroySessionKey).(func()); ok {
|
||||
return f
|
||||
}
|
||||
panic("create session must exist")
|
||||
}
|
||||
328
auth/middleware_test.go
Normal file
328
auth/middleware_test.go
Normal file
@@ -0,0 +1,328 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/traggo/server/test"
|
||||
)
|
||||
|
||||
func TestMiddleware_noAuthentication_noAuthentication(t *testing.T) {
|
||||
db := test.InMemoryDB(t)
|
||||
defer db.Close()
|
||||
spy := &requestSpy{}
|
||||
|
||||
Middleware(db.DB)(spy).ServeHTTP(httptest.NewRecorder(), httptest.NewRequest("GET", "/test", nil))
|
||||
|
||||
ctx := spy.req.Context()
|
||||
assert.Nil(t, GetUser(ctx))
|
||||
assert.Nil(t, GetDevice(ctx))
|
||||
}
|
||||
|
||||
func TestMiddleware_query_notExistingToken_noAuthentication(t *testing.T) {
|
||||
db := test.InMemoryDB(t)
|
||||
defer db.Close()
|
||||
spy := &requestSpy{}
|
||||
|
||||
Middleware(db.DB)(spy).ServeHTTP(httptest.NewRecorder(), httptest.NewRequest("GET", "/test?token=abc", nil))
|
||||
|
||||
ctx := spy.req.Context()
|
||||
assert.Nil(t, GetUser(ctx))
|
||||
assert.Nil(t, GetDevice(ctx))
|
||||
}
|
||||
|
||||
func TestMiddleware_query_validToken_authenticates(t *testing.T) {
|
||||
now := test.Time("2018-06-30T18:30:00Z")
|
||||
timeDispose := fakeTime(now)
|
||||
defer timeDispose()
|
||||
|
||||
db := test.InMemoryDB(t)
|
||||
defer db.Close()
|
||||
builder := db.User(1)
|
||||
user := builder.User
|
||||
device := builder.NewDevice(2, "abc", "test")
|
||||
spy := &requestSpy{}
|
||||
|
||||
Middleware(db.DB)(spy).ServeHTTP(httptest.NewRecorder(), httptest.NewRequest("GET", "/test?token=abc", nil))
|
||||
|
||||
ctx := spy.req.Context()
|
||||
assert.Equal(t, &user, GetUser(ctx))
|
||||
|
||||
device.ActiveAt = now
|
||||
assert.Equal(t, &device, GetDevice(ctx))
|
||||
}
|
||||
|
||||
func TestMiddleware_header_notExistingToken_noAuthentication(t *testing.T) {
|
||||
db := test.InMemoryDB(t)
|
||||
defer db.Close()
|
||||
spy := &requestSpy{}
|
||||
|
||||
request := httptest.NewRequest("GET", "/test?token=abc", nil)
|
||||
request.Header["Authorization"] = []string{"traggo abc"}
|
||||
Middleware(db.DB)(spy).ServeHTTP(httptest.NewRecorder(), request)
|
||||
|
||||
ctx := spy.req.Context()
|
||||
assert.Nil(t, GetUser(ctx))
|
||||
assert.Nil(t, GetDevice(ctx))
|
||||
}
|
||||
|
||||
func TestMiddleware_header_validToken_authenticates(t *testing.T) {
|
||||
now := test.Time("2018-06-30T18:30:00Z")
|
||||
timeDispose := fakeTime(now)
|
||||
defer timeDispose()
|
||||
|
||||
db := test.InMemoryDB(t)
|
||||
defer db.Close()
|
||||
builder := db.User(1)
|
||||
user := builder.User
|
||||
device := builder.NewDevice(2, "abc", "test")
|
||||
spy := &requestSpy{}
|
||||
|
||||
request := httptest.NewRequest("GET", "/test", nil)
|
||||
request.Header.Set("Authorization", "traggo abc")
|
||||
Middleware(db.DB)(spy).ServeHTTP(httptest.NewRecorder(), request)
|
||||
|
||||
ctx := spy.req.Context()
|
||||
assert.Equal(t, &user, GetUser(ctx))
|
||||
|
||||
device.ActiveAt = now
|
||||
assert.Equal(t, &device, GetDevice(ctx))
|
||||
}
|
||||
|
||||
func TestMiddleware_header_validToken_invalidAuthenticationType_noAuthentication(t *testing.T) {
|
||||
db := test.InMemoryDB(t)
|
||||
defer db.Close()
|
||||
db.User(1).NewDevice(2, "abc", "test")
|
||||
spy := &requestSpy{}
|
||||
|
||||
request := httptest.NewRequest("GET", "/test", nil)
|
||||
request.Header.Set("Authorization", "basic abc")
|
||||
|
||||
Middleware(db.DB)(spy).ServeHTTP(httptest.NewRecorder(), request)
|
||||
|
||||
ctx := spy.req.Context()
|
||||
assert.Nil(t, GetUser(ctx))
|
||||
assert.Nil(t, GetDevice(ctx))
|
||||
}
|
||||
|
||||
func TestMiddleware_cookie_notExistingToken_noAuthentication(t *testing.T) {
|
||||
db := test.InMemoryDB(t)
|
||||
defer db.Close()
|
||||
spy := &requestSpy{}
|
||||
|
||||
request := httptest.NewRequest("GET", "/test?token=abc", nil)
|
||||
request.AddCookie(&http.Cookie{
|
||||
Name: "traggo",
|
||||
Value: "abc",
|
||||
})
|
||||
Middleware(db.DB)(spy).ServeHTTP(httptest.NewRecorder(), request)
|
||||
|
||||
ctx := spy.req.Context()
|
||||
assert.Nil(t, GetUser(ctx))
|
||||
assert.Nil(t, GetDevice(ctx))
|
||||
}
|
||||
|
||||
func TestMiddleware_cookie_validToken_authenticates(t *testing.T) {
|
||||
now := test.Time("2018-06-30T18:30:00Z")
|
||||
timeDispose := fakeTime(now)
|
||||
defer timeDispose()
|
||||
|
||||
db := test.InMemoryDB(t)
|
||||
defer db.Close()
|
||||
builder := db.User(1)
|
||||
user := builder.User
|
||||
device := builder.NewDevice(2, "abc", "test")
|
||||
spy := &requestSpy{}
|
||||
|
||||
request := httptest.NewRequest("GET", "/test?token=abc", nil)
|
||||
request.AddCookie(&http.Cookie{
|
||||
Name: "traggo",
|
||||
Value: "abc",
|
||||
})
|
||||
Middleware(db.DB)(spy).ServeHTTP(httptest.NewRecorder(), request)
|
||||
|
||||
ctx := spy.req.Context()
|
||||
assert.Equal(t, &user, GetUser(ctx))
|
||||
|
||||
device.ActiveAt = now
|
||||
assert.Equal(t, &device, GetDevice(ctx))
|
||||
}
|
||||
|
||||
func TestMiddleware_createSession_setsCookie(t *testing.T) {
|
||||
db := test.InMemoryDB(t)
|
||||
defer db.Close()
|
||||
spy := &requestSpy{}
|
||||
recorder := httptest.NewRecorder()
|
||||
|
||||
request := httptest.NewRequest("GET", "/test", nil)
|
||||
Middleware(db.DB)(spy).ServeHTTP(recorder, request)
|
||||
|
||||
ctx := spy.req.Context()
|
||||
GetCreateSession(ctx)("new token", 60)
|
||||
|
||||
cookieHeader := recorder.Header().Get("Set-Cookie")
|
||||
assert.Equal(t, `traggo="new token"; Max-Age=60`, cookieHeader)
|
||||
}
|
||||
|
||||
func TestMiddleware_noCallbackExecuted_noCookieSet(t *testing.T) {
|
||||
db := test.InMemoryDB(t)
|
||||
defer db.Close()
|
||||
spy := &requestSpy{}
|
||||
recorder := httptest.NewRecorder()
|
||||
|
||||
request := httptest.NewRequest("GET", "/test", nil)
|
||||
Middleware(db.DB)(spy).ServeHTTP(recorder, request)
|
||||
|
||||
cookieHeader := recorder.Header().Get("Set-Cookie")
|
||||
assert.Equal(t, "", cookieHeader)
|
||||
}
|
||||
|
||||
func TestMiddleware_destroySession_destroysCookie(t *testing.T) {
|
||||
db := test.InMemoryDB(t)
|
||||
defer db.Close()
|
||||
spy := &requestSpy{}
|
||||
recorder := httptest.NewRecorder()
|
||||
|
||||
request := httptest.NewRequest("GET", "/test", nil)
|
||||
Middleware(db.DB)(spy).ServeHTTP(recorder, request)
|
||||
|
||||
ctx := spy.req.Context()
|
||||
GetDestroySession(ctx)()
|
||||
|
||||
cookieHeader := recorder.Header().Get("Set-Cookie")
|
||||
assert.Equal(t, "traggo=; Max-Age=0", cookieHeader)
|
||||
}
|
||||
|
||||
func TestMiddleware_impersonate_no_admin(t *testing.T) {
|
||||
now := test.Time("2018-06-30T18:30:00Z")
|
||||
timeDispose := fakeTime(now)
|
||||
defer timeDispose()
|
||||
|
||||
db := test.InMemoryDB(t)
|
||||
defer db.Close()
|
||||
builder := db.User(1)
|
||||
builder.NewDevice(2, "abc", "test")
|
||||
spy := &requestSpy{}
|
||||
recorder := httptest.NewRecorder()
|
||||
|
||||
request := httptest.NewRequest("GET", "/test?token=abc", nil)
|
||||
request.Header.Set("X-Traggo-Impersonate", "empty")
|
||||
Middleware(db.DB)(spy).ServeHTTP(recorder, request)
|
||||
|
||||
response := recorder.Result()
|
||||
assert.Equal(t, 200, response.StatusCode)
|
||||
|
||||
ctx := spy.req.Context()
|
||||
assert.Nil(t, GetUser(ctx))
|
||||
assert.Nil(t, GetDevice(ctx))
|
||||
}
|
||||
|
||||
func TestMiddleware_impersonate_invalid_personate_header(t *testing.T) {
|
||||
now := test.Time("2018-06-30T18:30:00Z")
|
||||
timeDispose := fakeTime(now)
|
||||
defer timeDispose()
|
||||
|
||||
db := test.InMemoryDB(t)
|
||||
defer db.Close()
|
||||
builder := db.User(1)
|
||||
user := builder.User
|
||||
user.Admin = true
|
||||
db.Save(user)
|
||||
builder.NewDevice(2, "abc", "test")
|
||||
spy := &requestSpy{}
|
||||
recorder := httptest.NewRecorder()
|
||||
|
||||
request := httptest.NewRequest("GET", "/test?token=abc", nil)
|
||||
request.Header.Set("X-Traggo-Impersonate", "invalid")
|
||||
Middleware(db.DB)(spy).ServeHTTP(recorder, request)
|
||||
|
||||
response := recorder.Result()
|
||||
assert.Equal(t, 200, response.StatusCode)
|
||||
|
||||
ctx := spy.req.Context()
|
||||
assert.Nil(t, GetUser(ctx))
|
||||
assert.Nil(t, GetDevice(ctx))
|
||||
}
|
||||
|
||||
func TestMiddleware_impersonate_non_existing_user(t *testing.T) {
|
||||
now := test.Time("2018-06-30T18:30:00Z")
|
||||
timeDispose := fakeTime(now)
|
||||
defer timeDispose()
|
||||
|
||||
db := test.InMemoryDB(t)
|
||||
defer db.Close()
|
||||
builder := db.User(1)
|
||||
user := builder.User
|
||||
user.Admin = true
|
||||
db.Save(user)
|
||||
builder.NewDevice(2, "abc", "test")
|
||||
spy := &requestSpy{}
|
||||
recorder := httptest.NewRecorder()
|
||||
|
||||
request := httptest.NewRequest("GET", "/test?token=abc", nil)
|
||||
request.Header.Set("X-Traggo-Impersonate", "42")
|
||||
Middleware(db.DB)(spy).ServeHTTP(recorder, request)
|
||||
|
||||
response := recorder.Result()
|
||||
assert.Equal(t, 200, response.StatusCode)
|
||||
|
||||
ctx := spy.req.Context()
|
||||
assert.Nil(t, GetUser(ctx))
|
||||
assert.Nil(t, GetDevice(ctx))
|
||||
}
|
||||
|
||||
func TestMiddleware_impersonate_happy(t *testing.T) {
|
||||
now := test.Time("2018-06-30T18:30:00Z")
|
||||
timeDispose := fakeTime(now)
|
||||
defer timeDispose()
|
||||
|
||||
db := test.InMemoryDB(t)
|
||||
defer db.Close()
|
||||
admin_builder := db.User(1)
|
||||
admin_user := admin_builder.User
|
||||
admin_user.Admin = true
|
||||
db.Save(admin_user)
|
||||
admin_device := admin_builder.NewDevice(2, "abc", "test")
|
||||
|
||||
builder := db.User(2)
|
||||
user := builder.User
|
||||
|
||||
spy := &requestSpy{}
|
||||
recorder := httptest.NewRecorder()
|
||||
|
||||
request := httptest.NewRequest("GET", "/test?token=abc", nil)
|
||||
request.Header.Set("X-Traggo-Impersonate", "2")
|
||||
Middleware(db.DB)(spy).ServeHTTP(recorder, request)
|
||||
|
||||
response := recorder.Result()
|
||||
assert.Equal(t, 200, response.StatusCode)
|
||||
|
||||
ctx := spy.req.Context()
|
||||
assert.Equal(t, &user, GetUser(ctx))
|
||||
|
||||
admin_device.ActiveAt = now
|
||||
assert.Equal(t, &admin_device, GetDevice(ctx))
|
||||
}
|
||||
|
||||
func TestGetCreateSession_panicsWhenMiddlewareWasNotExecuted(t *testing.T) {
|
||||
assert.Panics(t, func() {
|
||||
GetCreateSession(context.Background())
|
||||
})
|
||||
}
|
||||
|
||||
func TestGetDestroySession_panicsWhenMiddlewareWasNotExecuted(t *testing.T) {
|
||||
assert.Panics(t, func() {
|
||||
GetDestroySession(context.Background())
|
||||
})
|
||||
}
|
||||
|
||||
type requestSpy struct {
|
||||
req *http.Request
|
||||
}
|
||||
|
||||
func (s *requestSpy) ServeHTTP(_ http.ResponseWriter, req *http.Request) {
|
||||
s.req = req
|
||||
}
|
||||
43
auth/rand/random.go
Normal file
43
auth/rand/random.go
Normal file
@@ -0,0 +1,43 @@
|
||||
package rand
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
var (
|
||||
chars = []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789")
|
||||
|
||||
randRead = rand.Read
|
||||
)
|
||||
|
||||
func init() {
|
||||
// exits with 1 when crypto/rand is not available
|
||||
Token(1)
|
||||
}
|
||||
|
||||
func randBytes(count int) ([]byte, error) {
|
||||
bytes := make([]byte, count)
|
||||
_, err := randRead(bytes)
|
||||
return bytes, err
|
||||
}
|
||||
|
||||
// Token returns a random token.
|
||||
func Token(count int) string {
|
||||
bytes, err := randBytes(count)
|
||||
if err != nil {
|
||||
log.Panic().Msg("crypto/rand is not available")
|
||||
}
|
||||
return bytesToString(bytes, count)
|
||||
}
|
||||
|
||||
func bytesToString(bytes []byte, count int) string {
|
||||
token := make([]rune, count)
|
||||
|
||||
for index, item := range bytes {
|
||||
token[index] = chars[int(item)%len(chars)]
|
||||
}
|
||||
|
||||
return string(token)
|
||||
}
|
||||
30
auth/rand/random_test.go
Normal file
30
auth/rand/random_test.go
Normal file
@@ -0,0 +1,30 @@
|
||||
package rand
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestToken(t *testing.T) {
|
||||
for i := 1; i < 10; i++ {
|
||||
assert.Len(t, Token(i), i)
|
||||
}
|
||||
assert.Len(t, Token(120), 120)
|
||||
assert.Len(t, Token(222), 222)
|
||||
assert.Len(t, Token(555665), 555665)
|
||||
}
|
||||
|
||||
func TestToken_panics(t *testing.T) {
|
||||
old := randRead
|
||||
defer func() {
|
||||
randRead = old
|
||||
}()
|
||||
randRead = func(b []byte) (n int, err error) {
|
||||
return 0, errors.New("oops")
|
||||
}
|
||||
assert.Panics(t, func() {
|
||||
Token(1)
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user