wisski-cloud-distillery/internal/dis/component/auth/user.go
2023-01-06 18:59:09 +01:00

290 lines
6.9 KiB
Go

package auth
import (
"bytes"
"context"
"encoding/base64"
"errors"
"fmt"
"image/png"
"github.com/FAU-CDI/wisski-distillery/internal/models"
"github.com/pquerna/otp"
"github.com/pquerna/otp/totp"
"golang.org/x/crypto/bcrypt"
)
// ErrUserNotFound is returned when a user is not found
var ErrUserNotFound = errors.New("user not found")
// Users returns all users in the database
func (auth *Auth) Users(ctx context.Context) (users []*AuthUser, err error) {
// query the user table
table, err := auth.Dependencies.SQL.QueryTable(ctx, false, models.UserTable)
if err != nil {
return
}
// find all the users
var dUsers []models.User
err = table.Find(&dUsers).Error
if err != nil {
return nil, err
}
// and map them to high-level user objects
users = make([]*AuthUser, len(dUsers))
for i, user := range dUsers {
users[i] = &AuthUser{
User: user,
auth: auth,
}
}
return users, nil
}
// User returns a single user.
// If the user does not exist, returns ErrUserNotFound.
func (auth *Auth) User(ctx context.Context, name string) (user *AuthUser, err error) {
// quick and dirty check for the empty username (which is not allowed)
if name == "" {
return nil, ErrUserNotFound
}
// return the user
table, err := auth.Dependencies.SQL.QueryTable(ctx, false, models.UserTable)
if err != nil {
return
}
user = &AuthUser{}
// find the user
res := table.Where(&models.User{User: name}).Find(&user.User)
err = res.Error
if err != nil {
return
}
// check if the user was not found
if res.RowsAffected == 0 {
return nil, ErrUserNotFound
}
user.auth = auth
return
}
// CreateUser creates a new user and returns it.
// The user is not associated to any WissKIs, and has no password set.
func (auth *Auth) CreateUser(ctx context.Context, name string) (user *AuthUser, err error) {
// return the user
table, err := auth.Dependencies.SQL.QueryTable(ctx, false, models.UserTable)
if err != nil {
return
}
user = &AuthUser{
User: models.User{
User: name,
Enabled: false,
},
}
// do the create statement
err = table.Create(&user.User).Error
if err != nil {
return nil, err
}
user.auth = auth
return user, nil
}
// AuthUser represents an authorized user
type AuthUser struct {
auth *Auth
models.User
}
func (au *AuthUser) String() string {
if au == nil {
return "User{nil}"
}
hasPassword := len(au.PasswordHash) > 0
return fmt.Sprintf("User{Name:%q,Enabled:%t,HasPassword:%t,Admin:%t}", au.User.User, au.User.Enabled, hasPassword, au.User.Admin)
}
var (
ErrTOTPEnabled = errors.New("TOTP is enabled")
ErrTOTPDisabled = errors.New("TOTP is disabled")
ErrTOTPFailed = errors.New("TOTP failed")
)
func (au *AuthUser) TOTP() (*otp.Key, error) {
if au.TOTPURL == "" {
return nil, ErrTOTPDisabled
}
return otp.NewKeyFromURL(au.TOTPURL)
}
// CheckTOTP validates the given totp passcode against the saved secret.
// If totp is not enabled, any passcode will pass the check.
func (au *AuthUser) CheckTOTP(passcode string) error {
secret, err := au.TOTP()
if err != nil {
return err
}
if au.TOTPEnabled && !totp.Validate(passcode, secret.Secret()) {
return ErrTOTPFailed
}
return nil
}
// NewTOTP generates a new TOTP secret, returning a totp key.
func (au *AuthUser) NewTOTP(ctx context.Context) (*otp.Key, error) {
if au.User.TOTPEnabled {
return nil, ErrTOTPEnabled
}
key, err := totp.Generate(totp.GenerateOpts{
Issuer: "WissKI Distillery",
AccountName: au.User.User,
})
if err != nil {
return nil, err
}
au.User.TOTPURL = key.URL()
return key, au.Save(ctx)
}
func TOTPLink(secret *otp.Key, width, height int) (string, error) {
// make an image
img, err := secret.Image(width, height)
if err != nil {
return "", err
}
// encode image as base64
var buffer bytes.Buffer
if err := png.Encode(&buffer, img); err != nil {
return "", err
}
// return the image url
return "data:image/png;base64," + base64.StdEncoding.EncodeToString(buffer.Bytes()), nil
}
// EnableTOTP enables totp for the given user
func (au *AuthUser) EnableTOTP(ctx context.Context, passcode string) error {
secret, err := au.TOTP()
if err != nil {
return err
}
if !totp.Validate(passcode, secret.Secret()) {
return ErrTOTPFailed
}
au.User.TOTPEnabled = true
return au.Save(ctx)
}
// DisableTOTP disables totp for the given user
func (au *AuthUser) DisableTOTP(ctx context.Context) (err error) {
au.User.TOTPEnabled = false
au.User.TOTPURL = ""
return au.Save(ctx)
}
// SetPassword sets the password for this user and turns the user on
func (au *AuthUser) SetPassword(ctx context.Context, password []byte) (err error) {
au.User.PasswordHash, err = bcrypt.GenerateFromPassword(password, bcrypt.DefaultCost)
if err != nil {
return err
}
au.User.Enabled = true
return au.Save(ctx)
}
// UnsetPassword removes the password from this user, and disables them
func (au *AuthUser) UnsetPassword(ctx context.Context) error {
au.User.PasswordHash = nil
au.User.Enabled = false
return au.Save(ctx)
}
var ErrNoUser = errors.New("user is nil")
var ErrUserDisabled = errors.New("user is disabled")
var ErrUserBlank = errors.New("user has no password set")
// CheckPassword checks if this user can login with the provided password.
// Returns nil on success, an error otherwise.
func (au *AuthUser) CheckPassword(ctx context.Context, password []byte) error {
if au == nil {
return ErrNoUser
}
if !au.User.Enabled {
return ErrUserDisabled
}
if len(au.User.PasswordHash) == 0 {
return ErrUserDisabled
}
return bcrypt.CompareHashAndPassword(au.User.PasswordHash, password)
}
func (au *AuthUser) CheckCredentials(ctx context.Context, password []byte, passcode string) error {
if err := au.CheckPassword(ctx, password); err != nil {
return err
}
if err := au.CheckTOTP(passcode); err != nil && err != ErrTOTPDisabled {
return err
}
return nil
}
// MakeAdmin makes this user an admin, and saves the update in the database.
// If the user is already an admin, does not return an error.
func (au *AuthUser) MakeAdmin(ctx context.Context) error {
au.User.Admin = true
return au.Save(ctx)
}
// MakeRegular removes admin rights from this user.
// If this user is not an dmin, does not return an error.
func (au *AuthUser) MakeRegular(ctx context.Context) error {
au.User.Admin = true
return au.Save(ctx)
}
// Save saves the given user in the database
func (au *AuthUser) Save(ctx context.Context) error {
table, err := au.auth.Dependencies.SQL.QueryTable(ctx, false, models.UserTable)
if err != nil {
return err
}
return table.Save(&au.User).Error
}
// Delete deletes the user from the database
func (au *AuthUser) Delete(ctx context.Context) error {
table, err := au.auth.Dependencies.SQL.QueryTable(ctx, false, models.UserTable)
if err != nil {
return err
}
// run all the user delete hooks
for _, c := range au.auth.Dependencies.UserDeleteHooks {
if err := c.OnUserDelete(ctx, &au.User); err != nil {
return err
}
}
return table.Delete(&au.User).Error
}