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:
Mike Arpaia
2016-08-04 15:38:13 -07:00
committed by GitHub
parent d9f776c756
commit 4687812f39
13 changed files with 1008 additions and 510 deletions
+92 -144
View File
@@ -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
View File
@@ -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")
}
+10 -6
View File
@@ -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{
+2 -2
View File
@@ -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,
+1
View File
@@ -143,6 +143,7 @@ type Decorator struct {
var tables = [...]interface{}{
&User{},
&Session{},
&ScheduledQuery{},
&Pack{},
&DiscoveryQuery{},
+8 -2
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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)
}
+4 -3
View File
@@ -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"
}
}
}
+30 -58
View File
@@ -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
View File
@@ -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")
}
}
}