WIP: Add users system, required for uploading new pastes #77
@ -32,6 +32,10 @@ func main() {
|
||||
log.Fatal(errors.Wrap(err, "migrating database"))
|
||||
}
|
||||
|
||||
if err := db.CreateAdminUser(database, "admin"); err != nil {
|
||||
|
||||
log.Fatalln(err)
|
||||
yorick marked this conversation as resolved
mrngm
commented
(not (not `go fmt`ed)
|
||||
}
|
||||
|
||||
go rushlink.StartMetricsServer(*metricsListen, database, filestore)
|
||||
rushlink.StartMainServer(*httpListen, database, filestore, *rootURL)
|
||||
}
|
||||
|
178
handlers.go
@ -8,6 +8,7 @@ import (
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
@ -244,6 +245,12 @@ func (rl *rushlink) viewActionSuccess(w http.ResponseWriter, r *http.Request, p
|
||||
}
|
||||
|
||||
func (rl *rushlink) newPasteHandler(w http.ResponseWriter, r *http.Request) {
|
||||
// Check if the user is authenticated
|
||||
user := rl.authenticateUser(w, r, false, nil)
|
||||
if user == nil {
|
||||
return
|
||||
}
|
||||
|
||||
if err := r.ParseMultipartForm(formParseMaxMemory); err != nil {
|
||||
msg := fmt.Sprintf("could not parse form: %v\n", err)
|
||||
rl.renderError(w, r, http.StatusBadRequest, msg)
|
||||
@ -262,7 +269,7 @@ func (rl *rushlink) newPasteHandler(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
if shortensPrs {
|
||||
rl.newRedirectPasteHandler(w, r, shortens[0])
|
||||
rl.newRedirectPasteHandler(w, r, user, shortens[0])
|
||||
return
|
||||
}
|
||||
if fileHeadersPrs {
|
||||
@ -272,17 +279,163 @@ func (rl *rushlink) newPasteHandler(w http.ResponseWriter, r *http.Request) {
|
||||
rl.renderInternalServerError(w, r, err)
|
||||
return
|
||||
}
|
||||
rl.newFileUploadPasteHandler(w, r, file, *fileHeader)
|
||||
rl.newFileUploadPasteHandler(w, r, user, file, *fileHeader)
|
||||
return
|
||||
}
|
||||
}
|
||||
func (rl *rushlink) createUserHandler(w http.ResponseWriter, r *http.Request) {
|
||||
// Check if the user is authenticated as an admin
|
||||
user := rl.authenticateUser(w, r, true, nil)
|
||||
if user == nil {
|
||||
return
|
||||
}
|
||||
|
||||
func (rl *rushlink) newFileUploadPasteHandler(w http.ResponseWriter, r *http.Request, file multipart.File, header multipart.FileHeader) {
|
||||
if err := r.ParseMultipartForm(formParseMaxMemory); err != nil {
|
||||
msg := fmt.Sprintf("could not parse form: %v\n", err)
|
||||
rl.renderError(w, r, http.StatusBadRequest, msg)
|
||||
return
|
||||
}
|
||||
mrngm
commented
(I would be fine with leaving this out of the HTTP part) (I would be fine with leaving this out of the HTTP part)
|
||||
|
||||
new_username := r.FormValue("username")
|
||||
new_password := r.FormValue("password")
|
||||
new_admin, _ := strconv.ParseBool(r.FormValue("admin"))
|
||||
|
||||
// Create the user
|
||||
if err := db.NewUser(rl.db, new_username, new_password, new_admin); err != nil {
|
||||
// Failed to create the user, return a 500 Internal Server Error response
|
||||
http.Error(w, "Failed to create user", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
// Return a 201 Created response
|
||||
w.WriteHeader(http.StatusCreated)
|
||||
}
|
||||
mrngm
commented
Missing Missing `err` check
|
||||
|
||||
func (rl *rushlink) setWWWAuthenticate(w http.ResponseWriter, r *http.Request) {
|
||||
// Set authentication headers for Basic Authentication
|
||||
w.Header().Set("WWW-Authenticate", `Basic realm="Restricted"`)
|
||||
w.WriteHeader(http.StatusUnauthorized)
|
||||
}
|
||||
|
||||
func (rl *rushlink) authenticateUser(w http.ResponseWriter, r *http.Request, shouldBeAdmin bool, canAlsoBe *string) *db.User {
|
||||
// Check if the user is authenticated
|
||||
username, password, ok := r.BasicAuth()
|
||||
if !ok {
|
||||
// User is not authenticated, return a 401 Unauthorized response
|
||||
rl.setWWWAuthenticate(w, r)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Authenticate the user
|
||||
user, err := db.Authenticate(rl.db, username, password)
|
||||
if err != nil {
|
||||
rl.setWWWAuthenticate(w, r)
|
||||
log.Printf("authentication failure: %s", err)
|
||||
return nil
|
||||
}
|
||||
|
||||
if (shouldBeAdmin && !user.Admin && (canAlsoBe == nil || *canAlsoBe != user.User)) {
|
||||
yorick marked this conversation as resolved
mrngm
commented
This could be correct, but it's not exactly readable. It's not clear what It's perhaps better to add a function like:
and then implement the logic for authenticating the user
This could be correct, but it's not exactly readable. It's not clear what `canAlsoBe` means in this context, and what kind of values it should represent.
It's perhaps better to add a function like:
```go
func (rl *rushlink) setWWWAuthenticate(w http.ResponseWriter, r *http.Request) {
// Set authentication headers for Basic Authentication
w.Header().Set("WWW-Authenticate", `Basic realm="Restricted"`)
w.WriteHeader(http.StatusUnauthorized)
}
```
and then implement the logic for authenticating the user
```go
user, err := db.Authenticate(rl.db, username, password)
if err != nil {
log.Printf("authentication failure: %v", err)
rl.setWWWAuthenticate(w, r)
return nil
}
if shouldBeAdmin && !user.Admin {
log.Printf("user should be admin, but isn't: %v", err)
rl.setWWWAuthenticate(w, r)
return nil
}
// TODO: canAlsoBe?
// User has been authenticated
return user
```
|
||||
// Authentication failed, return a 401 Unauthorized response
|
||||
rl.setWWWAuthenticate(w, r)
|
||||
log.Printf("user '%s' should be admin (or '%s'), but isn't", username, canAlsoBe)
|
||||
return nil
|
||||
}
|
||||
return user
|
||||
}
|
||||
func (rl *rushlink) deleteUserHandler(w http.ResponseWriter, r *http.Request) {
|
||||
// Parse the user ID from the request URL
|
||||
vars := mux.Vars(r)
|
||||
userName := vars["user"]
|
||||
|
||||
// Check if the user is authenticated as an admin or self
|
||||
user := rl.authenticateUser(w, r, true, &userName)
|
||||
if user == nil {
|
||||
return
|
||||
}
|
||||
|
||||
// Delete the user
|
||||
if err := db.DeleteUser(rl.db, userName); err != nil {
|
||||
// Failed to delete the user, return a 500 Internal Server Error response
|
||||
http.Error(w, "Failed to delete user", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
// Return a 204 No Content response
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
}
|
||||
func (rl *rushlink) changeUserHandler(w http.ResponseWriter, r *http.Request) {
|
||||
// Parse the user ID from the request URL
|
||||
vars := mux.Vars(r)
|
||||
userName := vars["user"]
|
||||
|
||||
// Check if the user is authenticated as an admin or self
|
||||
user := rl.authenticateUser(w, r, true, &userName)
|
||||
if user == nil {
|
||||
return
|
||||
}
|
||||
|
||||
// Parse the request multipart form
|
||||
if err := r.ParseMultipartForm(formParseMaxMemory); err != nil {
|
||||
msg := fmt.Sprintf("could not parse form: %v\n", err)
|
||||
rl.renderError(w, r, http.StatusBadRequest, msg)
|
||||
return
|
||||
}
|
||||
|
||||
// Get the user record from the database
|
||||
updatedUser := new(db.User)
|
||||
if err := rl.db.Where("user = ?", userName).First(&updatedUser).Error; err != nil {
|
||||
// User record not found, return a 404 Not Found response
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
// Get the updated user data from the form
|
||||
if newUsername := r.FormValue("new_username"); newUsername != "" {
|
||||
updatedUser.User = newUsername
|
||||
}
|
||||
if newPassword := r.FormValue("new_password"); newPassword != "" {
|
||||
hashedPassword, err := db.HashPassword(newPassword)
|
||||
if err != nil {
|
||||
msg := fmt.Sprintf("could not hash password: %v\n", err)
|
||||
rl.renderError(w, r, http.StatusInternalServerError, msg)
|
||||
return
|
||||
}
|
||||
updatedUser.Password = hashedPassword
|
||||
}
|
||||
if newAdminStr := r.FormValue("new_admin"); newAdminStr != "" {
|
||||
newAdmin, err := strconv.ParseBool(newAdminStr)
|
||||
if err != nil {
|
||||
msg := fmt.Sprintf("could not parse admin status: %v\n", err)
|
||||
rl.renderError(w, r, http.StatusBadRequest, msg)
|
||||
return
|
||||
}
|
||||
updatedUser.Admin = newAdmin
|
||||
}
|
||||
|
||||
// Check if the user is trying to update their own admin status
|
||||
if updatedUser.Admin && !user.Admin {
|
||||
// User is trying to update their own admin status, return a 403 Forbidden response
|
||||
http.Error(w, "You do not have permission to update your own admin status", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
|
||||
// Update the user in the database
|
||||
if err := db.ChangeUser(rl.db, userName, updatedUser); err != nil {
|
||||
// Failed to update the user, return a 500 Internal Server Error response
|
||||
http.Error(w, "Failed to update user", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
// Return a 200 OK response
|
||||
w.WriteHeader(http.StatusOK)
|
||||
}
|
||||
|
||||
func (rl *rushlink) newFileUploadPasteHandler(w http.ResponseWriter, r *http.Request, user *db.User, file multipart.File, header multipart.FileHeader) {
|
||||
var fu *db.FileUpload
|
||||
var paste *db.Paste
|
||||
if err := rl.db.Transaction(func(tx *db.Database) error {
|
||||
var err error
|
||||
fu, err = db.NewFileUpload(rl.fs, file, header.Filename)
|
||||
fu, err = db.NewFileUpload(rl.fs, file, user, header.Filename)
|
||||
if err != nil {
|
||||
panic(errors.Wrap(err, "creating fileUpload"))
|
||||
}
|
||||
@ -290,7 +443,7 @@ func (rl *rushlink) newFileUploadPasteHandler(w http.ResponseWriter, r *http.Req
|
||||
panic(errors.Wrap(err, "saving fileUpload in db"))
|
||||
}
|
||||
|
||||
paste, err = shortenFileUploadID(tx, fu.PubID)
|
||||
paste, err = shortenFileUploadID(tx, user, fu.PubID)
|
||||
return err
|
||||
}); err != nil {
|
||||
panic(err)
|
||||
@ -298,7 +451,7 @@ func (rl *rushlink) newFileUploadPasteHandler(w http.ResponseWriter, r *http.Req
|
||||
rl.viewActionSuccess(w, r, paste, fu)
|
||||
}
|
||||
|
||||
func (rl *rushlink) newRedirectPasteHandler(w http.ResponseWriter, r *http.Request, rawurl string) {
|
||||
func (rl *rushlink) newRedirectPasteHandler(w http.ResponseWriter, r *http.Request, user *db.User, rawurl string) {
|
||||
userURL, err := url.Parse(rawurl)
|
||||
if err != nil {
|
||||
msg := fmt.Sprintf("invalid url (%v): %v", err, rawurl)
|
||||
@ -317,7 +470,7 @@ func (rl *rushlink) newRedirectPasteHandler(w http.ResponseWriter, r *http.Reque
|
||||
var paste *db.Paste
|
||||
if err := rl.db.Transaction(func(tx *db.Database) error {
|
||||
var err error
|
||||
paste, err = shortenURL(tx, userURL)
|
||||
paste, err = shortenURL(tx, user, userURL)
|
||||
return err
|
||||
}); err != nil {
|
||||
panic(err)
|
||||
@ -370,19 +523,19 @@ func (rl *rushlink) deletePasteHandler(w http.ResponseWriter, r *http.Request) {
|
||||
//
|
||||
// Returns the new paste key if the fileUpload was successfully added to the
|
||||
// database
|
||||
func shortenFileUploadID(tx *db.Database, id uuid.UUID) (*db.Paste, error) {
|
||||
return shorten(tx, db.PasteTypeFileUpload, id[:])
|
||||
func shortenFileUploadID(tx *db.Database, user *db.User, id uuid.UUID) (*db.Paste, error) {
|
||||
return shorten(tx, user, db.PasteTypeFileUpload, id[:])
|
||||
}
|
||||
|
||||
// Add a new URL to the database
|
||||
//
|
||||
// Returns the new paste key if the url was successfully shortened
|
||||
func shortenURL(tx *db.Database, userURL *url.URL) (*db.Paste, error) {
|
||||
return shorten(tx, db.PasteTypeRedirect, []byte(userURL.String()))
|
||||
func shortenURL(tx *db.Database, user *db.User, userURL *url.URL) (*db.Paste, error) {
|
||||
return shorten(tx, user, db.PasteTypeRedirect, []byte(userURL.String()))
|
||||
}
|
||||
|
||||
// Add a paste (of any kind) to the database with arbitrary content.
|
||||
func shorten(tx *db.Database, ty db.PasteType, content []byte) (*db.Paste, error) {
|
||||
func shorten(tx *db.Database, user *db.User, ty db.PasteType, content []byte) (*db.Paste, error) {
|
||||
// Generate the paste key
|
||||
var keyEntropy int
|
||||
if ty == db.PasteTypeFileUpload || ty == db.PasteTypePaste {
|
||||
@ -403,6 +556,7 @@ func shorten(tx *db.Database, ty db.PasteType, content []byte) (*db.Paste, error
|
||||
p := db.Paste{
|
||||
Type: ty,
|
||||
State: db.PasteStatePresent,
|
||||
CreatedBy: user.ID,
|
||||
Content: content,
|
||||
Key: pasteKey,
|
||||
DeleteToken: deleteToken,
|
||||
|
@ -38,6 +38,9 @@ type FileUpload struct {
|
||||
// UUID publically identifies this FileUpload.
|
||||
PubID uuid.UUID `gorm:"uniqueIndex"`
|
||||
|
||||
// User ID that created this file
|
||||
CreatedBy uint `gorm:"index"`
|
||||
yorick marked this conversation as resolved
mrngm
commented
(not (not `go fmt`ed)
|
||||
|
||||
// FileName contains the original filename of this FileUpload.
|
||||
FileName string
|
||||
|
||||
@ -126,7 +129,7 @@ func (fs *FileStore) filePath(pubID uuid.UUID, fileName string) string {
|
||||
//
|
||||
// Internally, this function detects the type of the file stored in `r` using
|
||||
// `http.DetectContentType`.
|
||||
func NewFileUpload(fs *FileStore, r io.Reader, fileName string) (*FileUpload, error) {
|
||||
func NewFileUpload(fs *FileStore, r io.Reader, user *User, fileName string) (*FileUpload, error) {
|
||||
// Generate a file ID
|
||||
pubID, err := uuid.NewRandom()
|
||||
if err != nil {
|
||||
@ -168,6 +171,7 @@ func NewFileUpload(fs *FileStore, r io.Reader, fileName string) (*FileUpload, er
|
||||
fu := &FileUpload{
|
||||
State: FileUploadStatePresent,
|
||||
PubID: pubID,
|
||||
CreatedBy: user.ID,
|
||||
FileName: baseName,
|
||||
ContentType: contentType,
|
||||
Checksum: hash.Sum32(),
|
||||
|
@ -42,5 +42,56 @@ func Gormigrate(db *gorm.DB) *gormigrate.Gormigrate {
|
||||
return tx.Migrator().DropTable(&FileUpload{}, &Paste{})
|
||||
},
|
||||
},
|
||||
{
|
||||
ID: "202304301337",
|
||||
Migrate: func(tx *gorm.DB) error {
|
||||
type User struct {
|
||||
ID uint `gorm:"primaryKey"`
|
||||
User string `gorm:"uniqueIndex"`
|
||||
Password string
|
||||
Admin bool
|
||||
CreatedAt time.Time
|
||||
UpdatedAt time.Time
|
||||
DeletedAt gorm.DeletedAt
|
||||
}
|
||||
type FileUpload struct {
|
||||
ID uint `gorm:"primaryKey"`
|
||||
State FileUploadState `gorm:"index"`
|
||||
PubID uuid.UUID `gorm:"uniqueIndex"`
|
||||
CreatedBy uint `gorm:"index"`
|
||||
FileName string
|
||||
ContentType string
|
||||
Checksum uint32
|
||||
CreatedAt time.Time
|
||||
UpdatedAt time.Time
|
||||
DeletedAt gorm.DeletedAt
|
||||
}
|
||||
type Paste struct {
|
||||
ID uint `gorm:"primaryKey"`
|
||||
Type PasteType `gorm:"index"`
|
||||
State PasteState `gorm:"index"`
|
||||
CreatedBy uint `gorm:"index"`
|
||||
Content []byte
|
||||
Key string `gorm:"uniqueIndex"`
|
||||
DeleteToken string
|
||||
CreatedAt time.Time
|
||||
UpdatedAt time.Time
|
||||
DeletedAt gorm.DeletedAt
|
||||
}
|
||||
return tx.AutoMigrate(&User{}, &FileUpload{}, &Paste{})
|
||||
},
|
||||
Rollback: func(tx *gorm.DB) error {
|
||||
if err := tx.Migrator().DropTable(&User{}); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := tx.Migrator().DropColumn(&FileUpload{}, "CreatedBy"); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := tx.Migrator().DropColumn(&Paste{}, "CreatedBy"); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
72
internal/db/password.go
Normal file
@ -0,0 +1,72 @@
|
||||
package db
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"crypto/subtle"
|
||||
"encoding/base64"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"golang.org/x/crypto/argon2"
|
||||
)
|
||||
|
||||
const (
|
||||
pwdSaltSize = 16
|
||||
pwdHashSize = 32
|
||||
|
||||
// chosen from https://cheatsheetseries.owasp.org/cheatsheets/Password_Storage_Cheat_Sheet.html
|
||||
pwdMemory = 64 * 1024
|
||||
pwdThreads = 1
|
||||
pwdIterations = 2
|
||||
pwdParams = "m=65536,t=2,p=1,v=19"
|
||||
pwdVersion = "v=19"
|
||||
pwdAlgo = "argon2id"
|
||||
)
|
||||
|
||||
func HashPassword(password string) (string, error) {
|
||||
if argon2.Version != 19 {
|
||||
// go has no static asserts
|
||||
panic("Unexpected argon2 version")
|
||||
}
|
||||
// Generate a salt for the password hash
|
||||
salt := make([]byte, pwdSaltSize)
|
||||
if _, err := rand.Read(salt); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
// Hash the password using argon2id
|
||||
hash := argon2.IDKey([]byte(password), salt, pwdIterations, pwdMemory, pwdThreads, pwdHashSize)
|
||||
|
||||
// Encode the salt and hash as a string in PHC format
|
||||
encodedSalt := base64.RawStdEncoding.EncodeToString(salt)
|
||||
encodedHash := base64.RawStdEncoding.EncodeToString(hash)
|
||||
return fmt.Sprintf("$%s$%s$%s$%s$%s", pwdAlgo, pwdVersion, pwdParams, encodedSalt, encodedHash), nil
|
||||
}
|
||||
|
||||
var errInvalidDBPasswordFormat = errors.New("invalid password format in db")
|
||||
|
||||
func comparePassword(hashedPassword string, password string) (bool, error) {
|
||||
// Extract the salt and hash from the hashed password string
|
||||
fields := strings.Split(hashedPassword, "$")
|
||||
if len(fields) != 6 || fields[1] != pwdAlgo || fields[2] != pwdVersion || fields[3] != pwdParams {
|
||||
return false, errInvalidDBPasswordFormat
|
||||
}
|
||||
encodedSalt, encodedHash := fields[4], fields[5]
|
||||
|
||||
// Decode the salt and hash from base64
|
||||
salt, err := base64.RawStdEncoding.DecodeString(encodedSalt)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
hash, err := base64.RawStdEncoding.DecodeString(encodedHash)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
// Hash the password using the extracted salt and parameters
|
||||
computedHash := argon2.IDKey([]byte(password), salt, pwdIterations, pwdMemory, pwdThreads, pwdHashSize)
|
||||
|
||||
// Compare the computed hash with the stored hash
|
||||
return subtle.ConstantTimeCompare(hash, computedHash) == 1, nil
|
||||
}
|
@ -24,6 +24,7 @@ type Paste struct {
|
||||
ID uint `gorm:"primaryKey"`
|
||||
Type PasteType `gorm:"index"`
|
||||
State PasteState `gorm:"index"`
|
||||
CreatedBy uint `gorm:"index"`
|
||||
Content []byte
|
||||
Key string `gorm:"uniqueIndex"`
|
||||
DeleteToken string
|
||||
|
129
internal/db/user.go
Normal file
@ -0,0 +1,129 @@
|
||||
package db
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"encoding/base64"
|
||||
"errors"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type User struct {
|
||||
ID uint `gorm:"primaryKey"`
|
||||
User string `gorm:"uniqueIndex"`
|
||||
Password string
|
||||
Admin bool
|
||||
CreatedAt time.Time
|
||||
UpdatedAt time.Time
|
||||
DeletedAt gorm.DeletedAt
|
||||
}
|
||||
|
||||
func NewUser(db *gorm.DB, username string, password string, admin bool) error {
|
||||
// Generate a new UUID for the user
|
||||
id, err := uuid.NewRandom()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Hash the password using argon2id
|
||||
hashedPassword, err := HashPassword(password)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Create a new user record
|
||||
user := &User{
|
||||
ID: uint(id.ID()),
|
||||
User: username,
|
||||
Password: hashedPassword,
|
||||
Admin: admin,
|
||||
}
|
||||
return db.Create(user).Error
|
||||
}
|
||||
|
||||
func Authenticate(db *gorm.DB, username string, password string) (*User, error) {
|
||||
// Get the user record by username
|
||||
var user User
|
||||
if err := db.Where("user = ?", username).First(&user).Error; err != nil {
|
||||
return nil, errors.New("user not found")
|
||||
}
|
||||
|
||||
// Compare the hashed password with the provided password
|
||||
valid, err := comparePassword(user.Password, password)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !valid {
|
||||
return nil, errors.New("invalid password")
|
||||
}
|
||||
|
||||
return &user, nil
|
||||
}
|
||||
|
||||
// DeleteUser deletes a user with the specified username from the database.
|
||||
func DeleteUser(db *gorm.DB, username string) error {
|
||||
// Find the user by username
|
||||
var user User
|
||||
yorick marked this conversation as resolved
mrngm
commented
(I would put these password-related (I would put these password-related `const` and `func`s in a separate file, because they do nothing with regards to users)
|
||||
if err := db.Where("user = ?", username).First(&user).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Delete the user
|
||||
if err := db.Delete(&user).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func ChangeUser(db *gorm.DB, username string, updatedUser *User) error {
|
||||
// Retrieve the existing user
|
||||
var existingUser User
|
||||
yorick marked this conversation as resolved
mrngm
commented
(nit: also define the (nit: also define the `2`, `64*1024`, `1` in the `var` block above, such that you can reuse it in `comparePassword`; alternatively, create a function `createHashFromPasswordAndSalt(password, salt []byte) []byte` that does this for you... also specifying why you chose these parameters)
|
||||
if err := db.Where("user = ?", username).First(&existingUser).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
yorick marked this conversation as resolved
mrngm
commented
According to the PHC format specification, padding characters are omitted. (Technically speaking, you should use According to the [PHC format specification](https://github.com/P-H-C/phc-string-format/blob/master/phc-sf-spec.md#b64), padding characters are omitted. `base64.URLEncoding` includes padding characters. Use `base64.RawURLEncoding` instead.
(Technically speaking, you should use `base64.RawStdEncoding`, because that follows the exact alphabet as the specification. If you decide that `base64.URLEncoding` (or `base64.RawURLEncoding`) also works, then make it clear in the comment that you deviate from the specification).
|
||||
|
||||
// Update the user fields
|
||||
yorick marked this conversation as resolved
mrngm
commented
Is there a specific reason you omitted the (optional) field Is there a specific reason you omitted the (optional) field `version`? The `argon2` package [defines one](https://pkg.go.dev/golang.org/x/crypto/argon2#Version), so it seems a good idea to include that as well.
|
||||
existingUser.User = updatedUser.User
|
||||
existingUser.Password = updatedUser.Password
|
||||
existingUser.Admin = updatedUser.Admin
|
||||
|
||||
// Save the updated user to the database
|
||||
yorick marked this conversation as resolved
mrngm
commented
It's not safe to do a slice expression that requires at least two returned elements on a function that may return an empty slice, or a slice of length one. It's not safe to do a slice expression that requires at least two returned elements on a function that may return an empty slice, or a slice of length one.
|
||||
if err := db.Save(&existingUser).Error; err != nil {
|
||||
return err
|
||||
yorick marked this conversation as resolved
mrngm
commented
(another option: add (another option: add `var errInvalidDBPasswordFormat = errors.New("invalid password format in db")` in the global scope, and return that here. Works nice with `errors.Is(err, errInvalidDBPasswordFormat), as I suppose we want to log such occurrences)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
yorick marked this conversation as resolved
mrngm
commented
(See comment above on (See comment above on `RawURLEncoding`)
|
||||
func CreateAdminUser(db *gorm.DB, adminUsername string) error {
|
||||
// Check if the admin user already exists
|
||||
var admins []User
|
||||
if err := db.Unscoped().Limit(1).Where("user = ?", adminUsername).Find(&admins).Error; err != nil {
|
||||
yorick marked this conversation as resolved
mrngm
commented
(Idem) (Idem)
|
||||
return err
|
||||
}
|
||||
if len(admins) > 0 {
|
||||
// already exists
|
||||
return nil
|
||||
}
|
||||
|
||||
// Generate a random 24-char password
|
||||
passwordBytes := make([]byte, 24)
|
||||
if _, err := rand.Read(passwordBytes); err != nil {
|
||||
yorick marked this conversation as resolved
mrngm
commented
`return subtle.ConstantTimeCompare(hash, computedHash) == 1, nil` (from `crypto/subtle`)
|
||||
return err
|
||||
}
|
||||
password := base64.URLEncoding.EncodeToString(passwordBytes)
|
||||
|
||||
// Create the admin user
|
||||
if err := NewUser(db, adminUsername, password, true); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Print out the generated password
|
||||
fmt.Printf("Generated password for admin user %s: %s\n", adminUsername, password)
|
||||
|
||||
return nil
|
||||
}
|
@ -106,6 +106,9 @@ func InitMainRouter(r *mux.Router, rl *rushlink) {
|
||||
r.HandleFunc("/{path:js/"+staticFilenameExpr+"}", rl.staticGetHandler).Methods("GET", "HEAD")
|
||||
r.HandleFunc("/", rl.indexGetHandler).Methods("GET", "HEAD")
|
||||
r.HandleFunc("/", rl.newPasteHandler).Methods("POST")
|
||||
r.HandleFunc("/users", rl.createUserHandler).Methods("POST")
|
||||
r.HandleFunc("/users/{user}", rl.changeUserHandler).Methods("POST")
|
||||
r.HandleFunc("/users/{user}", rl.deleteUserHandler).Methods("DELETE")
|
||||
r.HandleFunc("/"+urlKeyExpr, rl.viewPasteHandler).Methods("GET", "HEAD")
|
||||
r.HandleFunc("/"+urlKeyWithExtExpr, rl.viewPasteHandler).Methods("GET", "HEAD")
|
||||
r.HandleFunc("/"+urlKeyExpr+"/nr", rl.viewPasteHandlerNoRedirect).Methods("GET", "HEAD")
|
||||
|
I would be fine with a command line option that allows the operator to create users.