Alexander Belanger vor 5 Jahren
Ursprung
Commit
2c05a00ead

+ 3 - 0
INT_TEST.md

@@ -9,4 +9,7 @@ curl -d "{\"email\":\"hello\",\"password\":\"hello\"}" -H 'Content-Type: applica
 
 # should pass (without authentication)
 curl -d "{\"email\":\"belanger@getporter.dev\",\"password\":\"hello\"}" -H 'Content-Type: application/json' -X POST localhost:8080/api/users
+
+# should pass
+curl -X DELETE localhost:8080/api/users/1 -d "{\"password\":\"hello\"}"
 ```

+ 0 - 1
internal/adapter/gorm.go

@@ -9,7 +9,6 @@ import (
 )
 
 // New returns a new gorm database instance
-// TODO -- accept config to generate connection
 func New(conf *config.DBConf) (*gorm.DB, error) {
 	dsn := fmt.Sprintf(
 		"user=%s password=%s port=%d host=%s sslmode=disable",

+ 30 - 10
internal/forms/user.go

@@ -1,6 +1,7 @@
 package forms
 
 import (
+	"github.com/jinzhu/gorm"
 	"github.com/porter-dev/porter/internal/kubernetes"
 	"github.com/porter-dev/porter/internal/models"
 	"gopkg.in/yaml.v2"
@@ -18,16 +19,6 @@ type CreateUserForm struct {
 	Password string `json:"password" form:"required,max=255"`
 }
 
-// UpdateUserForm represents the accepted values for updating a user
-//
-// ID is a query parameter, the other two are sent in JSON body
-type UpdateUserForm struct {
-	WriteUserForm
-	ID              uint64   `form:"required"`
-	RawKubeConfig   string   `json:"rawKubeConfig" form:"required"`
-	AllowedClusters []string `json:"allowedClusters" form:"required"`
-}
-
 // ToUser converts a CreateUserForm to models.User
 //
 // TODO -- PASSWORD HASHING HERE
@@ -38,6 +29,16 @@ func (cuf *CreateUserForm) ToUser() (*models.User, error) {
 	}, nil
 }
 
+// UpdateUserForm represents the accepted values for updating a user
+//
+// ID is a query parameter, the other two are sent in JSON body
+type UpdateUserForm struct {
+	WriteUserForm
+	ID              uint     `form:"required"`
+	RawKubeConfig   string   `json:"rawKubeConfig" form:"required"`
+	AllowedClusters []string `json:"allowedClusters" form:"required"`
+}
+
 // ToUser converts an UpdateUserForm to models.User by parsing the kubeconfig
 // and the allowed clusters to generate a list of ClusterConfigs.
 func (uuf *UpdateUserForm) ToUser() (*models.User, error) {
@@ -52,7 +53,26 @@ func (uuf *UpdateUserForm) ToUser() (*models.User, error) {
 	clusters := conf.ToClusterConfigs(uuf.AllowedClusters)
 
 	return &models.User{
+		Model: gorm.Model{
+			ID: uuf.ID,
+		},
 		Clusters:      clusters,
 		RawKubeConfig: rawBytes,
 	}, nil
 }
+
+// DeleteUserForm represents the accepted values for deleting a user
+type DeleteUserForm struct {
+	WriteUserForm
+	ID       uint   `form:"required"`
+	Password string `json:"password" form:"required,max=255"`
+}
+
+// ToUser converts a DeleteUserForm to models.User using the user ID
+func (uuf *DeleteUserForm) ToUser() (*models.User, error) {
+	return &models.User{
+		Model: gorm.Model{
+			ID: uuf.ID,
+		},
+	}, nil
+}

+ 38 - 0
internal/models/cluster_configs_test.go

@@ -0,0 +1,38 @@
+package models_test
+
+import (
+	"testing"
+
+	"github.com/jinzhu/gorm"
+	"github.com/porter-dev/porter/internal/models"
+)
+
+func TestClusterConfigExternalize(t *testing.T) {
+	cc := &models.ClusterConfig{
+		Model: gorm.Model{
+			ID: 1,
+		},
+		Name:   "test",
+		Server: "localhost",
+		User:   "test",
+		UserID: 1,
+	}
+
+	extCC := *cc.Externalize()
+
+	if extCC.Name != cc.Name {
+		t.Errorf("Field: %s\t Int: %v\t Ext: %v\n", "Name", extCC.Name, cc.Name)
+	}
+
+	if extCC.Server != cc.Server {
+		t.Errorf("Field: %s\t Int: %v\t Ext: %v\n", "Server", extCC.Server, cc.Server)
+	}
+
+	if extCC.User != cc.User {
+		t.Errorf("Field: %s\t Int: %v\t Ext: %v\n", "User", extCC.User, cc.User)
+	}
+
+	if extCC.Context != cc.Context {
+		t.Errorf("Field: %s\t Int: %v\t Ext: %v\n", "Context", extCC.Context, cc.Context)
+	}
+}

+ 46 - 0
internal/models/user_test.go

@@ -0,0 +1,46 @@
+package models_test
+
+import (
+	"testing"
+
+	"github.com/jinzhu/gorm"
+	"github.com/porter-dev/porter/internal/models"
+)
+
+func TestUserExternalize(t *testing.T) {
+	// create a new user
+	user := &models.User{
+		Model: gorm.Model{
+			ID: 1,
+		},
+		Email:    "testing@testing.com",
+		Password: "testing123",
+		Clusters: []models.ClusterConfig{
+			models.ClusterConfig{
+				Name:   "test",
+				Server: "localhost",
+				User:   "test",
+				UserID: 1,
+			},
+		},
+		RawKubeConfig: []byte{},
+	}
+
+	extUser := *user.Externalize()
+
+	if extUser.ID != user.ID {
+		t.Errorf("Field: %s\t Int: %v\t Ext: %v\n", "ID", user.ID, extUser.ID)
+	}
+
+	if extUser.Email != user.Email {
+		t.Errorf("Field: %s\t Int: %v\t Ext: %v\n", "Email", user.Email, extUser.Email)
+	}
+
+	if len(extUser.Clusters) != 1 {
+		t.Errorf("Field: %s\t Int: %v\t Ext: %v\n", "Length Clusters", len(extUser.Clusters), 1)
+	}
+
+	if len(extUser.RawKubeConfig) != 0 {
+		t.Errorf("Field: %s\t Int: %v\t Ext: %v\n", "Length RawKubeConfig", len(extUser.RawKubeConfig), 0)
+	}
+}

+ 17 - 0
internal/queries/user.go

@@ -13,6 +13,15 @@ func CreateUser(db *gorm.DB, user *models.User) (*models.User, error) {
 	return user, nil
 }
 
+// ReadUser finds a single user based on their unique id
+func ReadUser(db *gorm.DB, id uint) (*models.User, error) {
+	user := &models.User{}
+	if err := db.Where("id = ?", id).First(&user).Error; err != nil {
+		return nil, err
+	}
+	return user, nil
+}
+
 // UpdateUser modifies an existing User in the database
 func UpdateUser(db *gorm.DB, user *models.User) (*models.User, error) {
 	if err := db.First(&models.User{}, user.ID).Updates(user).Error; err != nil {
@@ -21,3 +30,11 @@ func UpdateUser(db *gorm.DB, user *models.User) (*models.User, error) {
 
 	return user, nil
 }
+
+// DeleteUser deletes a single user using their unique id
+func DeleteUser(db *gorm.DB, user *models.User) (*models.User, error) {
+	if err := db.First(&models.User{}, user.ID).Delete(&user).Error; err != nil {
+		return nil, err
+	}
+	return user, nil
+}

+ 49 - 8
server/api/errors.go

@@ -6,11 +6,14 @@ import (
 	"net/http"
 
 	"github.com/go-playground/validator/v10"
+	"gorm.io/gorm"
 )
 
 const (
 	appErrDataWrite    = "data write error"
+	appErrDataRead     = "data read error"
 	appErrFormDecoding = "could not process JSON body"
+	appErrReadNotFound = "could not find requested object"
 )
 
 // HTTPError is the object returned when the API encounters an error: this
@@ -23,13 +26,6 @@ type HTTPError struct {
 // ErrorCode is a custom Porter error code, useful for frontend messages
 type ErrorCode int64
 
-// Enumeration of API error codes, represented as int64
-const (
-	ErrUserDecode ErrorCode = iota
-	ErrUserValidateFields
-	ErrUserDataWrite
-)
-
 // ------------------------ Error helper functions ------------------------ //
 
 // sendExternalError marshals an HTTPError into JSON: this function will return an error if
@@ -68,6 +64,7 @@ func (app *App) handleErrorFormDecoding(err error, code ErrorCode, w http.Respon
 	intErr := app.sendExternalError(err, errExt, w)
 
 	if intErr == nil {
+		app.logger.Warn().Err(err).Msg("")
 		w.WriteHeader(http.StatusUnprocessableEntity)
 	}
 }
@@ -93,11 +90,38 @@ func (app *App) handleErrorFormValidation(err error, code ErrorCode, w http.Resp
 	intErr := app.sendExternalError(err, errExt, w)
 
 	if intErr == nil {
+		app.logger.Warn().Err(err).Msg("")
 		w.WriteHeader(http.StatusUnprocessableEntity)
 	}
 }
 
-// handleErrorDataWrite handles a database write error
+// handleErrorRead handles an error in reading a record from the DB. If the record is
+// not found, the error message is more descriptive; otherwise, a generic dataRead
+// error is sent.
+func (app *App) handleErrorRead(err error, code ErrorCode, w http.ResponseWriter) {
+	// first check if the error is RecordNotFound -- send a more descriptive
+	// message if that is the case
+	if err == gorm.ErrRecordNotFound {
+		errExt := HTTPError{
+			Code:   code,
+			Errors: []string{appErrReadNotFound},
+		}
+
+		intErr := app.sendExternalError(err, errExt, w)
+
+		if intErr == nil {
+			app.logger.Warn().Err(err).Msg("")
+			w.WriteHeader(http.StatusNotFound)
+		}
+
+		return
+	}
+
+	app.handleErrorDataRead(err, code, w)
+}
+
+// handleErrorDataWrite handles a database write error due to either a connection
+// error with the database or failure to write that wasn't caught by the validators
 func (app *App) handleErrorDataWrite(err error, code ErrorCode, w http.ResponseWriter) {
 	errExt := HTTPError{
 		Code:   code,
@@ -107,10 +131,27 @@ func (app *App) handleErrorDataWrite(err error, code ErrorCode, w http.ResponseW
 	intErr := app.sendExternalError(err, errExt, w)
 
 	if intErr == nil {
+		app.logger.Warn().Err(err).Msg("")
 		w.WriteHeader(http.StatusUnprocessableEntity)
 	}
 }
 
+// handleErrorDataRead handles a database read error due to an internal error, such as
+// the database connection or gorm internals
+func (app *App) handleErrorDataRead(err error, code ErrorCode, w http.ResponseWriter) {
+	errExt := HTTPError{
+		Code:   code,
+		Errors: []string{appErrDataRead},
+	}
+
+	intErr := app.sendExternalError(err, errExt, w)
+
+	if intErr == nil {
+		app.logger.Warn().Err(err).Msg("")
+		w.WriteHeader(http.StatusInternalServerError)
+	}
+}
+
 // handleErrorInternalError is a catch-all for internal errors that occur during the
 // processing of a request
 func (app *App) handleErrorInternalError(err error, w http.ResponseWriter) {

+ 51 - 4
server/api/user_handler.go

@@ -12,6 +12,14 @@ import (
 	"gorm.io/gorm"
 )
 
+// Enumeration of user API error codes, represented as int64
+const (
+	ErrUserDecode ErrorCode = iota
+	ErrUserValidateFields
+	ErrUserDataWrite
+	ErrUserDataRead
+)
+
 // HandleCreateUser validates a user form entry, converts the user to a gorm
 // model, and saves the user to the database
 func (app *App) HandleCreateUser(w http.ResponseWriter, r *http.Request) {
@@ -25,10 +33,31 @@ func (app *App) HandleCreateUser(w http.ResponseWriter, r *http.Request) {
 	}
 }
 
-// HandleReadUser is majestic
+// HandleReadUser returns an externalized User (models.UserExternal)
+// based on an ID
 func (app *App) HandleReadUser(w http.ResponseWriter, r *http.Request) {
+	id, err := strconv.ParseUint(chi.URLParam(r, "id"), 0, 64)
+
+	if err != nil || id == 0 {
+		app.handleErrorFormDecoding(err, ErrUserDecode, w)
+		return
+	}
+
+	user, err := queries.ReadUser(app.db, uint(id))
+
+	if err != nil {
+		app.handleErrorRead(err, ErrUserDataRead, w)
+		return
+	}
+
+	extUser := user.Externalize()
+
+	if err := json.NewEncoder(w).Encode(extUser); err != nil {
+		app.handleErrorFormDecoding(err, ErrUserDecode, w)
+		return
+	}
+
 	w.WriteHeader(http.StatusOK)
-	w.Write([]byte("{}"))
 }
 
 // HandleUpdateUser validates an update user form entry, updates the user
@@ -42,7 +71,7 @@ func (app *App) HandleUpdateUser(w http.ResponseWriter, r *http.Request) {
 	}
 
 	form := &forms.UpdateUserForm{
-		ID: id,
+		ID: uint(id),
 	}
 
 	user, err := app.writeUser(form, queries.UpdateUser, w, r)
@@ -55,7 +84,25 @@ func (app *App) HandleUpdateUser(w http.ResponseWriter, r *http.Request) {
 
 // HandleDeleteUser is majestic
 func (app *App) HandleDeleteUser(w http.ResponseWriter, r *http.Request) {
-	w.WriteHeader(http.StatusAccepted)
+	id, err := strconv.ParseUint(chi.URLParam(r, "id"), 0, 64)
+
+	if err != nil || id == 0 {
+		app.handleErrorFormDecoding(err, ErrUserDecode, w)
+		return
+	}
+
+	// TODO -- HASH AND VERIFY PASSWORD BEFORE USER DELETION
+	form := &forms.DeleteUserForm{
+		ID:       uint(id),
+		Password: "testing",
+	}
+
+	user, err := app.writeUser(form, queries.DeleteUser, w, r)
+
+	if err == nil {
+		app.logger.Info().Msgf("User deleted: %d", user.ID)
+		w.WriteHeader(http.StatusAccepted)
+	}
 }
 
 // ------------------------ User handler helper functions ------------------------ //

+ 2 - 2
server/router/router.go

@@ -18,8 +18,8 @@ func New(a *api.App) *chi.Mux {
 		// /api/users routes
 		r.Method("POST", "/users", requestlog.NewHandler(a.HandleCreateUser, l))
 		r.Method("PUT", "/users/{id}", requestlog.NewHandler(a.HandleUpdateUser, l))
-
-		// r.Method("GET", "/users", requestlog.NewHandler(a.HandleReadUser, l))
+		r.Method("GET", "/users/{id}", requestlog.NewHandler(a.HandleReadUser, l))
+		r.Method("DELETE", "/users/{id}", requestlog.NewHandler(a.HandleDeleteUser, l))
 	})
 
 	return r