From 3c7c32423e373f2d7f4a53be22864944e54ae2e0 Mon Sep 17 00:00:00 2001 From: Miguel de la Cruz Date: Mon, 13 Sep 2021 14:52:00 +0200 Subject: [PATCH] Add token unwrapping, admin concept and user creation endpoint --- README.md | 3 +- server/api/auth.go | 70 ++++++++++++++++++++++++++++++ server/api/user.go | 37 ++++++++++++++++ server/api/utils.go | 7 +++ server/app/app.go | 8 ++-- server/app/auth.go | 4 +- server/app/user.go | 13 ++++-- server/cmd/craban/commands/user.go | 4 +- server/model/user.go | 1 + server/services/store/store.go | 3 +- server/services/store/user.go | 5 ++- server/web/web.go | 2 +- 12 files changed, 141 insertions(+), 16 deletions(-) diff --git a/README.md b/README.md index 12b1d43..271dced 100644 --- a/README.md +++ b/README.md @@ -26,7 +26,8 @@ Default configuration can be found in the - [X] Decide on a visual framework to work with (maybe react-material?) - [ ] Add webapp linter - [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 chat messages in game - [ ] Add websocket messages for new chat post diff --git a/server/api/auth.go b/server/api/auth.go index b128b33..c68c0fd 100644 --- a/server/api/auth.go +++ b/server/api/auth.go @@ -1,9 +1,21 @@ package api import ( + "context" + "errors" + "fmt" "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) { body := ParseBody(r) username := body.String("username") @@ -22,3 +34,61 @@ func (a *API) Login(w http.ResponseWriter, r *http.Request) { 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 +} diff --git a/server/api/user.go b/server/api/user.go index 1194fde..e90d60b 100644 --- a/server/api/user.go +++ b/server/api/user.go @@ -1,9 +1,46 @@ package api import ( + "fmt" "net/http" + + "git.ctrlz.es/mgdelacroix/craban/model" + + "github.com/rs/zerolog/log" ) 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) } diff --git a/server/api/utils.go b/server/api/utils.go index 892b840..7a097d6 100644 --- a/server/api/utils.go +++ b/server/api/utils.go @@ -30,6 +30,13 @@ func (b *Body) Int(name string) int { 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) { b, err := json.Marshal(data) if err != nil { diff --git a/server/app/app.go b/server/app/app.go index bbb481a..159ed60 100644 --- a/server/app/app.go +++ b/server/app/app.go @@ -6,13 +6,13 @@ import ( ) type App struct { - config *model.Config - store *store.Store + Config *model.Config + Store *store.Store } func NewApp(config *model.Config, store *store.Store) *App { return &App{ - config: config, - store: store, + Config: config, + Store: store, } } diff --git a/server/app/auth.go b/server/app/auth.go index 7426bea..745b8a8 100644 --- a/server/app/auth.go +++ b/server/app/auth.go @@ -8,7 +8,7 @@ import ( ) 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 { return "", nil } else if err != nil { @@ -19,7 +19,7 @@ func (a *App) Login(username, password string) (string, error) { return "", nil } - token, err := utils.GenerateToken(user.ID, *a.config.Secret) + token, err := utils.GenerateToken(user.ID, *a.Config.Secret) if err != nil { return "", fmt.Errorf("cannot generate token: %w", err) } diff --git a/server/app/user.go b/server/app/user.go index 17a8496..3479dd8 100644 --- a/server/app/user.go +++ b/server/app/user.go @@ -7,7 +7,7 @@ import ( "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) if err != nil { 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, Name: name, Mail: mail, + Admin: admin, } if err := newUser.IsValid(); err != nil { 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) { - return a.store.User().List() + return a.Store.User().List() } 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) } diff --git a/server/cmd/craban/commands/user.go b/server/cmd/craban/commands/user.go index 66ef84c..745f565 100644 --- a/server/cmd/craban/commands/user.go +++ b/server/cmd/craban/commands/user.go @@ -40,6 +40,7 @@ func CreateUserCmd() *cobra.Command { cmd.MarkFlagRequired("name") cmd.Flags().StringP("mail", "m", "", "the mail of the new user") cmd.MarkFlagRequired("mail") + cmd.Flags().BoolP("admin", "a", false, "sets the new user as an admin") return cmd } @@ -67,6 +68,7 @@ func createUserCmdF(cmd *cobra.Command, _ []string) { password, _ := cmd.Flags().GetString("password") name, _ := cmd.Flags().GetString("name") mail, _ := cmd.Flags().GetString("mail") + admin, _ := cmd.Flags().GetBool("admin") config, _ := cmd.Flags().GetString("config") srv, err := server.NewServerWithConfigPath(config) @@ -76,7 +78,7 @@ func createUserCmdF(cmd *cobra.Command, _ []string) { } 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 { log.Error().Err(err).Msg("user couldn't be created") os.Exit(1) diff --git a/server/model/user.go b/server/model/user.go index 2916e98..4b21650 100644 --- a/server/model/user.go +++ b/server/model/user.go @@ -10,6 +10,7 @@ type User struct { Mail string `json:"mail"` Username string `json:"username"` Password string `json:"-"` + Admin bool `json:"admin"` } func (u *User) IsValid() error { diff --git a/server/services/store/store.go b/server/services/store/store.go index 19ed544..f0578b3 100644 --- a/server/services/store/store.go +++ b/server/services/store/store.go @@ -57,7 +57,8 @@ CREATE TABLE IF NOT EXISTS users ( name VARCHAR(255) NOT NULL, mail 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 ( diff --git a/server/services/store/user.go b/server/services/store/user.go index 78a14aa..65935a3 100644 --- a/server/services/store/user.go +++ b/server/services/store/user.go @@ -8,7 +8,7 @@ import ( "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 { Conn *sql.DB @@ -34,6 +34,7 @@ func (us *UserStore) usersFromRows(rows *sql.Rows) ([]*model.User, error) { &user.Mail, &user.Username, &user.Password, + &user.Admin, ) if err != nil { 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) { query := us.Q().Insert("users"). 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() if err != nil { diff --git a/server/web/web.go b/server/web/web.go index 65e69b1..5dbadb7 100644 --- a/server/web/web.go +++ b/server/web/web.go @@ -38,7 +38,7 @@ func (w *WebServer) RegisterRoutes(api *api.API) { apiRouter := r.PathPrefix("/api").Subrouter() 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") staticFSHandler := StaticFSHandler{http.FileServer(http.FS(staticFSSub))}