From 7270d18955d948654a497c4fbaa8850ebb01d167 Mon Sep 17 00:00:00 2001 From: DevipriyaS17 Date: Thu, 5 Mar 2026 13:56:19 +0530 Subject: [PATCH] fix(redfish): fixed failures on redfish protocol validator --- redfish/component.go | 23 +++++- .../controller/http/v1/handler/errors.go | 74 ++++++++++++++++++- .../http/v1/handler/service_root.go | 11 ++- .../http/v1/handler/service_root_test.go | 4 +- .../controller/http/v1/handler/sessions.go | 2 +- .../http/v1/handler/sessions_test.go | 18 ++++- .../controller/http/v1/handler/systems.go | 2 + redfish/internal/usecase/sessions/usecase.go | 15 +--- 8 files changed, 123 insertions(+), 26 deletions(-) diff --git a/redfish/component.go b/redfish/component.go index 7dab92177..14cdf5753 100644 --- a/redfish/component.go +++ b/redfish/component.go @@ -169,10 +169,21 @@ func RegisterRoutes(router *gin.Engine, _ logger.Interface) error { return nil } - // Build middleware chain with OData header + // Build middleware chain with OData header validation and response middlewares := []redfishgenerated.MiddlewareFunc{ func(c *gin.Context) { - c.Header("OData-Version", "4.0") + // Validate OData-Version header if present in request + requestODataVersion := c.GetHeader("OData-Version") + if requestODataVersion != "" && requestODataVersion != v1.SupportedODataVersion { + c.Header("OData-Version", v1.SupportedODataVersion) + v1.PreconditionFailedError(c, "Unsupported OData-Version. Service supports OData-Version: "+v1.SupportedODataVersion) + c.Abort() + + return + } + + // Set OData-Version header in response + c.Header("OData-Version", v1.SupportedODataVersion) c.Next() }, } @@ -188,6 +199,14 @@ func RegisterRoutes(router *gin.Engine, _ logger.Interface) error { Middlewares: middlewares, }) + // Register /redfish endpoint manually (required by Redfish protocol) + // This endpoint must be publicly accessible without authentication + router.GET("/redfish", server.GetRedfish) + + // Per Redfish spec: POST to collection/Members is equivalent to POST to collection + // Add this route to support protocol validator requirements + router.POST("/redfish/v1/SessionService/Sessions/Members", server.PostRedfishV1SessionServiceSessions) + if componentConfig.AuthRequired { server.Logger.Info("Redfish API routes registered with authentication") } else { diff --git a/redfish/internal/controller/http/v1/handler/errors.go b/redfish/internal/controller/http/v1/handler/errors.go index db74ee5c6..2f4eddaf0 100644 --- a/redfish/internal/controller/http/v1/handler/errors.go +++ b/redfish/internal/controller/http/v1/handler/errors.go @@ -13,16 +13,26 @@ import ( const ( // HTTP header constants for Redfish responses - headerODataVersion = "OData-Version" - headerContentType = "Content-Type" - headerLocation = "Location" - headerRetryAfter = "Retry-After" + headerODataVersion = "OData-Version" + headerContentType = "Content-Type" + headerLocation = "Location" + headerRetryAfter = "Retry-After" + headerAllow = "Allow" + headerLink = "Link" + headerWWWAuthenticate = "WWW-Authenticate" // Header values contentTypeJSON = "application/json; charset=utf-8" contentTypeXML = "application/xml" odataVersion = "4.0" + // SupportedODataVersion is the OData version supported by this Redfish service (exported for use in middleware) + SupportedODataVersion = odataVersion + + // HTTP methods allowed for various endpoints + allowGetHead = "GET, HEAD" + allowGetHeadPatch = "GET, HEAD, PATCH" + // Common error messages msgInternalServerError = "An internal server error occurred." ) @@ -73,6 +83,10 @@ var errorConfigMap = map[string]ErrorConfig{ RegistryKey: "GeneralError", StatusCode: http.StatusBadRequest, }, + "PreconditionFailed": { + RegistryKey: "GeneralError", + StatusCode: http.StatusPreconditionFailed, + }, "Forbidden": { RegistryKey: "InsufficientPrivilege", StatusCode: http.StatusForbidden, @@ -192,6 +206,50 @@ func SetRedfishHeaders(c *gin.Context) { c.Header(headerContentType, contentTypeJSON) c.Header(headerODataVersion, odataVersion) c.Header("Cache-Control", "no-cache") + + // Add Link header with rel=describedby for schema references + uri := c.Request.URL.Path + if uri != "" && uri != "/redfish" { + // Link to the schema/metadata document for this resource + c.Header(headerLink, "; rel=describedby") + } + + allowedMethods := getAllowedMethodsForURI(uri) + if allowedMethods != "" { + c.Header(headerAllow, allowedMethods) + } +} + +// getAllowedMethodsForURI returns the allowed HTTP methods for a given URI +func getAllowedMethodsForURI(uri string) string { + // Most Redfish resources support GET + // Collection endpoints also support POST for creation + // Individual resources may support PATCH and DELETE + switch { + case uri == "/redfish/v1/" || uri == "/redfish/v1": + return allowGetHead + case uri == "/redfish/v1/$metadata": + return allowGetHead + case uri == "/redfish/v1/odata": + return allowGetHead + case uri == "/redfish/v1/SessionService": + return allowGetHeadPatch + case uri == "/redfish/v1/SessionService/Sessions": + return "GET, HEAD, POST" + case matchesPattern(uri, "/redfish/v1/SessionService/Sessions/"): + return "GET, HEAD, DELETE" + case uri == "/redfish/v1/Systems": + return allowGetHead + case matchesPattern(uri, "/redfish/v1/Systems/"): + return allowGetHeadPatch + default: + return allowGetHead + } +} + +// matchesPattern checks if a URI matches a given pattern (simple prefix match for now) +func matchesPattern(uri, pattern string) bool { + return len(uri) > len(pattern) && uri[:len(pattern)] == pattern } // createErrorResponse creates a Redfish error response using registry lookup. @@ -252,6 +310,8 @@ func MethodNotAllowedError(c *gin.Context) { // UnauthorizedError returns a Redfish-compliant 401 error func UnauthorizedError(c *gin.Context) { + // Add WWW-Authenticate header per Redfish spec for 401 responses + c.Header(headerWWWAuthenticate, `Basic realm="Redfish Service"`) sendRedfishError(c, "Unauthorized", "") } @@ -260,8 +320,14 @@ func BadRequestError(c *gin.Context, customMessage string) { sendRedfishError(c, "BadRequest", customMessage) } +// PreconditionFailedError returns a Redfish-compliant 412 error +func PreconditionFailedError(c *gin.Context, customMessage string) { + sendRedfishError(c, "PreconditionFailed", customMessage) +} + // ForbiddenError returns a Redfish-compliant 403 error for insufficient privileges func ForbiddenError(c *gin.Context) { + c.Header(headerWWWAuthenticate, `Basic realm="Redfish Service"`) sendRedfishError(c, "Forbidden", "") } diff --git a/redfish/internal/controller/http/v1/handler/service_root.go b/redfish/internal/controller/http/v1/handler/service_root.go index c0657f4c2..b4b8b6c82 100644 --- a/redfish/internal/controller/http/v1/handler/service_root.go +++ b/redfish/internal/controller/http/v1/handler/service_root.go @@ -274,6 +274,15 @@ func GetDefaultServices() []ODataService { } } +// GetRedfish handles GET /redfish +// Returns protocol version information. Must be publicly accessible without authentication. +func (s *RedfishServer) GetRedfish(c *gin.Context) { + // No Redfish headers for this endpoint - it's a simple JSON response + c.JSON(http.StatusOK, gin.H{ + "v1": "/redfish/v1/", + }) +} + // Path: GET /redfish/v1 // Spec: Redfish ServiceRoot.v1_19_0 // This is the entry point for the Redfish API, providing links to all available resources. @@ -350,7 +359,7 @@ func (s *RedfishServer) GetRedfishV1Odata(c *gin.Context) { } response := map[string]interface{}{ - "@odata.context": odataContextServiceRoot, + "@odata.context": "/redfish/v1/$metadata", "value": services, } c.JSON(http.StatusOK, response) diff --git a/redfish/internal/controller/http/v1/handler/service_root_test.go b/redfish/internal/controller/http/v1/handler/service_root_test.go index 0d1bf3ccc..eb104248f 100644 --- a/redfish/internal/controller/http/v1/handler/service_root_test.go +++ b/redfish/internal/controller/http/v1/handler/service_root_test.go @@ -529,8 +529,8 @@ func TestGetRedfishV1OdataResponseStructure(t *testing.T) { err := json.Unmarshal(w.Body.Bytes(), &response) require.NoError(t, err) - // Verify context - assert.Equal(t, "/redfish/v1/$metadata#ServiceRoot.ServiceRoot", response["@odata.context"]) + // Verify context - OData service document context is just the metadata URL per OData 4.0 spec + assert.Equal(t, "/redfish/v1/$metadata", response["@odata.context"]) // Verify value array exists valueArray, ok := response["value"].([]interface{}) diff --git a/redfish/internal/controller/http/v1/handler/sessions.go b/redfish/internal/controller/http/v1/handler/sessions.go index 11d9a6134..3e2844ed6 100644 --- a/redfish/internal/controller/http/v1/handler/sessions.go +++ b/redfish/internal/controller/http/v1/handler/sessions.go @@ -254,7 +254,7 @@ func (r *RedfishServer) PostRedfishV1SessionServiceSessions(c *gin.Context) { // Parse request body var request struct { UserName string `json:"UserName" binding:"required"` - Password string `json:"Password" binding:"required"` + Password string `json:"Password" binding:"required"` //nolint:gosec // False positive: This is a request field, not a hardcoded credential } if err := c.ShouldBindJSON(&request); err != nil { diff --git a/redfish/internal/controller/http/v1/handler/sessions_test.go b/redfish/internal/controller/http/v1/handler/sessions_test.go index 48a69656d..a55d8621b 100644 --- a/redfish/internal/controller/http/v1/handler/sessions_test.go +++ b/redfish/internal/controller/http/v1/handler/sessions_test.go @@ -283,7 +283,7 @@ func TestListSessions(t *testing.T) { assert.Equal(t, 1, len(members), "Should have 1 active session") assert.Equal(t, float64(1), resp["Members@odata.count"]) - // Try to create duplicate session - should fail with 409 + // Create another session for the same user - Per Redfish spec, multiple sessions are allowed body, _ = json.Marshal(createReq) req = httptest.NewRequest(http.MethodPost, "/redfish/v1/SessionService/Sessions", bytes.NewReader(body)) req.Header.Set("Content-Type", "application/json") @@ -291,7 +291,21 @@ func TestListSessions(t *testing.T) { w = httptest.NewRecorder() router.ServeHTTP(w, req) - assert.Equal(t, http.StatusConflict, w.Code, "Duplicate session should return 409 Conflict") + assert.Equal(t, http.StatusCreated, w.Code, "Multiple sessions per user should be allowed per Redfish spec") + + // List sessions again - should now have 2 sessions + req = httptest.NewRequest(http.MethodGet, "/redfish/v1/SessionService/Sessions", http.NoBody) + w = httptest.NewRecorder() + router.ServeHTTP(w, req) + + assert.Equal(t, http.StatusOK, w.Code) + err = json.Unmarshal(w.Body.Bytes(), &resp) + require.NoError(t, err) + + members, ok = resp["Members"].([]interface{}) + require.True(t, ok, "Members should be an array") + assert.Equal(t, 2, len(members), "Should have 2 active sessions") + assert.Equal(t, float64(2), resp["Members@odata.count"]) } // TestTokenCompatibility tests backward compatibility with Bearer tokens. diff --git a/redfish/internal/controller/http/v1/handler/systems.go b/redfish/internal/controller/http/v1/handler/systems.go index 3b36b520b..1d10b1806 100644 --- a/redfish/internal/controller/http/v1/handler/systems.go +++ b/redfish/internal/controller/http/v1/handler/systems.go @@ -109,6 +109,8 @@ func (s *RedfishServer) handleGetSystemError(c *gin.Context, err error, systemID // GetRedfishV1Systems handles GET requests for the systems collection func (s *RedfishServer) GetRedfishV1Systems(c *gin.Context) { + SetRedfishHeaders(c) + ctx := c.Request.Context() systemIDs, err := s.ComputerSystemUC.GetAll(ctx) diff --git a/redfish/internal/usecase/sessions/usecase.go b/redfish/internal/usecase/sessions/usecase.go index 3448b7776..c7465d7c3 100644 --- a/redfish/internal/usecase/sessions/usecase.go +++ b/redfish/internal/usecase/sessions/usecase.go @@ -35,26 +35,13 @@ func NewUseCase(repo Repository, cfg *config.Config) *UseCase { // CreateSession creates a new session with JWT token. // This integrates with DMT Console's existing JWT authentication. -// If a session already exists for this user, it returns an error to prevent multiple concurrent sessions. +// Per Redfish spec, multiple concurrent sessions per user are allowed. func (uc *UseCase) CreateSession(username, password, clientIP, userAgent string) (*entity.Session, string, error) { // Validate credentials using DMT Console's admin credentials if username != uc.config.AdminUsername || password != uc.config.AdminPassword { return nil, "", ErrInvalidCredentials } - // Check if an active session already exists for this user - existingSessions, err := uc.repo.List() - if err != nil { - return nil, "", fmt.Errorf("failed to check existing sessions: %w", err) - } - - for _, session := range existingSessions { - if session.Username == username && session.IsActive { - // Return error - cannot create duplicate session - return nil, "", ErrSessionAlreadyExists - } - } - // Generate unique session ID sessionID := uuid.New().String()