From 4d2ff4ae27cc88a2290e8368d7f2a1a3753d0c8e Mon Sep 17 00:00:00 2001 From: Miguel Cabrerizo Date: Sat, 13 Aug 2022 12:20:01 +0200 Subject: [PATCH] fix: closes #33 change plain user operation --- .vscode/launch.json | 2 +- README.md | 8 +++++ cmd/credentials.go | 20 +++++++++++- cmd/user_read.go | 13 ++++++++ server/api/handlers/user_update.go | 21 +++++++++++++ server/api/middleware/updater.go | 50 ++++++++++++++++++++++++++++++ server/api/server.go | 2 +- 7 files changed, 113 insertions(+), 3 deletions(-) create mode 100644 server/api/middleware/updater.go diff --git a/.vscode/launch.json b/.vscode/launch.json index f1aded0..715faf7 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -11,7 +11,7 @@ "mode": "auto", "program": "main.go", "args": [ - "server", "start", "--api-secret", "secret" + "user" ] } ] diff --git a/README.md b/README.md index 12c4a38..62ddcfe 100644 --- a/README.md +++ b/README.md @@ -78,6 +78,14 @@ $ glim logout $ glim server stop ``` +## Glim user types (roles) + +Glim has the following roles: + +- Manager. Can create, update and delete users and/or groups and set group memberships. +- Plain users. A plain user can get its user account information, update its name, email, ssh public key and password. A plain user can change its role and its memberships. +- Read-only. Can read users and groups information. + ## Secured communications by design Glim server will listen on 1323 TCP port (REST API) and on 1636 TCP (LDAPS) port and only TLS communications will be allowed in order to secure credentials and data exchange. diff --git a/cmd/credentials.go b/cmd/credentials.go index da3c45a..a2ecdbd 100644 --- a/cmd/credentials.go +++ b/cmd/credentials.go @@ -147,7 +147,6 @@ func AmIManager(token *types.Response) bool { claims := make(jwt.MapClaims) jwt.ParseWithClaims(token.AccessToken, claims, nil) - // Extract access token jti manager, ok := claims["manager"].(bool) if !ok { fmt.Printf("Could not parse access token. Please try to log in again\n") @@ -157,6 +156,25 @@ func AmIManager(token *types.Response) bool { return manager } +// AmIReadonly - TODO comment +func AmIReadonly(token *types.Response) bool { + claims := make(jwt.MapClaims) + jwt.ParseWithClaims(token.AccessToken, claims, nil) + + readonly, ok := claims["readonly"].(bool) + if !ok { + fmt.Printf("Could not parse access token. Please try to log in again\n") + os.Exit(1) + } + + return readonly +} + +// AmIManager - TODO comment +func AmIPlainUser(token *types.Response) bool { + return !AmIManager(token) && !AmIReadonly(token) +} + // WhichIsMyTokenUID - TODO comment func WhichIsMyTokenUID(token *types.Response) float64 { claims := make(jwt.MapClaims) diff --git a/cmd/user_read.go b/cmd/user_read.go index 359ce30..3e5ab54 100644 --- a/cmd/user_read.go +++ b/cmd/user_read.go @@ -192,6 +192,19 @@ func GetUserInfo() { getUser(uid) os.Exit(0) } + + // Check expiration + token := ReadCredentials() + if NeedsRefresh(token) { + Refresh(token.RefreshToken) + token = ReadCredentials() + } + if AmIPlainUser(token) { + uid = uint(WhichIsMyTokenUID(token)) + getUser(uid) + os.Exit(0) + } + getUsers() } diff --git a/server/api/handlers/user_update.go b/server/api/handlers/user_update.go index 27f65aa..476cd84 100644 --- a/server/api/handlers/user_update.go +++ b/server/api/handlers/user_update.go @@ -85,6 +85,12 @@ func (h *Handler) UpdateUser(c echo.Context) error { if !ok { return &echo.HTTPError{Code: http.StatusNotAcceptable, Message: "wrong token or missing info in token claims"} } + + manager, ok := claims["manager"].(bool) + if !ok { + return &echo.HTTPError{Code: http.StatusNotAcceptable, Message: "wrong token or missing info in token claims"} + } + if err := h.DB.Model(&models.User{}).Where("id = ?", uint(tokenUID)).First(&modifiedBy).Error; err != nil { return &echo.HTTPError{Code: http.StatusForbidden, Message: "wrong user attempting to update group"} } @@ -122,6 +128,9 @@ func (h *Handler) UpdateUser(c echo.Context) error { if err != nil { // Does username exist? if errors.Is(err, gorm.ErrRecordNotFound) { + if !manager { + return &echo.HTTPError{Code: http.StatusForbidden, Message: "only managers can update the username"} + } updatedUser["username"] = html.EscapeString(strings.TrimSpace(body.Username)) } } else { @@ -159,14 +168,23 @@ func (h *Handler) UpdateUser(c echo.Context) error { } if body.Manager != nil { + if !manager { + return &echo.HTTPError{Code: http.StatusForbidden, Message: "only managers can update manager status"} + } updatedUser["manager"] = *body.Manager } if body.Readonly != nil { + if !manager { + return &echo.HTTPError{Code: http.StatusForbidden, Message: "only managers can update readonly status"} + } updatedUser["readonly"] = *body.Readonly } if body.Locked != nil { + if !manager { + return &echo.HTTPError{Code: http.StatusForbidden, Message: "only managers can update locked status"} + } updatedUser["locked"] = *body.Locked } @@ -196,6 +214,9 @@ func (h *Handler) UpdateUser(c echo.Context) error { // Update group members if body.MemberOf != "" { + if !manager { + return &echo.HTTPError{Code: http.StatusForbidden, Message: "only managers can update group memberships"} + } members := strings.Split(body.MemberOf, ",") if body.ReplaceMembersOf { diff --git a/server/api/middleware/updater.go b/server/api/middleware/updater.go new file mode 100644 index 0000000..84aec0c --- /dev/null +++ b/server/api/middleware/updater.go @@ -0,0 +1,50 @@ +/* +Copyright © 2022 Miguel Ángel Álvarez Cabrerizo + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package middleware + +import ( + "fmt" + "net/http" + + "github.com/golang-jwt/jwt" + "github.com/labstack/echo/v4" +) + +//IsReader - TODO comment +func IsUpdater(next echo.HandlerFunc) echo.HandlerFunc { + return func(c echo.Context) error { + uid := c.Param("uid") + user := c.Get("user").(*jwt.Token) + claims := user.Claims.(jwt.MapClaims) + jwtID, ok := claims["uid"].(float64) + if !ok { + return &echo.HTTPError{Code: http.StatusNotAcceptable, Message: "wrong token or missing info in token claims"} + } + jwtReadonly, ok := claims["readonly"].(bool) + if !ok { + return &echo.HTTPError{Code: http.StatusNotAcceptable, Message: "wrong token or missing info in token claims"} + } + jwtManager, ok := claims["manager"].(bool) + if !ok { + return &echo.HTTPError{Code: http.StatusNotAcceptable, Message: "wrong token or missing info in token claims"} + } + if !jwtReadonly && (jwtManager || uid == fmt.Sprintf("%d", uint(jwtID))) { + return next(c) + } + return &echo.HTTPError{Code: http.StatusForbidden, Message: "user has no proper permissions"} + } +} diff --git a/server/api/server.go b/server/api/server.go index 6e7fc71..972fd5a 100644 --- a/server/api/server.go +++ b/server/api/server.go @@ -94,7 +94,7 @@ func Server(wg *sync.WaitGroup, shutdownChannel chan bool, settings types.APISet u.POST("", h.SaveUser, glimMiddleware.IsBlacklisted(blacklist), glimMiddleware.IsManager) u.GET("/:uid", h.FindUserByID, glimMiddleware.IsBlacklisted(blacklist), glimMiddleware.IsReader) u.GET("/:username/uid", h.FindUIDFromUsername, glimMiddleware.IsBlacklisted(blacklist), glimMiddleware.IsReader) - u.PUT("/:uid", h.UpdateUser, glimMiddleware.IsBlacklisted(blacklist), glimMiddleware.IsManager) + u.PUT("/:uid", h.UpdateUser, glimMiddleware.IsBlacklisted(blacklist), glimMiddleware.IsUpdater) u.DELETE("/:uid", h.DeleteUser, glimMiddleware.IsBlacklisted(blacklist), glimMiddleware.IsManager) u.POST("/:uid/passwd", h.Passwd, glimMiddleware.IsBlacklisted(blacklist))