diff --git a/api/go.mod b/api/go.mod index d858c98b7..d193fd5cd 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.20260907065449-0c752aada0a4 + github.com/margo/sandbox v1.0.0-rc.2.0.20260914132812-996e3c369a28 github.com/princjef/mageutil v1.0.0 go.opentelemetry.io/otel/trace v1.44.0 golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 @@ -52,6 +52,7 @@ require ( github.com/distribution/reference v0.6.0 // indirect github.com/felixge/httpsnoop v1.0.4 // indirect github.com/fxamacker/cbor/v2 v2.9.0 // indirect + github.com/go-jose/go-jose/v4 v4.1.3 // indirect github.com/go-openapi/swag/cmdutils v0.25.4 // indirect github.com/go-openapi/swag/conv v0.25.4 // indirect github.com/go-openapi/swag/fileutils v0.25.4 // indirect @@ -93,6 +94,7 @@ require ( github.com/russross/blackfriday/v2 v2.1.0 // indirect github.com/santhosh-tekuri/jsonschema/v6 v6.0.2 // indirect github.com/segmentio/ksuid v1.0.4 // indirect + github.com/spiffe/go-spiffe/v2 v2.6.0 // indirect github.com/x448/float16 v0.8.4 // indirect go.opentelemetry.io/auto/sdk v1.2.1 // indirect go.opentelemetry.io/contrib/bridges/otellogrus v0.3.0 // indirect diff --git a/api/go.sum b/api/go.sum index c8ee41baf..12a731e90 100644 --- a/api/go.sum +++ b/api/go.sum @@ -152,6 +152,8 @@ github.com/go-errors/errors v1.4.2 h1:J6MZopCL4uSllY1OfXM374weqZFFItUbrImctkmUxI github.com/go-errors/errors v1.4.2/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3BopGUQ5a5Og= github.com/go-gorp/gorp/v3 v3.1.0 h1:ItKF/Vbuj31dmV4jxA1qblpSwkl9g1typ24xoe70IGs= github.com/go-gorp/gorp/v3 v3.1.0/go.mod h1:dLEjIyyRNiXvNZ8PSmzpt1GsWAUK8kjVhEpjH8TixEw= +github.com/go-jose/go-jose/v4 v4.1.3 h1:CVLmWDhDVRa6Mi/IgCgaopNosCaHz7zrMeF9MlZRkrs= +github.com/go-jose/go-jose/v4 v4.1.3/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= @@ -293,8 +295,8 @@ github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de h1:9TO3cAIGXtEhn github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de/go.mod h1:zAbeS9B/r2mtpb6U+EI2rYA5OAXxsYw6wTamcNW+zcE= 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.20260907065449-0c752aada0a4 h1:Ski7cJ+qbMcrM+CRJ7exw5nJZAmt78hBjemOFFZLBt4= -github.com/margo/sandbox v1.0.0-rc.2.0.20260907065449-0c752aada0a4/go.mod h1:spPdVkDLs/kfUcvKXIIGmgrAmD+z7Po9QkbHVdEp03U= +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/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= @@ -424,6 +426,8 @@ github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiT github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk= github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spiffe/go-spiffe/v2 v2.6.0 h1:l+DolpxNWYgruGQVV0xsfeya3CsC7m8iBzDnMpsbLuo= +github.com/spiffe/go-spiffe/v2 v2.6.0/go.mod h1:gm2SeUoMZEtpnzPNs2Csc0D/gX33k1xIx7lEzqblHEs= github.com/spkg/bom v0.0.0-20160624110644-59b7046e48ad/go.mod h1:qLr4V1qq6nMqFKkMo8ZTx3f+BZEkzsRUY10Xsm2mwU0= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= diff --git a/api/pkg/apis/v1alpha1/managers/margo/database.go b/api/pkg/apis/v1alpha1/managers/margo/database.go index bcc780ac2..902e81bbb 100644 --- a/api/pkg/apis/v1alpha1/managers/margo/database.go +++ b/api/pkg/apis/v1alpha1/managers/margo/database.go @@ -18,8 +18,10 @@ import ( margoStdAPI "github.com/margo/sandbox/standard/generatedCode/wfm/sbi" ) -type PublishGroupName string -type PublishFeed string +type ( + PublishGroupName string + PublishFeed string +) var ( margoDbLogger = logger.NewLogger("coa.runtime") @@ -78,20 +80,11 @@ type DeploymentDatabaseRow struct { // DeviceDatabaseRow represents a device record in the database. // It contains device identification, capabilities, and synchronization information. type DeviceDatabaseRow struct { - // DeviceClientId is the unique identifier for the device + // DeviceClientId is the unique identifier for the device, MIAF compliant SPIFFE ID DeviceClientId string - // OAuthClientId is the unique identifier for the device auth - OAuthClientId string - - // Client secret is the information that helps the device to generate/ask for an oauth token - OAuthClientSecret string - - // OAuth token url - OAuthTokenURL string - - // unique signature that is bind to this device, eg TPM, certificate etc... - DevicePubCert string + // This is same as device capabilities.properties.id + DeviceId string // status of the onboarding OnboardingStatus margoNonStdAPI.DeviceOnboardStatus @@ -228,7 +221,6 @@ func (db *MargoDatabase) DeleteAppPackage(ctx context.Context, packageId string) Metadata: db.appPkgMetadata, ID: packageId, }) - if err != nil { db.MgrContext.Logger.ErrorfCtx(ctx, "DeleteAppPackage: Failed to delete app package '%s': %v", packageId, err) return fmt.Errorf("failed to delete app package '%s': %w", packageId, err) @@ -346,7 +338,6 @@ func (db *MargoDatabase) DeleteDeployment(ctx context.Context, deploymentId stri Metadata: db.deploymentMetadata, ID: deploymentId, }) - if err != nil { db.MgrContext.Logger.ErrorfCtx(ctx, "DeleteDeployment: Failed to delete deployment '%s': %v", deploymentId, err) return fmt.Errorf("failed to delete deployment '%s': %w", deploymentId, err) @@ -521,7 +512,6 @@ func (db *MargoDatabase) GetDeploymentsByDevice(ctx context.Context, deviceId st var deviceDeployments []DeploymentDatabaseRow for _, deployment := range allDeployments { - if deployment.DeploymentRequest.Spec.DeviceRef != nil && deployment.DeploymentRequest.Spec.DeviceRef.Id != nil { db.MgrContext.Logger.InfofCtx(ctx, "GetDeploymentsByDevice: Found deployment %s assigned to device %s", *deployment.DeploymentRequest.Id, *deployment.DeploymentRequest.Spec.DeviceRef.Id) @@ -597,48 +587,41 @@ func (db *MargoDatabase) UpsertDevice(ctx context.Context, device DeviceDatabase return nil } -func (db *MargoDatabase) GetDevice(ctx context.Context, deviceId string) (*DeviceDatabaseRow, error) { +func (db *MargoDatabase) GetDevice(ctx context.Context, deviceClientId string) (*DeviceDatabaseRow, error) { entry, err := db.StateProvider.Get(ctx, states.GetRequest{ Metadata: db.deviceMetadata, - ID: deviceId, + ID: deviceClientId, }) if err != nil { - db.MgrContext.Logger.ErrorfCtx(ctx, "GetDevice: Failed to get device '%s': %v", deviceId, err) - return nil, fmt.Errorf("failed to get device '%s': %w", deviceId, err) + // sending not found error separately to track it. + if verr, ok := err.(v1alpha2.COAError); ok { + if verr.State == v1alpha2.NotFound { + // do not modify the error to enable tracking + return nil, err + } + } + + db.MgrContext.Logger.ErrorfCtx(ctx, "GetDevice: Failed to get device '%s': %v", deviceClientId, err) + return nil, fmt.Errorf("failed to get device '%s': %w", deviceClientId, err) } var device DeviceDatabaseRow jData, _ := json.Marshal(entry.Body) err = json.Unmarshal(jData, &device) if err != nil { - db.MgrContext.Logger.ErrorfCtx(ctx, "GetDevice: Failed to unmarshal device '%s': %v", deviceId, err) - return nil, fmt.Errorf("failed to unmarshal device '%s': %w", deviceId, err) + db.MgrContext.Logger.ErrorfCtx(ctx, "GetDevice: Failed to unmarshal device '%s': %v", deviceClientId, err) + return nil, fmt.Errorf("failed to unmarshal device '%s': %w", deviceClientId, err) } - db.MgrContext.Logger.InfofCtx(ctx, "GetDevice: device '%s' retrieved successfully", deviceId) + db.MgrContext.Logger.InfofCtx(ctx, "GetDevice: device '%s' retrieved successfully", deviceClientId) return &device, nil } -func (db *MargoDatabase) GetDeviceUsingPubCert(ctx context.Context, cert string) (*DeviceDatabaseRow, error) { - devices, err := db.ListDevices(ctx) - if err != nil { - return nil, err - } - - for _, device := range devices { - if device.DevicePubCert == cert { - return &device, nil - } - } - return nil, fmt.Errorf("no device found with sign: %s", cert) -} - func (db *MargoDatabase) DeleteDevice(ctx context.Context, deviceId string) error { err := db.StateProvider.Delete(ctx, states.DeleteRequest{ Metadata: db.deviceMetadata, ID: deviceId, }) - if err != nil { db.MgrContext.Logger.ErrorfCtx(ctx, "DeleteDevice: Failed to delete device '%s': %v", deviceId, err) return fmt.Errorf("failed to delete device '%s': %w", deviceId, err) @@ -712,26 +695,19 @@ func (db *MargoDatabase) DeviceExists(ctx context.Context, deviceId string) (boo return true, nil } -func (db *MargoDatabase) DevicePubCertExists(ctx context.Context, deviceCert string) (DeviceDatabaseRow, bool, error) { - devices, err := db.ListDevices(ctx) - if err != nil { - return DeviceDatabaseRow{}, false, err - } - - for _, device := range devices { - if device.DevicePubCert == deviceCert { - return device, true, nil - } - } - return DeviceDatabaseRow{}, false, nil -} - -func (db *MargoDatabase) UpdateDeviceCapabilities(ctx context.Context, deviceId string, capabilities *margoStdAPI.DeviceCapabilitiesManifest) error { +func (db *MargoDatabase) UpdateDeviceCapabilities(ctx context.Context, deviceClientId string, capabilities *margoStdAPI.DeviceCapabilitiesManifest) error { // Get existing device - device, err := db.GetDevice(ctx, deviceId) + device, err := db.GetDevice(ctx, deviceClientId) if err != nil { - db.MgrContext.Logger.ErrorfCtx(ctx, "UpdateDeviceCapabilities: Failed to get device '%s': %v", deviceId, err) - return fmt.Errorf("failed to get device '%s' for capabilities update: %w", deviceId, err) + // sending not found error separately to track it. + if verr, ok := err.(v1alpha2.COAError); ok { + if verr.State == v1alpha2.NotFound { + // do not modify the error to enable tracking + return err + } + } + db.MgrContext.Logger.WarnfCtx(ctx, "UpdateDeviceCapabilities: Failed to get device '%s': %v", deviceClientId, err) + return fmt.Errorf("failed to get device '%s' for capabilities update: %w", deviceClientId, err) } // Update the capabilities @@ -741,11 +717,11 @@ func (db *MargoDatabase) UpdateDeviceCapabilities(ctx context.Context, deviceId // Save updated device err = db.UpsertDevice(ctx, *device) if err != nil { - db.MgrContext.Logger.ErrorfCtx(ctx, "UpdateDeviceCapabilities: Failed to update device capabilities for '%s': %v", deviceId, err) - return fmt.Errorf("failed to update device capabilities for '%s': %w", deviceId, err) + db.MgrContext.Logger.ErrorfCtx(ctx, "UpdateDeviceCapabilities: Failed to update device capabilities for '%s': %v", deviceClientId, err) + return fmt.Errorf("failed to update device capabilities for '%s': %w", deviceClientId, err) } - db.MgrContext.Logger.InfofCtx(ctx, "UpdateDeviceCapabilities: device '%s' capabilities updated successfully", deviceId) + db.MgrContext.Logger.InfofCtx(ctx, "UpdateDeviceCapabilities: device '%s' capabilities updated successfully", deviceClientId) return nil } @@ -857,7 +833,6 @@ func (db *MargoDatabase) UpsertDeploymentBundle(ctx context.Context, bundleRow D Body: bundleRow, }, }) - if err != nil { db.MgrContext.Logger.ErrorfCtx(ctx, "UpsertDeploymentBundle: Failed to store bundle: %v", err) @@ -995,7 +970,6 @@ func (db *MargoDatabase) DeleteDeploymentBundle(ctx context.Context, deviceClien if existingBundle != nil && existingBundle.ArchivePath != "" { if err := os.Remove(existingBundle.ArchivePath); err != nil && !os.IsNotExist(err) { db.MgrContext.Logger.WarnfCtx(ctx, "DeleteDeploymentBundle: Failed to delete archive file '%s': %v", existingBundle.ArchivePath, err) - } else { db.MgrContext.Logger.InfofCtx(ctx, "DeleteDeploymentBundle: Deleted archive file '%s'", existingBundle.ArchivePath) } diff --git a/api/pkg/apis/v1alpha1/managers/margo/device.go b/api/pkg/apis/v1alpha1/managers/margo/device.go index d14a846f3..e451ed232 100644 --- a/api/pkg/apis/v1alpha1/managers/margo/device.go +++ b/api/pkg/apis/v1alpha1/managers/margo/device.go @@ -5,8 +5,6 @@ import ( "crypto/sha256" "encoding/json" "fmt" - "math/rand/v2" - "os" "strings" "time" @@ -24,9 +22,7 @@ import ( margoStdAPI "github.com/margo/sandbox/standard/generatedCode/wfm/sbi" ) -var ( - deviceLogger = logger.NewLogger("coa.runtime") -) +var deviceLogger = logger.NewLogger("coa.runtime") type PackageData struct { CurrentState margoNonStdAPI.ApplicationPackageListResp @@ -307,7 +303,8 @@ func (s *DeviceManager) OnDeploymentStatus(ctx context.Context, deviceClientId, func (dm *DeviceManager) GetToken(ctx context.Context, clientId, clientSecret string, userClaims map[string]interface{}) (*TokenData, error) { // dm.AuthProvider.ValidateToken(ctx, ) - result, err := dm.KeycloakProvider.GetTokenWithClaims(ctx, + result, err := dm.KeycloakProvider.GetTokenWithClaims( + ctx, clientId, clientSecret, userClaims, @@ -324,112 +321,29 @@ func (dm *DeviceManager) GetToken(ctx context.Context, clientId, clientSecret st }, nil } -func (dm *DeviceManager) OnboardDevice(ctx context.Context, devicePubCert string) (*DeviceOnboardingData, error) { - var success bool - // Generate unique client ID for the device - clientID := generateDeviceClientID() - authClientSecret := "" - authTokenUrl := "" - - onboardStatus := margoNonStdAPI.DeviceOnboardStatusINPROGRESS +func (dm *DeviceManager) OnboardDevice(ctx context.Context, deviceClientId string, capabilities margoStdAPI.DeviceCapabilitiesManifest) error { if err := dm.Database.UpsertDevice(ctx, DeviceDatabaseRow{ - DeviceClientId: clientID, - OAuthClientSecret: authClientSecret, - OAuthClientId: clientID, - OAuthTokenURL: authTokenUrl, - DevicePubCert: devicePubCert, - OnboardingStatus: onboardStatus, - Capabilities: nil, - LastStateSync: time.Now().UTC(), - CreatedAt: time.Now().UTC(), - UpdatedAt: time.Now().UTC(), + DeviceClientId: deviceClientId, + DeviceId: capabilities.Properties.Id, + OnboardingStatus: margoNonStdAPI.DeviceOnboardStatusONBOARDED, + Capabilities: &capabilities, + LastStateSync: time.Now().UTC(), + CreatedAt: time.Now().UTC(), + UpdatedAt: time.Now().UTC(), }); err != nil { - return nil, fmt.Errorf("failed to save device details: %w", err) + return fmt.Errorf("failed to save device details: %w", err) } - defer func() { - onboardStatus = margoNonStdAPI.DeviceOnboardStatusONBOARDED - if !success { - onboardStatus = margoNonStdAPI.DeviceOnboardStatusFAILED - } - _ = dm.Database.UpsertDevice(ctx, DeviceDatabaseRow{ - DeviceClientId: clientID, - OAuthClientSecret: authClientSecret, - OAuthClientId: clientID, - OAuthTokenURL: authTokenUrl, - DevicePubCert: devicePubCert, - OnboardingStatus: onboardStatus, - Capabilities: nil, - LastStateSync: time.Now().UTC(), - CreatedAt: time.Now().UTC(), - UpdatedAt: time.Now().UTC(), - }) - }() - - // review: devise a cleaner way for this - if dm.KeycloakProvider != nil { - // Define client configuration - config := keycloak.ClientConfig{ - ClientID: clientID, - Enabled: true, - ServiceAccountsEnabled: true, - StandardFlowEnabled: false, - DirectAccessGrantsEnabled: true, - // Name: , - Attributes: &map[string]string{ - "device.onboarded": "true", - "created.by": "device-manager", - }, - } - - // Get admin token - clientResult, err := dm.KeycloakProvider.CreateClientWithClaims(ctx, config, map[string]interface{}{ - "deviceId": clientID, - }) - if err != nil { - success = false - return nil, fmt.Errorf("failed to authenticate with Keycloak: %s", err.Error()) - } - - // Verify client was created successfully - if clientResult.ClientID == "" { - success = false - return nil, fmt.Errorf("client creation returned empty ID") - } - if clientResult.ClientSecret == "" { - success = false - return nil, fmt.Errorf("client creation returned empty ID") - } - if clientResult.ClientUUID == "" { - success = false - return nil, fmt.Errorf("client creation returned empty ID") - } - if clientResult.TokenUrl == "" { - success = false - return nil, fmt.Errorf("client creation returned empty token url") - } - - clientID = clientResult.ClientID - authClientSecret = clientResult.ClientSecret - authTokenUrl = clientResult.TokenUrl - } - - success = true - // Log successful onboarding - deviceLogger.InfofCtx(context.Background(), "Successfully onboarded device", "clientId", clientID) + deviceLogger.InfofCtx(context.Background(), "Successfully onboarded device", "clientId", deviceClientId) - return &DeviceOnboardingData{ - ClientId: clientID, - ClientSecret: authClientSecret, - TokenEndpointUrl: authTokenUrl, - }, nil + return nil } func (dm *DeviceManager) ListDevices(ctx context.Context) (margoNonStdAPI.DeviceListResp, error) { devices := margoNonStdAPI.DeviceListResp{ - Items: []margoNonStdAPI.DeviceManifestResp{}, - Metadata: &margoNonStdAPI.PaginationMetadata{}, + Items: []margoNonStdAPI.DeviceManifestResp{}, + Metadata: &margoNonStdAPI.PaginationMetadata{}, } rows, err := dm.Database.ListDevices(ctx) @@ -445,7 +359,6 @@ func (dm *DeviceManager) ListDevices(ctx context.Context) (margoNonStdAPI.Device Id: &row.DeviceClientId, Spec: margoNonStdAPI.DeviceSpec{ Capabilities: row.Capabilities, - Signature: row.DevicePubCert, }, // setting unknown eligibility here, as it is not checked while fetching. after fetching, it should be checked for eligibility Eligible: p.Ptr(margoNonStdAPI.Unknown), @@ -458,18 +371,6 @@ func (dm *DeviceManager) ListDevices(ctx context.Context) (margoNonStdAPI.Device return devices, nil } -// Helper function to generate unique device ID -func generateDeviceClientID() string { - return fmt.Sprintf("client-%s-%d", fmt.Sprintf("%x", rand.Uint64()), time.Now().Unix()) - // return ) -} - -type DeviceOnboardingData struct { - ClientId string - ClientSecret string - TokenEndpointUrl string -} - func (s *DeviceManager) ShouldReplaceBundle(ctx context.Context, deviceClientId string, clientETag *string) (bool, string, *margoStdAPI.UnsignedAppStateManifest, error) { deviceLogger.InfofCtx(ctx, "ShouldReplaceBundle: Processing request for device %s", deviceClientId) @@ -482,7 +383,6 @@ func (s *DeviceManager) ShouldReplaceBundle(ctx context.Context, deviceClientId // Call GetBundle to retrieve bundle information bundleArchivePath, bundleManifest, err := s.GetBundle(ctx, deviceClientId, nil) - // Log the return values from GetBundle if err != nil { deviceLogger.InfofCtx(ctx, "ShouldReplaceBundle: GetBundle returned error: %v", err) @@ -663,7 +563,22 @@ func (s *DeviceManager) SaveDeviceCapabilities(ctx context.Context, deviceClient } func (s *DeviceManager) UpdateDeviceCapabilities(ctx context.Context, deviceClientId string, capabilities margoStdAPI.DeviceCapabilitiesManifest) error { - return s.Database.UpdateDeviceCapabilities(ctx, deviceClientId, &capabilities) + err := s.Database.UpdateDeviceCapabilities(ctx, deviceClientId, &capabilities) + if err == nil { + return nil + } + + verr, ok := err.(v1alpha2.COAError) + if !ok { + return err + } + + if verr.State != v1alpha2.NotFound { + return err + } + + // NOT FOUND ERROR HERE -- enroll it. + return s.OnboardDevice(ctx, deviceClientId, capabilities) } func (s *DeviceManager) GetDeviceCapabilities(ctx context.Context, deviceClientId string) (*margoStdAPI.DeviceCapabilitiesManifest, error) { @@ -674,10 +589,6 @@ func (s *DeviceManager) GetDeviceCapabilities(ctx context.Context, deviceClientI return device.Capabilities, nil } -func (s *DeviceManager) GetDeviceFromSignature(ctx context.Context, sign string) (*DeviceDatabaseRow, error) { - return s.Database.GetDeviceUsingPubCert(ctx, sign) -} - func (s *DeviceManager) GetDeviceClientUsingId(ctx context.Context, clientId string) (*DeviceDatabaseRow, error) { device, err := s.Database.GetDevice(ctx, clientId) if err != nil { @@ -692,13 +603,3 @@ func (s *DeviceManager) GetDeviceClientUsingId(ctx context.Context, clientId str return device, nil } - -func (s *DeviceManager) GetServerCA(ctx context.Context) ([]byte, error) { - // review: this function is not suitable for DeviceManager, it should be kept separately as settings in the vendor itself - serverCAPath, exists := s.Config.Properties["serverCAPath"] - if !exists || serverCAPath == "" { - return nil, fmt.Errorf("serverCAPath property is empty in the config") - } - - return os.ReadFile(serverCAPath) -} 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 ac968285b..27959df87 100644 --- a/api/pkg/apis/v1alpha1/vendors/margo/device-agent-vendor.go +++ b/api/pkg/apis/v1alpha1/vendors/margo/device-agent-vendor.go @@ -6,6 +6,7 @@ import ( "encoding/json" "fmt" "os" + "slices" "strings" "github.com/eclipse-symphony/symphony/api/pkg/apis/v1alpha1/managers/margo" @@ -16,6 +17,7 @@ import ( "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" "gopkg.in/yaml.v2" @@ -131,6 +133,13 @@ func (self *DeviceAgentVendor) updateDeviceCapabilities(request v1alpha2.COARequ "Missing deviceId parameter", v1alpha2.BadRequest) } + deviceSpiffeId, err := ExtractPeerSpiffeID(request) + if err != nil { + return createErrorResponse2(deviceVendorLogger, span, + v1alpha2.NewCOAError(nil, err.Error(), v1alpha2.BadRequest), + "failed to extract device spiffeId", v1alpha2.BadRequest) + } + // Parse request body using the correct DeviceCapabilities type var capabilities margoStdSbiAPI.DeviceCapabilitiesManifest if err := json.Unmarshal(request.Body, &capabilities); err != nil { @@ -138,7 +147,6 @@ func (self *DeviceAgentVendor) updateDeviceCapabilities(request v1alpha2.COARequ } // Validate required fields - if capabilities.Properties.Id == "" { return createErrorResponse2(deviceVendorLogger, span, v1alpha2.NewCOAError(nil, "device ID in properties is required", v1alpha2.BadRequest), @@ -153,7 +161,8 @@ func (self *DeviceAgentVendor) updateDeviceCapabilities(request v1alpha2.COARequ } // Call DeviceManager to update capabilities - err := self.DeviceManager.UpdateDeviceCapabilities(pCtx, deviceId, 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) } @@ -175,6 +184,13 @@ func (self *DeviceAgentVendor) onDeploymentStatusUpdate(request v1alpha2.COARequ }) defer span.End() + 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) + } + deploymentId := request.Parameters["__deploymentId"] if deploymentId == "" { return createErrorResponse2(deviceVendorLogger, span, @@ -193,12 +209,7 @@ func (self *DeviceAgentVendor) onDeploymentStatusUpdate(request v1alpha2.COARequ return createErrorResponse2(deviceVendorLogger, span, err, "Failed to update deployment status", v1alpha2.BadRequest) } - // Temporary workaround: Extract deviceId from request body if available, otherwise use empty string( need to figureout with MIAF) - deviceId := "" - if statusReq.DeviceId != nil { - deviceId = string(*statusReq.DeviceId) - } - if err := self.DeviceManager.OnDeploymentStatus(pCtx, deviceId, deploymentId, string(statusReq.Status.State)); err != nil { + 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) } @@ -231,24 +242,27 @@ func (self *DeviceAgentVendor) getDesiredManifest(request v1alpha2.COARequest) v "Not Acceptable", v1alpha2.NotAcceptable) } - // deviceId from query param or TODO: from mTLS SPIFFE ID - // TODO: MIAF SUP — extract deviceId from mTLS client certificate SPIFFE ID - deviceId := request.Parameters["deviceId"] // temporary PoC workaround + 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) + } - deviceVendorLogger.InfofCtx(pCtx, "Processing request for deviceClientId: %s", deviceId) + deviceVendorLogger.InfofCtx(pCtx, "Processing request for deviceClientId: %s", deviceClientId) // Fix: Use lowercase header key digest := headers["if-none-match"] deviceVendorLogger.DebugfCtx(pCtx, "If-None-Match digest: %s", digest) - shouldReplaceBundle, _, manifest, err := self.DeviceManager.ShouldReplaceBundle(pCtx, deviceId, &digest) + shouldReplaceBundle, _, manifest, err := self.DeviceManager.ShouldReplaceBundle(pCtx, deviceClientId, &digest) if err != nil { - deviceVendorLogger.ErrorfCtx(pCtx, "ShouldReplaceBundle failed for device %s: %v", deviceId, err) + deviceVendorLogger.ErrorfCtx(pCtx, "ShouldReplaceBundle failed for device %s: %v", deviceClientId, err) return createErrorResponse2(deviceVendorLogger, span, err, "Failed to get the desired states", v1alpha2.InternalError) } if manifest == nil { - deviceVendorLogger.ErrorfCtx(pCtx, "Manifest is nil for device %s", deviceId) + 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) @@ -271,10 +285,10 @@ func (self *DeviceAgentVendor) getDesiredManifest(request v1alpha2.COARequest) v etag = fmt.Sprintf("\"sha256:%x\"", hash) deviceVendorLogger.InfofCtx(pCtx, "Returning empty manifest for device %s - Version: %d, ETag: %s", - deviceId, manifestVersionInt, etag) + deviceClientId, manifestVersionInt, etag) } else { if manifest.Bundle.Digest == nil { - deviceVendorLogger.ErrorfCtx(pCtx, "Manifest bundle digest is nil for device %s", deviceId) + 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) @@ -284,7 +298,7 @@ func (self *DeviceAgentVendor) getDesiredManifest(request v1alpha2.COARequest) v etag = fmt.Sprintf("\"%s\"", *manifest.Bundle.Digest) deviceVendorLogger.InfofCtx(pCtx, "Returning bundle manifest for device %s - Version: %d, Digest: %s, Deployments: %d", - deviceId, manifestVersionInt, *manifest.Bundle.Digest, len(manifest.Deployments)) + deviceClientId, manifestVersionInt, *manifest.Bundle.Digest, len(manifest.Deployments)) } // Set headers directly in fasthttp context @@ -301,7 +315,7 @@ func (self *DeviceAgentVendor) getDesiredManifest(request v1alpha2.COARequest) v // Check if client already has this manifest (digest matches) if !shouldReplaceBundle { - deviceVendorLogger.InfofCtx(pCtx, "Bundle not modified for device %s, returning 304 - ETag: %s", deviceId, etag) + deviceVendorLogger.InfofCtx(pCtx, "Bundle not modified for device %s, returning 304 - ETag: %s", deviceClientId, etag) // Return NotModified state - COA framework will convert to HTTP 304 response := v1alpha2.COAResponse{ @@ -316,7 +330,7 @@ func (self *DeviceAgentVendor) getDesiredManifest(request v1alpha2.COARequest) v return response } - deviceVendorLogger.InfofCtx(pCtx, "Returning new manifest for device %s - ETag: %s", deviceId, etag) + deviceVendorLogger.InfofCtx(pCtx, "Returning new manifest for device %s - ETag: %s", deviceClientId, etag) // Serialize manifest manifestJSON, err := json.Marshal(manifest) @@ -358,13 +372,7 @@ func (self *DeviceAgentVendor) downloadBundle(request v1alpha2.COARequest) v1alp } accept := headers["accept"] if accept != "" { - validAccept := false - for _, validType := range acceptedTypes { - if accept == validType { - validAccept = true - break - } - } + validAccept := slices.Contains(acceptedTypes, accept) if !validAccept { return createErrorResponse2(deviceVendorLogger, span, v1alpha2.NewCOAError(nil, @@ -374,13 +382,11 @@ func (self *DeviceAgentVendor) downloadBundle(request v1alpha2.COARequest) v1alp } } - // Extract and validate parameters - // AFTER — PoC: deviceId from query param (TODO: MIAF — from mTLS SPIFFE ID) - deviceId := request.Parameters["deviceId"] // query param workaround - if deviceId == "" { + deviceClientId, err := ExtractPeerSpiffeID(request) + if err != nil { return createErrorResponse2(deviceVendorLogger, span, - v1alpha2.NewCOAError(nil, "deviceId is required", v1alpha2.BadRequest), - "Missing deviceId parameter", v1alpha2.BadRequest) + v1alpha2.NewCOAError(nil, err.Error(), v1alpha2.BadRequest), + "failed to extract device spiffeId", v1alpha2.BadRequest) } requestedDigest := request.Parameters["__digest"] @@ -394,7 +400,7 @@ func (self *DeviceAgentVendor) downloadBundle(request v1alpha2.COARequest) v1alp clientETag := headers["if-none-match"] // Get bundle from database - path, manifest, err := self.DeviceManager.GetBundle(pCtx, deviceId, &requestedDigest) + path, manifest, err := self.DeviceManager.GetBundle(pCtx, deviceClientId, &requestedDigest) if err != nil { return createErrorResponse2(deviceVendorLogger, span, err, "Bundle not found", v1alpha2.NotFound) @@ -420,7 +426,7 @@ func (self *DeviceAgentVendor) downloadBundle(request v1alpha2.COARequest) v1alp if clientETag != "" && clientETagClean == serverETagClean { deviceVendorLogger.InfofCtx(pCtx, "Bundle not modified for device %s (304) - ETag: %s", - deviceId, serverETag) + deviceClientId, serverETag) // Return 304 Not Modified return v1alpha2.COAResponse{ @@ -445,7 +451,7 @@ func (self *DeviceAgentVendor) downloadBundle(request v1alpha2.COARequest) v1alp if actualDigest != requestedDigest { deviceVendorLogger.ErrorfCtx(pCtx, "Bundle digest mismatch for device %s: requested=%s, actual=%s", - deviceId, requestedDigest, actualDigest) + deviceClientId, requestedDigest, actualDigest) // Per spec: "If the server cannot produce content whose digest matches this value // it MUST return 404 Not Found" @@ -459,7 +465,7 @@ func (self *DeviceAgentVendor) downloadBundle(request v1alpha2.COARequest) v1alp deviceVendorLogger.InfofCtx(pCtx, "Serving bundle for device %s with verified digest %s (%d bytes)", - deviceId, actualDigest, len(bundleData)) + deviceClientId, actualDigest, len(bundleData)) // Return with proper headers return createSuccessResponseWithHeaders( @@ -642,3 +648,34 @@ func ParseRequestHeaders(ctx context.Context) (map[string]string, error) { } return nil, nil } + +// ExtractTLSCertificates extracts the client (peer) certificates +// from the mTLS connection embedded in the COARequest context. +// +// Returns (clientSpiffeID, serverSpiffeID, error). +func ExtractPeerSpiffeID(request v1alpha2.COARequest) (string, error) { + fhCtx, ok := request.Context.Value(v1alpha2.COAFastHTTPContextKey).(*fasthttp.RequestCtx) + if !ok || fhCtx == nil { + return "", fmt.Errorf("fasthttp context not available in request") + } + + tlsState := fhCtx.TLSConnectionState() + if tlsState == nil { + return "", fmt.Errorf("TLS connection state is nil — connection may not be over mTLS") + } + + // ---------------------------------------------------------------- + // Extract client certificate (peer certificate presented during mTLS handshake). + // ---------------------------------------------------------------- + if len(tlsState.PeerCertificates) == 0 { + return "", fmt.Errorf("no client certificate presented by peer") + } + clientCert := tlsState.PeerCertificates[0] // leaf is always index 0 + + clientSpiffeID, err := parser.ParseSpiffeIdFromX509Svid(clientCert.Raw) // replace with parsed SPIFFE ID from clientCert + if err != nil { + return "", fmt.Errorf("failed to parse client spiffeId, err: %w", err) + } + + return clientSpiffeID, nil +} diff --git a/api/symphony-api-margo.json b/api/symphony-api-margo.json index 119b49b6c..072d8c064 100644 --- a/api/symphony-api-margo.json +++ b/api/symphony-api-margo.json @@ -741,8 +741,7 @@ "type": "managers.margo.device", "properties": { "providers.persistentstate": "redis-state", - "singleton": "true", - "serverCAPath": "./certificates/ca-cert.pem" + "singleton": "true" }, "providers": { "redis-state": { @@ -776,6 +775,45 @@ }, "pipeline": [ + { + "type": "middleware.http.cors", + "properties": { + "Access-Control-Allow-Headers": "authorization,Content-Type", + "Access-Control-Allow-Credentials": "true", + "Access-Control-Allow-Methods": "HEAD,GET,POST,PUT,DELETE,OPTIONS", + "Access-Control-Allow-Origin": "*" + } + } + ] + } + }, + { + "type": "bindings.http", + "config": { + "port": 8084, + "mtls": true, + "miaf": { + "mis": { + "endpoint": "https://mis.margo.org:9443", + "caPath": "./mis/https-ca.crt", + "cacheInterval": 60, + "trustDomain": "margo.org", + "trustBundle": { + "uri": "/.well-known/spiffe/bundle.json", + "path": "./mis/trust-bundle.json" + } + }, + "authzPath": "./mis/authorized-clients.json" + }, + "certProvider": { + "type": "certs.localfile", + "config": { + "name": "margo-sbi-svid-cert", + "cert": "./certificates/payload-cert.pem", + "key": "./certificates/payload-key.pem" + } + }, + "pipeline": [ { "type": "middleware.http.cors", "properties": { @@ -789,4 +827,4 @@ } } ] -} +} \ No newline at end of file diff --git a/cli/cmd/margo.go b/cli/cmd/margo.go index b61e2919f..2ca01da6a 100644 --- a/cli/cmd/margo.go +++ b/cli/cmd/margo.go @@ -4,9 +4,8 @@ import ( "encoding/json" "fmt" "os" - "time" - "strings" + "time" "github.com/eclipse-symphony/symphony/cli/utils" "github.com/ghodss/yaml" @@ -284,47 +283,47 @@ var MargoGetDeploymentCmd = &cobra.Command{ // Implementation functions func applyAppConfig(filename string) error { - yamlFile, err := os.ReadFile(filename) - if err != nil { - return fmt.Errorf("failed to read file: %w", err) - } - - var data map[string]interface{} - if err := yaml.Unmarshal(yamlFile, &data); err != nil { - return fmt.Errorf("failed to unmarshal YAML: %w", err) - } - - jsonFile, err := convertYamlToJson(yamlFile) - if err != nil { - return fmt.Errorf("failed to convert yaml to json: %w", err) - } - - // Determine resource type by spec fields since kind is removed - spec, _ := data["spec"].(map[string]interface{}) - if spec == nil { - return fmt.Errorf("spec not found in resource") - } - - switch { - case spec["sourceType"] != nil: - // ApplicationPackageManifestRequest has spec.sourceType - var appPkg nbi.ApplicationPackageManifestRequest - if err := json.Unmarshal(jsonFile, &appPkg); err != nil { - return fmt.Errorf("failed to unmarshal ApplicationPackage: %w", err) - } - return onboardAppPkg(&appPkg) - - case spec["appPackageRef"] != nil: - // ApplicationDeploymentManifestRequest has spec.appPackageRef - var deployment nbi.ApplicationDeploymentManifestRequest - if err := json.Unmarshal(jsonFile, &deployment); err != nil { - return fmt.Errorf("failed to unmarshal ApplicationDeployment: %w", err) - } - return createDeployment(&deployment) - - default: - return fmt.Errorf("cannot determine resource type: spec must contain 'sourceType' (ApplicationPackage) or 'appPackageRef' (ApplicationDeployment)") - } + yamlFile, err := os.ReadFile(filename) + if err != nil { + return fmt.Errorf("failed to read file: %w", err) + } + + var data map[string]interface{} + if err := yaml.Unmarshal(yamlFile, &data); err != nil { + return fmt.Errorf("failed to unmarshal YAML: %w", err) + } + + jsonFile, err := convertYamlToJson(yamlFile) + if err != nil { + return fmt.Errorf("failed to convert yaml to json: %w", err) + } + + // Determine resource type by spec fields since kind is removed + spec, _ := data["spec"].(map[string]interface{}) + if spec == nil { + return fmt.Errorf("spec not found in resource") + } + + switch { + case spec["sourceType"] != nil: + // ApplicationPackageManifestRequest has spec.sourceType + var appPkg nbi.ApplicationPackageManifestRequest + if err := json.Unmarshal(jsonFile, &appPkg); err != nil { + return fmt.Errorf("failed to unmarshal ApplicationPackage: %w", err) + } + return onboardAppPkg(&appPkg) + + case spec["appPackageRef"] != nil: + // ApplicationDeploymentManifestRequest has spec.appPackageRef + var deployment nbi.ApplicationDeploymentManifestRequest + if err := json.Unmarshal(jsonFile, &deployment); err != nil { + return fmt.Errorf("failed to unmarshal ApplicationDeployment: %w", err) + } + return createDeployment(&deployment) + + default: + return fmt.Errorf("cannot determine resource type: spec must contain 'sourceType' (ApplicationPackage) or 'appPackageRef' (ApplicationDeployment)") + } } // createNorthboundClient creates a configured northbound client @@ -564,7 +563,7 @@ func displayDevicesTable(resp nbi.DeviceListResp, eligibilityMarker bool) { t.SetOutputMirror(os.Stdout) tr := table.Row{ - "ID", "Signature", "Capabilities", "Deployment Type", "State", "CreatedAt", + "ID", "Capabilities", "Deployment Type", "State", "CreatedAt", } if eligibilityMarker { @@ -576,7 +575,7 @@ func displayDevicesTable(resp nbi.DeviceListResp, eligibilityMarker bool) { // Add data rows for _, device := range resp.Items { - if device.Id == nil || *device.Id == "" { + if device.Id == nil || *device.Id == "" { continue } @@ -597,8 +596,7 @@ func displayDevicesTable(resp nbi.DeviceListResp, eligibilityMarker bool) { cap, _ := json.Marshal(device.Spec.Capabilities) row := table.Row{ - truncateString(*device.Id, 40), - truncateString(device.Spec.Signature, 28), + *device.Id, // deliberately not truncating device Id truncateString(string(cap), 28), deploymentTypeStr, string(device.State.Onboard), @@ -614,13 +612,12 @@ func displayDevicesTable(resp nbi.DeviceListResp, eligibilityMarker bool) { t.AppendFooter(table.Row{ "", "", "", fmt.Sprintf("Page %d/%d", 1, 1), - fmt.Sprintf("Total: %d", 1), //resp.Metadata.TotalItems), + fmt.Sprintf("Total: %d", 1), // resp.Metadata.TotalItems), }) // Configure column settings t.SetColumnConfigs([]table.ColumnConfig{ - {Number: 1, WidthMax: 40}, // ID - {Number: 2, WidthMax: 28}, // Signature + {Number: 1, WidthMax: 60}, // ID {Number: 3, WidthMax: 28}, // Capabilities {Number: 4, WidthMax: 28}, // Deployment Type {Number: 5, WidthMax: 12}, // State @@ -668,8 +665,8 @@ func displayAppPackagesTable(resp nbi.ApplicationPackageListResp) { // Add footer with pagination t.AppendFooter(table.Row{ "", "", "", "", "", "", "", - fmt.Sprintf("Page %d/%d", 1, 1), //resp.Metadata.Page, resp.Metadata.TotalPages), - fmt.Sprintf("Total: %d", 1), //resp.Metadata.TotalItems), + fmt.Sprintf("Page %d/%d", 1, 1), // resp.Metadata.Page, resp.Metadata.TotalPages), + fmt.Sprintf("Total: %d", 1), // resp.Metadata.TotalItems), }) // Configure column settings @@ -792,7 +789,6 @@ func printAppPkgDetails(appPkg *nbi.ApplicationPackageManifestResp) { fmt.Printf(" Name: %s\n", appPkg.Metadata.Name) fmt.Printf(" API Version: %s\n", appPkg.ApiVersion) - fmt.Printf(" Metadata:\n") fmt.Printf(" Creation Timestamp: %s\n", appPkg.Metadata.CreationTimestamp) fmt.Printf(" Namespace: %s\n", *appPkg.Metadata.Namespace) @@ -805,7 +801,7 @@ func printAppPkgDetails(appPkg *nbi.ApplicationPackageManifestResp) { fmt.Printf(" OCI Source:\n") fmt.Printf(" URL: %s\n", ociRepo.RegistryUrl) fmt.Printf(" Repository: %s\n", *&ociRepo.Repository) - fmt.Printf(" Revision: %s\n", *ociRepo.Tag) + fmt.Printf(" Revision: %s\n", *ociRepo.Tag) } fmt.Printf(" Status:\n") diff --git a/cli/go.mod b/cli/go.mod index 64d7185fc..a1799730e 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.20260907065449-0c752aada0a4 + github.com/margo/sandbox v1.0.0-rc.2.0.20260914140431-3329621a9f84 github.com/princjef/mageutil v1.0.0 ) diff --git a/cli/go.sum b/cli/go.sum index 654111b88..4e057d57d 100644 --- a/cli/go.sum +++ b/cli/go.sum @@ -55,8 +55,8 @@ github.com/lestrrat-go/option v1.0.1 h1:oAzP2fvZGQKWkvHa1/SAcFolBEca1oN+mQ7eooNB github.com/lestrrat-go/option v1.0.1/go.mod h1:5ZHFbivi4xwXxhxY9XHDe2FHo6/Z7WWmtT7T5nBBp3I= 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.20260907065449-0c752aada0a4 h1:Ski7cJ+qbMcrM+CRJ7exw5nJZAmt78hBjemOFFZLBt4= -github.com/margo/sandbox v1.0.0-rc.2.0.20260907065449-0c752aada0a4/go.mod h1:spPdVkDLs/kfUcvKXIIGmgrAmD+z7Po9QkbHVdEp03U= +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/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/go.mod b/coa/go.mod index 3dac48583..f64b1167f 100644 --- a/coa/go.mod +++ b/coa/go.mod @@ -15,6 +15,7 @@ require ( github.com/fasthttp/router v1.4.20 github.com/golang-jwt/jwt/v4 v4.5.2 github.com/google/uuid v1.6.0 + github.com/margo/sandbox v1.0.0-rc.2.0.20260914132812-996e3c369a28 github.com/microsoft/ApplicationInsights-Go v0.4.4 github.com/pkg/errors v0.9.1 github.com/redis/go-redis/v9 v9.7.3 @@ -28,7 +29,7 @@ require ( go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.8.0 go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.43.0 go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.43.0 - go.opentelemetry.io/otel/exporters/stdout/stdoutlog v0.8.0 + go.opentelemetry.io/otel/exporters/stdout/stdoutlog v0.20.0 go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.32.0 go.opentelemetry.io/otel/exporters/zipkin v1.11.1 go.opentelemetry.io/otel/log v0.20.0 @@ -37,7 +38,7 @@ require ( go.opentelemetry.io/otel/sdk/log v0.20.0 go.opentelemetry.io/otel/sdk/metric v1.44.0 go.opentelemetry.io/otel/trace v1.44.0 - golang.org/x/sync v0.20.0 + golang.org/x/sync v0.21.0 google.golang.org/grpc v1.80.0 k8s.io/api v0.35.1 k8s.io/apimachinery v0.35.1 @@ -50,7 +51,8 @@ require ( github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.1 // indirect github.com/AzureAD/microsoft-authentication-library-for-go v1.4.2 // indirect github.com/VividCortex/ewma v1.1.1 // indirect - github.com/andybalholm/brotli v1.0.5 // indirect + github.com/andybalholm/brotli v1.1.0 // indirect + github.com/apapsch/go-jsonmerge/v2 v2.0.0 // indirect github.com/cenkalti/backoff/v4 v4.3.0 // indirect github.com/cenkalti/backoff/v5 v5.0.3 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect @@ -61,6 +63,7 @@ require ( github.com/fatih/color v1.18.0 // indirect github.com/fsnotify/fsnotify v1.9.0 // indirect github.com/fxamacker/cbor/v2 v2.9.0 // indirect + github.com/go-jose/go-jose/v4 v4.1.3 // indirect github.com/go-logr/logr v1.4.3 // indirect github.com/go-logr/stdr v1.2.2 // indirect github.com/go-openapi/jsonpointer v0.22.4 // indirect @@ -85,7 +88,7 @@ require ( github.com/itchyny/gojq v0.12.16 // indirect github.com/itchyny/timefmt-go v0.1.6 // indirect github.com/json-iterator/go v1.1.12 // indirect - github.com/klauspost/compress v1.18.4 // indirect + github.com/klauspost/compress v1.18.5 // indirect github.com/kylelemons/godebug v1.1.0 // indirect github.com/magefile/mage v1.15.0 // indirect github.com/mattn/go-colorable v0.1.14 // indirect @@ -94,14 +97,16 @@ require ( github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect + github.com/oapi-codegen/runtime v1.6.0 // indirect github.com/oliveagle/jsonpath v0.0.0-20180606110733-2e52cf6e6852 // indirect - github.com/openzipkin/zipkin-go v0.4.1 // indirect + github.com/openzipkin/zipkin-go v0.4.3 // indirect github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/princjef/mageutil v1.0.0 // indirect github.com/rivo/uniseg v0.4.7 // indirect github.com/savsgio/gotils v0.0.0-20230208104028-c358bd845dee // indirect github.com/spf13/pflag v1.0.10 // indirect + github.com/spiffe/go-spiffe/v2 v2.6.0 // indirect github.com/valyala/bytebufferpool v1.0.0 // indirect github.com/x448/float16 v0.8.4 // indirect go.opentelemetry.io/auto/sdk v1.2.1 // indirect @@ -109,13 +114,13 @@ require ( go.opentelemetry.io/proto/otlp v1.10.0 // indirect go.yaml.in/yaml/v2 v2.4.3 // indirect go.yaml.in/yaml/v3 v3.0.4 // indirect - golang.org/x/crypto v0.50.0 // indirect + golang.org/x/crypto v0.53.0 // indirect golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 // indirect - golang.org/x/net v0.53.0 // indirect + golang.org/x/net v0.55.0 // indirect golang.org/x/oauth2 v0.35.0 // indirect - golang.org/x/sys v0.45.0 // indirect - golang.org/x/term v0.42.0 // indirect - golang.org/x/text v0.36.0 // indirect + golang.org/x/sys v0.46.0 // indirect + golang.org/x/term v0.44.0 // indirect + golang.org/x/text v0.38.0 // indirect golang.org/x/time v0.14.0 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20260401024825-9d38bb4040a9 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20260401024825-9d38bb4040a9 // indirect diff --git a/coa/go.sum b/coa/go.sum index 9998f2468..32d1e89ab 100644 --- a/coa/go.sum +++ b/coa/go.sum @@ -18,10 +18,14 @@ github.com/AzureAD/microsoft-authentication-library-for-go v1.4.2 h1:oygO0locgZJ github.com/AzureAD/microsoft-authentication-library-for-go v1.4.2/go.mod h1:wP83P5OoQ5p6ip3ScPr0BAq0BvuPAvacpEuSzyouqAI= github.com/Masterminds/semver/v3 v3.4.0 h1:Zog+i5UMtVoCU8oKka5P7i9q9HgrJeGzI9SA1Xbatp0= github.com/Masterminds/semver/v3 v3.4.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM= +github.com/RaveNoX/go-jsoncommentstrip v1.0.0/go.mod h1:78ihd09MekBnJnxpICcwzCMzGrKSKYe4AqU6PDYYpjk= github.com/VividCortex/ewma v1.1.1 h1:MnEK4VOv6n0RSY4vtRe3h11qjxL3+t0B8yOL8iMXdcM= github.com/VividCortex/ewma v1.1.1/go.mod h1:2Tkkvm3sRDVXaiyucHiACn4cqf7DpdyLvmxzcbUokwA= -github.com/andybalholm/brotli v1.0.5 h1:8uQZIdzKmjc/iuPu7O2ioW48L81FgatrcpfFmiq/cCs= -github.com/andybalholm/brotli v1.0.5/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig= +github.com/andybalholm/brotli v1.1.0 h1:eLKJA0d02Lf0mVpIDgYnqXcUn0GqVmEFny3VuID1U3M= +github.com/andybalholm/brotli v1.1.0/go.mod h1:sms7XGricyQI9K10gOSf56VKKWS4oLer58Q+mhRPtnY= +github.com/apapsch/go-jsonmerge/v2 v2.0.0 h1:axGnT1gRIfimI7gJifB699GoE/oq+F2MU7Dml6nw9rQ= +github.com/apapsch/go-jsonmerge/v2 v2.0.0/go.mod h1:lvDnEdqiQrp0O42VQGgmlKpxL1AP2+08jFMw88y4klk= +github.com/bmatcuk/doublestar v1.1.1/go.mod h1:UD6OnuiIn0yFxxA2le/rnRU1G4RaI4UvFv1sNto9p6w= github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs= github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c= github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA= @@ -56,6 +60,8 @@ github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= github.com/fxamacker/cbor/v2 v2.9.0 h1:NpKPmjDBgUfBms6tr6JZkTHtfFGcMKsw3eGcmD/sapM= github.com/fxamacker/cbor/v2 v2.9.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ= +github.com/go-jose/go-jose/v4 v4.1.3 h1:CVLmWDhDVRa6Mi/IgCgaopNosCaHz7zrMeF9MlZRkrs= +github.com/go-jose/go-jose/v4 v4.1.3/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= @@ -95,6 +101,7 @@ github.com/go-openapi/testify/enable/yaml/v2 v2.0.2 h1:0+Y41Pz1NkbTHz8NngxTuAXxE github.com/go-openapi/testify/enable/yaml/v2 v2.0.2/go.mod h1:kme83333GCtJQHXQ8UKX3IBZu6z8T5Dvy5+CW3NLUUg= github.com/go-openapi/testify/v2 v2.0.2 h1:X999g3jeLcoY8qctY/c/Z8iBHTbwLz7R2WXd6Ub6wls= github.com/go-openapi/testify/v2 v2.0.2/go.mod h1:HCPmvFFnheKK2BuwSA0TbbdxJ3I16pjwMkYkP4Ywn54= +github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI= github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI= github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8= github.com/gofrs/uuid v3.3.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= @@ -112,8 +119,8 @@ github.com/google/gnostic-models v0.7.0/go.mod h1:whL5G0m6dmc5cPxKc5bdKdEN3UjI7O github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= -github.com/google/pprof v0.0.0-20250403155104-27863c87afa6 h1:BHT72Gu3keYf3ZEu2J0b1vyeLSOYI8bm5wbJM/8yDe8= -github.com/google/pprof v0.0.0-20250403155104-27863c87afa6/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA= +github.com/google/pprof v0.0.0-20250820193118-f64d9cf942d6 h1:EEHtgt9IwisQ2AZ4pIsMjahcegHh6rmhqxzIRQIyepY= +github.com/google/pprof v0.0.0-20250820193118-f64d9cf942d6/go.mod h1:I6V7YzU0XDpsHqbsyrghnFZLO1gwK6NPTNvmetQIk9U= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= @@ -128,10 +135,11 @@ github.com/itchyny/timefmt-go v0.1.6 h1:ia3s54iciXDdzWzwaVKXZPbiXzxxnv1SPGFfM/my github.com/itchyny/timefmt-go v0.1.6/go.mod h1:RRDZYC5s9ErkjQvTvvU7keJjxUYzIISJGxm9/mAERQg= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/juju/gnuflag v0.0.0-20171113085948-2ce1bb71843d/go.mod h1:2PavIy+JPciBPrBUjwbNvtwB6RQlve+hkpll6QSNmOE= github.com/keybase/go-keychain v0.0.1 h1:way+bWYa6lDppZoZcgMbYsvC7GxljxrskdNInRtuthU= github.com/keybase/go-keychain v0.0.1/go.mod h1:PdEILRW3i9D8JcdM+FmY6RwkHGnhHxXwkPPMeUgOK1k= -github.com/klauspost/compress v1.18.4 h1:RPhnKRAQ4Fh8zU2FY/6ZFDwTVTxgJ/EMydqSTzE9a2c= -github.com/klauspost/compress v1.18.4/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4= +github.com/klauspost/compress v1.18.5 h1:/h1gH5Ce+VWNLSWqPzOVn6XBO+vJbCNGvjoaGBFW2IE= +github.com/klauspost/compress v1.18.5/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= @@ -143,6 +151,8 @@ github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0 github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= 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/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= @@ -168,18 +178,22 @@ github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee h1:W5t00kpgFd github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= +github.com/oapi-codegen/nullable v1.1.0 h1:eAh8JVc5430VtYVnq00Hrbpag9PFRGWLjxR1/3KntMs= +github.com/oapi-codegen/nullable v1.1.0/go.mod h1:KUZ3vUzkmEKY90ksAmit2+5juDIhIZhfDl+0PwOQlFY= +github.com/oapi-codegen/runtime v1.6.0 h1:7Xx+GlueD6nRuyKoCPzL434Jfi3BetbiJOrzCHp/VPU= +github.com/oapi-codegen/runtime v1.6.0/go.mod h1:GwV7hC2hviaMzj+ITfHVRESK5J2W/GefVwIND/bMGvU= github.com/oliveagle/jsonpath v0.0.0-20180606110733-2e52cf6e6852 h1:Yl0tPBa8QPjGmesFh1D0rDy+q1Twx6FyU7VWHi8wZbI= github.com/oliveagle/jsonpath v0.0.0-20180606110733-2e52cf6e6852/go.mod h1:eqOVx5Vwu4gd2mmMZvVZsgIqNSaW3xxRThUJ0k/TPk4= github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.8.0 h1:VkHVNpR4iVnU8XQR6DBm8BqYjN7CRzw+xKUbVVbbW9w= github.com/onsi/ginkgo v1.8.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= -github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE= github.com/onsi/ginkgo/v2 v2.27.2 h1:LzwLj0b89qtIy6SSASkzlNvX6WktqurSHwkk2ipF/Ns= github.com/onsi/ginkgo/v2 v2.27.2/go.mod h1:ArE1D/XhNXBXCBkKOLkbsb2c81dQHCRcF5zwn/ykDRo= github.com/onsi/gomega v1.5.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= github.com/onsi/gomega v1.38.2 h1:eZCjf2xjZAqe+LeWvKb5weQ+NcPwX84kqJ0cZNxok2A= github.com/onsi/gomega v1.38.2/go.mod h1:W2MJcYxRGV63b418Ai34Ud0hEdTVXq9NW9+Sx6uXf3k= -github.com/openzipkin/zipkin-go v0.4.1 h1:kNd/ST2yLLWhaWrkgchya40TJabe8Hioj9udfPcEO5A= -github.com/openzipkin/zipkin-go v0.4.1/go.mod h1:qY0VqDSN1pOBN94dBc6w2GJlWLiovAyg7Qt6/I9HecM= +github.com/openzipkin/zipkin-go v0.4.3 h1:9EGwpqkgnwdEIJ+Od7QVSEIH+ocmm5nPat0G7sjsSdg= +github.com/openzipkin/zipkin-go v0.4.3/go.mod h1:M9wCJZFWCo2RiY+o1eBCEMe0Dp2S5LDHcMZmk3RmK7c= github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ= github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= @@ -202,6 +216,9 @@ github.com/sirupsen/logrus v1.9.4 h1:TsZE7l11zFCLZnZ+teH4Umoq5BhEIfIzfRDZ1Uzql2w github.com/sirupsen/logrus v1.9.4/go.mod h1:ftWc9WdOfJ0a92nsE2jF5u5ZwH8Bv2zdeOC42RjbV2g= github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk= github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spiffe/go-spiffe/v2 v2.6.0 h1:l+DolpxNWYgruGQVV0xsfeya3CsC7m8iBzDnMpsbLuo= +github.com/spiffe/go-spiffe/v2 v2.6.0/go.mod h1:gm2SeUoMZEtpnzPNs2Csc0D/gX33k1xIx7lEzqblHEs= +github.com/spkg/bom v0.0.0-20160624110644-59b7046e48ad/go.mod h1:qLr4V1qq6nMqFKkMo8ZTx3f+BZEkzsRUY10Xsm2mwU0= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= @@ -233,8 +250,8 @@ go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.43.0 h1:88Y4s2C8oTui1LGM6bT go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.43.0/go.mod h1:Vl1/iaggsuRlrHf/hfPJPvVag77kKyvrLeD10kpMl+A= go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.43.0 h1:RAE+JPfvEmvy+0LzyUA25/SGawPwIUbZ6u0Wug54sLc= go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.43.0/go.mod h1:AGmbycVGEsRx9mXMZ75CsOyhSP6MFIcj/6dnG+vhVjk= -go.opentelemetry.io/otel/exporters/stdout/stdoutlog v0.8.0 h1:CHXNXwfKWfzS65yrlB2PVds1IBZcdsX8Vepy9of0iRU= -go.opentelemetry.io/otel/exporters/stdout/stdoutlog v0.8.0/go.mod h1:zKU4zUgKiaRxrdovSS2amdM5gOc59slmo/zJwGX+YBg= +go.opentelemetry.io/otel/exporters/stdout/stdoutlog v0.20.0 h1:aZfdmtI6QU/DAPD4b7YZ5zuJgewxO1EW9miOZklqleU= +go.opentelemetry.io/otel/exporters/stdout/stdoutlog v0.20.0/go.mod h1:isNl10/Om5CBWu9jj8WOb2+tJLbCVXDgqwzCaJMnJ6w= go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.32.0 h1:cC2yDI3IQd0Udsux7Qmq8ToKAx1XCilTQECZ0KDZyTw= go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.32.0/go.mod h1:2PD5Ex6z8CFzDbTdOlwyNIUywRr1DN0ospafJM1wJ+s= go.opentelemetry.io/otel/exporters/zipkin v1.11.1 h1:JlJ3/oQoyqlrPDCfsSVFcHgGeHvZq+hr1VPWtiYCXTo= @@ -266,22 +283,22 @@ go.yaml.in/yaml/v2 v2.4.3/go.mod h1:zSxWcmIDjOzPXpjlTTbAsKokqkDNAVtZO0WOMiT90s8= go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/crypto v0.50.0 h1:zO47/JPrL6vsNkINmLoo/PH1gcxpls50DNogFvB5ZGI= -golang.org/x/crypto v0.50.0/go.mod h1:3muZ7vA7PBCE6xgPX7nkzzjiUq87kRItoJQM1Yo8S+Q= +golang.org/x/crypto v0.53.0 h1:QZ4Muo8THX6CizN2vPPd5fBGHyogrdK9fG4wLPFUsto= +golang.org/x/crypto v0.53.0/go.mod h1:DNLU434OwVakk9PzuwV8w62mAJpRJL3vsgcfp4Qnsio= golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 h1:2dVuKD2vS7b0QIHQbpyTISPd0LeHDbnYEryqj5Q1ug8= golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56/go.mod h1:M4RDyNAINzryxdtnbRXRL/OHtkFuWGRjvuhBJpk2IlY= -golang.org/x/mod v0.34.0 h1:xIHgNUUnW6sYkcM5Jleh05DvLOtwc6RitGHbDk4akRI= -golang.org/x/mod v0.34.0/go.mod h1:ykgH52iCZe79kzLLMhyCUzhMci+nQj+0XkbXpNYtVjY= +golang.org/x/mod v0.36.0 h1:JJjpVx6myfUsUdAzZuOSTTmRE0PfZeNWzzvKrP7amb4= +golang.org/x/mod v0.36.0/go.mod h1:moc6ELqsWcOw5Ef3xVprK5ul/MvtVvkIXLziUOICjUQ= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20200425230154-ff2c4b7c35a0/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= -golang.org/x/net v0.53.0 h1:d+qAbo5L0orcWAr0a9JweQpjXF19LMXJE8Ey7hwOdUA= -golang.org/x/net v0.53.0/go.mod h1:JvMuJH7rrdiCfbeHoo3fCQU24Lf5JJwT9W3sJFulfgs= +golang.org/x/net v0.55.0 h1:bcvxaJn3e1U6InsFWt1JUq1aSjnRxLzT2rtD2KfkDF8= +golang.org/x/net v0.55.0/go.mod h1:L5U2KuzuOe1lY7Z+aWVIKK6qEeJXnXV9yzGA+WCHJww= golang.org/x/oauth2 v0.35.0 h1:Mv2mzuHuZuY2+bkyWXIHMfhNdJAdwW3FuWeCPYN5GVQ= golang.org/x/oauth2 v0.35.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4= -golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= +golang.org/x/sync v0.21.0 h1:HLII4xRRTtCRkxYp4HNFF0Js/Og6q2i++KXbg0gHCwM= +golang.org/x/sync v0.21.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -292,17 +309,17 @@ golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.45.0 h1:dO4czNzziLiiXplLQgBCEpCvXQ3dnkn0SdaZSYdQ+FY= -golang.org/x/sys v0.45.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= -golang.org/x/term v0.42.0 h1:UiKe+zDFmJobeJ5ggPwOshJIVt6/Ft0rcfrXZDLWAWY= -golang.org/x/term v0.42.0/go.mod h1:Dq/D+snpsbazcBG5+F9Q1n2rXV8Ma+71xEjTRufARgY= +golang.org/x/sys v0.46.0 h1:noSf2Fq6F8DBgS+LysIkx7rIExoNHJsxOAtPp4rthXw= +golang.org/x/sys v0.46.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= +golang.org/x/term v0.44.0 h1:0rLvDRCtNj0gZkyIXhCyOb2OAzEhLVqc4B+hrsBhrmc= +golang.org/x/term v0.44.0/go.mod h1:7ze4MdzUzLXpSAoFP1H0bOI9aXDqveSvatT5vKcFh2Y= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.36.0 h1:JfKh3XmcRPqZPKevfXVpI1wXPTqbkE5f7JA92a55Yxg= -golang.org/x/text v0.36.0/go.mod h1:NIdBknypM8iqVmPiuco0Dh6P5Jcdk8lJL0CUebqK164= +golang.org/x/text v0.38.0 h1:sXmwo9DwP3OK9EZ7PqAdaooSGozfl/3a6/xJcbzPRhE= +golang.org/x/text v0.38.0/go.mod h1:YXZt3QhHUKYT53r2lLKFIVi6Ao1jdzrTR/KQ09qyxF4= golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI= golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4= -golang.org/x/tools v0.43.0 h1:12BdW9CeB3Z+J/I/wj34VMl8X+fEXBxVR90JeMX5E7s= -golang.org/x/tools v0.43.0/go.mod h1:uHkMso649BX2cZK6+RpuIPXS3ho2hZo4FVwfoy1vIk0= +golang.org/x/tools v0.45.0 h1:18qN3FAooORvApf5XjCXgsuayZOEtXf6JK18I3+ONa8= +golang.org/x/tools v0.45.0/go.mod h1:LuUGqqaXcXMEFEruIVJVm5mgDD8vww/z/SR1gQ4uE/0= gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4= gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E= google.golang.org/genproto/googleapis/api v0.0.0-20260401024825-9d38bb4040a9 h1:VPWxll4HlMw1Vs/qXtN7BvhZqsS9cdAittCNvVENElA= diff --git a/coa/pkg/apis/v1alpha2/bindings/http/http.go b/coa/pkg/apis/v1alpha2/bindings/http/http.go index 3b0e05178..0e6eeb49d 100644 --- a/coa/pkg/apis/v1alpha2/bindings/http/http.go +++ b/coa/pkg/apis/v1alpha2/bindings/http/http.go @@ -8,12 +8,15 @@ package http import ( "context" + "crypto/tls" "encoding/json" "fmt" + "net" "strings" "time" v1alpha2 "github.com/eclipse-symphony/symphony/coa/pkg/apis/v1alpha2" + "github.com/eclipse-symphony/symphony/coa/pkg/apis/v1alpha2/margo/mis/trustbundle" "github.com/eclipse-symphony/symphony/coa/pkg/apis/v1alpha2/providers" "github.com/eclipse-symphony/symphony/coa/pkg/apis/v1alpha2/providers/certs" autogen "github.com/eclipse-symphony/symphony/coa/pkg/apis/v1alpha2/providers/certs/autogen" @@ -22,6 +25,10 @@ import ( "github.com/eclipse-symphony/symphony/coa/pkg/apis/v1alpha2/utils" "github.com/eclipse-symphony/symphony/coa/pkg/logger/contexts" routing "github.com/fasthttp/router" + "github.com/margo/sandbox/shared-lib/mis/mtls" + "github.com/margo/sandbox/shared-lib/mis/parser" + "github.com/margo/sandbox/shared-lib/mis/validators" + "github.com/valyala/fasthttp" ) @@ -41,15 +48,113 @@ type HttpBindingConfig struct { Port int `json:"port"` Pipeline []MiddlewareConfig `json:"pipeline"` TLS bool `json:"tls"` + MTLS bool `json:"mtls"` + MIAF *MIAF `json:"miaf"` CertProvider CertProviderConfig `json:"certProvider"` } +// TrustBundle represents the operator provided SPIFFE trust bundle configuration. +type TrustBundle struct { + URI string `json:"uri"` + Path string `json:"path"` +} + +// MIS represents the Margo Identity Service configuration. +type MIS struct { + Endpoint string `json:"endpoint"` + CAPath string `json:"caPath"` + CacheInterval int `json:"cacheInterval"` + TrustDomain string `json:"trustDomain"` + TrustBundle TrustBundle `json:"trustBundle"` +} + +// MIAF represents the Margo Identity and Authorization Framework configuration. +type MIAF struct { + MIS *MIS `json:"mis"` + AuthzPath string `json:"authzPath"` // path to json file containing allowed client's spiffe id +} + +// ToMIAFInput converts an http.MIAF config object into a parser.MIAFInput +// suitable for passing to parser.ParseMIAFConfig. +func (miaf *MIAF) ToMIAFInput() parser.MIAFInput { + mis := miaf.MIS + + // Map TrustBundle only when at least one field is populated, + // matching parser.ParseMIAFConfig's nil-check on MISInput.TrustBundle. + var trustBundle *parser.TrustBundleInput + if mis.TrustBundle.URI != "" || mis.TrustBundle.Path != "" { + trustBundle = &parser.TrustBundleInput{ + URI: mis.TrustBundle.URI, + Path: mis.TrustBundle.Path, + } + } + + return parser.MIAFInput{ + X509: parser.MIAFx509Input{}, + MIS: parser.MISInput{ + Endpoint: mis.Endpoint, + CAPath: mis.CAPath, + TrustDomain: mis.TrustDomain, + TrustBundle: trustBundle, + }, + AuthzPath: miaf.AuthzPath, + } +} + // HttpBinding provides service endpoints as a fasthttp web server type HttpBinding struct { - CertProvider certs.ICertProvider - server *fasthttp.Server - pipeline Pipeline - errChan chan error + CertProvider certs.ICertProvider + ParsedMIAFConfig *parser.ParsedMIAFConfig + server *fasthttp.Server + pipeline Pipeline + errChan chan error + trustBundleCacher trustbundle.TrustMaterialCacherIfc +} + +// ValidateMIAFConfig validates the MIAF configuration based on the following rules: +// 1. MIS must not be nil. +// 2. mis.endpoint and mis.caPath must be present together (both or neither). +// 3. if trustBundle.uri is present, mis.endpoint & mis.caPath must be present. +// 4. if neither trustBundle.path nor endpoint is configured, fail. +// (trustBundle.path makes endpoint+caPath+trustBundle.uri optional; +// without it, endpoint+caPath are required and trustBundle.uri is optional) +// 5. trustDomain is required when endpoint+caPath are absent (static trust bundle mode). +func ValidateMIAFConfig(miaf *MIAF) error { + if miaf == nil { + return v1alpha2.NewCOAError(nil, "MIAF config is required but missing", v1alpha2.BadConfig) + } + + mis := miaf.MIS + if mis == nil { + return v1alpha2.NewCOAError(nil, "MIAF.MIS is required but missing", v1alpha2.BadConfig) + } + + hasEndpoint := mis.Endpoint != "" + hasCAPath := mis.CAPath != "" + hasTrustBundlePath := mis.TrustBundle.Path != "" + hasTrustBundleURI := mis.TrustBundle.URI != "" + + // Rule 2: endpoint and caPath must be present together + if hasEndpoint != hasCAPath { + return v1alpha2.NewCOAError(nil, "MIS endpoint and caPath must be configured together", v1alpha2.BadConfig) + } + + // Rule 3: if trustBundle.uri is present, endpoint & caPath must be present + if hasTrustBundleURI && (!hasEndpoint || !hasCAPath) { + return v1alpha2.NewCOAError(nil, "MIS endpoint and caPath are required when trustBundle.uri is specified", v1alpha2.BadConfig) + } + + // Rule 4: if neither trustBundle.path nor endpoint is configured, fail + if !hasTrustBundlePath && !hasEndpoint { + return v1alpha2.NewCOAError(nil, "either trustBundle.path or MIS endpoint+caPath must be configured", v1alpha2.BadConfig) + } + + // Rule 5: trustDomain is required in static trust bundle mode (no endpoint+caPath) + if hasTrustBundlePath && !hasEndpoint && mis.TrustDomain == "" { + return v1alpha2.NewCOAError(nil, "MIS trustDomain is required when using static trust bundle (trustBundle.path) without endpoint+caPath", v1alpha2.BadConfig) + } + + return nil } // Launch fasthttp server @@ -57,7 +162,6 @@ func (h *HttpBinding) Launch(config HttpBindingConfig, endpoints []v1alpha2.Endp handler := h.useRouter(endpoints) var err error h.pipeline, err = BuildPipeline(config, pubsubProvider) - if err != nil { return err } @@ -80,6 +184,61 @@ func (h *HttpBinding) Launch(config HttpBindingConfig, endpoints []v1alpha2.Endp } } + // For MIAF + if config.MTLS { + switch config.CertProvider.Type { + case "certs.localfile": + h.CertProvider = &localfile.LocalCertFileProvider{} + default: + return v1alpha2.NewCOAError(nil, fmt.Sprintf("cert provider type '%s' is not recognized or allowed for mTLS", config.CertProvider.Type), v1alpha2.BadConfig) + } + err = h.CertProvider.Init(config.CertProvider.Config) + if err != nil { + return err + } + + // localhost is a placeholder + cert, key, err := h.CertProvider.GetCert("localhost") + if err != nil { + return err + } + + // Validate Cert And Key + if ok, err := validators.ValidateX509SVID(cert, validators.PrincipalWFM); !ok { + return fmt.Errorf("failed to validate SVID, err: %w", err) + } + + if err := validators.ValidatePrivateKey(key); err != nil { + return fmt.Errorf("failed to validate private key, err: %w", err) + } + // assign validated items here + h.ParsedMIAFConfig.X509.CertPEM = cert + h.ParsedMIAFConfig.X509.KeyPEM = key + + // Now setup ways to obtain trustbundle, and a cache which can be accessed here + ccfg := trustbundle.TrustMaterialCacherConfig{ + MISEndpoint: h.ParsedMIAFConfig.MIS.Endpoint, + MISCAPem: h.ParsedMIAFConfig.MIS.CAPEM, + TrustDomain: h.ParsedMIAFConfig.MIS.TrustDomain, + Logger: httpLogger, + } + + if h.ParsedMIAFConfig.MIS.TrustBundle != nil { + ccfg.TrustBundleURI = h.ParsedMIAFConfig.MIS.TrustBundle.URI + ccfg.TrustBundleJSON = h.ParsedMIAFConfig.MIS.TrustBundle.BundleJSON + } + + tbc := trustbundle.New(ccfg) + // this starts the trust bundle cacher + err = tbc.Start() + if err != nil { + return fmt.Errorf("failed to get and cache trustbundle/trustdomain, err: %w", err) + } + + h.trustBundleCacher = tbc + + } + h.server = &fasthttp.Server{ Handler: h.pipeline.Apply(handler), } @@ -87,12 +246,38 @@ func (h *HttpBinding) Launch(config HttpBindingConfig, endpoints []v1alpha2.Endp go func() { var serverErr error if config.TLS { - cert, key, err := h.CertProvider.GetCert("localhost") //TODO: user proper host/DNS name + cert, key, err := h.CertProvider.GetCert("localhost") // TODO: user proper host/DNS name if err != nil { h.errChan <- v1alpha2.NewCOAError(nil, fmt.Sprintf("error getting TLS certificates: %s", err.Error()), v1alpha2.BadConfig) return } serverErr = h.server.ListenAndServeTLSEmbed(fmt.Sprintf(":%d", config.Port), cert, key) + } else if config.MTLS { + serverCert, err := parser.CertificateFromBytes(h.ParsedMIAFConfig.X509.CertPEM, h.ParsedMIAFConfig.X509.KeyPEM) + if err != nil { + h.errChan <- v1alpha2.NewCOAError(nil, fmt.Sprintf("error getting TLS certificates: %s", err.Error()), v1alpha2.BadConfig) + return + } + tlsConfig, err := mtls.NewMTLSServerConfig(serverCert, mtls.VerifierConfig{ + GetOwnTrustDomain: h.trustBundleCacher.GetTrustDomain, + GetTrustBundleBytes: h.trustBundleCacher.GetTrustBundle, + GetClientAllowList: func() []string { + return h.ParsedMIAFConfig.AuthorizedSPIFFEIDs + }, + }) + if err != nil { + h.errChan <- v1alpha2.NewCOAError(nil, fmt.Sprintf("error getting mTLS config: %s", err.Error()), v1alpha2.BadConfig) + return + } + + ln, err := net.Listen("tcp4", fmt.Sprintf(":%d", config.Port)) + if err != nil { + h.errChan <- v1alpha2.NewCOAError(nil, fmt.Sprintf("error creating tcp listner: %s", err.Error()), v1alpha2.BadConfig) + return + } + + lnTls := tls.NewListener(ln, tlsConfig) + serverErr = h.server.Serve(lnTls) } else { serverErr = h.server.ListenAndServe(fmt.Sprintf(":%d", config.Port)) } @@ -105,10 +290,14 @@ func (h *HttpBinding) Launch(config HttpBindingConfig, endpoints []v1alpha2.Endp select { case err := <-h.errChan: - httpLogger.ErrorCtx(context.Background(), "H (HttpBinding): Server error: %s", err.Error()) + if h.trustBundleCacher != nil { + // Stopping caching mechanism for trust bundle + h.trustBundleCacher.Stop() + } + httpLogger.ErrorCtx(context.Background(), "H (HttpBinding): Server error:", err.Error()) return err case <-time.After(10 * time.Second): - httpLogger.DebugCtx(context.Background(), "H (HttpBinding): Server started on port: %s", config.Port) + httpLogger.DebugCtx(context.Background(), "H (HttpBinding): Server started on port: ", config.Port) } return nil } @@ -125,6 +314,7 @@ func (h *HttpBinding) useRouter(endpoints []v1alpha2.Endpoint) fasthttp.RequestH router := h.getRouter(endpoints) return router.Handler } + func (h *HttpBinding) getRouter(endpoints []v1alpha2.Endpoint) *routing.Router { router := routing.New() router.SaveMatchedRoutePath = true @@ -183,7 +373,7 @@ func wrapAsHTTPHandler(endpoint v1alpha2.Endpoint, handler v1alpha2.COAHandler) v := reqCtx.UserValue(k) k = "__" + k if v == nil { - req.Parameters[k] = "" //TODO: chance to report on missing required parameters + req.Parameters[k] = "" // TODO: chance to report on missing required parameters } else { req.Parameters[k] = utils.FormatAsString(v) } @@ -217,8 +407,8 @@ func toHttpState(state v1alpha2.State) int { return fasthttp.StatusCreated case v1alpha2.Accepted: return fasthttp.StatusAccepted - case v1alpha2.NotModified: // ← ADDED for Bundle 304 issue - return fasthttp.StatusNotModified + case v1alpha2.NotModified: // ← ADDED for Bundle 304 issue + return fasthttp.StatusNotModified case v1alpha2.BadRequest: return fasthttp.StatusBadRequest case v1alpha2.Unauthorized: diff --git a/coa/pkg/apis/v1alpha2/host/host.go b/coa/pkg/apis/v1alpha2/host/host.go index 8c45e2630..30cabe14b 100644 --- a/coa/pkg/apis/v1alpha2/host/host.go +++ b/coa/pkg/apis/v1alpha2/host/host.go @@ -13,6 +13,7 @@ import ( "fmt" "os" "os/signal" + "strings" "sync" "syscall" "time" @@ -28,14 +29,20 @@ import ( "github.com/eclipse-symphony/symphony/coa/pkg/apis/v1alpha2/utils" "github.com/eclipse-symphony/symphony/coa/pkg/apis/v1alpha2/vendors" "github.com/eclipse-symphony/symphony/coa/pkg/logger" + mcp "github.com/margo/sandbox/shared-lib/mis/parser" + "github.com/margo/sandbox/shared-lib/mis/validators" "golang.org/x/sync/errgroup" ) -var log = logger.NewLogger("coa.runtime") -var defaultShutdownGracePeriod = "30s" +var ( + log = logger.NewLogger("coa.runtime") + defaultShutdownGracePeriod = "30s" +) -var hostIsReadyFlag bool = false -var rwLock sync.RWMutex +var ( + hostIsReadyFlag bool = false + rwLock sync.RWMutex +) func IsHostReady() bool { rwLock.RLock() @@ -107,7 +114,8 @@ func overrideWithEnvVariable(value string, env string) string { func (h *APIHost) Launch(config HostConfig, vendorFactories []vendors.IVendorFactory, managerFactories []mf.IManagerFactroy, - providerFactories []pf.IProviderFactory, wait bool) error { + providerFactories []pf.IProviderFactory, wait bool, +) error { h.Vendors = make([]VendorSpec, 0) h.Bindings = make([]bindings.IBinding, 0) log.Info("--- launching COA host ---") @@ -147,7 +155,8 @@ func (h *APIHost) Launch(config HostConfig, for _, providerFactory := range providerFactories { mProvider, err := providerFactory.CreateProvider( config.API.PubSub.Provider.Type, - config.API.PubSub.Provider.Config) + config.API.PubSub.Provider.Config, + ) if err != nil { return err } @@ -168,7 +177,8 @@ func (h *APIHost) Launch(config HostConfig, for _, providerFactory := range providerFactories { mProvider, err := providerFactory.CreateProvider( config.API.KeyLock.Provider.Type, - config.API.KeyLock.Provider.Config) + config.API.KeyLock.Provider.Config, + ) if err != nil { return err } @@ -185,13 +195,13 @@ func (h *APIHost) Launch(config HostConfig, if err != nil { return err } - for k, _ := range mProviders { + for k := range mProviders { if _, ok := providers[k]; ok { for ik, iv := range mProviders[k] { if _, ok := providers[k][ik]; !ok { providers[k][ik] = iv } else { - //TODO: what to do if there are conflicts? + // TODO: what to do if there are conflicts? } } } else { @@ -270,7 +280,8 @@ func (h *APIHost) Launch(config HostConfig, for _, providerFactory := range providerFactories { mProvider, err := providerFactory.CreateProvider( config.API.PubSub.Provider.Type, - config.API.PubSub.Provider.Config) + config.API.PubSub.Provider.Config, + ) if err != nil { return err } @@ -348,9 +359,69 @@ func (h *APIHost) launchHTTP(config interface{}, endpoints []v1alpha2.Endpoint, return nil, err } binding := &http.HttpBinding{} + + nme, me := segregateMargoInterface(endpoints) + endpoints = nme // by default, all non margo endpoints + + // MTLS is specifically for MARGO (MIAF Compliant) + if httpConfig.MTLS == true { + // This will only contain MARGO SBI Endpoint in case of mTLS + if len(me) == 0 { + return nil, fmt.Errorf("margo management interface missing, cannot serve margo interface") + } + endpoints = me + + // Validate MIAF Config here + err := http.ValidateMIAFConfig(httpConfig.MIAF) + if err != nil { + return nil, err + } + + // Parse MIAF Config here, almost everything except x509 SVID & key + pmc, err := mcp.ParseMIAFConfig(httpConfig.MIAF.ToMIAFInput(), "") + if err != nil { + return nil, err + } + + // Validate Authorized spiffe Ids here + for _, spid := range pmc.AuthorizedSPIFFEIDs { + // Authorization list for symphony will contain SPIFFE IDs of WFM-Clients, hence using principal WFMClient here. + err := validators.ValidateSpiffeID(spid, validators.PrincipalWFMClient) + if err != nil { + return nil, fmt.Errorf("failed to validate client spiffeId %s, err : %w", spid, err) + } + } + + // Certificates are not attached here, they will be in next step + binding.ParsedMIAFConfig = pmc + } + return binding, binding.Launch(httpConfig, endpoints, pubsubProvider) } +// separates out margo and non margo interface endpoints. +// Returns Non Margo Endpoints & Margo Interface endpoints +func segregateMargoInterface(eps []v1alpha2.Endpoint) (nme []v1alpha2.Endpoint, me []v1alpha2.Endpoint) { + for _, e := range eps { + + // only nbi should be separate, rest of the routes must remain same + if strings.HasPrefix(e.Route, "margo/nbi/v1") { + nme = append(nme, e) + continue + } + + if strings.HasPrefix(e.Route, "margo/api/v1") { // this is Margo Management Interface Route + me = append(me, e) + continue + } + + nme = append(nme, e) + me = append(me, e) + + } + return nme, me +} + func (h *APIHost) launchMQTT(config interface{}, endpoints []v1alpha2.Endpoint) (bindings.IBinding, error) { data, err := json.Marshal(config) if err != nil { diff --git a/coa/pkg/apis/v1alpha2/margo/mis/trustbundle/trustbundle.go b/coa/pkg/apis/v1alpha2/margo/mis/trustbundle/trustbundle.go new file mode 100644 index 000000000..f5e01544d --- /dev/null +++ b/coa/pkg/apis/v1alpha2/margo/mis/trustbundle/trustbundle.go @@ -0,0 +1,384 @@ +package trustbundle + +// Package trustbundle provides utilities including trust material caching +// for SPIFFE/MARGO identity and authorization framework (MIAF). + +import ( + "context" + "encoding/json" + "fmt" + "sync/atomic" + "time" + "unsafe" + + "github.com/eclipse-symphony/symphony/coa/pkg/logger" + "github.com/margo/sandbox/shared-lib/mis/trustbundle" +) + +// ── Configuration ───────────────────────────────────────────────────────────── + +// TrustMaterialCacherConfig holds all configuration required to initialise a +// TrustMaterialCacher. +type TrustMaterialCacherConfig struct { + // MISEndpoint is the base URL of the Margo Infrastructure Service, + // e.g. "https://mis.margo.org:9443". + MISEndpoint string + + // MISCAPem is the PEM-encoded CA certificate used for TLS verification + // when connecting to the MIS endpoint. + MISCAPem []byte + + // TrustBundleURI is an optional well-known URI path + // (e.g. "/.well-known/spiffe/bundle.json"). May be empty. + TrustBundleURI string + + // TrustBundleJSON is an optional operator-supplied fallback SPIFFE bundle + // in JWKS JSON format. May be nil. + TrustBundleJSON []byte + + // TrustDomain is the SPIFFE trust domain (e.g. "margo.org"). + // May be empty if not yet known; the MIS discovery response will populate it. + TrustDomain string + + // Interval is the polling cadence in seconds at which the trust bundle and + // trust domain are refreshed. If zero or negative, defaults to 60 seconds. + // This value is overridden by the spiffe_refresh_hint field in the fetched + // bundle when that field is present and positive. + Interval int + + // for logging + Logger logger.Logger +} + +// ── Interface ───────────────────────────────────────────────────────────────── + +// TrustMaterialCacherIfc defines the public contract for a trust-material cache. +type TrustMaterialCacherIfc interface { + // Start performs the initial trust-bundle fetch, seeds the cache, and + // launches the background refresh goroutine. Returns an error if the + // initial fetch fails. + Start() error + + // Stop signals the background refresh goroutine to exit and blocks until + // it has done so. + Stop() + + // GetTrustBundle returns the most recently cached raw SPIFFE bundle bytes + // (JWKS JSON format). Returns nil if the cache has not been seeded yet. + GetTrustBundle() []byte + + // GetTrustDomain returns the most recently cached SPIFFE trust domain + // string. Returns "" if the cache has not been seeded yet. + GetTrustDomain() string +} + +// ── Atomic string helper ────────────────────────────────────────────────────── + +// atomicString provides lock-free load/store semantics for a string value by +// storing a pointer to the underlying string header. +type atomicString struct { + p unsafe.Pointer // *string +} + +func (a *atomicString) Load() string { + p := atomic.LoadPointer(&a.p) + if p == nil { + return "" + } + return *(*string)(p) +} + +func (a *atomicString) Store(s string) { + atomic.StorePointer(&a.p, unsafe.Pointer(&s)) +} + +// ── Atomic bytes helper ─────────────────────────────────────────────────────── + +// atomicBytes provides lock-free load/store semantics for a []byte value. +type atomicBytes struct { + p unsafe.Pointer // *[]byte +} + +func (a *atomicBytes) Load() []byte { + p := atomic.LoadPointer(&a.p) + if p == nil { + return nil + } + return *(*[]byte)(p) +} + +func (a *atomicBytes) Store(b []byte) { + atomic.StorePointer(&a.p, unsafe.Pointer(&b)) +} + +// ── TrustMaterialCacher ─────────────────────────────────────────────────────── + +// trustBundle groups the cached SPIFFE bundle value and its associated ETag so +// that conditional HTTP requests (If-None-Match) can be made on refresh. +type cachedTrustBundle struct { + etag atomicString // ETag returned by the MIS server for the bundle + value atomicBytes // raw SPIFFE bundle bytes (JWKS JSON) +} + +// cachedTrustDomain groups the cached trust-domain value and its ETag. +type cachedTrustDomain struct { + etag atomicString // ETag associated with the discovery document + value atomicString // SPIFFE trust domain string, e.g. "margo.org" +} + +// TrustMaterialCacher is the concrete implementation of TrustMaterialCacherIfc. +// All exported state is accessed through atomic helpers; no mutex is required +// for the hot read path. +type TrustMaterialCacher struct { + // ── cached material ────────────────────────────────────────────────────── + trustBundle cachedTrustBundle + trustDomain cachedTrustDomain + + // ── configuration ──────────────────────────────────────────────────────── + cfg TrustMaterialCacherConfig + + // ── runtime helpers ────────────────────────────────────────────────────── + getter trustbundle.Getter // MIS trust-bundle retrieval client + interval time.Duration // current refresh interval (may be updated by spiffe_refresh_hint) + stopCh chan struct{} // closed by Stop() to signal the refresh goroutine + doneCh chan struct{} // closed by the refresh goroutine when it exits +} + +// New validates cfg and returns an initialised TrustMaterialCacherIfc. +// The returned cacher is ready to use; call Start() to begin background refresh. +func New(cfg TrustMaterialCacherConfig) TrustMaterialCacherIfc { + const defaultInterval = 60 * time.Second + + interval := time.Duration(cfg.Interval) * time.Second + if interval <= 0 { + interval = defaultInterval + } + + return &TrustMaterialCacher{ + cfg: cfg, + interval: interval, + stopCh: make(chan struct{}), + doneCh: make(chan struct{}), + } +} + +// ── TrustMaterialCacherIfc implementation ───────────────────────────────────── + +// Start performs the initial trust-bundle fetch, seeds the in-memory cache, and +// launches the background refresh goroutine. +// +// Errors during the initial fetch are returned immediately; the background +// goroutine is NOT started in that case. +func (c *TrustMaterialCacher) Start() error { + c.cfg.Logger.DebugCtx( + context.Background(), + "TrustMaterialCacher: starting, ", + "endpoint: ", c.cfg.MISEndpoint, + " interval: ", c.interval, + ) + + // ── Build the trustbundle.Getter ───────────────────────────────────────── + getter, err := trustbundle.New( + c.cfg.MISEndpoint, + c.cfg.MISCAPem, + c.cfg.TrustBundleURI, + c.cfg.TrustBundleJSON, + c.cfg.TrustDomain, + ) + if err != nil { + return fmt.Errorf("TrustMaterialCacher: failed to create trust-bundle getter: %w", err) + } + c.getter = getter + + // ── Initial fetch ──────────────────────────────────────────────────────── + trustDomain, bundleBytes, etag, err := c.getter.GetTrustBundle(context.Background(), "") + if err != nil { + return fmt.Errorf("TrustMaterialCacher: initial trust-bundle fetch failed: %w", err) + } + + // Seed the cache. + c.trustBundle.value.Store(bundleBytes) + c.trustBundle.etag.Store(etag) + c.trustDomain.value.Store(trustDomain) + + c.cfg.Logger.DebugCtx( + context.Background(), + "TrustMaterialCacher: initial fetch succeeded, ", + "trustDomain: ", trustDomain, + " etag: ", etag, + " bundleLen: ", len(bundleBytes), + ) + + // ── Derive refresh interval from spiffe_refresh_hint ───────────────────── + if hint, ok := extractRefreshHint(bundleBytes, c.cfg.Logger); ok { + hintDuration := time.Duration(hint) * time.Second + c.cfg.Logger.DebugCtx( + context.Background(), + "TrustMaterialCacher: using spiffe_refresh_hint from bundle. ", + "hint_seconds: ", hint, + " previous_interval: ", c.interval, + ) + c.interval = hintDuration + } else { + c.cfg.Logger.DebugCtx( + context.Background(), + "TrustMaterialCacher: no valid spiffe_refresh_hint in bundle; using configured interval. ", + "interval: ", c.interval, + ) + } + + // ── Launch background refresh goroutine ─────────────────────────────────── + go c.refreshLoop() + + c.cfg.Logger.DebugCtx( + context.Background(), + "TrustMaterialCacher: background refresh goroutine started", + ) + return nil +} + +// Stop signals the background refresh goroutine to exit and blocks until it +// has done so. +func (c *TrustMaterialCacher) Stop() { + c.cfg.Logger.DebugCtx(context.Background(), "TrustMaterialCacher: stopping") + close(c.stopCh) + <-c.doneCh + c.cfg.Logger.DebugCtx(context.Background(), "TrustMaterialCacher: stopped") +} + +// GetTrustBundle returns the most recently cached raw SPIFFE bundle bytes. +// Returns nil if the cache has not been seeded yet. +func (c *TrustMaterialCacher) GetTrustBundle() []byte { + return c.trustBundle.value.Load() +} + +// GetTrustDomain returns the most recently cached SPIFFE trust domain string. +// Returns "" if the cache has not been seeded yet. +func (c *TrustMaterialCacher) GetTrustDomain() string { + return c.trustDomain.value.Load() +} + +// ── Internal helpers ────────────────────────────────────────────────────────── + +// refreshLoop is the background goroutine that periodically refreshes the +// cached trust material. It runs until Stop() closes stopCh. +// +// Errors during a refresh cycle are logged but do NOT terminate the loop; the +// stale cached values remain in place until a successful refresh. +func (c *TrustMaterialCacher) refreshLoop() { + defer close(c.doneCh) + + ticker := time.NewTicker(c.interval) + defer ticker.Stop() + + for { + select { + case <-c.stopCh: + c.cfg.Logger.DebugCtx( + context.Background(), + "TrustMaterialCacher: refresh loop received stop signal; exiting", + ) + return + + case <-ticker.C: + c.cfg.Logger.DebugCtx( + context.Background(), + "TrustMaterialCacher: refresh tick; fetching trust bundle", + ) + c.refresh(ticker) + } + } +} + +// refresh performs a single trust-bundle refresh cycle. +// On success it updates the cache and, if spiffe_refresh_hint has changed, +// resets the ticker to the new interval. +// On error it logs the failure and leaves the existing cached values intact. +func (c *TrustMaterialCacher) refresh(ticker *time.Ticker) { + currentEtag := c.trustBundle.etag.Load() + + trustDomain, bundleBytes, newEtag, err := c.getter.GetTrustBundle( + context.Background(), + currentEtag, + ) + if err != nil { + // ErrNotModified is not a real error: the cached bundle is still valid. + if err == trustbundle.ErrNotModified { + c.cfg.Logger.DebugCtx( + context.Background(), + "TrustMaterialCacher: trust bundle not modified (304); retaining cached values", + ) + return + } + + // Any other error: log and retain stale cache. + c.cfg.Logger.ErrorCtx( + context.Background(), + "TrustMaterialCacher: refresh failed; retaining stale cache. ", + "error: ", err.Error(), + ) + return + } + + // Update the cache atomically. + c.trustBundle.value.Store(bundleBytes) + c.trustBundle.etag.Store(newEtag) + c.trustDomain.value.Store(trustDomain) + + // refresh() - cache updated + c.cfg.Logger.DebugCtx( + context.Background(), + "TrustMaterialCacher: cache updated, ", + "trustDomain: ", trustDomain, + "etag: ", newEtag, + "bundleLen: ", len(bundleBytes), + ) + + // Re-evaluate spiffe_refresh_hint and reset the ticker if it has changed. + if hint, ok := extractRefreshHint(bundleBytes, c.cfg.Logger); ok { + newInterval := time.Duration(hint) * time.Second + if newInterval != c.interval { + // refresh() - spiffe_refresh_hint changed + c.cfg.Logger.DebugCtx( + context.Background(), + "TrustMaterialCacher: spiffe_refresh_hint changed; resetting ticker. ", + "old_interval: ", c.interval, + "new_interval: ", newInterval, + ) + c.interval = newInterval + ticker.Reset(newInterval) + } + } +} + +// spiffeBundle is a minimal representation of a SPIFFE JWKS bundle used solely +// to extract the spiffe_refresh_hint field. +type spiffeBundle struct { + RefreshHint *int64 `json:"spiffe_refresh_hint"` +} + +// extractRefreshHint parses bundleBytes as a SPIFFE JWKS bundle and returns the +// value of spiffe_refresh_hint if it is present and a positive integer. +// Returns (0, false) when the field is absent, zero, or negative. +func extractRefreshHint(bundleBytes []byte, logger logger.Logger) (int64, bool) { + if len(bundleBytes) == 0 { + return 0, false + } + + var doc spiffeBundle + if err := json.Unmarshal(bundleBytes, &doc); err != nil { + // extractRefreshHint() - parse failure + logger.DebugCtx( + context.Background(), + "TrustMaterialCacher: failed to parse bundle for spiffe_refresh_hint. ", + "error: ", err.Error(), + ) + return 0, false + } + + if doc.RefreshHint == nil || *doc.RefreshHint <= 0 { + return 0, false + } + + return *doc.RefreshHint, true +}