fleet/auth.go
Mike Arpaia fe2bf7eb2b Moving sessions code into sub-package (#42)
Since the sessions code mostly stands on it's own, I wanted to break the
dependencies apart from it and move it into it's own package.
2016-08-05 10:47:41 -07:00

223 lines
5.6 KiB
Go

package main
import (
"crypto/rand"
"encoding/base64"
"errors"
"fmt"
"github.com/Sirupsen/logrus"
"github.com/gin-gonic/gin"
"github.com/jinzhu/gorm"
"golang.org/x/crypto/bcrypt"
)
// ViewerContext is a struct which represents the ability for an execution
// context to participate in certain actions. Most often, a ViewerContext is
// associated with an application user, but a ViewerContext can represent a
// variety of other execution contexts as well (script, test, etc). The main
// purpose of a ViewerContext is to assist in the authorization of sensitive
// actions.
type ViewerContext struct {
user *User
}
// IsAdmin indicates whether or not the current user can perform administrative
// actions.
func (vc *ViewerContext) IsAdmin() bool {
if vc.user != nil {
return vc.user.Admin && vc.user.Enabled
}
return false
}
// UserID is a helper that enables quick access to the user ID of the current
// user.
func (vc *ViewerContext) UserID() (uint, error) {
if vc.user != nil {
return vc.user.ID, nil
}
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
}
if !vc.user.Enabled {
return false
}
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 {
return false
}
if userID == id {
return true
}
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()
}
// GenerateVC generates a ViewerContext given a user struct
func GenerateVC(user *User) *ViewerContext {
return &ViewerContext{
user: user,
}
}
// EmptyVC is a utility which generates an empty ViewerContext. This is often
// used to represent users which are not logged in.
func EmptyVC() *ViewerContext {
return &ViewerContext{
user: nil,
}
}
// 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) *ViewerContext {
sm := NewSessionManager(c)
session, err := sm.Session()
if err != nil {
return EmptyVC()
}
return VCForID(GetDB(c), session.UserID)
}
func VCForID(db *gorm.DB, id uint) *ViewerContext {
// 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: id}}
err := db.Where(user).First(user).Error
if err != nil {
return EmptyVC()
}
return GenerateVC(user)
}
////////////////////////////////////////////////////////////////////////////////
// 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"`
}
func Login(c *gin.Context) {
var body LoginRequestBody
err := c.BindJSON(&body)
if err != nil {
logrus.Errorf("Error parsing Login post body: %s", err.Error())
return
}
db := GetDB(c)
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)
return
}
err = user.ValidatePassword(body.Password)
if err != nil {
logrus.Debugf("Invalid password for user: %s", body.Username)
UnauthorizedError(c)
return
}
sm := NewSessionManager(c)
sm.MakeSessionForUserID(user.ID)
err = sm.Save()
if err != nil {
DatabaseError(c)
return
}
c.JSON(200, GetUserResponseBody{
ID: user.ID,
Username: user.Username,
Name: user.Name,
Email: user.Email,
Admin: user.Admin,
Enabled: user.Enabled,
NeedsPasswordReset: user.NeedsPasswordReset,
})
}
func Logout(c *gin.Context) {
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)
}