diff --git a/api/go.mod b/api/go.mod index d193fd5cd..2cfd73dcc 100644 --- a/api/go.mod +++ b/api/go.mod @@ -26,7 +26,7 @@ require ( github.com/ghodss/yaml v1.0.0 github.com/itchyny/gojq v0.12.16 github.com/kr/pretty v0.3.1 - github.com/margo/sandbox v1.0.0-rc.2.0.20260914132812-996e3c369a28 + github.com/margo/sandbox v1.0.0-rc.2.0.20260917102142-7325356d9a0c github.com/princjef/mageutil v1.0.0 go.opentelemetry.io/otel/trace v1.44.0 golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 diff --git a/api/go.sum b/api/go.sum index 12a731e90..19d171627 100644 --- a/api/go.sum +++ b/api/go.sum @@ -297,6 +297,12 @@ github.com/magefile/mage v1.15.0 h1:BvGheCMAsG3bWUDbZ8AyXXpCNwU9u5CB6sM+HNb9HYg= github.com/magefile/mage v1.15.0/go.mod h1:z5UZb/iS3GoOSn0JgWuiw7dxlurVYTu+/jHXqQg881A= github.com/margo/sandbox v1.0.0-rc.2.0.20260914132812-996e3c369a28 h1:gBUseGX0Ob0vtzBPEKRGzraNAnv1wiPonR5nGfA7QJ8= github.com/margo/sandbox v1.0.0-rc.2.0.20260914132812-996e3c369a28/go.mod h1:GUXYfxV7olA9UNBHFXbcCncQphxLmbNTmncEva1XkTw= +github.com/margo/sandbox v1.0.0-rc.2.0.20260916085946-ec8c5fb42b0f h1:knEF6gDZulXTCmJYAvNAnwI31lUjSV8jd690tKL3qwA= +github.com/margo/sandbox v1.0.0-rc.2.0.20260916085946-ec8c5fb42b0f/go.mod h1:exvgewlHdcAP2CB6icHolWChDkpW0JLYBsier1AD3S8= +github.com/margo/sandbox v1.0.0-rc.2.0.20260916141039-c41545a08e1e h1:/O7eH/b+b6+Zp96YmcTbwioe3r4nHJRYtmm1mLBwQXo= +github.com/margo/sandbox v1.0.0-rc.2.0.20260916141039-c41545a08e1e/go.mod h1:exvgewlHdcAP2CB6icHolWChDkpW0JLYBsier1AD3S8= +github.com/margo/sandbox v1.0.0-rc.2.0.20260917102142-7325356d9a0c h1:dpkPwbLGS3u6M57gfs/O5OrNl09B5vMGGXdYqbM2smI= +github.com/margo/sandbox v1.0.0-rc.2.0.20260917102142-7325356d9a0c/go.mod h1:exvgewlHdcAP2CB6icHolWChDkpW0JLYBsier1AD3S8= github.com/matryer/is v1.3.0 h1:9qiso3jaJrOe6qBRJRBt2Ldht05qDiFP9le0JOIhRSI= github.com/matryer/is v1.3.0/go.mod h1:2fLPjFQM9rhQ15aVEtbuwhJinnOqrmgXPNdZsdwlWXA= github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= diff --git a/api/pkg/apis/v1alpha1/managers/margo/device.go b/api/pkg/apis/v1alpha1/managers/margo/device.go index e451ed232..521a099c7 100644 --- a/api/pkg/apis/v1alpha1/managers/margo/device.go +++ b/api/pkg/apis/v1alpha1/managers/margo/device.go @@ -52,6 +52,15 @@ type DeviceManager struct { needValidate bool } +// IsAuthorized checks if the device SPIFFE ID is permitted by WFM local policy. +// TODO: This has been already done duing MTLs handshake to avoid attack surface, so this function is a stub for now. In the future, we may implement additional authorization checks here. +func (s *DeviceManager) IsAuthorized(ctx context.Context, spiffeId string) error { + // mTLS handshake already validates the SPIFFE ID against the Trust Bundle. + deviceLogger.DebugfCtx(ctx, "IsAuthorized: PR1 stub — allowing spiffeId: %s", spiffeId) + //return fmt.Errorf("spiffeId %s is not authorized by WFM local policy (test)", spiffeId) + return nil +} + func (s *DeviceManager) Init(pCtx *contexts.VendorContext, config managers.ManagerConfig, providers map[string]providers.IProvider) error { err := s.Manager.Init(pCtx, config, providers) if err != nil { diff --git a/api/pkg/apis/v1alpha1/vendors/margo/device-agent-vendor.go b/api/pkg/apis/v1alpha1/vendors/margo/device-agent-vendor.go index 27959df87..c94c1931d 100644 --- a/api/pkg/apis/v1alpha1/vendors/margo/device-agent-vendor.go +++ b/api/pkg/apis/v1alpha1/vendors/margo/device-agent-vendor.go @@ -13,10 +13,10 @@ import ( "github.com/eclipse-symphony/symphony/coa/pkg/apis/v1alpha2" "github.com/eclipse-symphony/symphony/coa/pkg/apis/v1alpha2/managers" "github.com/eclipse-symphony/symphony/coa/pkg/apis/v1alpha2/observability" + "github.com/eclipse-symphony/symphony/coa/pkg/logger" "github.com/eclipse-symphony/symphony/coa/pkg/apis/v1alpha2/providers" "github.com/eclipse-symphony/symphony/coa/pkg/apis/v1alpha2/providers/pubsub" "github.com/eclipse-symphony/symphony/coa/pkg/apis/v1alpha2/vendors" - "github.com/eclipse-symphony/symphony/coa/pkg/logger" "github.com/margo/sandbox/shared-lib/mis/parser" margoStdSbiAPI "github.com/margo/sandbox/standard/generatedCode/wfm/sbi" "github.com/valyala/fasthttp" @@ -55,6 +55,18 @@ func (self *DeviceAgentVendor) Init(config vendors.VendorConfig, factories []man return nil } +func (self *DeviceAgentVendor) checkAuthorized(pCtx context.Context, spiffeId, instance string) *v1alpha2.COAResponse { + if err := self.DeviceManager.IsAuthorized(pCtx, spiffeId); err != nil { + deviceVendorLogger.WarnfCtx(pCtx, "V (MargoDeviceVendor): authorization denied for %s at %s: %v", spiffeId, instance, err) + resp := problemResponse(margoStdSbiAPI.NewNotAuthorized( + "The request is not authorized by the WFM's local policy.", + instance, + )) + return &resp + } + return nil +} + func (self *DeviceAgentVendor) GetEndpoints() []v1alpha2.Endpoint { route := DeviceAgentInterfaceDefaultBaseURL // if self.Route != "" { @@ -128,44 +140,61 @@ func (self *DeviceAgentVendor) updateDeviceCapabilities(request v1alpha2.COARequ // Extract deviceId from URL parameters deviceId := request.Parameters["__deviceId"] if deviceId == "" { - return createErrorResponse2(deviceVendorLogger, span, - v1alpha2.NewCOAError(nil, "deviceId is required", v1alpha2.BadRequest), - "Missing deviceId parameter", v1alpha2.BadRequest) + return problemResponse(margoStdSbiAPI.NewInvalidRequest( + "deviceId path parameter is required", + "/api/v1/capabilities/{deviceId}")) } deviceSpiffeId, err := ExtractPeerSpiffeID(request) + + // 403 — WFM local policy check if err != nil { - return createErrorResponse2(deviceVendorLogger, span, - v1alpha2.NewCOAError(nil, err.Error(), v1alpha2.BadRequest), - "failed to extract device spiffeId", v1alpha2.BadRequest) - } + return problemResponse(margoStdSbiAPI.NewInvalidRequest( + fmt.Sprintf("failed to extract device spiffeId: %s", err.Error()), + "/api/v1/capabilities/{deviceId}")) + } + + if resp := self.checkAuthorized(pCtx, deviceSpiffeId, fmt.Sprintf("/api/v1/capabilities/%s", deviceId)); resp != nil { + return *resp +} // Parse request body using the correct DeviceCapabilities type var capabilities margoStdSbiAPI.DeviceCapabilitiesManifest - if err := json.Unmarshal(request.Body, &capabilities); err != nil { - return createErrorResponse2(deviceVendorLogger, span, err, "Failed to parse device capabilities", v1alpha2.BadRequest) - } + if err := json.Unmarshal(request.Body, &capabilities); err != nil { + return problemResponse(margoStdSbiAPI.NewInvalidRequest( + fmt.Sprintf("failed to parse device capabilities: %s", err.Error()), + "/api/v1/capabilities/{deviceId}")) + } // Validate required fields if capabilities.Properties.Id == "" { - return createErrorResponse2(deviceVendorLogger, span, - v1alpha2.NewCOAError(nil, "device ID in properties is required", v1alpha2.BadRequest), - "Missing device ID in capabilities", v1alpha2.BadRequest) - } + return problemResponse(margoStdSbiAPI.NewSemanticError( + "Request body includes a semantic error.", + fmt.Sprintf("/api/v1/capabilities/%s", deviceId), + margoStdSbiAPI.FieldError{ + Field: "properties.id", + Message: "device ID in properties is required", + })) +} - // Validate deviceId matches the one in properties - if capabilities.Properties.Id != deviceId { - return createErrorResponse2(deviceVendorLogger, span, - v1alpha2.NewCOAError(nil, "device ID mismatch", v1alpha2.BadRequest), - "Device ID in URL does not match device ID in capabilities", v1alpha2.BadRequest) - } +if capabilities.Properties.Id != deviceId { + return problemResponse(margoStdSbiAPI.NewSemanticError( + "Request body includes a semantic error.", + fmt.Sprintf("/api/v1/capabilities/%s", deviceId), + margoStdSbiAPI.FieldError{ + Field: "properties.id", + Message: fmt.Sprintf("device ID mismatch: path=%q body=%q", deviceId, capabilities.Properties.Id), + })) +} // Call DeviceManager to update capabilities // deviceid is just for residing in properties. For identity, MIAF related identity needs to be used. err = self.DeviceManager.UpdateDeviceCapabilities(pCtx, deviceSpiffeId, capabilities) - if err != nil { - return createErrorResponse2(deviceVendorLogger, span, err, "Failed to update device capabilities", v1alpha2.InternalError) - } + if err != nil { + return problemResponse(margoStdSbiAPI.NewInternalError( + fmt.Sprintf("failed to update device capabilities: %s", err.Error()), + fmt.Sprintf("/api/v1/capabilities/%s", deviceId))) + } return v1alpha2.COAResponse{ State: v1alpha2.Created, @@ -186,31 +215,47 @@ func (self *DeviceAgentVendor) onDeploymentStatusUpdate(request v1alpha2.COARequ deviceClientId, err := ExtractPeerSpiffeID(request) if err != nil { - return createErrorResponse2(deviceVendorLogger, span, - v1alpha2.NewCOAError(nil, err.Error(), v1alpha2.BadRequest), - "failed to extract device spiffeId", v1alpha2.BadRequest) - } + return problemResponse(margoStdSbiAPI.NewInvalidRequest( + fmt.Sprintf("failed to extract device spiffeId: %s", err.Error()), + "/api/v1/deployments/{deploymentId}/status")) + } + + deploymentId := request.Parameters["__deploymentId"] - if deploymentId == "" { - return createErrorResponse2(deviceVendorLogger, span, - v1alpha2.NewCOAError(nil, "deploymentId is required", v1alpha2.BadRequest), - "Missing deploymentId parameter", v1alpha2.BadRequest) - } + if deploymentId == "" { + return problemResponse(margoStdSbiAPI.NewInvalidRequest( + "deploymentId path parameter is required", + "/api/v1/deployments/{deploymentId}/status")) + } + + if resp := self.checkAuthorized(pCtx, deviceClientId, fmt.Sprintf("/api/v1/deployments/%s/status", deploymentId)); resp != nil { + return *resp +} deviceVendorLogger.InfofCtx(pCtx, "V (MargoDeviceVendor): onDeploymentStatusUpdate, method: %s, %s", request.Method, string(request.Body)) // Parse request var statusReq margoStdSbiAPI.DeploymentStatusManifest if err := json.Unmarshal(request.Body, &statusReq); err != nil { - return createErrorResponse2(deviceVendorLogger, span, err, "Failed to parse the request", v1alpha2.BadRequest) - } + return problemResponse(margoStdSbiAPI.NewInvalidRequest( + fmt.Sprintf("failed to parse request: %s", err.Error()), + fmt.Sprintf("/api/v1/deployments/%s/status", deploymentId))) + } if err := self.validateStatusUpdateRequest(statusReq); err != nil { - return createErrorResponse2(deviceVendorLogger, span, err, "Failed to update deployment status", v1alpha2.BadRequest) - } + return problemResponse(margoStdSbiAPI.NewSemanticError( + "Request body includes a semantic error.", + fmt.Sprintf("/api/v1/deployments/%s/status", deploymentId), + margoStdSbiAPI.FieldError{ + Field: "status.state", + Message: err.Error(), + })) +} if err := self.DeviceManager.OnDeploymentStatus(pCtx, deviceClientId, deploymentId, string(statusReq.Status.State)); err != nil { - return createErrorResponse2(deviceVendorLogger, span, err, "Failed to update the status", v1alpha2.BadRequest) + return problemResponse(margoStdSbiAPI.NewInternalError( + fmt.Sprintf("failed to update deployment status: %s", err.Error()), + fmt.Sprintf("/api/v1/deployments/%s/status", deploymentId))) } return createSuccessResponse(span, v1alpha2.Created, (*int)(nil)) @@ -229,26 +274,33 @@ func (self *DeviceAgentVendor) getDesiredManifest(request v1alpha2.COARequest) v // Extract the fasthttp request from the context headers, err := ParseRequestHeaders(request.Context) if err != nil { - return createErrorResponse2(deviceVendorLogger, span, - v1alpha2.NewCOAError(err, "Failed to extract fasthttp request from context", v1alpha2.InternalError), - "Internal server error", v1alpha2.InternalError) + return problemResponse(margoStdSbiAPI.NewInternalError( + "failed to extract request headers", + "/api/v1/deployments")) } deviceVendorLogger.InfofCtx(pCtx, "V (MargoDeviceVendor): getDesiredManifest, parsedHeaders, method: sign(%v)", headers) if accept := headers["accept"]; accept != "application/vnd.margo.manifest.v1+json" { - return createErrorResponse2(deviceVendorLogger, span, - v1alpha2.NewCOAError(nil, "The accept header should be application/vnd.margo.manifest.v1+json", v1alpha2.NotAcceptable), - "Not Acceptable", v1alpha2.NotAcceptable) + return problemResponse(margoStdSbiAPI.NewServerCannotGenerateResponse( + "Accept header must be application/vnd.margo.manifest.v1+json", + "/api/v1/deployments")) } deviceClientId, err := ExtractPeerSpiffeID(request) + + if err != nil { - return createErrorResponse2(deviceVendorLogger, span, - v1alpha2.NewCOAError(nil, err.Error(), v1alpha2.BadRequest), - "failed to extract device spiffeId", v1alpha2.BadRequest) + return problemResponse(margoStdSbiAPI.NewInternalError( + "failed to extract device spiffeId", + "/api/v1/deployments")) } + // 403 — WFM local policy check + if resp := self.checkAuthorized(pCtx, deviceClientId, "/api/v1/deployments"); resp != nil { + return *resp +} + deviceVendorLogger.InfofCtx(pCtx, "Processing request for deviceClientId: %s", deviceClientId) // Fix: Use lowercase header key @@ -257,15 +309,16 @@ func (self *DeviceAgentVendor) getDesiredManifest(request v1alpha2.COARequest) v shouldReplaceBundle, _, manifest, err := self.DeviceManager.ShouldReplaceBundle(pCtx, deviceClientId, &digest) if err != nil { - deviceVendorLogger.ErrorfCtx(pCtx, "ShouldReplaceBundle failed for device %s: %v", deviceClientId, err) - return createErrorResponse2(deviceVendorLogger, span, err, "Failed to get the desired states", v1alpha2.InternalError) - } + return problemResponse(margoStdSbiAPI.NewInternalError( + fmt.Sprintf("failed to get desired states: %s", err.Error()), + "/api/v1/deployments")) + } if manifest == nil { deviceVendorLogger.ErrorfCtx(pCtx, "Manifest is nil for device %s", deviceClientId) - return createErrorResponse2(deviceVendorLogger, span, - v1alpha2.NewCOAError(nil, "manifest is nil", v1alpha2.InternalError), - "Internal server error", v1alpha2.InternalError) + return problemResponse(margoStdSbiAPI.NewInternalError( + "manifest is nil", + "/api/v1/deployments")) } // SPEC-COMPLIANT: Compute ETag as digest of the manifest JSON @@ -277,7 +330,8 @@ func (self *DeviceAgentVendor) getDesiredManifest(request v1alpha2.COARequest) v manifestJSON, err := json.Marshal(manifest) if err != nil { deviceVendorLogger.ErrorfCtx(pCtx, "Failed to marshal manifest for digest: %v", err) - return createErrorResponse2(deviceVendorLogger, span, err, "Failed to compute manifest digest", v1alpha2.InternalError) + return problemResponse(margoStdSbiAPI.NewInternalError(fmt.Sprintf("failed to compute manifest digest: %s", err.Error()), + "/api/v1/deployments")) } // Compute SHA-256 digest of the manifest JSON @@ -289,9 +343,8 @@ func (self *DeviceAgentVendor) getDesiredManifest(request v1alpha2.COARequest) v } else { if manifest.Bundle.Digest == nil { deviceVendorLogger.ErrorfCtx(pCtx, "Manifest bundle digest is nil for device %s", deviceClientId) - return createErrorResponse2(deviceVendorLogger, span, - v1alpha2.NewCOAError(nil, "manifest bundle digest is nil", v1alpha2.InternalError), - "Internal server error", v1alpha2.InternalError) + return problemResponse(margoStdSbiAPI.NewInternalError("manifest bundle digest is nil", + "/api/v1/deployments")) } // Bundle with deployments: Use bundle digest as ETag @@ -336,7 +389,8 @@ func (self *DeviceAgentVendor) getDesiredManifest(request v1alpha2.COARequest) v manifestJSON, err := json.Marshal(manifest) if err != nil { deviceVendorLogger.ErrorfCtx(pCtx, "Failed to marshal manifest: %v", err) - return createErrorResponse2(deviceVendorLogger, span, err, "Failed to marshal manifest", v1alpha2.InternalError) + return problemResponse(margoStdSbiAPI.NewInternalError(fmt.Sprintf("failed to marshal manifest: %s", err.Error()), + "/api/v1/deployments")) } return v1alpha2.COAResponse{ @@ -359,9 +413,9 @@ func (self *DeviceAgentVendor) downloadBundle(request v1alpha2.COARequest) v1alp // Extract headers headers, err := ParseRequestHeaders(request.Context) if err != nil { - return createErrorResponse2(deviceVendorLogger, span, - v1alpha2.NewCOAError(err, "Failed to extract headers", v1alpha2.InternalError), - "Internal server error", v1alpha2.InternalError) + return problemResponse(margoStdSbiAPI.NewInternalError( + "failed to extract request headers", + "/api/v1/bundles/{digest}")) } // Validate Accept header (406 Not Acceptable) @@ -374,45 +428,46 @@ func (self *DeviceAgentVendor) downloadBundle(request v1alpha2.COARequest) v1alp if accept != "" { validAccept := slices.Contains(acceptedTypes, accept) if !validAccept { - return createErrorResponse2(deviceVendorLogger, span, - v1alpha2.NewCOAError(nil, - "Accept header must be application/vnd.margo.bundle.v1+tar+gzip", - v1alpha2.NotAcceptable), - "Not Acceptable", v1alpha2.NotAcceptable) + return problemResponse(margoStdSbiAPI.NewServerCannotGenerateResponse( + "Accept header must be application/vnd.margo.bundle.v1+tar+gzip", + "/api/v1/bundles/{digest}")) } } deviceClientId, err := ExtractPeerSpiffeID(request) if err != nil { - return createErrorResponse2(deviceVendorLogger, span, - v1alpha2.NewCOAError(nil, err.Error(), v1alpha2.BadRequest), - "failed to extract device spiffeId", v1alpha2.BadRequest) + return problemResponse(margoStdSbiAPI.NewInvalidRequest( + fmt.Sprintf("failed to extract device spiffeId: %s", err.Error()), + "/api/v1/bundles/{digest}")) } + requestedDigest := request.Parameters["__digest"] + if requestedDigest == "" { - return createErrorResponse2(deviceVendorLogger, span, - v1alpha2.NewCOAError(nil, "digest is required", v1alpha2.BadRequest), - "Missing digest parameter", v1alpha2.BadRequest) + return problemResponse(margoStdSbiAPI.NewInvalidRequest( + "digest path parameter is required", + "/api/v1/bundles/{digest}")) } + if resp := self.checkAuthorized(pCtx, deviceClientId, fmt.Sprintf("/api/v1/bundles/%s", requestedDigest)); resp != nil { + return *resp +} + // Extract If-None-Match header from client clientETag := headers["if-none-match"] // Get bundle from database path, manifest, err := self.DeviceManager.GetBundle(pCtx, deviceClientId, &requestedDigest) if err != nil { - return createErrorResponse2(deviceVendorLogger, span, err, - "Bundle not found", v1alpha2.NotFound) + return problemResponse(margoStdSbiAPI.NewInvalidBundle( + fmt.Sprintf("bundle not found for digest %s", requestedDigest), + fmt.Sprintf("/api/v1/bundles/%s", requestedDigest))) } if path == "" || manifest == nil { - return createSuccessResponseWithHeaders( - span, - "application/vnd.margo.bundle.v1+tar+gzip", - nil, - v1alpha2.NotFound, - (*int)(nil), - ) + return problemResponse(margoStdSbiAPI.NewInvalidBundle( + fmt.Sprintf("bundle not found for digest %s", requestedDigest), + fmt.Sprintf("/api/v1/bundles/%s", requestedDigest))) } // Check If-None-Match before reading file @@ -440,8 +495,9 @@ func (self *DeviceAgentVendor) downloadBundle(request v1alpha2.COARequest) v1alp // Read bundle archive (this is the "exact bytes" that will be sent) bundleData, err := os.ReadFile(path) if err != nil { - return createErrorResponse2(deviceVendorLogger, span, err, - "Failed to read bundle", v1alpha2.InternalError) + return problemResponse(margoStdSbiAPI.NewInternalError( + fmt.Sprintf("failed to read bundle: %s", err.Error()), + fmt.Sprintf("/api/v1/bundles/%s", requestedDigest))) } // Verify digest of the bundle archive (Exact Bytes Rule) @@ -455,12 +511,9 @@ func (self *DeviceAgentVendor) downloadBundle(request v1alpha2.COARequest) v1alp // Per spec: "If the server cannot produce content whose digest matches this value // it MUST return 404 Not Found" - return createErrorResponse2(deviceVendorLogger, span, - v1alpha2.NewCOAError(nil, - fmt.Sprintf("Digest mismatch: requested %s, actual %s", - requestedDigest, actualDigest), - v1alpha2.NotFound), - "Bundle not found for the given digest", v1alpha2.NotFound) + return problemResponse(margoStdSbiAPI.NewInvalidBundle( + fmt.Sprintf("digest mismatch: requested %s, actual %s", requestedDigest, actualDigest), + fmt.Sprintf("/api/v1/bundles/%s", requestedDigest))) } deviceVendorLogger.InfofCtx(pCtx, @@ -495,16 +548,16 @@ func (self *DeviceAgentVendor) downloadDeployment(request v1alpha2.COARequest) v // Extract headers headers, err := ParseRequestHeaders(request.Context) if err != nil { - return createErrorResponse2(deviceVendorLogger, span, - v1alpha2.NewCOAError(err, "Failed to extract headers", v1alpha2.InternalError), - "Internal server error", v1alpha2.InternalError) + return problemResponse(margoStdSbiAPI.NewInternalError( + "failed to extract request headers", + "/api/v1/deployments/{deploymentId}/{digest}")) } // Validate Accept header (406 Not Acceptable) if accept := headers["accept"]; accept != "" && accept != "application/yaml" && accept != "*/*" { - return createErrorResponse2(deviceVendorLogger, span, - v1alpha2.NewCOAError(nil, "Accept header must be application/yaml", v1alpha2.NotAcceptable), - "Not Acceptable", v1alpha2.NotAcceptable) + return problemResponse(margoStdSbiAPI.NewServerCannotGenerateResponse( + "Accept header must be application/yaml", + "/api/v1/deployments/{deploymentId}/{digest}")) } // AFTER — no deviceId needed (deploymentId is sufficient to look up deployment) @@ -512,16 +565,28 @@ func (self *DeviceAgentVendor) downloadDeployment(request v1alpha2.COARequest) v deploymentId := request.Parameters["__deploymentId"] if deploymentId == "" { - return createErrorResponse2(deviceVendorLogger, span, - v1alpha2.NewCOAError(nil, "deploymentId is required", v1alpha2.BadRequest), - "Missing deploymentId parameter", v1alpha2.BadRequest) + return problemResponse(margoStdSbiAPI.NewInvalidRequest( + "deploymentId path parameter is required", + "/api/v1/deployments/{deploymentId}/{digest}")) } requestedDigest := request.Parameters["__digest"] if requestedDigest == "" { - return createErrorResponse2(deviceVendorLogger, span, - v1alpha2.NewCOAError(nil, "digest is required", v1alpha2.BadRequest), - "Missing digest parameter", v1alpha2.BadRequest) + return problemResponse(margoStdSbiAPI.NewInvalidRequest( + "digest path parameter is required", + "/api/v1/deployments/{deploymentId}/{digest}")) + } + + deviceClientId, err := ExtractPeerSpiffeID(request) +if err != nil { + return problemResponse(margoStdSbiAPI.NewInvalidRequest( + fmt.Sprintf("failed to extract device spiffeId: %s", err.Error()), + fmt.Sprintf("/api/v1/deployments/%s/%s", deploymentId, requestedDigest))) +} + + // 403 — WFM local policy check + if resp := self.checkAuthorized(pCtx, deviceClientId, fmt.Sprintf("/api/v1/deployments/%s/%s", deploymentId, requestedDigest)); resp != nil { + return *resp } // Extract If-None-Match header from client @@ -530,17 +595,14 @@ func (self *DeviceAgentVendor) downloadDeployment(request v1alpha2.COARequest) v // Get deployment from database deployment, err := self.DeviceManager.Database.GetDeployment(pCtx, deploymentId) if err != nil { - return createErrorResponse2(deviceVendorLogger, span, err, - "Deployment not found", v1alpha2.NotFound) + return problemResponse(margoStdSbiAPI.NewDeploymentNotFound( + fmt.Sprintf("deployment %s not found: %s", deploymentId, err.Error()), + fmt.Sprintf("/api/v1/deployments/%s/%s", deploymentId, requestedDigest))) } if deployment == nil { - return createSuccessResponseWithHeaders( - span, - "application/yaml", - nil, - v1alpha2.NotFound, - (*int)(nil), - ) + return problemResponse(margoStdSbiAPI.NewDeploymentNotFound( + fmt.Sprintf("deployment %s not found", deploymentId), + fmt.Sprintf("/api/v1/deployments/%s/%s", deploymentId, requestedDigest))) } var yamlContent []byte @@ -554,8 +616,9 @@ func (self *DeviceAgentVendor) downloadDeployment(request v1alpha2.COARequest) v yamlContent, err = yaml.Marshal(deployment.DesiredState.AppDeploymentManifest) if err != nil { - return createErrorResponse2(deviceVendorLogger, span, err, - "Failed to marshal deployment", v1alpha2.InternalError) + return problemResponse(margoStdSbiAPI.NewInternalError( + fmt.Sprintf("failed to marshal deployment: %s", err.Error()), + fmt.Sprintf("/api/v1/deployments/%s/%s", deploymentId, requestedDigest))) } } @@ -589,12 +652,9 @@ func (self *DeviceAgentVendor) downloadDeployment(request v1alpha2.COARequest) v // Per spec: "If the server cannot produce content whose digest matches this value // it MUST return 404 Not Found" - return createErrorResponse2(deviceVendorLogger, span, - v1alpha2.NewCOAError(nil, - fmt.Sprintf("Digest mismatch: requested %s, actual %s", - requestedDigest, actualDigest), - v1alpha2.NotFound), - "Deployment not found for the given digest", v1alpha2.NotFound) + return problemResponse(margoStdSbiAPI.NewDeploymentNotFound( + fmt.Sprintf("digest mismatch: requested %s, actual %s", requestedDigest, actualDigest), + fmt.Sprintf("/api/v1/deployments/%s/%s", deploymentId, requestedDigest))) } deviceVendorLogger.InfofCtx(pCtx, diff --git a/api/pkg/apis/v1alpha1/vendors/margo/problem_response.go b/api/pkg/apis/v1alpha1/vendors/margo/problem_response.go new file mode 100644 index 000000000..170c30a98 --- /dev/null +++ b/api/pkg/apis/v1alpha1/vendors/margo/problem_response.go @@ -0,0 +1,17 @@ +// api/pkg/apis/v1alpha1/vendors/margo/problem_response.go +package margo + +import ( + "github.com/eclipse-symphony/symphony/coa/pkg/apis/v1alpha2" + sbi "github.com/margo/sandbox/standard/generatedCode/wfm/sbi" +) + +// problemResponse converts a *sbi.ProblemDetail into a COAResponse. +func problemResponse(pd *sbi.ProblemDetail) v1alpha2.COAResponse { + body, _ := pd.MarshalJSON() + return v1alpha2.COAResponse{ + State: v1alpha2.GetHttpStatus(pd.Status), + Body: body, + ContentType: sbi.ProblemContentType, + } +} diff --git a/cli/go.mod b/cli/go.mod index a1799730e..b3c55394e 100644 --- a/cli/go.mod +++ b/cli/go.mod @@ -55,7 +55,7 @@ require ( github.com/cenkalti/backoff/v4 v4.3.0 github.com/eclipse-symphony/symphony/api v0.0.0-00010101000000-000000000000 github.com/ghodss/yaml v1.0.0 - github.com/margo/sandbox v1.0.0-rc.2.0.20260914140431-3329621a9f84 + github.com/margo/sandbox v1.0.0-rc.2.0.20260917102142-7325356d9a0c github.com/princjef/mageutil v1.0.0 ) diff --git a/cli/go.sum b/cli/go.sum index 4e057d57d..51f412e87 100644 --- a/cli/go.sum +++ b/cli/go.sum @@ -57,6 +57,12 @@ github.com/lestrrat-go/sfv v1.0.0 h1:+/VOs7lhUWAwNIklow4kPYlit0fPBC6HsF+GHKXrhGM github.com/lestrrat-go/sfv v1.0.0/go.mod h1:wawOORrbzB4Vh0QT7WtbWEbMuTbjS8OPxum1wZAYCiQ= github.com/margo/sandbox v1.0.0-rc.2.0.20260914140431-3329621a9f84 h1:XOE/A2gF4gKPVwcb9WWcUWjFwgakcan769/CWOmsGPI= github.com/margo/sandbox v1.0.0-rc.2.0.20260914140431-3329621a9f84/go.mod h1:GUXYfxV7olA9UNBHFXbcCncQphxLmbNTmncEva1XkTw= +github.com/margo/sandbox v1.0.0-rc.2.0.20260916085946-ec8c5fb42b0f h1:knEF6gDZulXTCmJYAvNAnwI31lUjSV8jd690tKL3qwA= +github.com/margo/sandbox v1.0.0-rc.2.0.20260916085946-ec8c5fb42b0f/go.mod h1:exvgewlHdcAP2CB6icHolWChDkpW0JLYBsier1AD3S8= +github.com/margo/sandbox v1.0.0-rc.2.0.20260916141039-c41545a08e1e h1:/O7eH/b+b6+Zp96YmcTbwioe3r4nHJRYtmm1mLBwQXo= +github.com/margo/sandbox v1.0.0-rc.2.0.20260916141039-c41545a08e1e/go.mod h1:exvgewlHdcAP2CB6icHolWChDkpW0JLYBsier1AD3S8= +github.com/margo/sandbox v1.0.0-rc.2.0.20260917102142-7325356d9a0c h1:dpkPwbLGS3u6M57gfs/O5OrNl09B5vMGGXdYqbM2smI= +github.com/margo/sandbox v1.0.0-rc.2.0.20260917102142-7325356d9a0c/go.mod h1:exvgewlHdcAP2CB6icHolWChDkpW0JLYBsier1AD3S8= github.com/matryer/is v1.3.0 h1:9qiso3jaJrOe6qBRJRBt2Ldht05qDiFP9le0JOIhRSI= github.com/matryer/is v1.3.0/go.mod h1:2fLPjFQM9rhQ15aVEtbuwhJinnOqrmgXPNdZsdwlWXA= github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= diff --git a/coa/pkg/apis/v1alpha2/types.go b/coa/pkg/apis/v1alpha2/types.go index b4bf03c93..f14b88c00 100644 --- a/coa/pkg/apis/v1alpha2/types.go +++ b/coa/pkg/apis/v1alpha2/types.go @@ -39,8 +39,10 @@ const ( NotAcceptable State = 406 Conflict State = 409 StatusUnprocessableEntity State = 422 + TooManyRequests State = 429 // InternalError = HTTP 500 InternalError State = 500 + ServiceUnavailable State = 503 // Config errors BadConfig State = 1000 MissingConfig State = 1001 @@ -172,10 +174,14 @@ func GetHttpStatus(code int) State { return NotFound case code == 405: return MethodNotAllowed - case code == 409: + case code == 409: return Conflict + case code == 422: + return StatusUnprocessableEntity case code >= 400 && code < 500: return BadRequest + case code == 503: + return ServiceUnavailable case code >= 500: return InternalError default: @@ -209,6 +215,10 @@ func (s State) String() string { return "Conflict" case StatusUnprocessableEntity: return "Unprocessable Entity" + case TooManyRequests: + return "Too Many Requests" + case ServiceUnavailable: + return "Service Unavailable" case InternalError: return "Internal Error" case BadConfig: