Add token unwrapping, admin concept and user creation endpoint

This commit is contained in:
Miguel de la Cruz 2021-09-13 14:52:00 +02:00
parent 7ebb14e431
commit 3c7c32423e
12 changed files with 141 additions and 16 deletions

View file

@ -26,7 +26,8 @@ Default configuration can be found in the
- [X] Decide on a visual framework to work with (maybe react-material?) - [X] Decide on a visual framework to work with (maybe react-material?)
- [ ] Add webapp linter - [ ] Add webapp linter
- [X] Add token and login endpoint - [X] Add token and login endpoint
- [ ] Add token reading wrapper at the API level - [X] Add token reading wrapper at the API level
- [X] Add the concept of admin users
- [ ] Add games management endpoints and webapp screens - [ ] Add games management endpoints and webapp screens
- [ ] Add chat messages in game - [ ] Add chat messages in game
- [ ] Add websocket messages for new chat post - [ ] Add websocket messages for new chat post

View file

@ -1,9 +1,21 @@
package api package api
import ( import (
"context"
"errors"
"fmt"
"net/http" "net/http"
"strconv"
"strings"
"git.ctrlz.es/mgdelacroix/craban/model"
"github.com/dgrijalva/jwt-go"
"github.com/rs/zerolog/log"
) )
const userContextKey = "user"
func (a *API) Login(w http.ResponseWriter, r *http.Request) { func (a *API) Login(w http.ResponseWriter, r *http.Request) {
body := ParseBody(r) body := ParseBody(r)
username := body.String("username") username := body.String("username")
@ -22,3 +34,61 @@ func (a *API) Login(w http.ResponseWriter, r *http.Request) {
w.Write([]byte(token)) w.Write([]byte(token))
} }
func (a *API) getUserFromToken(tokenStr string) (*model.User, error) {
token, err := jwt.Parse(tokenStr, func(token *jwt.Token) (interface{}, error) {
// Validate the alg is what you expect:
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
return nil, fmt.Errorf("Unexpected signing method: %v", token.Header["alg"])
}
return []byte(*a.App.Config.Secret), nil
})
if err != nil {
return nil, err
}
if claims, ok := token.Claims.(jwt.MapClaims); ok && token.Valid {
userIDClaim, ok := claims["userID"].(string)
if !ok {
return nil, errors.New("userID claim is not set")
}
userID, err := strconv.Atoi(userIDClaim)
if err != nil {
return nil, err
}
return a.App.GetUserByID(userID)
} else {
return nil, errors.New("Malformed claims")
}
}
func (a *API) Secured(fn func(http.ResponseWriter, *http.Request)) func(http.ResponseWriter, *http.Request) {
return func(w http.ResponseWriter, r *http.Request) {
tokenStr := r.Header.Get("Authorization")
if tokenStr == "" {
http.Error(w, http.StatusText(http.StatusForbidden), http.StatusForbidden)
return
}
if strings.HasPrefix(tokenStr, "Bearer ") {
tokenStr = tokenStr[7:]
}
user, err := a.getUserFromToken(tokenStr)
if err != nil {
log.Debug().Err(err).Str("token", tokenStr).Msg("cannot get user from token")
http.Error(w, http.StatusText(http.StatusForbidden), http.StatusForbidden)
return
}
// get user
ctx := context.WithValue(r.Context(), userContextKey, user)
fn(w, r.Clone(ctx))
}
}
func UserFromRequest(r *http.Request) (*model.User, bool) {
user, ok := r.Context().Value(userContextKey).(*model.User)
return user, ok
}

View file

@ -1,9 +1,46 @@
package api package api
import ( import (
"fmt"
"net/http" "net/http"
"git.ctrlz.es/mgdelacroix/craban/model"
"github.com/rs/zerolog/log"
) )
func (a *API) CreateUser(w http.ResponseWriter, r *http.Request) { func (a *API) CreateUser(w http.ResponseWriter, r *http.Request) {
user, _ := UserFromRequest(r)
if !user.Admin {
http.Error(w, http.StatusText(http.StatusForbidden), http.StatusForbidden)
return
}
body := ParseBody(r)
username := body.String("username")
password := body.String("password")
name := body.String("name")
mail := body.String("mail")
admin := body.Bool("admin")
newUser := &model.User{
Username: username,
Password: password,
Name: name,
Mail: mail,
}
if err := newUser.IsValid(); err != nil {
http.Error(w, fmt.Sprintf("%s: %s", http.StatusText(http.StatusBadRequest), err), http.StatusBadRequest)
return
}
createdUser, err := a.App.CreateUser(username, password, name, mail, admin)
if err != nil {
log.Error().Err(err).Msg("user couldn't be created")
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
return
}
JSON(w, createdUser, 201)
} }

