2016-08-01 23:32:20 +00:00
|
|
|
package main
|
|
|
|
|
|
|
|
import (
|
2016-08-04 22:44:54 +00:00
|
|
|
"crypto/rand"
|
2016-08-04 22:38:13 +00:00
|
|
|
"encoding/base64"
|
2016-08-01 23:32:20 +00:00
|
|
|
"errors"
|
|
|
|
"fmt"
|
|
|
|
|
|
|
|
"github.com/Sirupsen/logrus"
|
|
|
|
"github.com/gin-gonic/gin"
|
2016-08-05 17:47:41 +00:00
|
|
|
"github.com/jinzhu/gorm"
|
2016-08-01 23:32:20 +00:00
|
|
|
"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")
|
|
|
|
}
|
|
|
|
|
2016-08-04 22:38:13 +00:00
|
|
|
// CanPerformActions returns a bool indicating the current user's ability to
|
|
|
|
// perform the most basic actions on the site
|
2016-08-02 22:39:20 +00:00
|
|
|
func (vc *ViewerContext) CanPerformActions() bool {
|
2016-08-01 23:32:20 +00:00
|
|
|
if vc.user == nil {
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
|
|
|
|
if !vc.user.Enabled {
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
|
|
|
|
return true
|
|
|
|
}
|
|
|
|
|
2016-08-04 22:38:13 +00:00
|
|
|
// IsUserID returns true if the given user id the same as the user which is
|
|
|
|
// represented by this ViewerContext
|
2016-08-01 23:32:20 +00:00
|
|
|
func (vc *ViewerContext) IsUserID(id uint) bool {
|
|
|
|
userID, err := vc.UserID()
|
|
|
|
if err != nil {
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
if userID == id {
|
|
|
|
return true
|
|
|
|
}
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
|
2016-08-04 22:38:13 +00:00
|
|
|
// CanPerformWriteActionsOnUser returns a bool indicating the current user's
|
|
|
|
// ability to perform write actions on the given user
|
2016-08-02 22:39:20 +00:00
|
|
|
func (vc *ViewerContext) CanPerformWriteActionOnUser(u *User) bool {
|
|
|
|
return vc.CanPerformActions() && (vc.IsUserID(u.ID) || vc.IsAdmin())
|
2016-08-01 23:32:20 +00:00
|
|
|
}
|
|
|
|
|
2016-08-04 22:38:13 +00:00
|
|
|
// CanPerformReadActionsOnUser returns a bool indicating the current user's
|
|
|
|
// ability to perform read actions on the given user
|
2016-08-02 22:39:20 +00:00
|
|
|
func (vc *ViewerContext) CanPerformReadActionOnUser(u *User) bool {
|
|
|
|
return vc.CanPerformActions()
|
2016-08-01 23:32:20 +00:00
|
|
|
}
|
|
|
|
|
2016-08-04 22:38:13 +00:00
|
|
|
// 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)
|
2016-08-05 17:47:41 +00:00
|
|
|
session, err := sm.Session()
|
|
|
|
if err != nil {
|
|
|
|
return EmptyVC()
|
|
|
|
}
|
|
|
|
return VCForID(GetDB(c), session.UserID)
|
2016-08-04 22:38:13 +00:00
|
|
|
}
|
|
|
|
|
2016-08-05 17:47:41 +00:00
|
|
|
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()
|
|
|
|
}
|
2016-08-01 23:32:20 +00:00
|
|
|
|
2016-08-05 17:47:41 +00:00
|
|
|
return GenerateVC(user)
|
2016-08-01 23:32:20 +00:00
|
|
|
}
|
|
|
|
|
2016-08-04 22:38:13 +00:00
|
|
|
////////////////////////////////////////////////////////////////////////////////
|
|
|
|
// Login and password utilities
|
|
|
|
////////////////////////////////////////////////////////////////////////////////
|
2016-08-01 23:32:20 +00:00
|
|
|
|
2016-08-04 22:38:13 +00:00
|
|
|
// 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)
|
2016-08-01 23:32:20 +00:00
|
|
|
if err != nil {
|
2016-08-04 22:38:13 +00:00
|
|
|
return "", err
|
2016-08-01 23:32:20 +00:00
|
|
|
}
|
|
|
|
|
2016-08-04 22:38:13 +00:00
|
|
|
return base64.StdEncoding.EncodeToString(key), nil
|
2016-08-01 23:32:20 +00:00
|
|
|
}
|
|
|
|
|
2016-08-04 22:38:13 +00:00
|
|
|
func HashPassword(salt, password string) ([]byte, error) {
|
|
|
|
return bcrypt.GenerateFromPassword(
|
|
|
|
[]byte(fmt.Sprintf("%s%s", password, salt)),
|
|
|
|
config.App.BcryptCost,
|
|
|
|
)
|
2016-08-01 23:32:20 +00:00
|
|
|
}
|
|
|
|
|
2016-08-04 22:38:13 +00:00
|
|
|
func SaltAndHashPassword(password string) (string, []byte, error) {
|
|
|
|
salt, err := generateRandomText(config.App.SaltKeySize)
|
2016-08-01 23:32:20 +00:00
|
|
|
if err != nil {
|
2016-08-04 22:38:13 +00:00
|
|
|
return "", []byte{}, err
|
2016-08-01 23:32:20 +00:00
|
|
|
}
|
2016-08-04 22:38:13 +00:00
|
|
|
hashed, err := HashPassword(salt, password)
|
|
|
|
return salt, hashed, err
|
2016-08-01 23:32:20 +00:00
|
|
|
}
|
|
|
|
|
2016-08-04 22:38:13 +00:00
|
|
|
////////////////////////////////////////////////////////////////////////////////
|
|
|
|
// Authentication and authorization web endpoints
|
|
|
|
////////////////////////////////////////////////////////////////////////////////
|
|
|
|
|
2016-08-01 23:32:20 +00:00
|
|
|
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
|
|
|
|
}
|
|
|
|
|
2016-08-04 18:41:47 +00:00
|
|
|
db := GetDB(c)
|
2016-08-01 23:32:20 +00:00
|
|
|
|
2016-08-04 22:38:13 +00:00
|
|
|
user := &User{Username: body.Username}
|
|
|
|
err = db.Where(user).First(user).Error
|
2016-08-01 23:32:20 +00:00
|
|
|
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
|
|
|
|
}
|
|
|
|
|
2016-08-04 22:38:13 +00:00
|
|
|
sm := NewSessionManager(c)
|
2016-08-05 17:47:41 +00:00
|
|
|
sm.MakeSessionForUserID(user.ID)
|
2016-08-04 22:38:13 +00:00
|
|
|
err = sm.Save()
|
2016-08-01 23:32:20 +00:00
|
|
|
if err != nil {
|
|
|
|
DatabaseError(c)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2016-08-02 22:39:20 +00:00
|
|
|
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,
|
2016-08-01 23:32:20 +00:00
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
func Logout(c *gin.Context) {
|
2016-08-04 22:38:13 +00:00
|
|
|
sm := NewSessionManager(c)
|
2016-08-01 23:32:20 +00:00
|
|
|
|
2016-08-04 22:38:13 +00:00
|
|
|
err := sm.Destroy()
|
|
|
|
if err != nil {
|
|
|
|
DatabaseError(c)
|
|
|
|
return
|
2016-08-01 23:32:20 +00:00
|
|
|
}
|
|
|
|
|
2016-08-04 22:38:13 +00:00
|
|
|
err = sm.Save()
|
|
|
|
if err != nil {
|
|
|
|
DatabaseError(c)
|
|
|
|
return
|
|
|
|
}
|
2016-08-01 23:32:20 +00:00
|
|
|
|
2016-08-04 22:38:13 +00:00
|
|
|
c.JSON(200, nil)
|
2016-08-01 23:32:20 +00:00
|
|
|
}
|