diff --git a/internal/dataprovider/node.go b/internal/dataprovider/node.go index 8e40c480..eb08b103 100644 --- a/internal/dataprovider/node.go +++ b/internal/dataprovider/node.go @@ -132,17 +132,17 @@ func (n *Node) validate() error { return n.Data.validate() } -func (n *Node) authenticate(token string) (string, string, error) { +func (n *Node) authenticate(token string) (string, string, []string, error) { if err := n.Data.Key.TryDecrypt(); err != nil { providerLog(logger.LevelError, "unable to decrypt node key: %v", err) - return "", "", err + return "", "", nil, err } if token == "" { - return "", "", ErrInvalidCredentials + return "", "", nil, ErrInvalidCredentials } t, err := jwt.Parse([]byte(token), jwt.WithKey(jwa.HS256, []byte(n.Data.Key.GetPayload())), jwt.WithValidate(true)) if err != nil { - return "", "", fmt.Errorf("unable to parse and validate token: %v", err) + return "", "", nil, fmt.Errorf("unable to parse and validate token: %v", err) } var adminUsername, role string if admin, ok := t.Get("admin"); ok { @@ -151,14 +151,16 @@ func (n *Node) authenticate(token string) (string, string, error) { } } if adminUsername == "" { - return "", "", errors.New("no admin username associated with node token") + return "", "", nil, errors.New("no admin username associated with node token") } if r, ok := t.Get("role"); ok { if val, ok := r.(string); ok && val != "" { role = val } } - return adminUsername, role, nil + perms := getPermsFromToken(t) + + return adminUsername, role, perms, nil } // getBaseURL returns the base URL for this node @@ -175,7 +177,7 @@ func (n *Node) getBaseURL() string { } // generateAuthToken generates a new auth token -func (n *Node) generateAuthToken(username, role string) (string, error) { +func (n *Node) generateAuthToken(username, role string, permissions []string) (string, error) { if err := n.Data.Key.TryDecrypt(); err != nil { return "", fmt.Errorf("unable to decrypt node key: %w", err) } @@ -184,6 +186,7 @@ func (n *Node) generateAuthToken(username, role string) (string, error) { t := jwt.New() t.Set("admin", username) //nolint:errcheck t.Set("role", role) //nolint:errcheck + t.Set("perms", permissions) //nolint:errcheck t.Set(jwt.IssuedAtKey, now) //nolint:errcheck t.Set(jwt.JwtIDKey, xid.New().String()) //nolint:errcheck t.Set(jwt.NotBeforeKey, now.Add(-30*time.Second)) //nolint:errcheck @@ -197,14 +200,14 @@ func (n *Node) generateAuthToken(username, role string) (string, error) { } func (n *Node) prepareRequest(ctx context.Context, username, role, relativeURL, method string, - body io.Reader, + permissions []string, body io.Reader, ) (*http.Request, error) { url := fmt.Sprintf("%s%s", n.getBaseURL(), relativeURL) req, err := http.NewRequestWithContext(ctx, method, url, body) if err != nil { return nil, err } - token, err := n.generateAuthToken(username, role) + token, err := n.generateAuthToken(username, role, permissions) if err != nil { return nil, err } @@ -214,11 +217,11 @@ func (n *Node) prepareRequest(ctx context.Context, username, role, relativeURL, // SendGetRequest sends an HTTP GET request to this node. // The responseHolder must be a pointer -func (n *Node) SendGetRequest(username, role, relativeURL string, responseHolder any) error { +func (n *Node) SendGetRequest(username, role, relativeURL string, permissions []string, responseHolder any) error { ctx, cancel := context.WithTimeout(context.Background(), nodeReqTimeout) defer cancel() - req, err := n.prepareRequest(ctx, username, role, relativeURL, http.MethodGet, nil) + req, err := n.prepareRequest(ctx, username, role, relativeURL, http.MethodGet, permissions, nil) if err != nil { return err } @@ -246,11 +249,11 @@ func (n *Node) SendGetRequest(username, role, relativeURL string, responseHolder } // SendDeleteRequest sends an HTTP DELETE request to this node -func (n *Node) SendDeleteRequest(username, role, relativeURL string) error { +func (n *Node) SendDeleteRequest(username, role, relativeURL string, permissions []string) error { ctx, cancel := context.WithTimeout(context.Background(), nodeReqTimeout) defer cancel() - req, err := n.prepareRequest(ctx, username, role, relativeURL, http.MethodDelete, nil) + req, err := n.prepareRequest(ctx, username, role, relativeURL, http.MethodDelete, permissions, nil) if err != nil { return err } @@ -270,9 +273,9 @@ func (n *Node) SendDeleteRequest(username, role, relativeURL string) error { } // AuthenticateNodeToken check the validity of the provided token -func AuthenticateNodeToken(token string) (string, string, error) { +func AuthenticateNodeToken(token string) (string, string, []string, error) { if currentNode == nil { - return "", "", errNoClusterNodes + return "", "", nil, errNoClusterNodes } return currentNode.authenticate(token) } @@ -284,3 +287,21 @@ func GetNodeName() string { } return currentNode.Name } + +func getPermsFromToken(t jwt.Token) []string { + var perms []string + if p, ok := t.Get("perms"); ok { + switch v := p.(type) { + case []any: + for _, elem := range v { + switch elemValue := elem.(type) { + case string: + perms = append(perms, elemValue) + } + } + case []string: + perms = v + } + } + return perms +} diff --git a/internal/httpd/api_user.go b/internal/httpd/api_user.go index 749cefe2..d90c7e17 100644 --- a/internal/httpd/api_user.go +++ b/internal/httpd/api_user.go @@ -264,7 +264,9 @@ func disconnectUser(username, admin, role string) { logger.Warn(logSender, "", "unable to disconnect user %q, error getting node %q: %v", username, stat.Node, err) continue } - if err := n.SendDeleteRequest(admin, role, fmt.Sprintf("%s/%s", activeConnectionsPath, stat.ConnectionID)); err != nil { + perms := []string{dataprovider.PermAdminCloseConnections} + uri := fmt.Sprintf("%s/%s", activeConnectionsPath, stat.ConnectionID) + if err := n.SendDeleteRequest(admin, role, uri, perms); err != nil { logger.Warn(logSender, "", "unable to disconnect user %q from node %q, error: %v", username, n.Name, err) } } diff --git a/internal/httpd/api_utils.go b/internal/httpd/api_utils.go index 7fc7567a..5d7cf3e0 100644 --- a/internal/httpd/api_utils.go +++ b/internal/httpd/api_utils.go @@ -217,7 +217,9 @@ func handleCloseConnection(w http.ResponseWriter, r *http.Request) { sendAPIResponse(w, r, nil, http.StatusText(status), status) return } - if err := n.SendDeleteRequest(claims.Username, claims.Role, fmt.Sprintf("%s/%s", activeConnectionsPath, connectionID)); err != nil { + perms := []string{dataprovider.PermAdminCloseConnections} + uri := fmt.Sprintf("%s/%s", activeConnectionsPath, connectionID) + if err := n.SendDeleteRequest(claims.Username, claims.Role, uri, perms); err != nil { logger.Warn(logSender, "", "unable to delete connection id %q from node %q: %v", connectionID, n.Name, err) sendAPIResponse(w, r, nil, "Not Found", http.StatusNotFound) return @@ -243,7 +245,8 @@ func getNodesConnections(admin, role string) []common.ConnectionStatus { defer wg.Done() var stats []common.ConnectionStatus - if err := node.SendGetRequest(admin, role, activeConnectionsPath, &stats); err != nil { + perms := []string{dataprovider.PermAdminViewConnections} + if err := node.SendGetRequest(admin, role, activeConnectionsPath, perms, &stats); err != nil { logger.Warn(logSender, "", "unable to get connections from node %s: %v", node.Name, err) return } diff --git a/internal/httpd/middleware.go b/internal/httpd/middleware.go index 89dd6b68..9e5f2d70 100644 --- a/internal/httpd/middleware.go +++ b/internal/httpd/middleware.go @@ -22,6 +22,7 @@ import ( "net/url" "slices" "strings" + "time" "github.com/go-chi/jwtauth/v5" "github.com/rs/xid" @@ -369,15 +370,22 @@ func checkNodeToken(tokenAuth *jwtauth.JWTAuth) func(next http.Handler) http.Han if len(token) > 7 && strings.ToUpper(token[0:6]) == "BEARER" { token = token[7:] } - admin, role, err := dataprovider.AuthenticateNodeToken(token) + if invalidatedJWTTokens.Get(token) { + logger.Debug(logSender, "", "the node token has been invalidated") + sendAPIResponse(w, r, fmt.Errorf("the provided token is not valid"), "", http.StatusUnauthorized) + return + } + admin, role, perms, err := dataprovider.AuthenticateNodeToken(token) if err != nil { logger.Debug(logSender, "", "unable to authenticate node token %q: %v", token, err) sendAPIResponse(w, r, fmt.Errorf("the provided token cannot be authenticated"), "", http.StatusUnauthorized) return } + defer invalidatedJWTTokens.Add(token, time.Now().Add(2*time.Minute).UTC()) + c := jwtTokenClaims{ Username: admin, - Permissions: []string{dataprovider.PermAdminViewConnections, dataprovider.PermAdminCloseConnections}, + Permissions: perms, NodeID: dataprovider.GetNodeName(), Role: role, }