From 3487feed5cd73b5d5fb1e86b4f8653b1b947f944 Mon Sep 17 00:00:00 2001 From: James Tombleson Date: Sun, 31 Mar 2024 17:46:04 -0700 Subject: [PATCH 01/28] moved models into domain --- api/domain/dto.go | 4 ++++ api/domain/entities.go | 21 +++++++++++++++++++++ api/domain/models.go | 6 ++++++ api/domain/requests.go | 5 +++++ api/domain/responses.go | 12 ++++++++++++ api/domain/scopes.go | 8 ++++++++ api/models/recipe.go | 12 ------------ api/models/std.go | 6 ------ api/models/users.go | 15 --------------- 9 files changed, 56 insertions(+), 33 deletions(-) create mode 100644 api/domain/dto.go create mode 100644 api/domain/entities.go create mode 100644 api/domain/models.go create mode 100644 api/domain/requests.go create mode 100644 api/domain/responses.go create mode 100644 api/domain/scopes.go delete mode 100644 api/models/recipe.go delete mode 100644 api/models/std.go delete mode 100644 api/models/users.go diff --git a/api/domain/dto.go b/api/domain/dto.go new file mode 100644 index 0000000..2a8abb3 --- /dev/null +++ b/api/domain/dto.go @@ -0,0 +1,4 @@ +package domain + +type UserDto struct { +} diff --git a/api/domain/entities.go b/api/domain/entities.go new file mode 100644 index 0000000..f26454a --- /dev/null +++ b/api/domain/entities.go @@ -0,0 +1,21 @@ +package domain + +import "time" + +type UserEntity struct { + Id int + CreatedAt time.Time + LastUpdated time.Time + Name string + Hash string + Scopes string +} + +type RecipeEntity struct { + Id int32 + CreatedAt time.Time + LastUpdated time.Time + Title string + Thumbnail string + Content string +} diff --git a/api/domain/models.go b/api/domain/models.go new file mode 100644 index 0000000..a3028ef --- /dev/null +++ b/api/domain/models.go @@ -0,0 +1,6 @@ +package domain + +type EnvConfig struct { + AdminToken string + JwtSecret string +} \ No newline at end of file diff --git a/api/domain/requests.go b/api/domain/requests.go new file mode 100644 index 0000000..ac66daa --- /dev/null +++ b/api/domain/requests.go @@ -0,0 +1,5 @@ +package domain + +type HelloBodyRequest struct { + Name string `json:"name" validate:"required"` +} \ No newline at end of file diff --git a/api/domain/responses.go b/api/domain/responses.go new file mode 100644 index 0000000..b69a21d --- /dev/null +++ b/api/domain/responses.go @@ -0,0 +1,12 @@ +package domain + +type ErrorResponse struct { + HttpCode int `json:"code"` + Message string `json:"message"` +} + +type HelloWhoResponse struct { + Success bool `json:"success"` + Error string `json:"error"` + Message string `json:"message"` +} diff --git a/api/domain/scopes.go b/api/domain/scopes.go new file mode 100644 index 0000000..49e60b5 --- /dev/null +++ b/api/domain/scopes.go @@ -0,0 +1,8 @@ +package domain + +const ( + ScopeAll = "all" + ScopeRecipeRead = "recipe:read" + ScopeRecipeCreate = "recipe:create" + ScopeRecipeDelete = "recipe:delete" +) diff --git a/api/models/recipe.go b/api/models/recipe.go deleted file mode 100644 index 9f98366..0000000 --- a/api/models/recipe.go +++ /dev/null @@ -1,12 +0,0 @@ -package models - -import "time" - -type RecipeModel struct { - Id int32 - Title string - Thumbnail string - Content string - CreatedAt time.Time - LastUpdated time.Time -} diff --git a/api/models/std.go b/api/models/std.go deleted file mode 100644 index efd0955..0000000 --- a/api/models/std.go +++ /dev/null @@ -1,6 +0,0 @@ -package models - -type ErrorResponse struct { - HttpCode int `json:"code"` - Message string `json:"message"` -} diff --git a/api/models/users.go b/api/models/users.go deleted file mode 100644 index 14560ca..0000000 --- a/api/models/users.go +++ /dev/null @@ -1,15 +0,0 @@ -package models - -import "time" - -type UserModel struct { - Id int - Name string - Hash string - CreatedAt time.Time - LastUpdated time.Time -} - -type UserDto struct { - -} From 09235760d2d1306b380e88a4592ea8335be10e82 Mon Sep 17 00:00:00 2001 From: James Tombleson Date: Sun, 31 Mar 2024 17:46:20 -0700 Subject: [PATCH 02/28] Added a config service --- api/services/env.go | 21 +++++++++++++++++++++ go.mod | 1 + go.sum | 2 ++ 3 files changed, 24 insertions(+) create mode 100644 api/services/env.go diff --git a/api/services/env.go b/api/services/env.go new file mode 100644 index 0000000..4b39063 --- /dev/null +++ b/api/services/env.go @@ -0,0 +1,21 @@ +package services + +import ( + "go-cook/api/domain" + "log" + "os" + + "github.com/joho/godotenv" +) + +func NewEnvConfig() domain.EnvConfig { + err := godotenv.Load() + if err != nil { + log.Println(err) + } + + return domain.EnvConfig{ + AdminToken: os.Getenv("AdminToken"), + JwtSecret: os.Getenv("JwtSecret"), + } +} diff --git a/go.mod b/go.mod index adbb909..19e3cb0 100644 --- a/go.mod +++ b/go.mod @@ -25,6 +25,7 @@ require ( github.com/google/uuid v1.5.0 // indirect github.com/huandu/go-sqlbuilder v1.25.0 // indirect github.com/huandu/xstrings v1.3.2 // indirect + github.com/joho/godotenv v1.5.1 // indirect github.com/josharian/intern v1.0.0 // indirect github.com/labstack/echo-jwt/v4 v4.2.0 // indirect github.com/labstack/echo/v4 v4.11.4 // indirect diff --git a/go.sum b/go.sum index a04d568..e484048 100644 --- a/go.sum +++ b/go.sum @@ -43,6 +43,8 @@ github.com/huandu/go-sqlbuilder v1.25.0 h1:h1l+6CqeCviPJCnkEZoRGNdfZ5RO9BKMvG3A+ github.com/huandu/go-sqlbuilder v1.25.0/go.mod h1:nUVmMitjOmn/zacMLXT0d3Yd3RHoO2K+vy906JzqxMI= github.com/huandu/xstrings v1.3.2 h1:L18LIDzqlW6xN2rEkpdV8+oL/IXWJ1APd+vsdYy4Wdw= github.com/huandu/xstrings v1.3.2/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= +github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= +github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= github.com/kisielk/sqlstruct v0.0.0-20201105191214-5f3e10d3ab46/go.mod h1:yyMNCyc/Ib3bDTKd379tNMpB/7/H5TjM2Y9QJ5THLbE= From 6af8a2a0cda821eb733c6c43ca9e66be3a1c22aa Mon Sep 17 00:00:00 2001 From: James Tombleson Date: Sun, 31 Mar 2024 17:46:40 -0700 Subject: [PATCH 03/28] refactored for domain --- api/repositories/recipe.go | 26 ++++++++++----------- api/repositories/users.go | 47 +++++++++++++++++++++++++++----------- 2 files changed, 47 insertions(+), 26 deletions(-) diff --git a/api/repositories/recipe.go b/api/repositories/recipe.go index 6ee15b3..93addeb 100644 --- a/api/repositories/recipe.go +++ b/api/repositories/recipe.go @@ -3,15 +3,15 @@ package repositories import ( "database/sql" "errors" - "go-cook/api/models" + "go-cook/api/domain" ) type IRecipeTable interface { - Create(models.RecipeModel) error - List() ([]models.RecipeModel, error) - Get(id int) (models.RecipeModel, error) - Update(id int, entity models.RecipeModel) error - Delete(id int) error + Create(domain.RecipeEntity) error + List() ([]domain.RecipeEntity, error) + Get(id int) (domain.RecipeEntity, error) + Update(id int, entity domain.RecipeEntity) error + Delete(id int) error } type RecipeRepository struct { @@ -24,22 +24,22 @@ func NewRecipeRepository(client *sql.DB) RecipeRepository { } } -func (rr RecipeRepository) Create(models.RecipeModel) error { +func (rr RecipeRepository) Create(domain.RecipeEntity) error { return errors.New("not implemented") } -func (rr RecipeRepository) List() ([]models.RecipeModel, error) { - return []models.RecipeModel{}, errors.New("not implemented") +func (rr RecipeRepository) List() ([]domain.RecipeEntity, error) { + return []domain.RecipeEntity{}, errors.New("not implemented") } -func (rr RecipeRepository) Get(id int) (models.RecipeModel, error) { - return models.RecipeModel{}, errors.New("not implemented") +func (rr RecipeRepository) Get(id int) (domain.RecipeEntity, error) { + return domain.RecipeEntity{}, errors.New("not implemented") } -func (rr RecipeRepository) Update(id int, entity models.RecipeModel) error { +func (rr RecipeRepository) Update(id int, entity domain.RecipeEntity) error { return errors.New("not implemented") } func (rr RecipeRepository) Delete(id int) error { return errors.New("not implemented") -} \ No newline at end of file +} diff --git a/api/repositories/users.go b/api/repositories/users.go index 1ee2e91..05f9cf0 100644 --- a/api/repositories/users.go +++ b/api/repositories/users.go @@ -4,7 +4,7 @@ import ( "database/sql" "errors" "fmt" - "go-cook/api/models" + "go-cook/api/domain" "time" "github.com/huandu/go-sqlbuilder" @@ -12,16 +12,17 @@ import ( ) const ( - TableName string = "users" + TableName string = "users" ErrUserNotFound string = "requested user was not found" ) type IUserTable interface { - GetByName(name string) (models.UserModel, error) + GetByName(name string) (domain.UserEntity, error) Create(name, password string) (int64, error) - Update(id int, entity models.UserModel) error + Update(id int, entity domain.UserEntity) error UpdatePassword(name, password string) error CheckUserHash(name, password string) error + AddScope(name, scope string) error } // Creates a new instance of UserRepository with the bound sql @@ -35,7 +36,7 @@ type UserRepository struct { connection *sql.DB } -func (ur UserRepository) GetByName(name string) (models.UserModel, error) { +func (ur UserRepository) GetByName(name string) (domain.UserEntity, error) { builder := sqlbuilder.NewSelectBuilder() builder.Select("*").From("users").Where( builder.E("Name", name), @@ -44,12 +45,12 @@ func (ur UserRepository) GetByName(name string) (models.UserModel, error) { rows, err := ur.connection.Query(query, args...) if err != nil { - return models.UserModel{}, err + return domain.UserEntity{}, err } data := ur.processRows(rows) - if (len(data) == 0) { - return models.UserModel{}, errors.New(ErrUserNotFound) + if len(data) == 0 { + return domain.UserEntity{}, errors.New(ErrUserNotFound) } return data[0], nil @@ -77,7 +78,7 @@ func (ur UserRepository) Create(name, password string) (int64, error) { return 1, nil } -func (ur UserRepository) Update(id int, entity models.UserModel) error { +func (ur UserRepository) Update(id int, entity domain.UserEntity) error { return errors.New("not implemented") } @@ -109,8 +110,26 @@ func (ur UserRepository) CheckUserHash(name, password string) error { return nil } -func (ur UserRepository) processRows(rows *sql.Rows) []models.UserModel { - items := []models.UserModel{} +func (ur UserRepository) AddScope(name, scope string) error { + builder := sqlbuilder.NewUpdateBuilder() + builder.Update("users") + builder.Set ( + builder.Assign("Scopes", scope), + ) + builder.Where( + builder.Equal("Name", name), + ) + query, args := builder.Build() + + _, err := ur.connection.Exec(query, args...) + if err != nil { + return err + } + return nil +} + +func (ur UserRepository) processRows(rows *sql.Rows) []domain.UserEntity { + items := []domain.UserEntity{} for rows.Next() { var id int @@ -118,15 +137,17 @@ func (ur UserRepository) processRows(rows *sql.Rows) []models.UserModel { var hash string var createdAt time.Time var lastUpdated time.Time - err := rows.Scan(&id, &name, &hash, &createdAt, &lastUpdated) + var scopes string + err := rows.Scan(&id, &name, &hash, &createdAt, &lastUpdated, &scopes) if err != nil { fmt.Println(err) } - items = append(items, models.UserModel{ + items = append(items, domain.UserEntity{ Id: id, Name: name, Hash: hash, + Scopes: scopes, CreatedAt: createdAt, LastUpdated: lastUpdated, }) From 0d253270b793e922a85770ed471d54674fbf7c98 Mon Sep 17 00:00:00 2001 From: James Tombleson Date: Sun, 31 Mar 2024 17:47:09 -0700 Subject: [PATCH 04/28] added config service on startup --- main.go | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/main.go b/main.go index 0896dc4..411f3f4 100644 --- a/main.go +++ b/main.go @@ -3,6 +3,7 @@ package main import ( "database/sql" v1 "go-cook/api/handlers/v1" + "go-cook/api/services" "log" "net/http" @@ -21,6 +22,8 @@ func main() { log.Fatal(err) } + cfg := services.NewEnvConfig() + e := echo.New() e.Validator = &CustomValidator{ validator: validator.New(), @@ -30,7 +33,7 @@ func main() { e.Pre(middleware.Recover()) v1Group := e.Group("/api/v1") - v1Api := v1.NewHandler(db) + v1Api := v1.NewHandler(db, cfg) v1Api.Register(v1Group) e.Logger.Fatal(e.Start(":1323")) From e0253f04e76d967dd76254d8e4e7eb7e0dd14909 Mon Sep 17 00:00:00 2001 From: James Tombleson Date: Sun, 31 Mar 2024 17:47:24 -0700 Subject: [PATCH 05/28] Added Scopes to the user table --- api/migrations/20240329211828_user_scopes.sql | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 api/migrations/20240329211828_user_scopes.sql diff --git a/api/migrations/20240329211828_user_scopes.sql b/api/migrations/20240329211828_user_scopes.sql new file mode 100644 index 0000000..f75cfe9 --- /dev/null +++ b/api/migrations/20240329211828_user_scopes.sql @@ -0,0 +1,10 @@ +-- +goose Up +-- +goose StatementBegin +SELECT 'up SQL query'; +ALTER Table USERS ADD Scopes TEXT; +-- +goose StatementEnd +-- +goose Down +-- +goose StatementBegin +SELECT 'down SQL query'; +ALTER TABLE USERS DROP COLUMN Scopes; +-- +goose StatementEnd \ No newline at end of file From 7360d429ffd89e2d12b7e939cba73bc68593b21c Mon Sep 17 00:00:00 2001 From: James Tombleson Date: Sun, 31 Mar 2024 17:47:39 -0700 Subject: [PATCH 06/28] domain update --- api/services/userService.go | 16 +++++----------- 1 file changed, 5 insertions(+), 11 deletions(-) diff --git a/api/services/userService.go b/api/services/userService.go index 193c53d..79d95a6 100644 --- a/api/services/userService.go +++ b/api/services/userService.go @@ -3,7 +3,7 @@ package services import ( "database/sql" "errors" - "go-cook/api/models" + "go-cook/api/domain" "go-cook/api/repositories" "strings" @@ -36,12 +36,6 @@ func (us UserService) DoesUserExist(username string) error { } func (us UserService) DoesPasswordMatchHash(username, password string) error { - //passwordBytes := []byte(password) - //hash, err := bcrypt.GenerateFromPassword(passwordBytes, bcrypt.DefaultCost) - //if err != nil { - // return err - //} - model, err := us.GetUser(username) if err != nil { return err @@ -55,18 +49,18 @@ func (us UserService) DoesPasswordMatchHash(username, password string) error { return nil } -func (us UserService) GetUser(username string) (models.UserModel, error) { +func (us UserService) GetUser(username string) (domain.UserEntity, error) { return us.repo.GetByName(username) } -func (us UserService) CreateNewUser(name, password string) (models.UserModel, error) { +func (us UserService) CreateNewUser(name, password string) (domain.UserEntity, error) { err := us.CheckPasswordForRequirements(password) if err != nil { - return models.UserModel{}, err + return domain.UserEntity{}, err } us.repo.Create(name, password) - return models.UserModel{}, nil + return domain.UserEntity{}, nil } func (us UserService) CheckPasswordForRequirements(password string) error { From 538ff69c00c24002758fd67d8db06ff76e6323f3 Mon Sep 17 00:00:00 2001 From: James Tombleson Date: Sun, 31 Mar 2024 17:47:52 -0700 Subject: [PATCH 07/28] ignoring all .env files --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index ddc0925..46ef0fe 100644 --- a/.gitignore +++ b/.gitignore @@ -10,6 +10,7 @@ *.dylib go-cook *.db +*.env # Test binary, built with `go test -c` *.test From 0d90db89e554d0db3c17f985938f5e9fd1cd0395 Mon Sep 17 00:00:00 2001 From: James Tombleson Date: Sun, 31 Mar 2024 17:48:23 -0700 Subject: [PATCH 08/28] added config to the Handler and trying out a standard payload return for unauthorized --- api/handlers/v1/handler.go | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/api/handlers/v1/handler.go b/api/handlers/v1/handler.go index 9de5b13..6ca447c 100644 --- a/api/handlers/v1/handler.go +++ b/api/handlers/v1/handler.go @@ -2,8 +2,10 @@ package v1 import ( "database/sql" + "go-cook/api/domain" "go-cook/api/repositories" "go-cook/api/services" + "net/http" "github.com/golang-jwt/jwt/v5" echojwt "github.com/labstack/echo-jwt/v4" @@ -11,16 +13,19 @@ import ( ) type Handler struct { + Config domain.EnvConfig + UserService services.UserService userRepo repositories.IUserTable recipeRepo repositories.IRecipeTable } -func NewHandler(conn *sql.DB) *Handler { +func NewHandler(conn *sql.DB, cfg domain.EnvConfig) *Handler { return &Handler{ + Config: cfg, UserService: services.NewUserService(conn), - userRepo: repositories.NewUserRepository(conn), - recipeRepo: repositories.NewRecipeRepository(conn), + userRepo: repositories.NewUserRepository(conn), + recipeRepo: repositories.NewRecipeRepository(conn), } } @@ -29,7 +34,7 @@ func (h *Handler) Register(v1 *echo.Group) { NewClaimsFunc: func(c echo.Context) jwt.Claims { return new(JwtToken) }, - SigningKey: []byte("ThisIsABadSecretDontReallyUseThis"), + SigningKey: []byte(h.Config.JwtSecret), } v1.POST("/login", h.AuthLogin) @@ -53,3 +58,10 @@ func (h *Handler) Register(v1 *echo.Group) { //users.POST("/login", h.LoginUser) //users.POST("/update/password", h.UpdatePassword) } + +func (h *Handler) ReturnUnauthorizedResponse(c echo.Context, message string) error { + return c.JSON(http.StatusUnauthorized, domain.ErrorResponse{ + HttpCode: http.StatusUnauthorized, + Message: message, + }) +} From ec24600269dcdf9e43caa2576f728cf6c55f6ade Mon Sep 17 00:00:00 2001 From: James Tombleson Date: Sun, 31 Mar 2024 17:48:44 -0700 Subject: [PATCH 09/28] mostly pushing things into domain --- api/handlers/v1/demo.go | 32 ++++++++++++-------------------- 1 file changed, 12 insertions(+), 20 deletions(-) diff --git a/api/handlers/v1/demo.go b/api/handlers/v1/demo.go index 597cb6f..d8bfcdc 100644 --- a/api/handlers/v1/demo.go +++ b/api/handlers/v1/demo.go @@ -2,20 +2,14 @@ package v1 import ( "fmt" - "go-cook/api/models" + "go-cook/api/domain" "net/http" "github.com/labstack/echo/v4" ) -type HelloWhoResponse struct { - Success bool `json:"success"` - Error string `error:"error"` - Message string `json:"message"` -} - func (h *Handler) DemoHello(c echo.Context) error { - return c.JSON(http.StatusOK, HelloWhoResponse{ + return c.JSON(http.StatusOK, domain.HelloWhoResponse{ Success: true, Message: "Hello world!", }) @@ -23,27 +17,23 @@ func (h *Handler) DemoHello(c echo.Context) error { func (h *Handler) HelloWho(c echo.Context) error { name := c.Param("who") - return c.JSON(http.StatusOK, HelloWhoResponse{ + return c.JSON(http.StatusOK, domain.HelloWhoResponse{ Success: true, Message: fmt.Sprintf("Hello, %s", name), }) } -type HelloBodyRequest struct { - Name string `json:"name" validate:"required"` -} - func (h *Handler) HelloBody(c echo.Context) error { - request := HelloBodyRequest{} + request := domain.HelloBodyRequest{} err := (&echo.DefaultBinder{}).BindBody(c, &request) if err != nil { - return c.JSON(http.StatusBadRequest, HelloWhoResponse{ + return c.JSON(http.StatusBadRequest, domain.HelloWhoResponse{ Success: false, Error: err.Error(), }) } - return c.JSON(http.StatusOK, HelloWhoResponse{ + return c.JSON(http.StatusOK, domain.HelloWhoResponse{ Success: true, Message: fmt.Sprintf("Hello, %s", request.Name), }) @@ -52,10 +42,12 @@ func (h *Handler) HelloBody(c echo.Context) error { func (h *Handler) ProtectedRoute(c echo.Context) error { token, err := h.getJwtToken(c) if err != nil { - return c.JSON(http.StatusForbidden, models.ErrorResponse{ - HttpCode: http.StatusForbidden, - Message: err.Error(), - }) + h.ReturnUnauthorizedResponse(c, err.Error()) + } + + err = token.IsValid(domain.ScopeRecipeRead) + if err != nil { + h.ReturnUnauthorizedResponse(c, ErrJwtScopeMissing) } return c.JSON(http.StatusOK, token) From 8f10fbfba1d49ab6c7d994becc83e842950daa30 Mon Sep 17 00:00:00 2001 From: James Tombleson Date: Sun, 31 Mar 2024 17:49:09 -0700 Subject: [PATCH 10/28] mostly reworking how the JWT is processed --- api/handlers/v1/auth.go | 71 ++++++++++++++++++++++++++++++++--------- 1 file changed, 56 insertions(+), 15 deletions(-) diff --git a/api/handlers/v1/auth.go b/api/handlers/v1/auth.go index 86a7828..93bcc98 100644 --- a/api/handlers/v1/auth.go +++ b/api/handlers/v1/auth.go @@ -2,9 +2,10 @@ package v1 import ( "errors" - "go-cook/api/models" + "go-cook/api/domain" "go-cook/api/repositories" "net/http" + "strings" "time" "github.com/golang-jwt/jwt/v5" @@ -15,19 +16,57 @@ const ( ErrJwtMissing = "auth token is missing" ErrJwtClaimsMissing = "claims missing on token" ErrJwtExpired = "auth token has expired" + ErrJwtScopeMissing = "required scope is missing" ) type JwtToken struct { Exp time.Time `json:"exp"` + Iss string `json:"iss"` Authorized bool `json:"authorized"` UserName string `json:"username"` - Token string `json:"token"` + Scopes []string `json:"scopes"` jwt.RegisteredClaims } -func generateJwt(username string) (string, error) { - //TODO use env here - secret := []byte("ThisIsABadSecretDontReallyUseThis") +func (j JwtToken) IsValid(scope string) error { + err := j.hasExpired() + if err != nil { + return err + } + + err = j.hasScope(scope) + if err != nil { + return err + } + + return nil +} + +func (j JwtToken) hasExpired() error { + // Check to see if the token has expired + hasExpired := j.Exp.Compare(time.Now()) + if hasExpired == -1 { + return errors.New(ErrJwtExpired) + } + return nil +} + +func (j JwtToken) hasScope(scope string) error { + // they have the scope to access everything, so let them pass. + if strings.Contains(domain.ScopeAll, scope) { + return nil + } + + for _, s := range j.Scopes { + if strings.Contains(s, scope) { + return nil + } + } + return errors.New(ErrJwtScopeMissing) +} + +func (h *Handler) generateJwt(username string) (string, error) { + secret := []byte(h.Config.JwtSecret) token := jwt.New(jwt.SigningMethodHS256) claims := token.Claims.(jwt.MapClaims) @@ -35,6 +74,10 @@ func generateJwt(username string) (string, error) { claims["authorized"] = true claims["username"] = username + var scopes []string + scopes = append(scopes, domain.ScopeRecipeRead) + claims["scopes"] = scopes + tokenString, err := token.SignedString(secret) if err != nil { return "", err @@ -50,7 +93,7 @@ func (h *Handler) AuthRegister(c echo.Context) error { // if we have an err, validate that if its not user not found. // if the user is not found, we can use that name if err.Error() != repositories.ErrUserNotFound { - return c.JSON(http.StatusInternalServerError, models.ErrorResponse{ + return c.JSON(http.StatusInternalServerError, domain.ErrorResponse{ HttpCode: http.StatusInternalServerError, Message: err.Error(), }) @@ -60,7 +103,7 @@ func (h *Handler) AuthRegister(c echo.Context) error { password := c.QueryParam("password") err = h.UserService.CheckPasswordForRequirements(password) if err != nil { - return c.JSON(http.StatusInternalServerError, models.ErrorResponse{ + return c.JSON(http.StatusInternalServerError, domain.ErrorResponse{ HttpCode: http.StatusInternalServerError, Message: err.Error(), }) @@ -68,7 +111,7 @@ func (h *Handler) AuthRegister(c echo.Context) error { _, err = h.userRepo.Create(username, password) if err != nil { - return c.JSON(http.StatusInternalServerError, models.ErrorResponse{ + return c.JSON(http.StatusInternalServerError, domain.ErrorResponse{ HttpCode: http.StatusInternalServerError, Message: err.Error(), }) @@ -93,7 +136,7 @@ func (h *Handler) AuthLogin(c echo.Context) error { return c.JSON(http.StatusInternalServerError, err) } - token, err := generateJwt(username) + token, err := h.generateJwt(username) if err != nil { return c.JSON(http.StatusInternalServerError, err) } @@ -101,6 +144,10 @@ func (h *Handler) AuthLogin(c echo.Context) error { return c.JSON(http.StatusOK, token) } +func (h *Handler) AddScope(c echo.Context) error { + +} + func (h *Handler) RefreshJwtToken(c echo.Context) error { return nil } @@ -118,11 +165,5 @@ func (h *Handler) getJwtToken(c echo.Context) (JwtToken, error) { return JwtToken{}, errors.New(ErrJwtClaimsMissing) } - // Check to see if the token has expired - hasExpired := claims.Exp.Compare(time.Now()) - if hasExpired == -1 { - return JwtToken{}, errors.New(ErrJwtExpired) - } - return *claims, nil } From 615f1184ab8c4ca4fe57ad10c3e66f662c09e59a Mon Sep 17 00:00:00 2001 From: James Tombleson Date: Sun, 31 Mar 2024 18:05:33 -0700 Subject: [PATCH 11/28] if a user provides the env admin token, a token will generate with god permissions --- api/handlers/v1/auth.go | 45 +++++++++++++++++++++++++++++++++----- api/handlers/v1/handler.go | 7 ++++++ 2 files changed, 46 insertions(+), 6 deletions(-) diff --git a/api/handlers/v1/auth.go b/api/handlers/v1/auth.go index 93bcc98..d35d1bb 100644 --- a/api/handlers/v1/auth.go +++ b/api/handlers/v1/auth.go @@ -17,6 +17,7 @@ const ( ErrJwtClaimsMissing = "claims missing on token" ErrJwtExpired = "auth token has expired" ErrJwtScopeMissing = "required scope is missing" + ErrUserNotFound = "requested user does not exist" ) type JwtToken struct { @@ -86,6 +87,24 @@ func (h *Handler) generateJwt(username string) (string, error) { return tokenString, nil } +func (h *Handler) generateAdminJwt(username string) (string, error) { + secret := []byte(h.Config.JwtSecret) + + token := jwt.New(jwt.SigningMethodHS256) + claims := token.Claims.(jwt.MapClaims) + claims["exp"] = time.Now().Add(10 * time.Minute) + claims["authorized"] = true + claims["username"] = username + claims["scopes"] = domain.ScopeAll + + tokenString, err := token.SignedString(secret) + if err != nil { + return "", err + } + + return tokenString, nil +} + func (h *Handler) AuthRegister(c echo.Context) error { username := c.QueryParam("username") _, err := h.userRepo.GetByName(username) @@ -124,29 +143,43 @@ func (h *Handler) AuthLogin(c echo.Context) error { username := c.QueryParam("username") password := c.QueryParam("password") + // Check to see if they are trying to login with the admin token + if username == "" { + if h.Config.AdminToken != password { + return h.ReturnUnauthorizedResponse(c, ErrUserNotFound) + } + + token, err := h.generateAdminJwt("admin") + if err != nil { + return h.InternalServerErrorResponse(c, err.Error()) + } + + return c.JSON(http.StatusOK, token) + } + // check if the user exists err := h.UserService.DoesUserExist(username) if err != nil { - return c.JSON(http.StatusInternalServerError, err) + return h.InternalServerErrorResponse(c, err.Error()) } // make sure the hash matches err = h.UserService.DoesPasswordMatchHash(username, password) if err != nil { - return c.JSON(http.StatusInternalServerError, err) + return h.InternalServerErrorResponse(c, err.Error()) } token, err := h.generateJwt(username) if err != nil { - return c.JSON(http.StatusInternalServerError, err) + return h.InternalServerErrorResponse(c, err.Error()) } return c.JSON(http.StatusOK, token) } -func (h *Handler) AddScope(c echo.Context) error { - -} +//func (h *Handler) AddScope(c echo.Context) error { +// +//} func (h *Handler) RefreshJwtToken(c echo.Context) error { return nil diff --git a/api/handlers/v1/handler.go b/api/handlers/v1/handler.go index 6ca447c..5268b45 100644 --- a/api/handlers/v1/handler.go +++ b/api/handlers/v1/handler.go @@ -65,3 +65,10 @@ func (h *Handler) ReturnUnauthorizedResponse(c echo.Context, message string) err Message: message, }) } + +func (h *Handler) InternalServerErrorResponse(c echo.Context, message string) error { + return c.JSON(http.StatusServiceUnavailable, domain.ErrorResponse{ + HttpCode: http.StatusInternalServerError, + Message: message, + }) +} From 593ce11c6b9df8fbce4e0c98160e57332c04fb52 Mon Sep 17 00:00:00 2001 From: James Tombleson Date: Sun, 31 Mar 2024 18:09:24 -0700 Subject: [PATCH 12/28] moved jwt things to its own file --- api/handlers/v1/jwt.go | 95 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 95 insertions(+) create mode 100644 api/handlers/v1/jwt.go diff --git a/api/handlers/v1/jwt.go b/api/handlers/v1/jwt.go new file mode 100644 index 0000000..58d1bef --- /dev/null +++ b/api/handlers/v1/jwt.go @@ -0,0 +1,95 @@ +package v1 + +import ( + "errors" + "go-cook/api/domain" + "strings" + "time" + + "github.com/golang-jwt/jwt/v5" +) + +type JwtToken struct { + Exp time.Time `json:"exp"` + Iss string `json:"iss"` + Authorized bool `json:"authorized"` + UserName string `json:"username"` + Scopes []string `json:"scopes"` + jwt.RegisteredClaims +} + +func (j JwtToken) IsValid(scope string) error { + err := j.hasExpired() + if err != nil { + return err + } + + err = j.hasScope(scope) + if err != nil { + return err + } + + return nil +} + +func (j JwtToken) hasExpired() error { + // Check to see if the token has expired + hasExpired := j.Exp.Compare(time.Now()) + if hasExpired == -1 { + return errors.New(ErrJwtExpired) + } + return nil +} + +func (j JwtToken) hasScope(scope string) error { + // they have the scope to access everything, so let them pass. + if strings.Contains(domain.ScopeAll, scope) { + return nil + } + + for _, s := range j.Scopes { + if strings.Contains(s, scope) { + return nil + } + } + return errors.New(ErrJwtScopeMissing) +} + +func (h *Handler) generateJwt(username string) (string, error) { + secret := []byte(h.Config.JwtSecret) + + token := jwt.New(jwt.SigningMethodHS256) + claims := token.Claims.(jwt.MapClaims) + claims["exp"] = time.Now().Add(10 * time.Minute) + claims["authorized"] = true + claims["username"] = username + + var scopes []string + scopes = append(scopes, domain.ScopeRecipeRead) + claims["scopes"] = scopes + + tokenString, err := token.SignedString(secret) + if err != nil { + return "", err + } + + return tokenString, nil +} + +func (h *Handler) generateAdminJwt(username string) (string, error) { + secret := []byte(h.Config.JwtSecret) + + token := jwt.New(jwt.SigningMethodHS256) + claims := token.Claims.(jwt.MapClaims) + claims["exp"] = time.Now().Add(10 * time.Minute) + claims["authorized"] = true + claims["username"] = username + claims["scopes"] = domain.ScopeAll + + tokenString, err := token.SignedString(secret) + if err != nil { + return "", err + } + + return tokenString, nil +} From f591dadd3b71744eaec222bdcd71cf2107d05395 Mon Sep 17 00:00:00 2001 From: James Tombleson Date: Sun, 31 Mar 2024 18:10:53 -0700 Subject: [PATCH 13/28] adminToken validation now has its own method --- api/handlers/v1/auth.go | 111 +++++----------------------------------- 1 file changed, 14 insertions(+), 97 deletions(-) diff --git a/api/handlers/v1/auth.go b/api/handlers/v1/auth.go index d35d1bb..b3f9a07 100644 --- a/api/handlers/v1/auth.go +++ b/api/handlers/v1/auth.go @@ -5,8 +5,6 @@ import ( "go-cook/api/domain" "go-cook/api/repositories" "net/http" - "strings" - "time" "github.com/golang-jwt/jwt/v5" "github.com/labstack/echo/v4" @@ -20,91 +18,6 @@ const ( ErrUserNotFound = "requested user does not exist" ) -type JwtToken struct { - Exp time.Time `json:"exp"` - Iss string `json:"iss"` - Authorized bool `json:"authorized"` - UserName string `json:"username"` - Scopes []string `json:"scopes"` - jwt.RegisteredClaims -} - -func (j JwtToken) IsValid(scope string) error { - err := j.hasExpired() - if err != nil { - return err - } - - err = j.hasScope(scope) - if err != nil { - return err - } - - return nil -} - -func (j JwtToken) hasExpired() error { - // Check to see if the token has expired - hasExpired := j.Exp.Compare(time.Now()) - if hasExpired == -1 { - return errors.New(ErrJwtExpired) - } - return nil -} - -func (j JwtToken) hasScope(scope string) error { - // they have the scope to access everything, so let them pass. - if strings.Contains(domain.ScopeAll, scope) { - return nil - } - - for _, s := range j.Scopes { - if strings.Contains(s, scope) { - return nil - } - } - return errors.New(ErrJwtScopeMissing) -} - -func (h *Handler) generateJwt(username string) (string, error) { - secret := []byte(h.Config.JwtSecret) - - token := jwt.New(jwt.SigningMethodHS256) - claims := token.Claims.(jwt.MapClaims) - claims["exp"] = time.Now().Add(10 * time.Minute) - claims["authorized"] = true - claims["username"] = username - - var scopes []string - scopes = append(scopes, domain.ScopeRecipeRead) - claims["scopes"] = scopes - - tokenString, err := token.SignedString(secret) - if err != nil { - return "", err - } - - return tokenString, nil -} - -func (h *Handler) generateAdminJwt(username string) (string, error) { - secret := []byte(h.Config.JwtSecret) - - token := jwt.New(jwt.SigningMethodHS256) - claims := token.Claims.(jwt.MapClaims) - claims["exp"] = time.Now().Add(10 * time.Minute) - claims["authorized"] = true - claims["username"] = username - claims["scopes"] = domain.ScopeAll - - tokenString, err := token.SignedString(secret) - if err != nil { - return "", err - } - - return tokenString, nil -} - func (h *Handler) AuthRegister(c echo.Context) error { username := c.QueryParam("username") _, err := h.userRepo.GetByName(username) @@ -145,16 +58,7 @@ func (h *Handler) AuthLogin(c echo.Context) error { // Check to see if they are trying to login with the admin token if username == "" { - if h.Config.AdminToken != password { - return h.ReturnUnauthorizedResponse(c, ErrUserNotFound) - } - - token, err := h.generateAdminJwt("admin") - if err != nil { - return h.InternalServerErrorResponse(c, err.Error()) - } - - return c.JSON(http.StatusOK, token) + return h.validateAdminToken(c, password) } // check if the user exists @@ -177,6 +81,19 @@ func (h *Handler) AuthLogin(c echo.Context) error { return c.JSON(http.StatusOK, token) } +func (h *Handler) validateAdminToken(c echo.Context, password string) error { + if h.Config.AdminToken != password { + return h.ReturnUnauthorizedResponse(c, ErrUserNotFound) + } + + token, err := h.generateAdminJwt("admin") + if err != nil { + return h.InternalServerErrorResponse(c, err.Error()) + } + + return c.JSON(http.StatusOK, token) +} + //func (h *Handler) AddScope(c echo.Context) error { // //} From b3ee4e420becec6ce1dc622189c4d7c6d1f60fdb Mon Sep 17 00:00:00 2001 From: James Tombleson Date: Mon, 1 Apr 2024 17:48:38 -0700 Subject: [PATCH 14/28] found a bug that would let the same username get used over and over --- api/handlers/v1/auth.go | 37 +++++++++++++++++++++++++------------ 1 file changed, 25 insertions(+), 12 deletions(-) diff --git a/api/handlers/v1/auth.go b/api/handlers/v1/auth.go index b3f9a07..c4e2348 100644 --- a/api/handlers/v1/auth.go +++ b/api/handlers/v1/auth.go @@ -4,6 +4,7 @@ import ( "errors" "go-cook/api/domain" "go-cook/api/repositories" + "log" "net/http" "github.com/golang-jwt/jwt/v5" @@ -11,16 +12,20 @@ import ( ) const ( - ErrJwtMissing = "auth token is missing" - ErrJwtClaimsMissing = "claims missing on token" - ErrJwtExpired = "auth token has expired" - ErrJwtScopeMissing = "required scope is missing" - ErrUserNotFound = "requested user does not exist" + ErrJwtMissing = "auth token is missing" + ErrJwtClaimsMissing = "claims missing on token" + ErrJwtExpired = "auth token has expired" + ErrJwtScopeMissing = "required scope is missing" + ErrUserNotFound = "requested user does not exist" + ErrUsernameAlreadyExists = "the requested username already exists" ) func (h *Handler) AuthRegister(c echo.Context) error { - username := c.QueryParam("username") - _, err := h.userRepo.GetByName(username) + username := c.FormValue("username") + password := c.FormValue("password") + + //username := c.QueryParam("username") + exists, err := h.userRepo.GetByName(username) if err != nil { // if we have an err, validate that if its not user not found. // if the user is not found, we can use that name @@ -31,8 +36,11 @@ func (h *Handler) AuthRegister(c echo.Context) error { }) } } + if exists.Name == username { + return h.InternalServerErrorResponse(c, ErrUsernameAlreadyExists) + } - password := c.QueryParam("password") + //password := c.QueryParam("password") err = h.UserService.CheckPasswordForRequirements(password) if err != nil { return c.JSON(http.StatusInternalServerError, domain.ErrorResponse{ @@ -41,7 +49,7 @@ func (h *Handler) AuthRegister(c echo.Context) error { }) } - _, err = h.userRepo.Create(username, password) + _, err = h.userRepo.Create(username, password, domain.ScopeRecipeRead) if err != nil { return c.JSON(http.StatusInternalServerError, domain.ErrorResponse{ HttpCode: http.StatusInternalServerError, @@ -53,8 +61,13 @@ func (h *Handler) AuthRegister(c echo.Context) error { } func (h *Handler) AuthLogin(c echo.Context) error { - username := c.QueryParam("username") - password := c.QueryParam("password") + formValues, err := c.FormParams() + if err != nil { + h.InternalServerErrorResponse(c, err.Error()) + } + log.Println(formValues) + username := formValues.Get("name") + password := formValues.Get("password") // Check to see if they are trying to login with the admin token if username == "" { @@ -62,7 +75,7 @@ func (h *Handler) AuthLogin(c echo.Context) error { } // check if the user exists - err := h.UserService.DoesUserExist(username) + err = h.UserService.DoesUserExist(username) if err != nil { return h.InternalServerErrorResponse(c, err.Error()) } From c071212df5edbdf1122f1f451a4ef5e2fe352c07 Mon Sep 17 00:00:00 2001 From: James Tombleson Date: Mon, 1 Apr 2024 17:48:54 -0700 Subject: [PATCH 15/28] when a user is made, the default scope is now defined --- api/repositories/users.go | 4 ++-- api/repositories/users_test.go | 3 ++- api/services/userService.go | 4 ++-- 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/api/repositories/users.go b/api/repositories/users.go index 05f9cf0..a1e17a1 100644 --- a/api/repositories/users.go +++ b/api/repositories/users.go @@ -18,7 +18,7 @@ const ( type IUserTable interface { GetByName(name string) (domain.UserEntity, error) - Create(name, password string) (int64, error) + Create(name, password, scope string) (int64, error) Update(id int, entity domain.UserEntity) error UpdatePassword(name, password string) error CheckUserHash(name, password string) error @@ -56,7 +56,7 @@ func (ur UserRepository) GetByName(name string) (domain.UserEntity, error) { return data[0], nil } -func (ur UserRepository) Create(name, password string) (int64, error) { +func (ur UserRepository) Create(name, password, scope string) (int64, error) { passwordBytes := []byte(password) hash, err := bcrypt.GenerateFromPassword(passwordBytes, bcrypt.DefaultCost) if err != nil { diff --git a/api/repositories/users_test.go b/api/repositories/users_test.go index 120b53b..66b1dd3 100644 --- a/api/repositories/users_test.go +++ b/api/repositories/users_test.go @@ -2,6 +2,7 @@ package repositories_test import ( "database/sql" + "go-cook/api/domain" "go-cook/api/repositories" "log" "testing" @@ -20,7 +21,7 @@ func TestCanCreateNewUser(t *testing.T) { defer db.Close() repo := repositories.NewUserRepository(db) - updated, err := repo.Create("testing", "NotSecure") + updated, err := repo.Create("testing", "NotSecure", domain.ScopeRecipeRead) if err != nil { log.Println(err) t.FailNow() diff --git a/api/services/userService.go b/api/services/userService.go index 79d95a6..aca994a 100644 --- a/api/services/userService.go +++ b/api/services/userService.go @@ -53,13 +53,13 @@ func (us UserService) GetUser(username string) (domain.UserEntity, error) { return us.repo.GetByName(username) } -func (us UserService) CreateNewUser(name, password string) (domain.UserEntity, error) { +func (us UserService) CreateNewUser(name, password, scope string) (domain.UserEntity, error) { err := us.CheckPasswordForRequirements(password) if err != nil { return domain.UserEntity{}, err } - us.repo.Create(name, password) + us.repo.Create(name, password, domain.ScopeRecipeRead) return domain.UserEntity{}, nil } From 373f7da6789069654470e4f9bd99deb2875b5d72 Mon Sep 17 00:00:00 2001 From: James Tombleson Date: Mon, 1 Apr 2024 17:50:07 -0700 Subject: [PATCH 16/28] auth now uses UrlFormEncoded like it should --- api/handlers/v1/auth.go | 22 ++++++++-------------- 1 file changed, 8 insertions(+), 14 deletions(-) diff --git a/api/handlers/v1/auth.go b/api/handlers/v1/auth.go index c4e2348..4d7475e 100644 --- a/api/handlers/v1/auth.go +++ b/api/handlers/v1/auth.go @@ -4,7 +4,6 @@ import ( "errors" "go-cook/api/domain" "go-cook/api/repositories" - "log" "net/http" "github.com/golang-jwt/jwt/v5" @@ -12,11 +11,11 @@ import ( ) const ( - ErrJwtMissing = "auth token is missing" - ErrJwtClaimsMissing = "claims missing on token" - ErrJwtExpired = "auth token has expired" - ErrJwtScopeMissing = "required scope is missing" - ErrUserNotFound = "requested user does not exist" + ErrJwtMissing = "auth token is missing" + ErrJwtClaimsMissing = "claims missing on token" + ErrJwtExpired = "auth token has expired" + ErrJwtScopeMissing = "required scope is missing" + ErrUserNotFound = "requested user does not exist" ErrUsernameAlreadyExists = "the requested username already exists" ) @@ -61,13 +60,8 @@ func (h *Handler) AuthRegister(c echo.Context) error { } func (h *Handler) AuthLogin(c echo.Context) error { - formValues, err := c.FormParams() - if err != nil { - h.InternalServerErrorResponse(c, err.Error()) - } - log.Println(formValues) - username := formValues.Get("name") - password := formValues.Get("password") + username := c.FormValue("name") + password := c.FormValue("password") // Check to see if they are trying to login with the admin token if username == "" { @@ -75,7 +69,7 @@ func (h *Handler) AuthLogin(c echo.Context) error { } // check if the user exists - err = h.UserService.DoesUserExist(username) + err := h.UserService.DoesUserExist(username) if err != nil { return h.InternalServerErrorResponse(c, err.Error()) } From d9625e0b7d5a41d993481b75f12067619b71a97b Mon Sep 17 00:00:00 2001 From: James Tombleson Date: Tue, 2 Apr 2024 16:30:14 -0700 Subject: [PATCH 17/28] updated how the err response should look to follow a pattern --- api/domain/responses.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api/domain/responses.go b/api/domain/responses.go index b69a21d..6bc2658 100644 --- a/api/domain/responses.go +++ b/api/domain/responses.go @@ -1,7 +1,7 @@ package domain type ErrorResponse struct { - HttpCode int `json:"code"` + Success bool `json:"success"` Message string `json:"message"` } From 29f6dc0bb08b320cf69361bfa05643bf03ccaf0c Mon Sep 17 00:00:00 2001 From: James Tombleson Date: Wed, 3 Apr 2024 16:09:31 -0700 Subject: [PATCH 18/28] minor domain updates --- api/domain/dto.go | 3 +++ api/domain/requests.go | 7 ++++++- api/domain/responses.go | 4 ++-- 3 files changed, 11 insertions(+), 3 deletions(-) diff --git a/api/domain/dto.go b/api/domain/dto.go index 2a8abb3..c2f184f 100644 --- a/api/domain/dto.go +++ b/api/domain/dto.go @@ -1,4 +1,7 @@ package domain type UserDto struct { + Id int `json:"id"` + Name string `json:"name"` + Scopes string `json:"scopes"` } diff --git a/api/domain/requests.go b/api/domain/requests.go index ac66daa..66762da 100644 --- a/api/domain/requests.go +++ b/api/domain/requests.go @@ -2,4 +2,9 @@ package domain type HelloBodyRequest struct { Name string `json:"name" validate:"required"` -} \ No newline at end of file +} + +type AddScopeRequest struct { + Username string `json:"name"` + Scopes []string `json:"scopes" validate:"required"` +} diff --git a/api/domain/responses.go b/api/domain/responses.go index 6bc2658..445e05c 100644 --- a/api/domain/responses.go +++ b/api/domain/responses.go @@ -1,8 +1,8 @@ package domain type ErrorResponse struct { - Success bool `json:"success"` - Message string `json:"message"` + Success bool `json:"success"` + Message string `json:"message"` } type HelloWhoResponse struct { From f67ed03c9d285819ffd9fda6bff00d2df631e7bc Mon Sep 17 00:00:00 2001 From: James Tombleson Date: Wed, 3 Apr 2024 16:10:25 -0700 Subject: [PATCH 19/28] add and remove scopes methods to user service --- api/services/userService.go | 57 +++++++++++++++++++++++++++++++++++++ 1 file changed, 57 insertions(+) diff --git a/api/services/userService.go b/api/services/userService.go index aca994a..816cbff 100644 --- a/api/services/userService.go +++ b/api/services/userService.go @@ -53,6 +53,63 @@ func (us UserService) GetUser(username string) (domain.UserEntity, error) { return us.repo.GetByName(username) } +func (us UserService) AddScopes(username string, scopes []string) error { + usr, err := us.repo.GetByName(username) + if err != nil { + return err + } + + if usr.Name != username { + return errors.New(repositories.ErrUserNotFound) + } + + newScopes := strings.Split(usr.Scopes, ",") + + // check the current scopes + for _, item := range strings.Split(usr.Scopes, ",") { + if !us.doesScopeExist(scopes, item) { + newScopes = append(newScopes, item) + } + } + return us.repo.UpdateScopes(username, strings.Join(newScopes, ",")) +} + +func (us UserService) RemoveScopes(username string, scopes []string) error { + usr, err := us.repo.GetByName(username) + if err != nil { + return err + } + + if usr.Name != username { + return errors.New(repositories.ErrUserNotFound) + } + + var newScopes []string + + // check all the scopes that are currently assigned + for _, item := range strings.Split(usr.Scopes, ",") { + + // check the scopes given, if one matches skip it + if us.doesScopeExist(scopes, item) { + continue + } + + // did not match, add it + newScopes = append(newScopes, item) + } + + return us.repo.UpdateScopes(username, strings.Join(newScopes, ",")) +} + +func (us UserService) doesScopeExist(scopes []string, target string) bool { + for _, item := range scopes { + if item == target { + return true + } + } + return false +} + func (us UserService) CreateNewUser(name, password, scope string) (domain.UserEntity, error) { err := us.CheckPasswordForRequirements(password) if err != nil { From f159582d3495befd1515bf4398f9962b729558da Mon Sep 17 00:00:00 2001 From: James Tombleson Date: Wed, 3 Apr 2024 16:11:00 -0700 Subject: [PATCH 20/28] user repo now adds a default scope on create and scopes can be replaced for a user --- api/repositories/users.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/api/repositories/users.go b/api/repositories/users.go index a1e17a1..b46f8df 100644 --- a/api/repositories/users.go +++ b/api/repositories/users.go @@ -22,7 +22,7 @@ type IUserTable interface { Update(id int, entity domain.UserEntity) error UpdatePassword(name, password string) error CheckUserHash(name, password string) error - AddScope(name, scope string) error + UpdateScopes(name, scope string) error } // Creates a new instance of UserRepository with the bound sql @@ -66,8 +66,8 @@ func (ur UserRepository) Create(name, password, scope string) (int64, error) { dt := time.Now() queryBuilder := sqlbuilder.NewInsertBuilder() queryBuilder.InsertInto("users") - queryBuilder.Cols("Name", "Hash", "LastUpdated", "CreatedAt") - queryBuilder.Values(name, string(hash), dt, dt) + queryBuilder.Cols("Name", "Hash", "LastUpdated", "CreatedAt", "Scopes") + queryBuilder.Values(name, string(hash), dt, dt, scope) query, args := queryBuilder.Build() _, err = ur.connection.Exec(query, args...) @@ -110,7 +110,7 @@ func (ur UserRepository) CheckUserHash(name, password string) error { return nil } -func (ur UserRepository) AddScope(name, scope string) error { +func (ur UserRepository) UpdateScopes(name, scope string) error { builder := sqlbuilder.NewUpdateBuilder() builder.Update("users") builder.Set ( From 02c6f4aae7498121f59a67f62bb9e008b05ac050 Mon Sep 17 00:00:00 2001 From: James Tombleson Date: Wed, 3 Apr 2024 16:11:46 -0700 Subject: [PATCH 21/28] admin jwt now follows the same schema --- api/handlers/v1/jwt.go | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/api/handlers/v1/jwt.go b/api/handlers/v1/jwt.go index 58d1bef..88da586 100644 --- a/api/handlers/v1/jwt.go +++ b/api/handlers/v1/jwt.go @@ -84,7 +84,10 @@ func (h *Handler) generateAdminJwt(username string) (string, error) { claims["exp"] = time.Now().Add(10 * time.Minute) claims["authorized"] = true claims["username"] = username - claims["scopes"] = domain.ScopeAll + + var scopes []string + scopes = append(scopes, domain.ScopeAll) + claims["scopes"] = scopes tokenString, err := token.SignedString(secret) if err != nil { From 7dc072e849ad66dc509588c3474202da65b31048 Mon Sep 17 00:00:00 2001 From: James Tombleson Date: Wed, 3 Apr 2024 16:12:18 -0700 Subject: [PATCH 22/28] scopes/add now requires jwt and minor ez response methods --- api/handlers/v1/auth.go | 52 +++++++++++++++++++++++++++++++------- api/handlers/v1/handler.go | 16 +++++++----- 2 files changed, 53 insertions(+), 15 deletions(-) diff --git a/api/handlers/v1/auth.go b/api/handlers/v1/auth.go index 4d7475e..86ae1c1 100644 --- a/api/handlers/v1/auth.go +++ b/api/handlers/v1/auth.go @@ -30,8 +30,9 @@ func (h *Handler) AuthRegister(c echo.Context) error { // if the user is not found, we can use that name if err.Error() != repositories.ErrUserNotFound { return c.JSON(http.StatusInternalServerError, domain.ErrorResponse{ - HttpCode: http.StatusInternalServerError, - Message: err.Error(), + + Message: err.Error(), + Success: true, }) } } @@ -43,16 +44,16 @@ func (h *Handler) AuthRegister(c echo.Context) error { err = h.UserService.CheckPasswordForRequirements(password) if err != nil { return c.JSON(http.StatusInternalServerError, domain.ErrorResponse{ - HttpCode: http.StatusInternalServerError, - Message: err.Error(), + Success: false, + Message: err.Error(), }) } _, err = h.userRepo.Create(username, password, domain.ScopeRecipeRead) if err != nil { return c.JSON(http.StatusInternalServerError, domain.ErrorResponse{ - HttpCode: http.StatusInternalServerError, - Message: err.Error(), + Success: false, + Message: err.Error(), }) } @@ -101,9 +102,42 @@ func (h *Handler) validateAdminToken(c echo.Context, password string) error { return c.JSON(http.StatusOK, token) } -//func (h *Handler) AddScope(c echo.Context) error { -// -//} +func (h *Handler) AddScope(c echo.Context) error { + token, err := h.getJwtToken(c) + if err != nil { + return h.ReturnUnauthorizedResponse(c, err.Error()) + } + + err = token.IsValid(domain.ScopeAll) + if err != nil { + return h.ReturnUnauthorizedResponse(c, err.Error()) + } + + request := domain.AddScopeRequest{} + err = (&echo.DefaultBinder{}).BindBody(c, &request) + if err != nil { + return c.JSON(http.StatusBadRequest, domain.ErrorResponse{ + Success: false, + Message: err.Error(), + }) + } + + err = h.UserService.AddScopes(request.Username, request.Scopes) + if err != nil { + return h.InternalServerErrorResponse(c, err.Error()) + } + + return c.JSON(http.StatusOK, domain.ErrorResponse{ + Success: true, + }) +} + +func (h *Handler) RemoveScope(c echo.Context) error { + return c.JSON(http.StatusOK, domain.ErrorResponse{ + Success: false, + Message: "Not Implemented", + }) +} func (h *Handler) RefreshJwtToken(c echo.Context) error { return nil diff --git a/api/handlers/v1/handler.go b/api/handlers/v1/handler.go index 5268b45..6db17fe 100644 --- a/api/handlers/v1/handler.go +++ b/api/handlers/v1/handler.go @@ -37,10 +37,14 @@ func (h *Handler) Register(v1 *echo.Group) { SigningKey: []byte(h.Config.JwtSecret), } - v1.POST("/login", h.AuthLogin) - v1.POST("/register", h.AuthRegister) - demo := v1.Group("/demo") + auth := v1.Group("/auth") + auth.POST("/login", h.AuthLogin) + auth.POST("/register", h.AuthRegister) + auth.Use(echojwt.WithConfig(jwtConfig)) + auth.POST("/scopes/add", h.AddScope) + //auth.POST("/refresh", h.RefreshJwtToken) + demo := v1.Group("/demo") demo.GET("/hello", h.DemoHello) demo.GET("/hello/:who", h.HelloWho) @@ -61,14 +65,14 @@ func (h *Handler) Register(v1 *echo.Group) { func (h *Handler) ReturnUnauthorizedResponse(c echo.Context, message string) error { return c.JSON(http.StatusUnauthorized, domain.ErrorResponse{ - HttpCode: http.StatusUnauthorized, - Message: message, + Success: false, + Message: message, }) } func (h *Handler) InternalServerErrorResponse(c echo.Context, message string) error { return c.JSON(http.StatusServiceUnavailable, domain.ErrorResponse{ - HttpCode: http.StatusInternalServerError, + Success: false, Message: message, }) } From 2e0596c924abd850fa65cc96e8b63fb9cf36cb9d Mon Sep 17 00:00:00 2001 From: James Tombleson Date: Wed, 3 Apr 2024 16:12:34 -0700 Subject: [PATCH 23/28] rest test file has been updated --- rest.http | 40 +++++++++++++++++++++++++++++++++++++--- 1 file changed, 37 insertions(+), 3 deletions(-) diff --git a/rest.http b/rest.http index a5a5dc3..e3fb84c 100644 --- a/rest.http +++ b/rest.http @@ -1,7 +1,41 @@ +### Create a standard User +POST http://localhost:1323/api/v1/auth/register?username=test&password=test1234! +Content-Type: application/x-www-form-urlencoded + +name=test&password=test1234! +### Login with user +POST http://localhost:1323/api/v1/auth/login +Content-Type: application/x-www-form-urlencoded + +name=test&password=test1234! + +### Login with the admin token +POST http://localhost:1323/api/v1/auth/login +Content-Type: application/x-www-form-urlencoded + +password=lol + + +### Add Scope to test user +POST http://localhost:1323/api/v1/auth/scopes/add +Content-Type: application/json +Authorization: Bearer + +{ + "name": "test", + "scopes": [ + "recipe:create" + ] +} + +### Remove scope from test user +POST http://localhost:1323/api/v1/auth/scopes/remove +Content-Type: application/json +Authorization: Bearer + + ### -POST http://localhost:1323/api/v1/register?username=test&password=test1234! -### -POST http://localhost:1323/api/v1/login?username=test&password=test1234! +POST http://localhost:1323/api/v1/ ### GET http://localhost:1323/api/v1/demo/hello ### From 8a43c166a847ccec80b271c0d0c2b72221c91414 Mon Sep 17 00:00:00 2001 From: James Tombleson Date: Thu, 4 Apr 2024 15:29:39 -0700 Subject: [PATCH 24/28] Renamed struct to update a users scopes --- api/domain/requests.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api/domain/requests.go b/api/domain/requests.go index 66762da..83a0a78 100644 --- a/api/domain/requests.go +++ b/api/domain/requests.go @@ -4,7 +4,7 @@ type HelloBodyRequest struct { Name string `json:"name" validate:"required"` } -type AddScopeRequest struct { +type UpdateScopesRequest struct { Username string `json:"name"` Scopes []string `json:"scopes" validate:"required"` } From 9bc36bae7ffbe76b6a5c5be650f7d60443da27cb Mon Sep 17 00:00:00 2001 From: James Tombleson Date: Thu, 4 Apr 2024 15:30:22 -0700 Subject: [PATCH 25/28] if the admin token is null then it will fail an admin login. Also added the remove scopes logic and it worked for me --- api/handlers/v1/auth.go | 39 ++++++++++++++++++++++++++++++++++----- 1 file changed, 34 insertions(+), 5 deletions(-) diff --git a/api/handlers/v1/auth.go b/api/handlers/v1/auth.go index 86ae1c1..9d8355a 100644 --- a/api/handlers/v1/auth.go +++ b/api/handlers/v1/auth.go @@ -90,6 +90,12 @@ func (h *Handler) AuthLogin(c echo.Context) error { } func (h *Handler) validateAdminToken(c echo.Context, password string) error { + // if the admin token is blank, then the admin wanted this disabled. + // this will fail right away and not progress. + if h.Config.AdminToken == "" { + return h.InternalServerErrorResponse(c, ErrUserNotFound) + } + if h.Config.AdminToken != password { return h.ReturnUnauthorizedResponse(c, ErrUserNotFound) } @@ -102,7 +108,7 @@ func (h *Handler) validateAdminToken(c echo.Context, password string) error { return c.JSON(http.StatusOK, token) } -func (h *Handler) AddScope(c echo.Context) error { +func (h *Handler) AddScopes(c echo.Context) error { token, err := h.getJwtToken(c) if err != nil { return h.ReturnUnauthorizedResponse(c, err.Error()) @@ -113,7 +119,7 @@ func (h *Handler) AddScope(c echo.Context) error { return h.ReturnUnauthorizedResponse(c, err.Error()) } - request := domain.AddScopeRequest{} + request := domain.UpdateScopesRequest{} err = (&echo.DefaultBinder{}).BindBody(c, &request) if err != nil { return c.JSON(http.StatusBadRequest, domain.ErrorResponse{ @@ -132,10 +138,33 @@ func (h *Handler) AddScope(c echo.Context) error { }) } -func (h *Handler) RemoveScope(c echo.Context) error { +func (h *Handler) RemoveScopes(c echo.Context) error { + token, err := h.getJwtToken(c) + if err != nil { + return h.ReturnUnauthorizedResponse(c, err.Error()) + } + + err = token.IsValid(domain.ScopeAll) + if err != nil { + return h.ReturnUnauthorizedResponse(c, err.Error()) + } + + request := domain.UpdateScopesRequest{} + err = (&echo.DefaultBinder{}).BindBody(c, &request) + if err != nil { + return c.JSON(http.StatusBadRequest, domain.ErrorResponse{ + Success: false, + Message: err.Error(), + }) + } + + err = h.UserService.RemoveScopes(request.Username, request.Scopes) + if err != nil { + return h.InternalServerErrorResponse(c, err.Error()) + } + return c.JSON(http.StatusOK, domain.ErrorResponse{ - Success: false, - Message: "Not Implemented", + Success: true, }) } From 8f0e8e4d85aafc2269ba313f7d1f6b105d129a1b Mon Sep 17 00:00:00 2001 From: James Tombleson Date: Thu, 4 Apr 2024 15:30:38 -0700 Subject: [PATCH 26/28] added remove scopes to the handler --- api/handlers/v1/handler.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/api/handlers/v1/handler.go b/api/handlers/v1/handler.go index 6db17fe..8904920 100644 --- a/api/handlers/v1/handler.go +++ b/api/handlers/v1/handler.go @@ -41,9 +41,9 @@ func (h *Handler) Register(v1 *echo.Group) { auth.POST("/login", h.AuthLogin) auth.POST("/register", h.AuthRegister) auth.Use(echojwt.WithConfig(jwtConfig)) - auth.POST("/scopes/add", h.AddScope) - //auth.POST("/refresh", h.RefreshJwtToken) - + auth.POST("/scopes/add", h.AddScopes) + auth.POST("/scopes/remove", h.RemoveScopes) + demo := v1.Group("/demo") demo.GET("/hello", h.DemoHello) demo.GET("/hello/:who", h.HelloWho) From 69fb7a683b20e2641745fe49edd461f37902e4d4 Mon Sep 17 00:00:00 2001 From: James Tombleson Date: Thu, 4 Apr 2024 15:30:59 -0700 Subject: [PATCH 27/28] updated how it looks to see what scopes to add --- api/services/userService.go | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/api/services/userService.go b/api/services/userService.go index 816cbff..5c21787 100644 --- a/api/services/userService.go +++ b/api/services/userService.go @@ -63,15 +63,15 @@ func (us UserService) AddScopes(username string, scopes []string) error { return errors.New(repositories.ErrUserNotFound) } - newScopes := strings.Split(usr.Scopes, ",") + currentScopes := strings.Split(usr.Scopes, ",") // check the current scopes - for _, item := range strings.Split(usr.Scopes, ",") { - if !us.doesScopeExist(scopes, item) { - newScopes = append(newScopes, item) + for _, item := range scopes { + if !strings.Contains(usr.Scopes, item) { + currentScopes = append(currentScopes, item) } } - return us.repo.UpdateScopes(username, strings.Join(newScopes, ",")) + return us.repo.UpdateScopes(username, strings.Join(currentScopes, ",")) } func (us UserService) RemoveScopes(username string, scopes []string) error { From e0a517a7654e4a472a59f961e93aa3321ea1993e Mon Sep 17 00:00:00 2001 From: James Tombleson Date: Thu, 4 Apr 2024 15:31:23 -0700 Subject: [PATCH 28/28] Added the example on how to remove a scope from someone --- rest.http | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/rest.http b/rest.http index e3fb84c..ad8faed 100644 --- a/rest.http +++ b/rest.http @@ -1,5 +1,5 @@ ### Create a standard User -POST http://localhost:1323/api/v1/auth/register?username=test&password=test1234! +POST http://localhost:1323/api/v1/auth/register Content-Type: application/x-www-form-urlencoded name=test&password=test1234! @@ -9,6 +9,7 @@ Content-Type: application/x-www-form-urlencoded name=test&password=test1234! + ### Login with the admin token POST http://localhost:1323/api/v1/auth/login Content-Type: application/x-www-form-urlencoded @@ -33,6 +34,12 @@ POST http://localhost:1323/api/v1/auth/scopes/remove Content-Type: application/json Authorization: Bearer +{ + "name": "test", + "scopes": [ + "recipe:create" + ] +} ### POST http://localhost:1323/api/v1/