View file

@ -30,6 +30,13 @@ func (b *Body) Int(name string) int {
return 0 return 0
} }
func (b *Body) Bool(name string) bool {
if res, ok := (*b)[name].(bool); ok {
return res
}
return false
}
func JSON(w http.ResponseWriter, data interface{}, statusCode int) { func JSON(w http.ResponseWriter, data interface{}, statusCode int) {
b, err := json.Marshal(data) b, err := json.Marshal(data)
if err != nil { if err != nil {

View file

@ -6,13 +6,13 @@ import (
) )
type App struct { type App struct {
config *model.Config Config *model.Config
store *store.Store Store *store.Store
} }
func NewApp(config *model.Config, store *store.Store) *App { func NewApp(config *model.Config, store *store.Store) *App {
return &App{ return &App{
config: config, Config: config,
store: store, Store: store,
} }
} }

View file

@ -8,7 +8,7 @@ import (
) )
func (a *App) Login(username, password string) (string, error) { func (a *App) Login(username, password string) (string, error) {
user, err := a.store.User().GetByUsername(username) user, err := a.Store.User().GetByUsername(username)
if err == sql.ErrNoRows { if err == sql.ErrNoRows {
return "", nil return "", nil
} else if err != nil { } else if err != nil {
@ -19,7 +19,7 @@ func (a *App) Login(username, password string) (string, error) {
return "", nil return "", nil
} }
token, err := utils.GenerateToken(user.ID, *a.config.Secret) token, err := utils.GenerateToken(user.ID, *a.Config.Secret)
if err != nil { if err != nil {
return "", fmt.Errorf("cannot generate token: %w", err) return "", fmt.Errorf("cannot generate token: %w", err)
} }

View file

@ -7,7 +7,7 @@ import (
"git.ctrlz.es/mgdelacroix/craban/utils" "git.ctrlz.es/mgdelacroix/craban/utils"
) )
func (a *App) CreateUser(username, password, name, mail string) (*model.User, error) { func (a *App) CreateUser(username, password, name, mail string, admin bool) (*model.User, error) {
hashedPassword, err := utils.Encrypt(password) hashedPassword, err := utils.Encrypt(password)
if err != nil { if err != nil {
return nil, fmt.Errorf("cannot create user: %w", err) return nil, fmt.Errorf("cannot create user: %w", err)
@ -18,19 +18,24 @@ func (a *App) CreateUser(username, password, name, mail string) (*model.User, er
Password: hashedPassword, Password: hashedPassword,
Name: name, Name: name,
Mail: mail, Mail: mail,
Admin: admin,
} }
if err := newUser.IsValid(); err != nil { if err := newUser.IsValid(); err != nil {
return nil, fmt.Errorf("invalid user for creation: %w", err) return nil, fmt.Errorf("invalid user for creation: %w", err)
} }
return a.store.User().Create(newUser) return a.Store.User().Create(newUser)
} }
func (a *App) ListUsers() ([]*model.User, error) { func (a *App) ListUsers() ([]*model.User, error) {
return a.store.User().List() return a.Store.User().List()
} }
func (a *App) DeleteUserByUsername(username string) error { func (a *App) DeleteUserByUsername(username string) error {
return a.store.User().DeleteByUsername(username) return a.Store.User().DeleteByUsername(username)
}
func (a *App) GetUserByID(userID int) (*model.User, error) {
return a.Store.User().GetByID(userID)
} }

View file

@ -40,6 +40,7 @@ func CreateUserCmd() *cobra.Command {
cmd.MarkFlagRequired("name") cmd.MarkFlagRequired("name")
cmd.Flags().StringP("mail", "m", "", "the mail of the new user") cmd.Flags().StringP("mail", "m", "", "the mail of the new user")
cmd.MarkFlagRequired("mail") cmd.MarkFlagRequired("mail")
cmd.Flags().BoolP("admin", "a", false, "sets the new user as an admin")
return cmd return cmd
} }
@ -67,6 +68,7 @@ func createUserCmdF(cmd *cobra.Command, _ []string) {
password, _ := cmd.Flags().GetString("password") password, _ := cmd.Flags().GetString("password")
name, _ := cmd.Flags().GetString("name") name, _ := cmd.Flags().GetString("name")
mail, _ := cmd.Flags().GetString("mail") mail, _ := cmd.Flags().GetString("mail")
admin, _ := cmd.Flags().GetBool("admin")
config, _ := cmd.Flags().GetString("config") config, _ := cmd.Flags().GetString("config")
srv, err := server.NewServerWithConfigPath(config) srv, err := server.NewServerWithConfigPath(config)
@ -76,7 +78,7 @@ func createUserCmdF(cmd *cobra.Command, _ []string) {
} }
defer srv.Store.Close() defer srv.Store.Close()
user, err := srv.App.CreateUser(username, password, name, mail) user, err := srv.App.CreateUser(username, password, name, mail, admin)
if err != nil { if err != nil {
log.Error().Err(err).Msg("user couldn't be created") log.Error().Err(err).Msg("user couldn't be created")
os.Exit(1) os.Exit(1)

View file

@ -10,6 +10,7 @@ type User struct {
Mail string `json:"mail"` Mail string `json:"mail"`
Username string `json:"username"` Username string `json:"username"`
Password string `json:"-"` Password string `json:"-"`
Admin bool `json:"admin"`
} }
func (u *User) IsValid() error { func (u *User) IsValid() error {

View file

@ -57,7 +57,8 @@ CREATE TABLE IF NOT EXISTS users (
name VARCHAR(255) NOT NULL, name VARCHAR(255) NOT NULL,
mail VARCHAR(255) UNIQUE NOT NULL, mail VARCHAR(255) UNIQUE NOT NULL,
username VARCHAR(255) UNIQUE NOT NULL, username VARCHAR(255) UNIQUE NOT NULL,
password VARCHAR(255) NOT NULL password VARCHAR(255) NOT NULL,
admin BOOLEAN NOT NULL
); );
CREATE TABLE IF NOT EXISTS game ( CREATE TABLE IF NOT EXISTS game (

View file

@ -8,7 +8,7 @@ import (
"git.ctrlz.es/mgdelacroix/craban/model" "git.ctrlz.es/mgdelacroix/craban/model"
) )
var userColumns = []string{"id", "name", "mail", "username", "password"} var userColumns = []string{"id", "name", "mail", "username", "password", "admin"}
type UserStore struct { type UserStore struct {
Conn *sql.DB Conn *sql.DB
@ -34,6 +34,7 @@ func (us *UserStore) usersFromRows(rows *sql.Rows) ([]*model.User, error) {
&user.Mail, &user.Mail,
&user.Username, &user.Username,
&user.Password, &user.Password,
&user.Admin,
) )
if err != nil { if err != nil {
return nil, err return nil, err
@ -94,7 +95,7 @@ func (us *UserStore) GetByUsername(username string) (*model.User, error) {
func (us *UserStore) Create(user *model.User) (*model.User, error) { func (us *UserStore) Create(user *model.User) (*model.User, error) {
query := us.Q().Insert("users"). query := us.Q().Insert("users").
Columns(userColumns[1:]...). Columns(userColumns[1:]...).
Values(user.Name, user.Mail, user.Username, user.Password) Values(user.Name, user.Mail, user.Username, user.Password, user.Admin)
res, err := query.Exec() res, err := query.Exec()
if err != nil { if err != nil {

View file

@ -38,7 +38,7 @@ func (w *WebServer) RegisterRoutes(api *api.API) {
apiRouter := r.PathPrefix("/api").Subrouter() apiRouter := r.PathPrefix("/api").Subrouter()
apiRouter.HandleFunc("/login", api.Login).Methods("POST") apiRouter.HandleFunc("/login", api.Login).Methods("POST")
apiRouter.HandleFunc("/user", api.CreateUser).Methods("POST") apiRouter.HandleFunc("/user", api.Secured(api.CreateUser)).Methods("POST")
staticFSSub, _ := fs.Sub(staticFS, "static") staticFSSub, _ := fs.Sub(staticFS, "static")
staticFSHandler := StaticFSHandler{http.FileServer(http.FS(staticFSSub))} staticFSHandler := StaticFSHandler{http.FileServer(http.FS(staticFSSub))}