Sessions in MySQL (#37)
* Sessions in MySQL * Reclaiming some names * session renewal without new cookies on every request * comments and docstrings * light organization in vc generation * go vet * endpoints for session management * Merging @zwass' commit with mine * Updating salt generation to use crypt/rand * use getRandomText for session keys * VC no longer needs a DB or to return an error * getRandomText docstring * Only use session via the SessionBackend API * Set session backend with the request, similar to db
This commit is contained in:
@@ -1,15 +1,14 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"errors"
|
||||
"fmt"
|
||||
"math/rand"
|
||||
"time"
|
||||
|
||||
"github.com/Sirupsen/logrus"
|
||||
"github.com/dgrijalva/jwt-go"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/jinzhu/gorm"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
)
|
||||
|
||||
@@ -23,12 +22,6 @@ type ViewerContext struct {
|
||||
user *User
|
||||
}
|
||||
|
||||
// JWT returns a JWT token in serialized string form given a ViewerContext as
|
||||
// well as a potential error in the event that things have gone wrong.
|
||||
func (vc *ViewerContext) JWT() (string, error) {
|
||||
return GenerateJWT(vc.user.ID)
|
||||
}
|
||||
|
||||
// IsAdmin indicates whether or not the current user can perform administrative
|
||||
// actions.
|
||||
func (vc *ViewerContext) IsAdmin() bool {
|
||||
@@ -47,6 +40,8 @@ func (vc *ViewerContext) UserID() (uint, error) {
|
||||
return 0, errors.New("No user set")
|
||||
}
|
||||
|
||||
// CanPerformActions returns a bool indicating the current user's ability to
|
||||
// perform the most basic actions on the site
|
||||
func (vc *ViewerContext) CanPerformActions() bool {
|
||||
if vc.user == nil {
|
||||
return false
|
||||
@@ -59,6 +54,8 @@ func (vc *ViewerContext) CanPerformActions() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
// IsUserID returns true if the given user id the same as the user which is
|
||||
// represented by this ViewerContext
|
||||
func (vc *ViewerContext) IsUserID(id uint) bool {
|
||||
userID, err := vc.UserID()
|
||||
if err != nil {
|
||||
@@ -70,88 +67,18 @@ func (vc *ViewerContext) IsUserID(id uint) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
// CanPerformWriteActionsOnUser returns a bool indicating the current user's
|
||||
// ability to perform write actions on the given user
|
||||
func (vc *ViewerContext) CanPerformWriteActionOnUser(u *User) bool {
|
||||
return vc.CanPerformActions() && (vc.IsUserID(u.ID) || vc.IsAdmin())
|
||||
}
|
||||
|
||||
// CanPerformReadActionsOnUser returns a bool indicating the current user's
|
||||
// ability to perform read actions on the given user
|
||||
func (vc *ViewerContext) CanPerformReadActionOnUser(u *User) bool {
|
||||
return vc.CanPerformActions()
|
||||
}
|
||||
|
||||
// GenerateJWT generates a JWT token in serialized string form given a
|
||||
// ViewerContext as well as a potential error in the event that things have
|
||||
// gone wrong.
|
||||
func GenerateJWT(userID uint) (string, error) {
|
||||
token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
|
||||
"user_id": userID,
|
||||
// "Not Before": https://tools.ietf.org/html/rfc7519#section-4.1.5
|
||||
"nbf": time.Now().UTC().Unix(),
|
||||
// "Expiration Time": https://tools.ietf.org/html/rfc7519#section-4.1.4
|
||||
"exp": time.Now().UTC().AddDate(0, 2, 0).Unix(),
|
||||
})
|
||||
|
||||
return token.SignedString([]byte(config.App.JWTKey))
|
||||
}
|
||||
|
||||
// ParseJWT attempts to parse a JWT token in serialized string form into a
|
||||
// JWT token in a deserialized jwt.Token struct.
|
||||
func ParseJWT(token string) (*jwt.Token, error) {
|
||||
return jwt.Parse(token, func(t *jwt.Token) (interface{}, error) {
|
||||
method, ok := t.Method.(*jwt.SigningMethodHMAC)
|
||||
if !ok || method != jwt.SigningMethodHS256 {
|
||||
return nil, errors.New("Unexpected signing method")
|
||||
}
|
||||
return []byte(config.App.JWTKey), nil
|
||||
})
|
||||
}
|
||||
|
||||
// JWTRenewalMiddleware optimistically tries to renew the user's JWT token.
|
||||
// This allows kolide to have sessions that last forever, assuming that a user
|
||||
// logs in and uses the application within a reasonable time window (which is
|
||||
// defined in the JWT token generation method). If anything goes wrong, this
|
||||
// middleware will back off and defer recovery of the situation to the
|
||||
// downstream web request.
|
||||
func JWTRenewalMiddleware(c *gin.Context) {
|
||||
session := GetSession(c)
|
||||
tokenCookie := session.Get("jwt")
|
||||
if tokenCookie == nil {
|
||||
c.Next()
|
||||
return
|
||||
}
|
||||
|
||||
tokenString, ok := tokenCookie.(string)
|
||||
if !ok {
|
||||
c.Next()
|
||||
return
|
||||
}
|
||||
|
||||
token, err := ParseJWT(tokenString)
|
||||
if err != nil {
|
||||
c.Next()
|
||||
return
|
||||
}
|
||||
|
||||
claims, ok := token.Claims.(jwt.MapClaims)
|
||||
|
||||
if !ok || !token.Valid {
|
||||
c.Next()
|
||||
return
|
||||
}
|
||||
|
||||
userID := uint(claims["user_id"].(float64))
|
||||
|
||||
jwt, err := GenerateJWT(userID)
|
||||
if err != nil {
|
||||
c.Next()
|
||||
return
|
||||
}
|
||||
|
||||
session.Set("jwt", jwt)
|
||||
session.Save()
|
||||
|
||||
c.Next()
|
||||
}
|
||||
|
||||
// GenerateVC generates a ViewerContext given a user struct
|
||||
func GenerateVC(user *User) *ViewerContext {
|
||||
return &ViewerContext{
|
||||
@@ -170,38 +97,72 @@ func EmptyVC() *ViewerContext {
|
||||
// VC accepts a web request context and a database handler and attempts
|
||||
// to parse a user's jwt token out of the active session, validate the token,
|
||||
// and generate an appropriate ViewerContext given the data in the session.
|
||||
func VC(c *gin.Context, db *gorm.DB) (*ViewerContext, error) {
|
||||
session := GetSession(c)
|
||||
tokenCookie := session.Get("jwt")
|
||||
if tokenCookie == nil {
|
||||
return nil, errors.New("jwt session attribute not set")
|
||||
}
|
||||
|
||||
tokenString, ok := tokenCookie.(string)
|
||||
if !ok {
|
||||
return nil, errors.New("jwt token was not string")
|
||||
}
|
||||
|
||||
token, err := ParseJWT(tokenString)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
claims, ok := token.Claims.(jwt.MapClaims)
|
||||
if !ok || !token.Valid {
|
||||
return nil, errors.New("Invalid token")
|
||||
}
|
||||
|
||||
userID := uint(claims["user_id"].(float64))
|
||||
var user User
|
||||
err = db.Where("id = ?", userID).First(&user).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return GenerateVC(&user), nil
|
||||
|
||||
func VC(c *gin.Context) *ViewerContext {
|
||||
sm := NewSessionManager(c)
|
||||
return sm.VC()
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
// JSON Web Tokens
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
// Given a session key create a JWT to be delivered to the client
|
||||
func GenerateJWT(sessionKey string) (string, error) {
|
||||
token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
|
||||
"session_key": sessionKey,
|
||||
})
|
||||
|
||||
return token.SignedString([]byte(config.App.JWTKey))
|
||||
}
|
||||
|
||||
// ParseJWT attempts to parse a JWT token in serialized string form into a
|
||||
// JWT token in a deserialized jwt.Token struct.
|
||||
func ParseJWT(token string) (*jwt.Token, error) {
|
||||
return jwt.Parse(token, func(t *jwt.Token) (interface{}, error) {
|
||||
method, ok := t.Method.(*jwt.SigningMethodHMAC)
|
||||
if !ok || method != jwt.SigningMethodHS256 {
|
||||
return nil, errors.New("Unexpected signing method")
|
||||
}
|
||||
return []byte(config.App.JWTKey), nil
|
||||
})
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
// Login and password utilities
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
// generateRandomText return a string generated by filling in keySize bytes with
|
||||
// random data and then base64 encoding those bytes
|
||||
func generateRandomText(keySize int) (string, error) {
|
||||
key := make([]byte, keySize)
|
||||
_, err := rand.Read(key)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return base64.StdEncoding.EncodeToString(key), nil
|
||||
}
|
||||
|
||||
func HashPassword(salt, password string) ([]byte, error) {
|
||||
return bcrypt.GenerateFromPassword(
|
||||
[]byte(fmt.Sprintf("%s%s", password, salt)),
|
||||
config.App.BcryptCost,
|
||||
)
|
||||
}
|
||||
|
||||
func SaltAndHashPassword(password string) (string, []byte, error) {
|
||||
salt, err := generateRandomText(config.App.SaltKeySize)
|
||||
if err != nil {
|
||||
return "", []byte{}, err
|
||||
}
|
||||
hashed, err := HashPassword(salt, password)
|
||||
return salt, hashed, err
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
// Authentication and authorization web endpoints
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
type LoginRequestBody struct {
|
||||
Username string `json:"username" binding:"required"`
|
||||
Password string `json:"password" binding:"required"`
|
||||
@@ -217,8 +178,8 @@ func Login(c *gin.Context) {
|
||||
|
||||
db := GetDB(c)
|
||||
|
||||
var user User
|
||||
err = db.Where("username = ?", body.Username).First(&user).Error
|
||||
user := &User{Username: body.Username}
|
||||
err = db.Where(user).First(user).Error
|
||||
if err != nil {
|
||||
logrus.Debugf("User not found: %s", body.Username)
|
||||
UnauthorizedError(c)
|
||||
@@ -232,15 +193,13 @@ func Login(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
token, err := GenerateVC(&user).JWT()
|
||||
sm := NewSessionManager(c)
|
||||
sm.MakeSessionForUser(user)
|
||||
err = sm.Save()
|
||||
if err != nil {
|
||||
logrus.Fatalf("Error generating token: %s", err.Error())
|
||||
DatabaseError(c)
|
||||
return
|
||||
}
|
||||
session := GetSession(c)
|
||||
session.Set("jwt", token)
|
||||
session.Save()
|
||||
|
||||
c.JSON(200, GetUserResponseBody{
|
||||
ID: user.ID,
|
||||
@@ -254,30 +213,19 @@ func Login(c *gin.Context) {
|
||||
}
|
||||
|
||||
func Logout(c *gin.Context) {
|
||||
session := GetSession(c)
|
||||
session.Clear()
|
||||
sm := NewSessionManager(c)
|
||||
|
||||
err := sm.Destroy()
|
||||
if err != nil {
|
||||
DatabaseError(c)
|
||||
return
|
||||
}
|
||||
|
||||
err = sm.Save()
|
||||
if err != nil {
|
||||
DatabaseError(c)
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(200, nil)
|
||||
}
|
||||
|
||||
func generateRandomText(length int) string {
|
||||
rand.Seed(time.Now().UTC().UnixNano())
|
||||
const chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
|
||||
result := make([]byte, length)
|
||||
for i := 0; i < length; i++ {
|
||||
result[i] = chars[rand.Intn(len(chars))]
|
||||
}
|
||||
return string(result)
|
||||
}
|
||||
|
||||
func HashPassword(salt, password string) ([]byte, error) {
|
||||
return bcrypt.GenerateFromPassword(
|
||||
[]byte(fmt.Sprintf("%s%s", salt, password)),
|
||||
config.App.BcryptCost,
|
||||
)
|
||||
}
|
||||
|
||||
func SaltAndHashPassword(password string) (string, []byte, error) {
|
||||
salt := generateRandomText(config.App.SaltLength)
|
||||
hashed, err := HashPassword(salt, password)
|
||||
return salt, hashed, err
|
||||
}
|
||||
|
||||
+18
-34
@@ -4,19 +4,11 @@ import (
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
"unicode/utf8"
|
||||
|
||||
"github.com/dgrijalva/jwt-go"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
func TestGenerateRandomText(t *testing.T) {
|
||||
text := generateRandomText(12)
|
||||
if utf8.RuneCountInString(text) != 12 {
|
||||
t.Fatal("generateRandomText generated the wrong length string")
|
||||
}
|
||||
}
|
||||
|
||||
func TestGenerateVC(t *testing.T) {
|
||||
db := openTestDB()
|
||||
|
||||
@@ -25,24 +17,27 @@ func TestGenerateVC(t *testing.T) {
|
||||
t.Fatal(err.Error())
|
||||
}
|
||||
|
||||
tokenString, err := GenerateVC(user).JWT()
|
||||
if err != nil {
|
||||
t.Fatal(err.Error())
|
||||
vc := GenerateVC(user)
|
||||
if !vc.IsAdmin() {
|
||||
t.Fatal("User is not an admin")
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TestGenerateJWT(t *testing.T) {
|
||||
tokenString, err := GenerateJWT("4")
|
||||
token, err := ParseJWT(tokenString)
|
||||
if err != nil {
|
||||
t.Fatal(err.Error())
|
||||
}
|
||||
|
||||
claims, ok := token.Claims.(jwt.MapClaims)
|
||||
if !ok || !token.Valid {
|
||||
t.Fatal("Token is invalid")
|
||||
}
|
||||
|
||||
userID := uint(claims["user_id"].(float64))
|
||||
if userID != user.ID {
|
||||
t.Fatal("Claims are incorrect. userID is %d", userID)
|
||||
sessionKey := claims["session_key"].(string)
|
||||
if sessionKey != "4" {
|
||||
t.Fatalf("Claims are incorrect. session key is %s", sessionKey)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -50,9 +45,6 @@ func TestVC(t *testing.T) {
|
||||
db := openTestDB()
|
||||
r := createEmptyTestServer(db)
|
||||
|
||||
r.Use(testSessionMiddleware)
|
||||
r.Use(JWTRenewalMiddleware)
|
||||
|
||||
user, err := NewUser(db, "marpaia", "foobar", "mike@kolide.co", false, false)
|
||||
if err != nil {
|
||||
t.Fatal(err.Error())
|
||||
@@ -64,32 +56,27 @@ func TestVC(t *testing.T) {
|
||||
}
|
||||
|
||||
r.GET("/admin_login", func(c *gin.Context) {
|
||||
token, err := GenerateVC(admin).JWT()
|
||||
sm := NewSessionManager(c)
|
||||
sm.MakeSessionForUser(admin)
|
||||
err := sm.Save()
|
||||
if err != nil {
|
||||
t.Fatal(err.Error())
|
||||
}
|
||||
session := GetSession(c)
|
||||
session.Set("jwt", token)
|
||||
session.Save()
|
||||
c.JSON(200, nil)
|
||||
})
|
||||
|
||||
r.GET("/user_login", func(c *gin.Context) {
|
||||
token, err := GenerateVC(user).JWT()
|
||||
sm := NewSessionManager(c)
|
||||
sm.MakeSessionForUser(user)
|
||||
err := sm.Save()
|
||||
if err != nil {
|
||||
t.Fatal(err.Error())
|
||||
}
|
||||
session := GetSession(c)
|
||||
session.Set("jwt", token)
|
||||
session.Save()
|
||||
c.JSON(200, nil)
|
||||
})
|
||||
|
||||
r.GET("/admin", func(c *gin.Context) {
|
||||
vc, err := VC(c, db)
|
||||
if err != nil {
|
||||
t.Fatal(err.Error())
|
||||
}
|
||||
vc := VC(c)
|
||||
if !vc.IsAdmin() {
|
||||
t.Fatal("Not admin")
|
||||
}
|
||||
@@ -97,10 +84,7 @@ func TestVC(t *testing.T) {
|
||||
})
|
||||
|
||||
r.GET("/user", func(c *gin.Context) {
|
||||
vc, err := VC(c, db)
|
||||
if err != nil {
|
||||
t.Fatal(err.Error())
|
||||
}
|
||||
vc := VC(c)
|
||||
if vc.IsAdmin() {
|
||||
t.Fatal("Not user")
|
||||
}
|
||||
|
||||
@@ -19,9 +19,11 @@ type serverConfigData struct {
|
||||
}
|
||||
|
||||
type appConfigData struct {
|
||||
BcryptCost int `json:"bcrypt_cost"`
|
||||
SaltLength int `json:"salt_length"`
|
||||
JWTKey string `json:"jwt_key"`
|
||||
BcryptCost int `json:"bcrypt_cost"`
|
||||
JWTKey string `json:"jwt_key"`
|
||||
SaltKeySize int `json:"salt_key_size"`
|
||||
SessionKeySize int `json:"session_key_size"`
|
||||
SessionExpirationSeconds float64 `json:"session_expiration_seconds"`
|
||||
}
|
||||
|
||||
type configData struct {
|
||||
@@ -48,9 +50,11 @@ var defaultServerConfigData = serverConfigData{
|
||||
}
|
||||
|
||||
var defaultAppConfigData = appConfigData{
|
||||
BcryptCost: 12,
|
||||
SaltLength: 32,
|
||||
JWTKey: "very secure",
|
||||
BcryptCost: 12,
|
||||
JWTKey: "very secure",
|
||||
SessionKeySize: 64,
|
||||
SaltKeySize: 24,
|
||||
SessionExpirationSeconds: 60 * 60 * 24 * 90,
|
||||
}
|
||||
|
||||
var defaultConfigData = configData{
|
||||
|
||||
@@ -129,8 +129,8 @@ func main() {
|
||||
|
||||
fmt.Printf("=> %s %s application starting on https://%s\n", app.Name, version, config.Server.Address)
|
||||
fmt.Println("=> Run `kolide help serve` for more startup options")
|
||||
fmt.Println("Use Ctrl-C to stop\n\n")
|
||||
|
||||
fmt.Println("Use Ctrl-C to stop")
|
||||
fmt.Print("\n\n")
|
||||
CreateServer(db).RunTLS(
|
||||
config.Server.Address,
|
||||
config.Server.Cert,
|
||||
|
||||
@@ -143,6 +143,7 @@ type Decorator struct {
|
||||
|
||||
var tables = [...]interface{}{
|
||||
&User{},
|
||||
&Session{},
|
||||
&ScheduledQuery{},
|
||||
&Pack{},
|
||||
&DiscoveryQuery{},
|
||||
|
||||
@@ -40,6 +40,7 @@ func MalformedRequestError(c *gin.Context) {
|
||||
func createEmptyTestServer(db *gorm.DB) *gin.Engine {
|
||||
server := gin.New()
|
||||
server.Use(DatabaseMiddleware(db))
|
||||
server.Use(SessionBackendMiddleware)
|
||||
return server
|
||||
}
|
||||
|
||||
@@ -56,6 +57,7 @@ func DatabaseMiddleware(db *gorm.DB) gin.HandlerFunc {
|
||||
func CreateServer(db *gorm.DB) *gin.Engine {
|
||||
server := gin.New()
|
||||
server.Use(DatabaseMiddleware(db))
|
||||
server.Use(SessionBackendMiddleware)
|
||||
|
||||
// TODO: The following loggers are not synchronized with each other or
|
||||
// logrus.StandardLogger() used through the rest of the codebase. As
|
||||
@@ -77,8 +79,6 @@ func CreateServer(db *gorm.DB) *gin.Engine {
|
||||
|
||||
// Kolide application API endpoints
|
||||
kolide := v1.Group("/kolide")
|
||||
kolide.Use(SessionMiddleware)
|
||||
kolide.Use(JWTRenewalMiddleware)
|
||||
|
||||
kolide.POST("/login", Login)
|
||||
kolide.GET("/logout", Logout)
|
||||
@@ -92,6 +92,12 @@ func CreateServer(db *gorm.DB) *gin.Engine {
|
||||
kolide.PATCH("/user/admin", SetUserAdminState)
|
||||
kolide.PATCH("/user/enabled", SetUserEnabledState)
|
||||
|
||||
kolide.POST("/user/sessions", GetInfoAboutSessionsForUser)
|
||||
kolide.DELETE("/user/sessions", DeleteSessionsForUser)
|
||||
|
||||
kolide.DELETE("/session", DeleteSession)
|
||||
kolide.POST("/session", GetInfoAboutSession)
|
||||
|
||||
// osquery API endpoints
|
||||
osquery := v1.Group("/osquery")
|
||||
osquery.POST("/enroll", OsqueryEnroll)
|
||||
|
||||
+539
-59
@@ -1,90 +1,570 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/Sirupsen/logrus"
|
||||
"github.com/dgrijalva/jwt-go"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/gorilla/context"
|
||||
"github.com/gorilla/sessions"
|
||||
"github.com/jinzhu/gorm"
|
||||
)
|
||||
|
||||
// GetSession allows you to get the Session object given a web request. This
|
||||
// is often used in HTTP handlers as the main entry point into managing and
|
||||
// manipulating the session
|
||||
func GetSession(c *gin.Context) *Session {
|
||||
return c.MustGet("Session").(*Session)
|
||||
var (
|
||||
// An error returned by SessionBackend.Get() if no session record was found
|
||||
// in the database
|
||||
ErrNoActiveSession = errors.New("Active session is not present in the database")
|
||||
|
||||
// An error returned by SessionBackend methods when no session object has
|
||||
// been created yet but the requested action requires one
|
||||
ErrSessionNotCreated = errors.New("The session has not been created")
|
||||
|
||||
// An error returned by SessionBackend.Get() when a session is requested but
|
||||
// it has expired
|
||||
ErrSessionExpired = errors.New("The session has expired")
|
||||
)
|
||||
|
||||
const (
|
||||
// The name of the session cookie
|
||||
CookieName = "KolideSession"
|
||||
)
|
||||
|
||||
// Session is the model object which represents what an active session is
|
||||
type Session struct {
|
||||
BaseModel
|
||||
UserID uint `gorm:"not null"`
|
||||
Key string `gorm:"not null;unique_index:idx_session_unique_key"`
|
||||
AccessedAt time.Time
|
||||
}
|
||||
|
||||
// SessionMiddleware is the middleware used for production session management.
|
||||
// Tests should use `testSessionMiddleware`, which follows the same pattern,
|
||||
// but creates a session configured for testing.
|
||||
func SessionMiddleware(c *gin.Context) {
|
||||
CreateSession("Session", sessions.NewCookieStore([]byte("c")))(c)
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
// Managing sessions
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
// SessionManager is a management object which helps with the administration of
|
||||
// sessions within the application. Use NewSessionManager to create an instance
|
||||
type SessionManager struct {
|
||||
backend SessionBackend
|
||||
request *http.Request
|
||||
writer http.ResponseWriter
|
||||
session *Session
|
||||
vc *ViewerContext
|
||||
db *gorm.DB
|
||||
}
|
||||
|
||||
// CreateSessions is a helper which returns a gin.HandlerFunc which creates
|
||||
// a new session management middleware given the name of the session to manage
|
||||
// and the session storage mechanism. This is commonly used to generate session
|
||||
// middleware given a variety of settings in both production and testing
|
||||
// environments
|
||||
func CreateSession(name string, store sessions.Store) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
s := &Session{name, c.Request, store, nil, c.Writer}
|
||||
c.Set("Session", s)
|
||||
defer context.Clear(c.Request)
|
||||
c.Next()
|
||||
// NewSessionManager allows you to get a SessionManager instance for a given
|
||||
// web request. Unless you're interacting with login, logout, or core auth
|
||||
// code, this should be abstracted by the ViewerContext pattern.
|
||||
func NewSessionManager(c *gin.Context) *SessionManager {
|
||||
return &SessionManager{
|
||||
request: c.Request,
|
||||
backend: GetSessionBackend(c),
|
||||
writer: c.Writer,
|
||||
db: GetDB(c),
|
||||
}
|
||||
}
|
||||
|
||||
// Session is a convenience wrapper around gorilla sessions, which is provided
|
||||
// by github.com/gorilla/sessions
|
||||
type Session struct {
|
||||
name string
|
||||
request *http.Request
|
||||
store sessions.Store
|
||||
session *sessions.Session
|
||||
writer http.ResponseWriter
|
||||
// Get the ViewerContext instance for a user represented by the active session
|
||||
func (sm *SessionManager) VC() *ViewerContext {
|
||||
if sm.session == nil {
|
||||
cookie, err := sm.request.Cookie(CookieName)
|
||||
if err != nil {
|
||||
switch err {
|
||||
case http.ErrNoCookie:
|
||||
// No cookie was set
|
||||
return EmptyVC()
|
||||
default:
|
||||
// Something went wrong and the cookie may or may not be set
|
||||
logrus.Errorf("Couldn't get cookie: %s", err.Error())
|
||||
return EmptyVC()
|
||||
}
|
||||
}
|
||||
|
||||
token, err := ParseJWT(cookie.Value)
|
||||
if err != nil {
|
||||
logrus.Errorf("Couldn't parse JWT token string from cookie: %s", err.Error())
|
||||
return EmptyVC()
|
||||
}
|
||||
|
||||
claims, ok := token.Claims.(jwt.MapClaims)
|
||||
if !ok {
|
||||
logrus.Error("Could not parse the claims from the JWT token")
|
||||
return EmptyVC()
|
||||
}
|
||||
|
||||
sessionKeyClaim, ok := claims["session_key"]
|
||||
if !ok {
|
||||
logrus.Warn("JWT did not have session_key claim")
|
||||
return EmptyVC()
|
||||
}
|
||||
|
||||
sessionKey, ok := sessionKeyClaim.(string)
|
||||
if !ok {
|
||||
logrus.Warn("JWT session_key claim was not a string")
|
||||
return EmptyVC()
|
||||
}
|
||||
|
||||
session, err := sm.backend.FindKey(sessionKey)
|
||||
if err != nil {
|
||||
switch err {
|
||||
case ErrNoActiveSession:
|
||||
// If the code path got this far, it's likely that the user was logged
|
||||
// in some time in the past, but their session has been expired since
|
||||
// their last usage of the application
|
||||
return EmptyVC()
|
||||
default:
|
||||
logrus.Errorf("Couldn't call Get on backend object: %s", err.Error())
|
||||
return EmptyVC()
|
||||
}
|
||||
}
|
||||
sm.session = session
|
||||
}
|
||||
|
||||
if sm.vc == nil {
|
||||
// Generating a VC requires a user struct. Attempt to populate one using
|
||||
// the user id of the current session holder
|
||||
user := &User{BaseModel: BaseModel{ID: sm.session.UserID}}
|
||||
err := sm.db.Where(user).First(user).Error
|
||||
if err != nil {
|
||||
return EmptyVC()
|
||||
}
|
||||
|
||||
sm.vc = GenerateVC(user)
|
||||
}
|
||||
|
||||
return sm.vc
|
||||
}
|
||||
|
||||
// Session returns the gorilla session from the Session struct and allows you
|
||||
// to use any of the functionality of the underlying sessions.Session struct
|
||||
func (s *Session) Session() *sessions.Session {
|
||||
if s.session == nil {
|
||||
var err error
|
||||
s.session, err = s.store.Get(s.request, s.name)
|
||||
// MakeSessionForUserID creates a session in the database for a given user id.
|
||||
// You must call Save() after calling this.
|
||||
func (sm *SessionManager) MakeSessionForUserID(id uint) error {
|
||||
session, err := sm.backend.Create(id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
sm.session = session
|
||||
return nil
|
||||
}
|
||||
|
||||
// MakeSessionForUserID creates a session in the database for a given user
|
||||
// You must call Save() after calling this.
|
||||
func (sm *SessionManager) MakeSessionForUser(u *User) error {
|
||||
return sm.MakeSessionForUserID(u.ID)
|
||||
}
|
||||
|
||||
// Save writes the current session to a token and delivers the token as a cookie
|
||||
// to the user. Save must be called after every write action on this struct
|
||||
// (MakeSessionForUser, Destroy, etc.)
|
||||
func (sm *SessionManager) Save() error {
|
||||
token, err := GenerateJWT(sm.session.Key)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// TODO: set proper flags on cookie for maximum security
|
||||
http.SetCookie(sm.writer, &http.Cookie{
|
||||
Name: CookieName,
|
||||
Value: token,
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Destroy deletes the active session from the database and erases the session
|
||||
// instance from this object's access. You must call Save() after calling this.
|
||||
func (sm *SessionManager) Destroy() error {
|
||||
if sm.backend != nil {
|
||||
err := sm.backend.Destroy(sm.session)
|
||||
if err != nil {
|
||||
logrus.Error(err.Error())
|
||||
return err
|
||||
}
|
||||
}
|
||||
return s.session
|
||||
return nil
|
||||
}
|
||||
|
||||
// Set simply sets a session key value pair which will be stored in the
|
||||
// current session for later usage
|
||||
func (s *Session) Set(key interface{}, val interface{}) {
|
||||
s.Session().Values[key] = val
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
// Session Backend API
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
// SessionBackend is the abstract interface that all session backends must
|
||||
// conform to. SessionBackend instances are only expected to exist within the
|
||||
// context of a single request.
|
||||
type SessionBackend interface {
|
||||
// Given a session key, find and return a session object or an error if one
|
||||
// could not be found for the given key
|
||||
FindKey(key string) (*Session, error)
|
||||
|
||||
// Given a session id, find and return a session object or an error if one
|
||||
// could not be found for the given id
|
||||
FindID(id uint) (*Session, error)
|
||||
|
||||
// Find all of the active sessions for a given user
|
||||
FindAllForUser(id uint) ([]*Session, error)
|
||||
|
||||
// Create a session object tied to the given user ID
|
||||
Create(userID uint) (*Session, error)
|
||||
|
||||
// Destroy the currently tracked session
|
||||
Destroy(session *Session) error
|
||||
|
||||
// Destroy all of the sessions for a given user
|
||||
DestroyAllForUser(id uint) error
|
||||
|
||||
// Mark the currently tracked session as access to extend expiration
|
||||
MarkAccessed(session *Session) error
|
||||
}
|
||||
|
||||
// Get retrieves a session key value pair which has previously been set
|
||||
func (s *Session) Get(key interface{}) interface{} {
|
||||
return s.Session().Values[key]
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
// Session Backend Plugins
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
// GormSessionBackend stores sessions using a pre-instantiated gorm database
|
||||
// object
|
||||
type GormSessionBackend struct {
|
||||
db *gorm.DB
|
||||
}
|
||||
|
||||
// Delete deletes a session key value pair which has previously been set
|
||||
func (s *Session) Delete(key interface{}) {
|
||||
delete(s.Session().Values, key)
|
||||
}
|
||||
|
||||
// Clear deletes all session key value pairs that are set
|
||||
func (s *Session) Clear() {
|
||||
for key := range s.Session().Values {
|
||||
s.Delete(key)
|
||||
func (s *GormSessionBackend) validate(session *Session) error {
|
||||
if time.Since(session.AccessedAt).Seconds() >= config.App.SessionExpirationSeconds {
|
||||
err := s.db.Delete(session).Error
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return ErrSessionExpired
|
||||
}
|
||||
|
||||
err := s.MarkAccessed(session)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Save writes the session, which is required after altering the session in any
|
||||
// way
|
||||
func (s *Session) Save() error {
|
||||
return s.Session().Save(s.request, s.writer)
|
||||
func (s *GormSessionBackend) FindID(id uint) (*Session, error) {
|
||||
session := &Session{
|
||||
BaseModel: BaseModel{
|
||||
ID: id,
|
||||
},
|
||||
}
|
||||
|
||||
err := s.db.Where(session).First(session).Error
|
||||
if err != nil {
|
||||
switch err {
|
||||
case gorm.ErrRecordNotFound:
|
||||
return nil, ErrNoActiveSession
|
||||
default:
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
err = s.validate(session)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return session, nil
|
||||
|
||||
}
|
||||
|
||||
func (s *GormSessionBackend) FindKey(key string) (*Session, error) {
|
||||
session := &Session{
|
||||
Key: key,
|
||||
}
|
||||
|
||||
err := s.db.Where(session).First(session).Error
|
||||
if err != nil {
|
||||
switch err {
|
||||
case gorm.ErrRecordNotFound:
|
||||
return nil, ErrNoActiveSession
|
||||
default:
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
err = s.validate(session)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return session, nil
|
||||
}
|
||||
|
||||
func (s *GormSessionBackend) FindAllForUser(id uint) ([]*Session, error) {
|
||||
var sessions []*Session
|
||||
err := s.db.Where("user_id = ?", id).Find(&sessions).Error
|
||||
return sessions, err
|
||||
}
|
||||
|
||||
func (s *GormSessionBackend) Create(userID uint) (*Session, error) {
|
||||
key, err := generateRandomText(config.App.SessionKeySize)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
session := &Session{
|
||||
UserID: userID,
|
||||
Key: key,
|
||||
}
|
||||
|
||||
err = s.db.Create(session).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = s.MarkAccessed(session)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return session, nil
|
||||
}
|
||||
|
||||
func (s *GormSessionBackend) Destroy(session *Session) error {
|
||||
err := s.db.Delete(session).Error
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *GormSessionBackend) DestroyAllForUser(id uint) error {
|
||||
return s.db.Delete(&Session{}, "user_id = ?", id).Error
|
||||
}
|
||||
|
||||
func (s *GormSessionBackend) MarkAccessed(session *Session) error {
|
||||
session.AccessedAt = time.Now().UTC()
|
||||
return s.db.Save(session).Error
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
// Session management HTTP endpoints
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
// Setting the session backend via a middleware
|
||||
func SessionBackendMiddleware(c *gin.Context) {
|
||||
db := GetDB(c)
|
||||
c.Set("SessionBackend", &GormSessionBackend{db})
|
||||
c.Next()
|
||||
}
|
||||
|
||||
// Get the database connection from the context, or panic
|
||||
func GetSessionBackend(c *gin.Context) SessionBackend {
|
||||
return c.MustGet("SessionBackend").(SessionBackend)
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
// Session management HTTP endpoints
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
type DeleteSessionRequestBody struct {
|
||||
SessionID uint `json:"session_id" binding:"required"`
|
||||
}
|
||||
|
||||
func DeleteSession(c *gin.Context) {
|
||||
var body DeleteSessionRequestBody
|
||||
err := c.BindJSON(&body)
|
||||
if err != nil {
|
||||
logrus.Errorf(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
vc := VC(c)
|
||||
if !vc.CanPerformActions() {
|
||||
UnauthorizedError(c)
|
||||
return
|
||||
}
|
||||
|
||||
sb := GetSessionBackend(c)
|
||||
|
||||
session, err := sb.FindID(body.SessionID)
|
||||
if err != nil {
|
||||
|
||||
}
|
||||
|
||||
db := GetDB(c)
|
||||
user := &User{
|
||||
BaseModel: BaseModel{
|
||||
ID: session.UserID,
|
||||
},
|
||||
}
|
||||
err = db.Where(user).First(user).Error
|
||||
if err != nil {
|
||||
DatabaseError(c)
|
||||
return
|
||||
}
|
||||
|
||||
if !vc.CanPerformWriteActionOnUser(user) {
|
||||
UnauthorizedError(c)
|
||||
return
|
||||
}
|
||||
|
||||
err = sb.Destroy(session)
|
||||
if err != nil {
|
||||
DatabaseError(c)
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(200, nil)
|
||||
}
|
||||
|
||||
type DeleteSessionsForUserRequestBody struct {
|
||||
ID uint `json:"id"`
|
||||
Username string `json:"username"`
|
||||
}
|
||||
|
||||
func DeleteSessionsForUser(c *gin.Context) {
|
||||
var body DeleteSessionsForUserRequestBody
|
||||
err := c.BindJSON(&body)
|
||||
if err != nil {
|
||||
logrus.Errorf(err.Error())
|
||||
}
|
||||
|
||||
vc := VC(c)
|
||||
if !vc.CanPerformActions() {
|
||||
UnauthorizedError(c)
|
||||
return
|
||||
}
|
||||
|
||||
db := GetDB(c)
|
||||
var user User
|
||||
user.ID = body.ID
|
||||
user.Username = body.Username
|
||||
err = db.Where(&user).First(&user).Error
|
||||
if err != nil {
|
||||
DatabaseError(c)
|
||||
return
|
||||
}
|
||||
|
||||
if !vc.CanPerformWriteActionOnUser(&user) {
|
||||
UnauthorizedError(c)
|
||||
return
|
||||
}
|
||||
|
||||
sb := GetSessionBackend(c)
|
||||
err = sb.DestroyAllForUser(user.ID)
|
||||
err = db.Delete(&Session{}, "user_id = ?", user.ID).Error
|
||||
if err != nil {
|
||||
DatabaseError(c)
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(200, nil)
|
||||
|
||||
}
|
||||
|
||||
type GetInfoAboutSessionRequestBody struct {
|
||||
SessionKey string `json:"session_key" binding:"required"`
|
||||
}
|
||||
|
||||
type SessionInfoResponseBody struct {
|
||||
SessionID uint `json:"session_id"`
|
||||
UserID uint `json:"user_id"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
AccessedAt time.Time `json:"created_at"`
|
||||
}
|
||||
|
||||
func GetInfoAboutSession(c *gin.Context) {
|
||||
var body GetInfoAboutSessionRequestBody
|
||||
err := c.BindJSON(&body)
|
||||
if err != nil {
|
||||
logrus.Errorf(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
vc := VC(c)
|
||||
if !vc.CanPerformActions() {
|
||||
UnauthorizedError(c)
|
||||
return
|
||||
}
|
||||
|
||||
sb := GetSessionBackend(c)
|
||||
session, err := sb.FindKey(body.SessionKey)
|
||||
if err != nil {
|
||||
DatabaseError(c)
|
||||
return
|
||||
}
|
||||
|
||||
db := GetDB(c)
|
||||
var user User
|
||||
user.ID = session.UserID
|
||||
err = db.Where(&user).First(&user).Error
|
||||
if err != nil {
|
||||
DatabaseError(c)
|
||||
return
|
||||
}
|
||||
|
||||
if !vc.IsAdmin() && !vc.IsUserID(user.ID) {
|
||||
UnauthorizedError(c)
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(200, &SessionInfoResponseBody{
|
||||
SessionID: session.ID,
|
||||
UserID: session.UserID,
|
||||
CreatedAt: session.CreatedAt,
|
||||
AccessedAt: session.AccessedAt,
|
||||
})
|
||||
}
|
||||
|
||||
type GetInfoAboutSessionsForUserRequestBody struct {
|
||||
ID uint `json:"id"`
|
||||
Username string `json:"username"`
|
||||
}
|
||||
|
||||
type GetInfoAboutSessionsForUserResponseBody struct {
|
||||
Sessions []*SessionInfoResponseBody `json:"sessions"`
|
||||
}
|
||||
|
||||
func GetInfoAboutSessionsForUser(c *gin.Context) {
|
||||
var body GetInfoAboutSessionsForUserRequestBody
|
||||
err := c.BindJSON(&body)
|
||||
if err != nil {
|
||||
logrus.Errorf(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
vc := VC(c)
|
||||
if !vc.CanPerformActions() {
|
||||
UnauthorizedError(c)
|
||||
return
|
||||
}
|
||||
|
||||
db := GetDB(c)
|
||||
var user User
|
||||
user.ID = body.ID
|
||||
user.Username = body.Username
|
||||
err = db.Where(&user).First(&user).Error
|
||||
if err != nil {
|
||||
DatabaseError(c)
|
||||
return
|
||||
}
|
||||
|
||||
if !vc.IsAdmin() && !vc.IsUserID(user.ID) {
|
||||
UnauthorizedError(c)
|
||||
return
|
||||
}
|
||||
|
||||
sb := GetSessionBackend(c)
|
||||
sessions, err := sb.FindAllForUser(user.ID)
|
||||
if err != nil {
|
||||
DatabaseError(c)
|
||||
return
|
||||
}
|
||||
|
||||
var response []*SessionInfoResponseBody
|
||||
for _, session := range sessions {
|
||||
response = append(response, &SessionInfoResponseBody{
|
||||
SessionID: session.ID,
|
||||
UserID: session.UserID,
|
||||
CreatedAt: session.CreatedAt,
|
||||
AccessedAt: session.AccessedAt,
|
||||
})
|
||||
}
|
||||
|
||||
c.JSON(200, &GetInfoAboutSessionsForUserResponseBody{
|
||||
Sessions: response,
|
||||
})
|
||||
}
|
||||
|
||||
+83
-139
@@ -8,166 +8,110 @@ import (
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
func TestSessionGetSet(t *testing.T) {
|
||||
db := openTestDB()
|
||||
r := createEmptyTestServer(db)
|
||||
r.Use(testSessionMiddleware)
|
||||
r.Use(JWTRenewalMiddleware)
|
||||
|
||||
r.GET("/set", func(c *gin.Context) {
|
||||
session := GetSession(c)
|
||||
session.Set("key", "foobar")
|
||||
session.Save()
|
||||
c.JSON(200, nil)
|
||||
})
|
||||
|
||||
r.GET("/get", func(c *gin.Context) {
|
||||
session := GetSession(c)
|
||||
if session.Get("key") != "foobar" {
|
||||
t.Fatal("Session writing failed")
|
||||
}
|
||||
c.String(200, "OK")
|
||||
})
|
||||
|
||||
res1 := httptest.NewRecorder()
|
||||
req1, _ := http.NewRequest("GET", "/set", nil)
|
||||
r.ServeHTTP(res1, req1)
|
||||
|
||||
res2 := httptest.NewRecorder()
|
||||
req2, _ := http.NewRequest("GET", "/get", nil)
|
||||
req2.Header.Set("Cookie", res1.Header().Get("Set-Cookie"))
|
||||
r.ServeHTTP(res2, req2)
|
||||
type MockResponseWriter struct {
|
||||
}
|
||||
|
||||
func TestSessionDeleteKey(t *testing.T) {
|
||||
db := openTestDB()
|
||||
r := createEmptyTestServer(db)
|
||||
r.Use(testSessionMiddleware)
|
||||
r.Use(JWTRenewalMiddleware)
|
||||
|
||||
r.GET("/set", func(c *gin.Context) {
|
||||
session := GetSession(c)
|
||||
session.Set("key", "foobar")
|
||||
session.Save()
|
||||
c.JSON(200, nil)
|
||||
})
|
||||
|
||||
r.GET("/delete", func(c *gin.Context) {
|
||||
session := GetSession(c)
|
||||
session.Delete("key")
|
||||
session.Save()
|
||||
c.JSON(200, nil)
|
||||
})
|
||||
|
||||
r.GET("/get", func(c *gin.Context) {
|
||||
session := GetSession(c)
|
||||
if session.Get("key") != nil {
|
||||
t.Fatal("Session deleting failed")
|
||||
}
|
||||
c.JSON(200, nil)
|
||||
})
|
||||
|
||||
res1 := httptest.NewRecorder()
|
||||
req1, _ := http.NewRequest("GET", "/set", nil)
|
||||
r.ServeHTTP(res1, req1)
|
||||
|
||||
res2 := httptest.NewRecorder()
|
||||
req2, _ := http.NewRequest("GET", "/delete", nil)
|
||||
req2.Header.Set("Cookie", res1.Header().Get("Set-Cookie"))
|
||||
r.ServeHTTP(res2, req2)
|
||||
|
||||
res3 := httptest.NewRecorder()
|
||||
req3, _ := http.NewRequest("GET", "/get", nil)
|
||||
req3.Header.Set("Cookie", res2.Header().Get("Set-Cookie"))
|
||||
r.ServeHTTP(res3, req3)
|
||||
func (w *MockResponseWriter) Header() http.Header {
|
||||
return map[string][]string{}
|
||||
}
|
||||
|
||||
func TestSessionFlashes(t *testing.T) {
|
||||
db := openTestDB()
|
||||
r := createEmptyTestServer(db)
|
||||
r.Use(testSessionMiddleware)
|
||||
r.Use(JWTRenewalMiddleware)
|
||||
|
||||
r.GET("/set", func(c *gin.Context) {
|
||||
session := GetSession(c)
|
||||
session.Session().AddFlash("foobar")
|
||||
session.Save()
|
||||
c.JSON(200, nil)
|
||||
})
|
||||
|
||||
r.GET("/flash", func(c *gin.Context) {
|
||||
session := GetSession(c)
|
||||
l := len(session.Session().Flashes())
|
||||
if l != 1 {
|
||||
t.Fatal("Flashes count does not equal 1. Equals ", l)
|
||||
}
|
||||
session.Save()
|
||||
c.JSON(200, nil)
|
||||
})
|
||||
|
||||
r.GET("/check", func(c *gin.Context) {
|
||||
session := GetSession(c)
|
||||
l := len(session.Session().Flashes())
|
||||
if l != 0 {
|
||||
t.Fatal("flashes count is not 0 after reading. Equals ", l)
|
||||
}
|
||||
session.Save()
|
||||
c.JSON(200, nil)
|
||||
})
|
||||
|
||||
res1 := httptest.NewRecorder()
|
||||
req1, _ := http.NewRequest("GET", "/set", nil)
|
||||
r.ServeHTTP(res1, req1)
|
||||
|
||||
res2 := httptest.NewRecorder()
|
||||
req2, _ := http.NewRequest("GET", "/flash", nil)
|
||||
req2.Header.Set("Cookie", res1.Header().Get("Set-Cookie"))
|
||||
r.ServeHTTP(res2, req2)
|
||||
|
||||
res3 := httptest.NewRecorder()
|
||||
req3, _ := http.NewRequest("GET", "/check", nil)
|
||||
req3.Header.Set("Cookie", res2.Header().Get("Set-Cookie"))
|
||||
r.ServeHTTP(res3, req3)
|
||||
func (w *MockResponseWriter) Write([]byte) (int, error) {
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
func TestSessionClear(t *testing.T) {
|
||||
db := openTestDB()
|
||||
r := createEmptyTestServer(db)
|
||||
func (w *MockResponseWriter) WriteHeader(int) {
|
||||
}
|
||||
|
||||
data := map[string]string{
|
||||
"key": "val",
|
||||
"foo": "bar",
|
||||
func TestSessionManagerVC(t *testing.T) {
|
||||
db := openTestDB()
|
||||
|
||||
admin, err := NewUser(db, "admin", "foobar", "admin@kolide.co", true, false)
|
||||
if err != nil {
|
||||
t.Fatal(err.Error())
|
||||
}
|
||||
store := getTestStore()
|
||||
r.Use(CreateSession(testSessionName, store))
|
||||
r.Use(JWTRenewalMiddleware)
|
||||
|
||||
r.GET("/set", func(c *gin.Context) {
|
||||
session := GetSession(c)
|
||||
for k, v := range data {
|
||||
session.Set(k, v)
|
||||
backend := &GormSessionBackend{db}
|
||||
session, err := backend.Create(admin.ID)
|
||||
if err != nil {
|
||||
t.Fatal(err.Error())
|
||||
}
|
||||
|
||||
if session.UserID != admin.ID {
|
||||
t.Fatal("IDs do not match")
|
||||
}
|
||||
|
||||
token, err := GenerateJWT(session.Key)
|
||||
|
||||
cookie := &http.Cookie{
|
||||
Name: CookieName,
|
||||
Value: token,
|
||||
}
|
||||
|
||||
req, err := http.NewRequest("GET", "/", nil)
|
||||
if err != nil {
|
||||
t.Fatal(err.Error())
|
||||
}
|
||||
req.AddCookie(cookie)
|
||||
|
||||
writer := &MockResponseWriter{}
|
||||
|
||||
sm := &SessionManager{
|
||||
request: req,
|
||||
writer: writer,
|
||||
backend: backend,
|
||||
db: db,
|
||||
}
|
||||
vc := sm.VC()
|
||||
|
||||
if !vc.IsAdmin() {
|
||||
t.Fatal("User should be admin")
|
||||
}
|
||||
|
||||
vcID, _ := vc.UserID()
|
||||
if vcID != admin.ID {
|
||||
t.Fatal("IDs don't match")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSessionCreation(t *testing.T) {
|
||||
db := openTestDB()
|
||||
r := createEmptyTestServer(db)
|
||||
admin, _ := NewUser(db, "admin", "foobar", "admin@kolide.co", true, false)
|
||||
|
||||
r.GET("/login", func(c *gin.Context) {
|
||||
sm := NewSessionManager(c)
|
||||
sm.MakeSessionForUser(admin)
|
||||
err := sm.Save()
|
||||
if err != nil {
|
||||
t.Fatal(err.Error())
|
||||
}
|
||||
session.Clear()
|
||||
session.Save()
|
||||
c.JSON(200, nil)
|
||||
})
|
||||
|
||||
r.GET("/check", func(c *gin.Context) {
|
||||
session := GetSession(c)
|
||||
for k, v := range data {
|
||||
if session.Get(k) == v {
|
||||
t.Fatal("Session clear failed")
|
||||
}
|
||||
r.GET("/resource", func(c *gin.Context) {
|
||||
sm := NewSessionManager(c)
|
||||
vc := sm.VC()
|
||||
if !vc.IsAdmin() {
|
||||
t.Fatal("Request is not admin")
|
||||
}
|
||||
c.JSON(200, nil)
|
||||
})
|
||||
|
||||
r.GET("/nope", func(c *gin.Context) {
|
||||
sm := NewSessionManager(c)
|
||||
vc := sm.VC()
|
||||
if !vc.IsAdmin() {
|
||||
t.Fatal("Request is not admin")
|
||||
}
|
||||
c.JSON(200, nil)
|
||||
})
|
||||
|
||||
res1 := httptest.NewRecorder()
|
||||
req1, _ := http.NewRequest("GET", "/set", nil)
|
||||
req1, _ := http.NewRequest("GET", "/login", nil)
|
||||
r.ServeHTTP(res1, req1)
|
||||
|
||||
res2 := httptest.NewRecorder()
|
||||
req2, _ := http.NewRequest("GET", "/check", nil)
|
||||
req2, _ := http.NewRequest("GET", "/resource", nil)
|
||||
req2.Header.Set("Cookie", res1.Header().Get("Set-Cookie"))
|
||||
r.ServeHTTP(res2, req2)
|
||||
}
|
||||
|
||||
+77
-19
@@ -1,7 +1,11 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
jwt "github.com/dgrijalva/jwt-go"
|
||||
"github.com/jinzhu/gorm"
|
||||
)
|
||||
|
||||
func TestUserAndAccountManagement(t *testing.T) {
|
||||
@@ -21,18 +25,18 @@ func TestUserAndAccountManagement(t *testing.T) {
|
||||
req.Login("admin", "foobar", &adminSession)
|
||||
|
||||
// Once admin is logged in, create a user using a valid admin session
|
||||
req.CreateAndCheckUser("user1", "foobar", "user1@kolide.co", "", false, false, &adminSession)
|
||||
req.CreateAndCheckUser("user1", "foobar", "user1@kolide.co", "", false, false, adminSession)
|
||||
|
||||
// Once admin is logged in, create another admin account using a valid
|
||||
// admin session
|
||||
req.CreateAndCheckUser("admin2", "foobar", "admin2@kolide.co", "", true, false, &adminSession)
|
||||
req.CreateAndCheckUser("admin2", "foobar", "admin2@kolide.co", "", true, false, adminSession)
|
||||
|
||||
// Once admin has created admin2, log in with admin2 to get a session
|
||||
// context for admin2
|
||||
req.Login("admin2", "foobar", &admin2Session)
|
||||
|
||||
// Use an admin created via the API to create a user via the API
|
||||
req.CreateAndCheckUser("user2", "foobar", "user2@kolide.co", "", false, false, &admin2Session)
|
||||
req.CreateAndCheckUser("user2", "foobar", "user2@kolide.co", "", false, false, admin2Session)
|
||||
|
||||
// Once admin has created user1, log in with user1 to get a session context
|
||||
// for user1
|
||||
@@ -43,65 +47,119 @@ func TestUserAndAccountManagement(t *testing.T) {
|
||||
req.Login("user2", "foobar", &user2Session)
|
||||
|
||||
// Get info on user2 as admin2
|
||||
req.GetAndCheckUser("user2", &admin2Session)
|
||||
req.GetAndCheckUser("user2", admin2Session)
|
||||
|
||||
// Get info on admin2 as user2
|
||||
req.GetAndCheckUser("admin2", &user2Session)
|
||||
req.GetAndCheckUser("admin2", user2Session)
|
||||
|
||||
// Get session info for admin
|
||||
adminSessionInfo := req.GetUserSessionInfo("admin", adminSession)
|
||||
if len(adminSessionInfo.Sessions) != 1 {
|
||||
t.Fatalf("Expected 1 session, found %d", len(adminSessionInfo.Sessions))
|
||||
}
|
||||
|
||||
// Pull the token out of the JWT token and get the session info via that
|
||||
token, err := ParseJWT(strings.Split(adminSession, "=")[1])
|
||||
if err != nil {
|
||||
t.Fatal(err.Error())
|
||||
}
|
||||
sessionKey := token.Claims.(jwt.MapClaims)["session_key"].(string)
|
||||
|
||||
adminSessionInfoVerify := req.GetSessionInfo(sessionKey, adminSession)
|
||||
|
||||
if adminSessionInfo.Sessions[0].SessionID != adminSessionInfoVerify.SessionID {
|
||||
t.Fatal("Session IDs don't match")
|
||||
}
|
||||
|
||||
// Delete the admin session
|
||||
req.DeleteSession(adminSessionInfo.Sessions[0].SessionID, adminSession)
|
||||
|
||||
// Verify the session was deleted
|
||||
sessionVerify := &Session{
|
||||
Key: sessionKey,
|
||||
}
|
||||
err = req.db.Where(sessionVerify).First(sessionVerify).Error
|
||||
if err != gorm.ErrRecordNotFound {
|
||||
t.Fatal("Record should not exist in the database")
|
||||
}
|
||||
|
||||
// Re-login as admin
|
||||
req.Login("admin", "foobar", &adminSession)
|
||||
var adminSession2 string
|
||||
req.Login("admin", "foobar", &adminSession2)
|
||||
|
||||
// Get session info for admin
|
||||
adminSessionInfo = req.GetUserSessionInfo("admin", adminSession)
|
||||
if len(adminSessionInfo.Sessions) != 2 {
|
||||
t.Fatalf("Expected 2 sessions, found %d", len(adminSessionInfo.Sessions))
|
||||
}
|
||||
|
||||
// Delete all admin session as admin2
|
||||
req.DeleteUserSessions("admin", admin2Session)
|
||||
|
||||
// Verify there are no admin sessions left
|
||||
adminSessionInfo = req.GetUserSessionInfo("admin", admin2Session)
|
||||
if len(adminSessionInfo.Sessions) != 0 {
|
||||
t.Fatalf("Expected 0 sessions, found %d", len(adminSessionInfo.Sessions))
|
||||
}
|
||||
|
||||
// Re-login as admin
|
||||
req.Login("admin", "foobar", &adminSession)
|
||||
|
||||
// Modify user1 as admin
|
||||
req.ModifyAndCheckUser("user1", "user1@kolide.co", "User One", false, false, &adminSession)
|
||||
req.ModifyAndCheckUser("user1", "user1@kolide.co", "User One", false, false, adminSession)
|
||||
|
||||
// Modify user2 as user2
|
||||
req.ModifyAndCheckUser("user2", "user2@kolide.co", "User Two", false, false, &user2Session)
|
||||
req.ModifyAndCheckUser("user2", "user2@kolide.co", "User Two", false, false, user2Session)
|
||||
|
||||
// admin resets user1 password
|
||||
req.ChangePassword("user1", "", "bazz1", &adminSession)
|
||||
req.ChangePassword("user1", "", "bazz1", adminSession)
|
||||
|
||||
// user1 logs in with new password
|
||||
req.Login("user1", "bazz1", &user1Session)
|
||||
|
||||
// user2 resets user2 password
|
||||
req.ChangePassword("user2", "foobar", "bazz2", &user2Session)
|
||||
req.ChangePassword("user2", "foobar", "bazz2", user2Session)
|
||||
|
||||
// user2 logs in with new password
|
||||
req.Login("user2", "bazz2", &user2Session)
|
||||
|
||||
// admin2 promotes user2 to admin
|
||||
req.SetAdminStateAndCheckUser("user2", true, &admin2Session)
|
||||
req.SetAdminStateAndCheckUser("user2", true, admin2Session)
|
||||
|
||||
// user2 is admin
|
||||
resp := req.GetUser("user2", &user2Session)
|
||||
resp := req.GetUser("user2", user2Session)
|
||||
if !resp.Admin {
|
||||
t.Fatal("user2 should be an admin")
|
||||
}
|
||||
|
||||
// admin demotes user2 from admin
|
||||
req.SetAdminStateAndCheckUser("user2", false, &adminSession)
|
||||
req.SetAdminStateAndCheckUser("user2", false, adminSession)
|
||||
|
||||
// user2 is no longer an admin
|
||||
resp = req.GetUser("user2", &user2Session)
|
||||
resp = req.GetUser("user2", user2Session)
|
||||
if resp.Admin {
|
||||
t.Fatal("user2 shouldn't be an admin")
|
||||
}
|
||||
|
||||
// admin sets user1 as no longer enabled
|
||||
req.SetEnabledStateAndCheckUser("user1", false, &adminSession)
|
||||
req.SetEnabledStateAndCheckUser("user1", false, adminSession)
|
||||
|
||||
// user1 is no longer enabled
|
||||
resp = req.GetUser("user1", &user2Session)
|
||||
resp = req.GetUser("user1", user2Session)
|
||||
if resp.Enabled {
|
||||
t.Fatal("user1 shouldn't be enabled")
|
||||
}
|
||||
|
||||
// admin2 re-enables user1
|
||||
req.SetEnabledStateAndCheckUser("user1", true, &admin2Session)
|
||||
req.SetEnabledStateAndCheckUser("user1", true, admin2Session)
|
||||
|
||||
// user1 can view user2
|
||||
req.GetUser("user2", &user2Session)
|
||||
req.GetUser("user2", user2Session)
|
||||
|
||||
// Delete admin2 as admin1
|
||||
req.DeleteAndCheckUser("admin2", &adminSession)
|
||||
req.DeleteAndCheckUser("admin2", adminSession)
|
||||
|
||||
// Delete user2 as admin
|
||||
req.DeleteAndCheckUser("user2", &adminSession)
|
||||
req.DeleteAndCheckUser("user2", adminSession)
|
||||
}
|
||||
|
||||
+134
-38
@@ -8,20 +8,9 @@ import (
|
||||
"testing"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/gorilla/sessions"
|
||||
"github.com/jinzhu/gorm"
|
||||
)
|
||||
|
||||
const testSessionName = "TestSession"
|
||||
|
||||
func getTestStore() sessions.Store {
|
||||
return sessions.NewCookieStore([]byte("test"))
|
||||
}
|
||||
|
||||
func testSessionMiddleware(c *gin.Context) {
|
||||
CreateSession(testSessionName, getTestStore())(c)
|
||||
}
|
||||
|
||||
type IntegrationRequests struct {
|
||||
r *gin.Engine
|
||||
db *gorm.DB
|
||||
@@ -70,7 +59,7 @@ func (req *IntegrationRequests) Login(username, password string, sessionOut *str
|
||||
return
|
||||
}
|
||||
|
||||
func (req *IntegrationRequests) CreateUser(username, password, email string, admin, reset bool, session *string) *GetUserResponseBody {
|
||||
func (req *IntegrationRequests) CreateUser(username, password, email string, admin, reset bool, session string) *GetUserResponseBody {
|
||||
response := httptest.NewRecorder()
|
||||
body, err := json.Marshal(CreateUserRequestBody{
|
||||
Username: username,
|
||||
@@ -88,14 +77,13 @@ func (req *IntegrationRequests) CreateUser(username, password, email string, adm
|
||||
buff.Write(body)
|
||||
request, _ := http.NewRequest("PUT", "/api/v1/kolide/user", buff)
|
||||
request.Header.Set("Content-Type", "application/json")
|
||||
request.Header.Set("Cookie", *session)
|
||||
request.Header.Set("Cookie", session)
|
||||
req.r.ServeHTTP(response, request)
|
||||
|
||||
if response.Code != 200 {
|
||||
req.t.Fatalf("Response code: %d", response.Code)
|
||||
return nil
|
||||
}
|
||||
*session = response.Header().Get("Set-Cookie")
|
||||
|
||||
var responseBody GetUserResponseBody
|
||||
err = json.Unmarshal(response.Body.Bytes(), &responseBody)
|
||||
@@ -107,7 +95,7 @@ func (req *IntegrationRequests) CreateUser(username, password, email string, adm
|
||||
return &responseBody
|
||||
}
|
||||
|
||||
func (req *IntegrationRequests) GetUser(username string, session *string) *GetUserResponseBody {
|
||||
func (req *IntegrationRequests) GetUser(username, session string) *GetUserResponseBody {
|
||||
response := httptest.NewRecorder()
|
||||
body, err := json.Marshal(GetUserRequestBody{
|
||||
Username: username,
|
||||
@@ -121,14 +109,13 @@ func (req *IntegrationRequests) GetUser(username string, session *string) *GetUs
|
||||
buff.Write(body)
|
||||
request, _ := http.NewRequest("POST", "/api/v1/kolide/user", buff)
|
||||
request.Header.Set("Content-Type", "application/json")
|
||||
request.Header.Set("Cookie", *session)
|
||||
request.Header.Set("Cookie", session)
|
||||
req.r.ServeHTTP(response, request)
|
||||
|
||||
if response.Code != 200 {
|
||||
req.t.Fatalf("Response code: %d", response.Code)
|
||||
return nil
|
||||
}
|
||||
*session = response.Header().Get("Set-Cookie")
|
||||
|
||||
var responseBody GetUserResponseBody
|
||||
err = json.Unmarshal(response.Body.Bytes(), &responseBody)
|
||||
@@ -140,7 +127,7 @@ func (req *IntegrationRequests) GetUser(username string, session *string) *GetUs
|
||||
return &responseBody
|
||||
}
|
||||
|
||||
func (req *IntegrationRequests) ModifyUser(username, name, email string, session *string) *GetUserResponseBody {
|
||||
func (req *IntegrationRequests) ModifyUser(username, name, email, session string) *GetUserResponseBody {
|
||||
response := httptest.NewRecorder()
|
||||
body, err := json.Marshal(ModifyUserRequestBody{
|
||||
Username: username,
|
||||
@@ -156,14 +143,13 @@ func (req *IntegrationRequests) ModifyUser(username, name, email string, session
|
||||
buff.Write(body)
|
||||
request, _ := http.NewRequest("PATCH", "/api/v1/kolide/user", buff)
|
||||
request.Header.Set("Content-Type", "application/json")
|
||||
request.Header.Set("Cookie", *session)
|
||||
request.Header.Set("Cookie", session)
|
||||
req.r.ServeHTTP(response, request)
|
||||
|
||||
if response.Code != 200 {
|
||||
req.t.Fatalf("Response code: %d", response.Code)
|
||||
return nil
|
||||
}
|
||||
*session = response.Header().Get("Set-Cookie")
|
||||
|
||||
var responseBody GetUserResponseBody
|
||||
err = json.Unmarshal(response.Body.Bytes(), &responseBody)
|
||||
@@ -175,7 +161,7 @@ func (req *IntegrationRequests) ModifyUser(username, name, email string, session
|
||||
return &responseBody
|
||||
}
|
||||
|
||||
func (req *IntegrationRequests) DeleteUser(username string, session *string) {
|
||||
func (req *IntegrationRequests) DeleteUser(username, session string) {
|
||||
response := httptest.NewRecorder()
|
||||
body, err := json.Marshal(DeleteUserRequestBody{
|
||||
Username: username,
|
||||
@@ -189,19 +175,18 @@ func (req *IntegrationRequests) DeleteUser(username string, session *string) {
|
||||
buff.Write(body)
|
||||
request, _ := http.NewRequest("DELETE", "/api/v1/kolide/user", buff)
|
||||
request.Header.Set("Content-Type", "application/json")
|
||||
request.Header.Set("Cookie", *session)
|
||||
request.Header.Set("Cookie", session)
|
||||
req.r.ServeHTTP(response, request)
|
||||
|
||||
if response.Code != 200 {
|
||||
req.t.Fatalf("Response code: %d", response.Code)
|
||||
return
|
||||
}
|
||||
*session = response.Header().Get("Set-Cookie")
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (req *IntegrationRequests) ChangePassword(username, currentPassword, newPassword string, session *string) *GetUserResponseBody {
|
||||
func (req *IntegrationRequests) ChangePassword(username, currentPassword, newPassword, session string) *GetUserResponseBody {
|
||||
response := httptest.NewRecorder()
|
||||
body, err := json.Marshal(ChangePasswordRequestBody{
|
||||
Username: username,
|
||||
@@ -218,14 +203,13 @@ func (req *IntegrationRequests) ChangePassword(username, currentPassword, newPas
|
||||
buff.Write(body)
|
||||
request, _ := http.NewRequest("PATCH", "/api/v1/kolide/user/password", buff)
|
||||
request.Header.Set("Content-Type", "application/json")
|
||||
request.Header.Set("Cookie", *session)
|
||||
request.Header.Set("Cookie", session)
|
||||
req.r.ServeHTTP(response, request)
|
||||
|
||||
if response.Code != 200 {
|
||||
req.t.Fatalf("Response code: %d", response.Code)
|
||||
return nil
|
||||
}
|
||||
*session = response.Header().Get("Set-Cookie")
|
||||
|
||||
var responseBody GetUserResponseBody
|
||||
err = json.Unmarshal(response.Body.Bytes(), &responseBody)
|
||||
@@ -236,7 +220,121 @@ func (req *IntegrationRequests) ChangePassword(username, currentPassword, newPas
|
||||
return &responseBody
|
||||
}
|
||||
|
||||
func (req *IntegrationRequests) SetAdminState(username string, admin bool, session *string) *GetUserResponseBody {
|
||||
func (req *IntegrationRequests) GetUserSessionInfo(username, session string) *GetInfoAboutSessionsForUserResponseBody {
|
||||
response := httptest.NewRecorder()
|
||||
body, err := json.Marshal(GetInfoAboutSessionsForUserRequestBody{
|
||||
Username: username,
|
||||
})
|
||||
if err != nil {
|
||||
req.t.Fatal(err.Error())
|
||||
return nil
|
||||
}
|
||||
|
||||
buff := new(bytes.Buffer)
|
||||
buff.Write(body)
|
||||
request, _ := http.NewRequest("POST", "/api/v1/kolide/user/sessions", buff)
|
||||
request.Header.Set("Content-Type", "application/json")
|
||||
request.Header.Set("Cookie", session)
|
||||
req.r.ServeHTTP(response, request)
|
||||
|
||||
if response.Code != 200 {
|
||||
req.t.Fatalf("Response code: %d", response.Code)
|
||||
return nil
|
||||
}
|
||||
|
||||
var responseBody GetInfoAboutSessionsForUserResponseBody
|
||||
err = json.Unmarshal(response.Body.Bytes(), &responseBody)
|
||||
if err != nil {
|
||||
req.t.Fatal(err.Error())
|
||||
return nil
|
||||
}
|
||||
|
||||
return &responseBody
|
||||
}
|
||||
|
||||
func (req *IntegrationRequests) DeleteUserSessions(username, session string) {
|
||||
response := httptest.NewRecorder()
|
||||
body, err := json.Marshal(GetInfoAboutSessionsForUserRequestBody{
|
||||
Username: username,
|
||||
})
|
||||
if err != nil {
|
||||
req.t.Fatal(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
buff := new(bytes.Buffer)
|
||||
buff.Write(body)
|
||||
request, _ := http.NewRequest("DELETE", "/api/v1/kolide/user/sessions", buff)
|
||||
request.Header.Set("Content-Type", "application/json")
|
||||
request.Header.Set("Cookie", session)
|
||||
req.r.ServeHTTP(response, request)
|
||||
|
||||
if response.Code != 200 {
|
||||
req.t.Fatalf("Response code: %d", response.Code)
|
||||
return
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (req *IntegrationRequests) GetSessionInfo(sessionKey, session string) *SessionInfoResponseBody {
|
||||
response := httptest.NewRecorder()
|
||||
body, err := json.Marshal(GetInfoAboutSessionRequestBody{
|
||||
SessionKey: sessionKey,
|
||||
})
|
||||
if err != nil {
|
||||
req.t.Fatal(err.Error())
|
||||
return nil
|
||||
}
|
||||
|
||||
buff := new(bytes.Buffer)
|
||||
buff.Write(body)
|
||||
request, _ := http.NewRequest("POST", "/api/v1/kolide/session", buff)
|
||||
request.Header.Set("Content-Type", "application/json")
|
||||
request.Header.Set("Cookie", session)
|
||||
req.r.ServeHTTP(response, request)
|
||||
|
||||
if response.Code != 200 {
|
||||
req.t.Fatalf("Response code: %d", response.Code)
|
||||
return nil
|
||||
}
|
||||
|
||||
var responseBody SessionInfoResponseBody
|
||||
err = json.Unmarshal(response.Body.Bytes(), &responseBody)
|
||||
if err != nil {
|
||||
req.t.Fatal(err.Error())
|
||||
return nil
|
||||
}
|
||||
|
||||
return &responseBody
|
||||
}
|
||||
|
||||
func (req *IntegrationRequests) DeleteSession(sessionID uint, session string) {
|
||||
response := httptest.NewRecorder()
|
||||
body, err := json.Marshal(DeleteSessionRequestBody{
|
||||
SessionID: sessionID,
|
||||
})
|
||||
if err != nil {
|
||||
req.t.Fatal(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
buff := new(bytes.Buffer)
|
||||
buff.Write(body)
|
||||
request, _ := http.NewRequest("DELETE", "/api/v1/kolide/session", buff)
|
||||
request.Header.Set("Content-Type", "application/json")
|
||||
request.Header.Set("Cookie", session)
|
||||
req.r.ServeHTTP(response, request)
|
||||
|
||||
if response.Code != 200 {
|
||||
req.t.Fatalf("Response code: %d", response.Code)
|
||||
return
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (req *IntegrationRequests) SetAdminState(username string, admin bool, session string) *GetUserResponseBody {
|
||||
response := httptest.NewRecorder()
|
||||
body, err := json.Marshal(SetUserAdminStateRequestBody{
|
||||
Username: username,
|
||||
@@ -251,14 +349,13 @@ func (req *IntegrationRequests) SetAdminState(username string, admin bool, sessi
|
||||
buff.Write(body)
|
||||
request, _ := http.NewRequest("PATCH", "/api/v1/kolide/user/admin", buff)
|
||||
request.Header.Set("Content-Type", "application/json")
|
||||
request.Header.Set("Cookie", *session)
|
||||
request.Header.Set("Cookie", session)
|
||||
req.r.ServeHTTP(response, request)
|
||||
|
||||
if response.Code != 200 {
|
||||
req.t.Fatalf("Response code: %d", response.Code)
|
||||
return nil
|
||||
}
|
||||
*session = response.Header().Get("Set-Cookie")
|
||||
|
||||
var responseBody GetUserResponseBody
|
||||
err = json.Unmarshal(response.Body.Bytes(), &responseBody)
|
||||
@@ -269,7 +366,7 @@ func (req *IntegrationRequests) SetAdminState(username string, admin bool, sessi
|
||||
return &responseBody
|
||||
}
|
||||
|
||||
func (req *IntegrationRequests) SetEnabledState(username string, enabled bool, session *string) *GetUserResponseBody {
|
||||
func (req *IntegrationRequests) SetEnabledState(username string, enabled bool, session string) *GetUserResponseBody {
|
||||
response := httptest.NewRecorder()
|
||||
body, err := json.Marshal(SetUserEnabledStateRequestBody{
|
||||
Username: username,
|
||||
@@ -284,14 +381,13 @@ func (req *IntegrationRequests) SetEnabledState(username string, enabled bool, s
|
||||
buff.Write(body)
|
||||
request, _ := http.NewRequest("PATCH", "/api/v1/kolide/user/enabled", buff)
|
||||
request.Header.Set("Content-Type", "application/json")
|
||||
request.Header.Set("Cookie", *session)
|
||||
request.Header.Set("Cookie", session)
|
||||
req.r.ServeHTTP(response, request)
|
||||
|
||||
if response.Code != 200 {
|
||||
req.t.Fatalf("Response code: %d", response.Code)
|
||||
return nil
|
||||
}
|
||||
*session = response.Header().Get("Set-Cookie")
|
||||
|
||||
var responseBody GetUserResponseBody
|
||||
err = json.Unmarshal(response.Body.Bytes(), &responseBody)
|
||||
@@ -328,22 +424,22 @@ func (req *IntegrationRequests) CheckUser(username, email, name string, admin, r
|
||||
return
|
||||
}
|
||||
|
||||
func (req *IntegrationRequests) GetAndCheckUser(username string, session *string) {
|
||||
func (req *IntegrationRequests) GetAndCheckUser(username, session string) {
|
||||
resp := req.GetUser(username, session)
|
||||
req.CheckUser(username, resp.Email, resp.Name, resp.Admin, resp.NeedsPasswordReset, resp.Enabled)
|
||||
}
|
||||
|
||||
func (req *IntegrationRequests) CreateAndCheckUser(username, password, email, name string, admin, reset bool, session *string) {
|
||||
func (req *IntegrationRequests) CreateAndCheckUser(username, password, email, name string, admin, reset bool, session string) {
|
||||
resp := req.CreateUser(username, password, email, admin, reset, session)
|
||||
req.CheckUser(username, email, name, admin, reset, resp.Enabled)
|
||||
}
|
||||
|
||||
func (req *IntegrationRequests) ModifyAndCheckUser(username, email, name string, admin, reset bool, session *string) {
|
||||
func (req *IntegrationRequests) ModifyAndCheckUser(username, email, name string, admin, reset bool, session string) {
|
||||
resp := req.ModifyUser(username, name, email, session)
|
||||
req.CheckUser(username, email, name, admin, reset, resp.Enabled)
|
||||
}
|
||||
|
||||
func (req *IntegrationRequests) DeleteAndCheckUser(username string, session *string) {
|
||||
func (req *IntegrationRequests) DeleteAndCheckUser(username, session string) {
|
||||
req.DeleteUser(username, session)
|
||||
|
||||
var user User
|
||||
@@ -353,12 +449,12 @@ func (req *IntegrationRequests) DeleteAndCheckUser(username string, session *str
|
||||
}
|
||||
}
|
||||
|
||||
func (req *IntegrationRequests) SetEnabledStateAndCheckUser(username string, enabled bool, session *string) {
|
||||
func (req *IntegrationRequests) SetEnabledStateAndCheckUser(username string, enabled bool, session string) {
|
||||
resp := req.SetEnabledState(username, enabled, session)
|
||||
req.CheckUser(username, resp.Email, resp.Name, resp.Admin, resp.NeedsPasswordReset, enabled)
|
||||
}
|
||||
|
||||
func (req *IntegrationRequests) SetAdminStateAndCheckUser(username string, admin bool, session *string) {
|
||||
func (req *IntegrationRequests) SetAdminStateAndCheckUser(username string, admin bool, session string) {
|
||||
resp := req.SetAdminState(username, admin, session)
|
||||
req.CheckUser(username, resp.Email, resp.Name, admin, resp.NeedsPasswordReset, resp.Enabled)
|
||||
}
|
||||
|
||||
@@ -12,10 +12,11 @@
|
||||
},
|
||||
"app": {
|
||||
"bcrypt_cost": 12,
|
||||
"salt_length": 32,
|
||||
"jwt_key": "very secure"
|
||||
"salt_key_size": 12,
|
||||
"jwt_key": "very secure",
|
||||
"session_key_size": 64
|
||||
},
|
||||
"osquery": {
|
||||
"enroll_secret": "super secure"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -52,7 +52,8 @@ func NewUser(db *gorm.DB, username, password, email string, admin, needsPassword
|
||||
// to validate it against the hash stored in the database after joining the
|
||||
// supplied password with the stored password salt
|
||||
func (u *User) ValidatePassword(password string) error {
|
||||
saltAndPass := []byte(fmt.Sprintf("%s%s", u.Salt, password))
|
||||
saltAndPass := []byte(fmt.Sprintf("%s%s", password, u.Salt))
|
||||
logrus.Info(string(saltAndPass))
|
||||
return bcrypt.CompareHashAndPassword(u.Password, saltAndPass)
|
||||
}
|
||||
|
||||
@@ -80,6 +81,10 @@ func (u *User) MakeAdmin(db *gorm.DB) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
// User management web endpoints
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
type GetUserRequestBody struct {
|
||||
ID uint `json:"id"`
|
||||
Username string `json:"username"`
|
||||
@@ -103,15 +108,13 @@ func GetUser(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
db := GetDB(c)
|
||||
|
||||
vc, err := VC(c, db)
|
||||
if err != nil {
|
||||
logrus.Errorf("Could not create VC: %s", err.Error())
|
||||
DatabaseError(c) // TODO tampered?
|
||||
vc := VC(c)
|
||||
if !vc.CanPerformActions() {
|
||||
UnauthorizedError(c)
|
||||
return
|
||||
}
|
||||
|
||||
db := GetDB(c)
|
||||
var user User
|
||||
user.ID = body.ID
|
||||
user.Username = body.Username
|
||||
@@ -153,20 +156,13 @@ func CreateUser(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
db := GetDB(c)
|
||||
|
||||
vc, err := VC(c, db)
|
||||
if err != nil {
|
||||
logrus.Errorf("Could not create VC: %s", err.Error())
|
||||
DatabaseError(c)
|
||||
return
|
||||
}
|
||||
|
||||
vc := VC(c)
|
||||
if !vc.IsAdmin() {
|
||||
UnauthorizedError(c)
|
||||
return
|
||||
}
|
||||
|
||||
db := GetDB(c)
|
||||
user, err := NewUser(db, body.Username, body.Password, body.Email, body.Admin, body.NeedsPasswordReset)
|
||||
if err != nil {
|
||||
logrus.Errorf("Error creating new user: %s", err.Error())
|
||||
@@ -200,12 +196,9 @@ func ModifyUser(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
db := GetDB(c)
|
||||
|
||||
vc, err := VC(c, db)
|
||||
if err != nil {
|
||||
logrus.Errorf("Could not create VC: %s", err.Error())
|
||||
DatabaseError(c)
|
||||
vc := VC(c)
|
||||
if !vc.CanPerformActions() {
|
||||
UnauthorizedError(c)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -213,6 +206,7 @@ func ModifyUser(c *gin.Context) {
|
||||
user.ID = body.ID
|
||||
user.Username = body.Username
|
||||
|
||||
db := GetDB(c)
|
||||
err = db.Where(&user).First(&user).Error
|
||||
if err != nil {
|
||||
DatabaseError(c)
|
||||
@@ -260,20 +254,13 @@ func DeleteUser(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
db := GetDB(c)
|
||||
|
||||
vc, err := VC(c, db)
|
||||
if err != nil {
|
||||
logrus.Errorf("Could not create VC: %s", err.Error())
|
||||
DatabaseError(c)
|
||||
return
|
||||
}
|
||||
|
||||
vc := VC(c)
|
||||
if !vc.IsAdmin() {
|
||||
UnauthorizedError(c)
|
||||
return
|
||||
}
|
||||
|
||||
db := GetDB(c)
|
||||
var user User
|
||||
user.ID = body.ID
|
||||
user.Username = body.Username
|
||||
@@ -320,8 +307,13 @@ func ChangeUserPassword(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
db := GetDB(c)
|
||||
vc := VC(c)
|
||||
if !vc.CanPerformActions() {
|
||||
UnauthorizedError(c)
|
||||
return
|
||||
}
|
||||
|
||||
db := GetDB(c)
|
||||
var user User
|
||||
user.ID = body.ID
|
||||
user.Username = body.Username
|
||||
@@ -331,13 +323,6 @@ func ChangeUserPassword(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
vc, err := VC(c, db)
|
||||
if err != nil {
|
||||
logrus.Errorf("Could not create VC: %s", err.Error())
|
||||
DatabaseError(c)
|
||||
return
|
||||
}
|
||||
|
||||
if !vc.IsAdmin() {
|
||||
if !vc.IsUserID(user.ID) {
|
||||
UnauthorizedError(c)
|
||||
@@ -352,7 +337,8 @@ func ChangeUserPassword(c *gin.Context) {
|
||||
err = user.SetPassword(db, body.NewPassword)
|
||||
if err != nil {
|
||||
logrus.Errorf("Error setting user password: %s", err.Error())
|
||||
// xxx don't try to write to the db?
|
||||
DatabaseError(c) // probably not this
|
||||
return
|
||||
}
|
||||
|
||||
err = db.Save(&user).Error
|
||||
@@ -386,20 +372,13 @@ func SetUserAdminState(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
db := GetDB(c)
|
||||
|
||||
vc, err := VC(c, db)
|
||||
if err != nil {
|
||||
logrus.Errorf("Could not create VC: %s", err.Error())
|
||||
DatabaseError(c)
|
||||
return
|
||||
}
|
||||
|
||||
vc := VC(c)
|
||||
if !vc.IsAdmin() {
|
||||
UnauthorizedError(c)
|
||||
return
|
||||
}
|
||||
|
||||
db := GetDB(c)
|
||||
var user User
|
||||
user.ID = body.ID
|
||||
user.Username = body.Username
|
||||
@@ -441,20 +420,13 @@ func SetUserEnabledState(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
db := GetDB(c)
|
||||
|
||||
vc, err := VC(c, db)
|
||||
if err != nil {
|
||||
logrus.Errorf("Could not create VC: %s", err.Error())
|
||||
DatabaseError(c)
|
||||
return
|
||||
}
|
||||
|
||||
vc := VC(c)
|
||||
if !vc.IsAdmin() {
|
||||
UnauthorizedError(c)
|
||||
return
|
||||
}
|
||||
|
||||
db := GetDB(c)
|
||||
var user User
|
||||
user.ID = body.ID
|
||||
user.Username = body.Username
|
||||
|
||||
+10
-6
@@ -41,14 +41,18 @@ func TestValidatePassword(t *testing.T) {
|
||||
t.Fatal(err.Error())
|
||||
}
|
||||
|
||||
err = user.ValidatePassword("foobar")
|
||||
if err != nil {
|
||||
t.Fatal("Password validation failed")
|
||||
{
|
||||
err := user.ValidatePassword("foobar")
|
||||
if err != nil {
|
||||
t.Error("Password validation failed")
|
||||
}
|
||||
}
|
||||
|
||||
err = user.ValidatePassword("not correct")
|
||||
if err == nil {
|
||||
t.Fatal("Incorrect password worked")
|
||||
{
|
||||
err := user.ValidatePassword("different")
|
||||
if err == nil {
|
||||
t.Error("Incorrect password worked")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user