From 0619b497f7deabc830faeaa7e59108a2993b63b1 Mon Sep 17 00:00:00 2001 From: Nathan Coad Date: Thu, 4 Jan 2024 09:43:59 +1100 Subject: [PATCH] new add role feature --- README.md | 3 ++- controllers/auth.go | 50 ++++++++++++++++++++++++++++++++++++++-- main.go | 4 +++- models/role.go | 56 +++++++++++++++++++++++++++++++++++++++++---- test.env | 1 + 5 files changed, 105 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index 2182fb5..c28ef37 100644 --- a/README.md +++ b/README.md @@ -29,11 +29,12 @@ Written by Nathan Coad (nathan.coad@dell.com) | LOG_FILE | Specify the name/path of file to write log messages to | /var/log/smt.log | ./smt.log | BIND_IP | Specify the local IP address to bind to. | 127.0.0.1 | Primary IPv4 address | | BIND_PORT | Specify the TCP/IP port to bind to. | 443 | 8443 | +| LDAP_BIND_ADDRESS | If LDAP integration is needed, specify the LDAP Bind address | ldaps://dc.domain.com:636 | No default specified | | TLS_KEY_FILE | Specify the filename of the TLS certificate private key (must be unencrypted) in PEM format | key.pem | privkey.pem | | TLS_CERT_FILE | Specify the filename of the TLS certificate file in PEM format | cert.pem | cert.pem | | TOKEN_HOUR_LIFESPAN | Number of hours that the JWT token returned at login is valid | 12 | No default specified, must define this value | | API_SECRET | Secret to use when generating JWT token | 3c55990bd479322e2053db3a8 | No default specified, must define this value | -| INITIAL_PASSWORD | Password to set for builtin Administrator account created when first started, can remove this value after first start. Can specify in plaintext or bcrypt hash | $2a$10$s39a82wrRAdOJVZEkkrSReVnXprz5mxU30ZBO.dHPYTncQCsUD9ce | password +| INITIAL_PASSWORD | Password to set for builtin Administrator account created when first started, can remove this value after first start. Can specify in plaintext or bcrypt hash | $2a$10$s39a82wrRAdOJVZEkkrSReVnXprz5mxU30ZBO.dHPYTncQCsUD9ce | password | | SECRETS_KEY | Key to use for AES256 GCM encryption. Must be exactly 32 bytes | AES256Key-32Characters1234567890 | No default specified, must define this value or use /api/unlock at runtime | If the TLS certificate and key files cannot be located in the specified location, a self signed certificate will be generated with a 1 year validity period. diff --git a/controllers/auth.go b/controllers/auth.go index 9b3e27c..275d85e 100644 --- a/controllers/auth.go +++ b/controllers/auth.go @@ -2,6 +2,7 @@ package controllers import ( "errors" + "fmt" "html" "log" "net/http" @@ -29,6 +30,13 @@ type DeleteInput struct { UserName string `json:"userName" binding:"required"` } +type AddRoleInput struct { + RoleName string `json:"roleName" binding:"required"` + LdapGroup string `json:"ldapGroup"` + ReadOnly bool `json:"readOnly"` + Admin bool `json:"admin"` +} + func DeleteUser(c *gin.Context) { var input DeleteInput @@ -63,7 +71,7 @@ func DeleteUser(c *gin.Context) { } } -func Register(c *gin.Context) { +func RegisterUser(c *gin.Context) { var input RegisterInput if err := c.ShouldBindJSON(&input); err != nil { @@ -117,7 +125,45 @@ func Register(c *gin.Context) { return } - c.JSON(http.StatusOK, gin.H{"message": "registration success"}) + c.JSON(http.StatusOK, gin.H{"message": "user registration success"}) +} + +func AddRole(c *gin.Context) { + var input AddRoleInput + + if err := c.ShouldBindJSON(&input); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + // define the new role properties + r := models.Role{} + r.RoleName = input.RoleName + r.ReadOnly = input.ReadOnly + r.Admin = input.Admin + r.LdapGroup = input.LdapGroup + + // Check if role already exists + testRole, _ := models.GetRoleByName(r.RoleName) + log.Printf("AddRole checking if role '%s' already exists\n", r.RoleName) + + if (models.Role{} == testRole) { + log.Printf("AddRole confirmed no existing rolename\n") + } else { + errorString := fmt.Sprintf("attempt to register conflicting rolename '%s'", r.RoleName) + log.Printf("Register error : '%s'\n", errorString) + c.JSON(http.StatusBadRequest, gin.H{"error": errorString}) + return + } + + _, err := r.AddRole() + + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"Error creating role": err.Error()}) + return + } + + c.JSON(http.StatusOK, gin.H{"message": "role creation success"}) } func Login(c *gin.Context) { diff --git a/main.go b/main.go index 0f4ce26..2561236 100644 --- a/main.go +++ b/main.go @@ -248,8 +248,10 @@ func main() { adminOnly := router.Group("/api/admin") adminOnly.Use(middlewares.JwtAuthAdminMiddleware()) adminOnly.POST("/user/delete", controllers.DeleteUser) - adminOnly.POST("/user/register", controllers.Register) + adminOnly.POST("/user/register", controllers.RegisterUser) // TODO deprecate + adminOnly.POST("/user/add", controllers.RegisterUser) adminOnly.GET("/roles", controllers.GetRoles) + adminOnly.POST("/role/add", controllers.AddRole) adminOnly.GET("/users", controllers.GetUsers) // TODO Make unlock an admin only function diff --git a/models/role.go b/models/role.go index 6c4826a..36a7270 100644 --- a/models/role.go +++ b/models/role.go @@ -1,12 +1,16 @@ package models -import "log" +import ( + "errors" + "log" +) type Role struct { - RoleId int `db:"RoleId"` - RoleName string `db:"RoleName"` - ReadOnly bool `db:"ReadOnly"` - Admin bool `db:"Admin"` + RoleId int `db:"RoleId"` + RoleName string `db:"RoleName"` + LdapGroup string `db:"LdapGroup"` + ReadOnly bool `db:"ReadOnly"` + Admin bool `db:"Admin"` } func QueryRoles() ([]Role, error) { @@ -35,3 +39,45 @@ func QueryRoles() ([]Role, error) { return results, nil } + +// AddRole adds a new role definition to the database +func (r *Role) AddRole() (*Role, error) { + var err error + + // Validate role not already in use + _, err = GetRoleByName(r.RoleName) + + // TODO + + if err != nil && err.Error() == "role not found" { + log.Printf("AddRole confirmed no existing role, continuing with creation of role '%s'\n", r.RoleName) + + result, err := db.NamedExec(("INSERT INTO roles (RoleName, ReadOnly, Admin, LdapGroup) VALUES (:RoleName, :ReadOnly, :Admin, :LdapGroup);"), r) + + if err != nil { + log.Printf("AddRole error executing sql record : '%s'\n", err) + return &Role{}, err + } else { + affected, _ := result.RowsAffected() + id, _ := result.LastInsertId() + log.Printf("AddRole insert returned result id '%d' affecting %d row(s).\n", id, affected) + } + } else { + log.Printf("AddRole RoleName already exists : '%v'\n", err) + } + + return r, nil +} + +func GetRoleByName(rolename string) (Role, error) { + + var r Role + + // Query database for matching user object + err := db.QueryRowx("SELECT * FROM roles WHERE RoleName=?", rolename).StructScan(&r) + if err != nil { + return r, errors.New("role not found") + } + + return r, nil +} diff --git a/test.env b/test.env index b45fed1..b794f2b 100644 --- a/test.env +++ b/test.env @@ -4,5 +4,6 @@ INITIAL_PASSWORD=Password123 TOKEN_HOUR_LIFESPAN=168 BIND_IP= BIND_PORT=8443 +LDAP_BIND_ADDRESS= TLS_KEY_FILE=key.pem TLS_CERT_FILE=cert.pem \ No newline at end of file