diff --git a/codecov.yml b/codecov.yml index 8537facdc..e076cf098 100644 --- a/codecov.yml +++ b/codecov.yml @@ -4,4 +4,5 @@ coverage: range: 25...100 # Specify files or directories to ignore ignore: -- "internal/usecase/devices/wsman/*" \ No newline at end of file +- "internal/usecase/devices/wsman/*" +- "internal/mocks/*" \ No newline at end of file diff --git a/internal/controller/httpapi/v1/boot.go b/internal/controller/httpapi/v1/boot.go new file mode 100644 index 000000000..cb0c97f15 --- /dev/null +++ b/internal/controller/httpapi/v1/boot.go @@ -0,0 +1,63 @@ +package v1 + +import ( + "net/http" + + "github.com/gin-gonic/gin" + + "github.com/device-management-toolkit/console/internal/entity/dto/v1" +) + +func (r *deviceManagementRoutes) getBootCapabilities(c *gin.Context) { + guid := c.Param("guid") + + capabilities, err := r.d.GetBootCapabilities(c.Request.Context(), guid) + if err != nil { + r.l.Error(err, "http - v1 - getBootCapabilities") + ErrorResponse(c, err) + + return + } + + c.JSON(http.StatusOK, capabilities) +} + +func (r *deviceManagementRoutes) setRPEEnabled(c *gin.Context) { + guid := c.Param("guid") + + var req dto.RPERequest + if err := c.ShouldBindJSON(&req); err != nil { + ErrorResponse(c, err) + + return + } + + if err := r.d.SetRPEEnabled(c.Request.Context(), guid, req.Enabled); err != nil { + r.l.Error(err, "http - v1 - setRPEEnabled") + ErrorResponse(c, err) + + return + } + + c.JSON(http.StatusOK, nil) +} + +func (r *deviceManagementRoutes) sendRemoteErase(c *gin.Context) { + guid := c.Param("guid") + + var req dto.RemoteEraseRequest + if err := c.ShouldBindJSON(&req); err != nil { + ErrorResponse(c, err) + + return + } + + if err := r.d.SendRemoteErase(c.Request.Context(), guid, req.EraseMask); err != nil { + r.l.Error(err, "http - v1 - sendRemoteErase") + ErrorResponse(c, err) + + return + } + + c.JSON(http.StatusOK, nil) +} diff --git a/internal/controller/httpapi/v1/boot_test.go b/internal/controller/httpapi/v1/boot_test.go new file mode 100644 index 000000000..2a0c52d95 --- /dev/null +++ b/internal/controller/httpapi/v1/boot_test.go @@ -0,0 +1,192 @@ +package v1 + +import ( + "bytes" + "context" + "encoding/json" + "net/http" + "net/http/httptest" + "testing" + + "github.com/stretchr/testify/require" + + "github.com/device-management-toolkit/console/internal/entity/dto/v1" + "github.com/device-management-toolkit/console/internal/mocks" +) + +func TestGetBootCapabilities(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + mock func(m *mocks.MockDeviceManagementFeature) + expectedCode int + response interface{} + }{ + { + name: "getBootCapabilities - successful retrieval", + mock: func(m *mocks.MockDeviceManagementFeature) { + m.EXPECT().GetBootCapabilities(context.Background(), "valid-guid"). + Return(dto.BootCapabilities{IDER: true, SOL: true}, nil) + }, + expectedCode: http.StatusOK, + response: dto.BootCapabilities{IDER: true, SOL: true}, + }, + { + name: "getBootCapabilities - service failure", + mock: func(m *mocks.MockDeviceManagementFeature) { + m.EXPECT().GetBootCapabilities(context.Background(), "valid-guid"). + Return(dto.BootCapabilities{}, ErrGeneral) + }, + expectedCode: http.StatusInternalServerError, + response: nil, + }, + } + + for _, tc := range tests { + tc := tc + + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + deviceManagement, engine := deviceManagementTest(t) + tc.mock(deviceManagement) + + req, err := http.NewRequestWithContext(context.Background(), http.MethodGet, "/api/v1/amt/boot/capabilities/valid-guid", http.NoBody) + require.NoError(t, err) + + w := httptest.NewRecorder() + engine.ServeHTTP(w, req) + + require.Equal(t, tc.expectedCode, w.Code) + + if tc.expectedCode == http.StatusOK { + jsonBytes, _ := json.Marshal(tc.response) + require.Equal(t, string(jsonBytes), w.Body.String()) + } + }) + } +} + +func TestSetRPEEnabled(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + requestBody interface{} + mock func(m *mocks.MockDeviceManagementFeature) + expectedCode int + }{ + { + name: "setRPEEnabled - successful (enabled=true)", + requestBody: dto.RPERequest{Enabled: true}, + mock: func(m *mocks.MockDeviceManagementFeature) { + m.EXPECT().SetRPEEnabled(context.Background(), "valid-guid", true). + Return(nil) + }, + expectedCode: http.StatusOK, + }, + { + name: "setRPEEnabled - successful (enabled=false)", + requestBody: dto.RPERequest{Enabled: false}, + mock: func(m *mocks.MockDeviceManagementFeature) { + m.EXPECT().SetRPEEnabled(context.Background(), "valid-guid", false). + Return(nil) + }, + expectedCode: http.StatusOK, + }, + { + name: "setRPEEnabled - invalid JSON payload", + requestBody: "invalid-json", + mock: func(_ *mocks.MockDeviceManagementFeature) { + }, + expectedCode: http.StatusInternalServerError, + }, + { + name: "setRPEEnabled - service failure", + requestBody: dto.RPERequest{Enabled: true}, + mock: func(m *mocks.MockDeviceManagementFeature) { + m.EXPECT().SetRPEEnabled(context.Background(), "valid-guid", true). + Return(ErrGeneral) + }, + expectedCode: http.StatusInternalServerError, + }, + } + + for _, tc := range tests { + tc := tc + + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + deviceManagement, engine := deviceManagementTest(t) + tc.mock(deviceManagement) + + reqBody, _ := json.Marshal(tc.requestBody) + req, err := http.NewRequestWithContext(context.Background(), http.MethodPost, "/api/v1/amt/boot/rpe/valid-guid", bytes.NewBuffer(reqBody)) + require.NoError(t, err) + + w := httptest.NewRecorder() + engine.ServeHTTP(w, req) + + require.Equal(t, tc.expectedCode, w.Code) + }) + } +} + +func TestSendRemoteErase(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + requestBody interface{} + mock func(m *mocks.MockDeviceManagementFeature) + expectedCode int + }{ + { + name: "sendRemoteErase - successful", + requestBody: dto.RemoteEraseRequest{EraseMask: 3}, + mock: func(m *mocks.MockDeviceManagementFeature) { + m.EXPECT().SendRemoteErase(context.Background(), "valid-guid", 3). + Return(nil) + }, + expectedCode: http.StatusOK, + }, + { + name: "sendRemoteErase - invalid JSON payload", + requestBody: "invalid-json", + mock: func(_ *mocks.MockDeviceManagementFeature) { + }, + expectedCode: http.StatusInternalServerError, + }, + { + name: "sendRemoteErase - service failure", + requestBody: dto.RemoteEraseRequest{EraseMask: 1}, + mock: func(m *mocks.MockDeviceManagementFeature) { + m.EXPECT().SendRemoteErase(context.Background(), "valid-guid", 1). + Return(ErrGeneral) + }, + expectedCode: http.StatusInternalServerError, + }, + } + + for _, tc := range tests { + tc := tc + + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + deviceManagement, engine := deviceManagementTest(t) + tc.mock(deviceManagement) + + reqBody, _ := json.Marshal(tc.requestBody) + req, err := http.NewRequestWithContext(context.Background(), http.MethodPost, "/api/v1/amt/remoteErase/valid-guid", bytes.NewBuffer(reqBody)) + require.NoError(t, err) + + w := httptest.NewRecorder() + engine.ServeHTTP(w, req) + + require.Equal(t, tc.expectedCode, w.Code) + }) + } +} diff --git a/internal/controller/httpapi/v1/devicemanagement.go b/internal/controller/httpapi/v1/devicemanagement.go index 3b1347865..b8ee6aba1 100644 --- a/internal/controller/httpapi/v1/devicemanagement.go +++ b/internal/controller/httpapi/v1/devicemanagement.go @@ -30,6 +30,9 @@ func NewAmtRoutes(handler *gin.RouterGroup, d devices.Feature, amt amtexplorer.F h.POST("alarmOccurrences/:guid", r.createAlarmOccurrences) h.DELETE("alarmOccurrences/:guid", r.deleteAlarmOccurrences) + h.GET("boot/capabilities/:guid", r.getBootCapabilities) + h.POST("boot/rpe/:guid", r.setRPEEnabled) + h.POST("remoteErase/:guid", r.sendRemoteErase) h.GET("hardwareInfo/:guid", r.getHardwareInfo) h.GET("diskInfo/:guid", r.getDiskInfo) h.GET("power/state/:guid", r.getPowerState) diff --git a/internal/controller/httpapi/v1/devicemanagement_test.go b/internal/controller/httpapi/v1/devicemanagement_test.go index ee0741e80..9db1a0842 100644 --- a/internal/controller/httpapi/v1/devicemanagement_test.go +++ b/internal/controller/httpapi/v1/devicemanagement_test.go @@ -133,7 +133,7 @@ func TestDeviceManagement(t *testing.T) { OCR: false, OptInState: 0, Redirection: false, - RemoteErase: false, + RemoteEraseEnabled: false, UserConsent: "", WinREBootSupported: false, }, diff --git a/internal/controller/httpapi/v1/features.go b/internal/controller/httpapi/v1/features.go index 5c95394a3..ffb27a039 100644 --- a/internal/controller/httpapi/v1/features.go +++ b/internal/controller/httpapi/v1/features.go @@ -45,7 +45,9 @@ func (r *deviceManagementRoutes) getFeatures(c *gin.Context) { HTTPSBootSupported: features.HTTPSBootSupported, WinREBootSupported: features.WinREBootSupported, LocalPBABootSupported: features.LocalPBABootSupported, - RemoteErase: features.RemoteErase, + RemoteEraseEnabled: features.RemoteEraseEnabled, + RemoteEraseSupported: features.RemoteEraseSupported, + PlatformEraseCaps: features.PlatformEraseCaps, } c.JSON(http.StatusOK, v1Features) diff --git a/internal/controller/openapi/devicemanagement.go b/internal/controller/openapi/devicemanagement.go index 57b629cff..a1afc4ed6 100644 --- a/internal/controller/openapi/devicemanagement.go +++ b/internal/controller/openapi/devicemanagement.go @@ -172,6 +172,20 @@ func (f *FuegoAdapter) registerPowerRoutes() { fuego.OptionDescription("Retrieve power capabilities for a device"), fuego.OptionPath("guid", "Device GUID"), ) + + fuego.Get(f.server, "/api/v1/admin/amt/boot/capabilities/{guid}", f.getBootCapabilities, + fuego.OptionTags("Device Management"), + fuego.OptionSummary("Get Boot Capabilities"), + fuego.OptionDescription("Read AMT_BootCapabilities.PlatformErase to determine Remote Platform Erase (RPE) support in the BIOS"), + fuego.OptionPath("guid", "Device GUID"), + ) + + fuego.Post(f.server, "/api/v1/admin/amt/boot/rpe/{guid}", f.setRPEEnabled, + fuego.OptionTags("Device Management"), + fuego.OptionSummary("Set RPE Enabled"), + fuego.OptionDescription("Enable or disable Remote Platform Erase (RPE) in Intel AMT via CIM_BootService.RequestStateChange. Requires administrative privileges and BIOS support."), + fuego.OptionPath("guid", "Device GUID"), + ) } func (f *FuegoAdapter) registerLogsAndAlarmRoutes() { @@ -359,6 +373,14 @@ func (f *FuegoAdapter) getPowerCapabilities(_ fuego.ContextNoBody) (dto.PowerCap return dto.PowerCapabilities{}, nil } +func (f *FuegoAdapter) getBootCapabilities(_ fuego.ContextNoBody) (dto.BootCapabilities, error) { + return dto.BootCapabilities{}, nil +} + +func (f *FuegoAdapter) setRPEEnabled(_ fuego.ContextWithBody[dto.RPERequest]) (any, error) { + return nil, nil +} + func (f *FuegoAdapter) getAlarmOccurrences(_ fuego.ContextNoBody) ([]dto.AlarmClockOccurrence, error) { return []dto.AlarmClockOccurrence{}, nil } diff --git a/internal/controller/openapi/devicemanagement_test.go b/internal/controller/openapi/devicemanagement_test.go new file mode 100644 index 000000000..3696180b9 --- /dev/null +++ b/internal/controller/openapi/devicemanagement_test.go @@ -0,0 +1,59 @@ +package openapi + +import ( + "encoding/json" + "testing" + + "github.com/stretchr/testify/require" + + dto "github.com/device-management-toolkit/console/internal/entity/dto/v1" + "github.com/device-management-toolkit/console/internal/usecase" + "github.com/device-management-toolkit/console/pkg/logger" +) + +func newTestAdapter() *FuegoAdapter { + log := logger.New("error") + + return NewFuegoAdapter(usecase.Usecases{}, log) +} + +func TestGetBootCapabilities(t *testing.T) { + t.Parallel() + + f := newTestAdapter() + + result, err := f.getBootCapabilities(nil) + + require.NoError(t, err) + require.Equal(t, dto.BootCapabilities{}, result) +} + +func TestSetRPEEnabled(t *testing.T) { + t.Parallel() + + f := newTestAdapter() + + result, err := f.setRPEEnabled(nil) + + require.NoError(t, err) + require.Nil(t, result) +} + +func TestRegisterPowerRoutes_IncludesBootEndpoints(t *testing.T) { + t.Parallel() + + f := newTestAdapter() + f.RegisterDeviceManagementRoutes() + + specBytes, err := f.GetOpenAPISpec() + require.NoError(t, err) + + var spec map[string]interface{} + require.NoError(t, json.Unmarshal(specBytes, &spec)) + + paths, ok := spec["paths"].(map[string]interface{}) + require.True(t, ok) + + require.Contains(t, paths, "/api/v1/admin/amt/boot/capabilities/{guid}", "boot capabilities route should be registered") + require.Contains(t, paths, "/api/v1/admin/amt/boot/rpe/{guid}", "set RPE enabled route should be registered") +} diff --git a/internal/controller/ws/v1/interface.go b/internal/controller/ws/v1/interface.go index abcebe431..029f1447c 100644 --- a/internal/controller/ws/v1/interface.go +++ b/internal/controller/ws/v1/interface.go @@ -64,6 +64,8 @@ type Feature interface { GetDeviceCertificate(c context.Context, guid string) (dto.Certificate, error) AddCertificate(c context.Context, guid string, certInfo dto.CertInfo) (string, error) DeleteCertificate(c context.Context, guid, instanceID string) error + GetBootCapabilities(ctx context.Context, guid string) (dto.BootCapabilities, error) + SetRPEEnabled(ctx context.Context, guid string, enabled bool) error GetBootSourceSetting(ctx context.Context, guid string) ([]dto.BootSources, error) // KVM Screen Settings GetKVMScreenSettings(c context.Context, guid string) (dto.KVMScreenSettings, error) diff --git a/internal/entity/dto/v1/bootcapabilities.go b/internal/entity/dto/v1/bootcapabilities.go new file mode 100644 index 000000000..3b8b93c96 --- /dev/null +++ b/internal/entity/dto/v1/bootcapabilities.go @@ -0,0 +1,40 @@ +package dto + +type RPERequest struct { + Enabled bool `json:"enabled"` +} + +type RemoteEraseRequest struct { + EraseMask int `json:"eraseMask"` +} + +type BootCapabilities struct { + IDER bool `json:"IDER,omitempty"` + SOL bool `json:"SOL,omitempty"` + BIOSReflash bool `json:"BIOSReflash,omitempty"` + BIOSSetup bool `json:"BIOSSetup,omitempty"` + BIOSPause bool `json:"BIOSPause,omitempty"` + ForcePXEBoot bool `json:"ForcePXEBoot,omitempty"` + ForceHardDriveBoot bool `json:"ForceHardDriveBoot,omitempty"` + ForceHardDriveSafeModeBoot bool `json:"ForceHardDriveSafeModeBoot,omitempty"` + ForceDiagnosticBoot bool `json:"ForceDiagnosticBoot,omitempty"` + ForceCDorDVDBoot bool `json:"ForceCDorDVDBoot,omitempty"` + VerbosityScreenBlank bool `json:"VerbosityScreenBlank,omitempty"` + PowerButtonLock bool `json:"PowerButtonLock,omitempty"` + ResetButtonLock bool `json:"ResetButtonLock,omitempty"` + KeyboardLock bool `json:"KeyboardLock,omitempty"` + SleepButtonLock bool `json:"SleepButtonLock,omitempty"` + UserPasswordBypass bool `json:"UserPasswordBypass,omitempty"` + ForcedProgressEvents bool `json:"ForcedProgressEvents,omitempty"` + VerbosityVerbose bool `json:"VerbosityVerbose,omitempty"` + VerbosityQuiet bool `json:"VerbosityQuiet,omitempty"` + ConfigurationDataReset bool `json:"ConfigurationDataReset,omitempty"` + BIOSSecureBoot bool `json:"BIOSSecureBoot,omitempty"` + SecureErase bool `json:"SecureErase,omitempty"` + ForceWinREBoot bool `json:"ForceWinREBoot,omitempty"` + ForceUEFILocalPBABoot bool `json:"ForceUEFILocalPBABoot,omitempty"` + ForceUEFIHTTPSBoot bool `json:"ForceUEFIHTTPSBoot,omitempty"` + AMTSecureBootControl bool `json:"AMTSecureBootControl,omitempty"` + UEFIWiFiCoExistenceAndProfileShare bool `json:"UEFIWiFiCoExistenceAndProfileShare,omitempty"` + PlatformErase int `json:"PlatformErase,omitempty"` +} diff --git a/internal/entity/dto/v1/features.go b/internal/entity/dto/v1/features.go index 426a07746..aa9194623 100644 --- a/internal/entity/dto/v1/features.go +++ b/internal/entity/dto/v1/features.go @@ -23,14 +23,17 @@ type Features struct { HTTPSBootSupported bool `json:"httpsBootSupported" example:"true"` WinREBootSupported bool `json:"winREBootSupported" example:"true"` LocalPBABootSupported bool `json:"localPBABootSupported" example:"true"` - RemoteErase bool `json:"remoteErase" example:"true"` + RemoteEraseEnabled bool `json:"remoteEraseEnabled" example:"true"` + RemoteEraseSupported bool `json:"remoteEraseSupported" example:"true"` + PlatformEraseCaps int `json:"platformEraseCaps,omitempty" example:"15"` + EnablePlatformErase bool `json:"enablePlatformErase" example:"true"` } type FeaturesRequest struct { - UserConsent string `json:"userConsent" example:"kvm"` - EnableSOL bool `json:"enableSOL" example:"true"` - EnableIDER bool `json:"enableIDER" example:"true"` - EnableKVM bool `json:"enableKVM" example:"true"` - OCR bool `json:"ocr" example:"true"` - RemoteErase bool `json:"remoteErase" example:"true"` + UserConsent string `json:"userConsent" example:"kvm"` + EnableSOL bool `json:"enableSOL" example:"true"` + EnableIDER bool `json:"enableIDER" example:"true"` + EnableKVM bool `json:"enableKVM" example:"true"` + OCR bool `json:"ocr" example:"true"` + EnablePlatformErase bool `json:"enablePlatformErase" example:"true"` } diff --git a/internal/entity/dto/v1/getfeatures.go b/internal/entity/dto/v1/getfeatures.go index d62dd5698..9bb7e08d5 100644 --- a/internal/entity/dto/v1/getfeatures.go +++ b/internal/entity/dto/v1/getfeatures.go @@ -12,5 +12,7 @@ type GetFeaturesResponse struct { HTTPSBootSupported bool `json:"httpsBootSupported" binding:"required" example:"false"` WinREBootSupported bool `json:"winREBootSupported" binding:"required" example:"false"` LocalPBABootSupported bool `json:"localPBABootSupported" binding:"required" example:"false"` - RemoteErase bool `json:"remoteErase" binding:"required" example:"false"` + RemoteEraseEnabled bool `json:"remoteEraseEnabled" binding:"required" example:"false"` + RemoteEraseSupported bool `json:"remoteEraseSupported" example:"false"` + PlatformEraseCaps int `json:"platformEraseCaps,omitempty" example:"15"` } diff --git a/internal/entity/dto/v2/features.go b/internal/entity/dto/v2/features.go index 8152ec245..7c4964be7 100644 --- a/internal/entity/dto/v2/features.go +++ b/internal/entity/dto/v2/features.go @@ -24,5 +24,7 @@ type Features struct { HTTPSBootSupported bool `json:"httpBootSupported,omitempty" example:"true"` WinREBootSupported bool `json:"winREBootSupported,omitempty" example:"true"` LocalPBABootSupported bool `json:"localPBABootSupported,omitempty" example:"true"` - RemoteErase bool `json:"remoteErase" example:"true"` + RemoteEraseEnabled bool `json:"remoteEraseEnabled" example:"true"` + RemoteEraseSupported bool `json:"remoteEraseSupported" example:"true"` + PlatformEraseCaps int `json:"platformEraseCaps,omitempty" example:"15"` } diff --git a/internal/mocks/devicemanagement_mocks.go b/internal/mocks/devicemanagement_mocks.go index 11a564515..cef998284 100644 --- a/internal/mocks/devicemanagement_mocks.go +++ b/internal/mocks/devicemanagement_mocks.go @@ -594,6 +594,21 @@ func (mr *MockDeviceManagementFeatureMockRecorder) GetAuditLog(ctx, startIndex, return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetAuditLog", reflect.TypeOf((*MockDeviceManagementFeature)(nil).GetAuditLog), ctx, startIndex, guid) } +// GetBootCapabilities mocks base method. +func (m *MockDeviceManagementFeature) GetBootCapabilities(ctx context.Context, guid string) (dto.BootCapabilities, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetBootCapabilities", ctx, guid) + ret0, _ := ret[0].(dto.BootCapabilities) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetBootCapabilities indicates an expected call of GetBootCapabilities. +func (mr *MockDeviceManagementFeatureMockRecorder) GetBootCapabilities(ctx, guid any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetBootCapabilities", reflect.TypeOf((*MockDeviceManagementFeature)(nil).GetBootCapabilities), ctx, guid) +} + // GetBootSourceSetting mocks base method. func (m *MockDeviceManagementFeature) GetBootSourceSetting(c context.Context, guid string) ([]dto.BootSources, error) { m.ctrl.T.Helper() @@ -790,21 +805,6 @@ func (mr *MockDeviceManagementFeatureMockRecorder) GetHardwareInfo(ctx, guid any return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetHardwareInfo", reflect.TypeOf((*MockDeviceManagementFeature)(nil).GetHardwareInfo), ctx, guid) } -// SetLinkPreference mocks base method. -func (m *MockDeviceManagementFeature) SetLinkPreference(c context.Context, guid string, req dto.LinkPreferenceRequest) (dto.LinkPreferenceResponse, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "SetLinkPreference", c, guid, req) - ret0, _ := ret[0].(dto.LinkPreferenceResponse) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// SetLinkPreference indicates an expected call of SetLinkPreference. -func (mr *MockDeviceManagementFeatureMockRecorder) SetLinkPreference(c, guid, req any) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetLinkPreference", reflect.TypeOf((*MockDeviceManagementFeature)(nil).SetLinkPreference), c, guid, req) -} - // GetKVMScreenSettings mocks base method. func (m *MockDeviceManagementFeature) GetKVMScreenSettings(c context.Context, guid string) (dto.KVMScreenSettings, error) { m.ctrl.T.Helper() @@ -1016,6 +1016,49 @@ func (mr *MockDeviceManagementFeatureMockRecorder) SetKVMScreenSettings(c, guid, return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetKVMScreenSettings", reflect.TypeOf((*MockDeviceManagementFeature)(nil).SetKVMScreenSettings), c, guid, req) } +// SetLinkPreference mocks base method. +func (m *MockDeviceManagementFeature) SetLinkPreference(c context.Context, guid string, req dto.LinkPreferenceRequest) (dto.LinkPreferenceResponse, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "SetLinkPreference", c, guid, req) + ret0, _ := ret[0].(dto.LinkPreferenceResponse) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// SetLinkPreference indicates an expected call of SetLinkPreference. +func (mr *MockDeviceManagementFeatureMockRecorder) SetLinkPreference(c, guid, req any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetLinkPreference", reflect.TypeOf((*MockDeviceManagementFeature)(nil).SetLinkPreference), c, guid, req) +} + +// SetRPEEnabled mocks base method. +func (m *MockDeviceManagementFeature) SetRPEEnabled(ctx context.Context, guid string, enabled bool) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "SetRPEEnabled", ctx, guid, enabled) + ret0, _ := ret[0].(error) + return ret0 +} + +// SetRPEEnabled indicates an expected call of SetRPEEnabled. +func (mr *MockDeviceManagementFeatureMockRecorder) SetRPEEnabled(ctx, guid, enabled any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetRPEEnabled", reflect.TypeOf((*MockDeviceManagementFeature)(nil).SetRPEEnabled), ctx, guid, enabled) +} + +// SendRemoteErase mocks base method. +func (m *MockDeviceManagementFeature) SendRemoteErase(ctx context.Context, guid string, eraseMask int) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "SendRemoteErase", ctx, guid, eraseMask) + ret0, _ := ret[0].(error) + return ret0 +} + +// SendRemoteErase indicates an expected call of SendRemoteErase. +func (mr *MockDeviceManagementFeatureMockRecorder) SendRemoteErase(ctx, guid, eraseMask any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SendRemoteErase", reflect.TypeOf((*MockDeviceManagementFeature)(nil).SendRemoteErase), ctx, guid, eraseMask) +} + // Update mocks base method. func (m *MockDeviceManagementFeature) Update(ctx context.Context, d *dto.Device) (*dto.Device, error) { m.ctrl.T.Helper() diff --git a/internal/mocks/wsman_mocks.go b/internal/mocks/wsman_mocks.go index e11ebf0e5..9761ae955 100644 --- a/internal/mocks/wsman_mocks.go +++ b/internal/mocks/wsman_mocks.go @@ -239,6 +239,21 @@ func (mr *MockManagementMockRecorder) GetAuditLog(startIndex any) *gomock.Call { return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetAuditLog", reflect.TypeOf((*MockManagement)(nil).GetAuditLog), startIndex) } +// GetBootCapabilities mocks base method. +func (m *MockManagement) GetBootCapabilities() (boot.BootCapabilitiesResponse, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetBootCapabilities") + ret0, _ := ret[0].(boot.BootCapabilitiesResponse) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetBootCapabilities indicates an expected call of GetBootCapabilities. +func (mr *MockManagementMockRecorder) GetBootCapabilities() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetBootCapabilities", reflect.TypeOf((*MockManagement)(nil).GetBootCapabilities)) +} + // GetBootData mocks base method. func (m *MockManagement) GetBootData() (boot.BootSettingDataResponse, error) { m.ctrl.T.Helper() @@ -690,6 +705,34 @@ func (mr *MockManagementMockRecorder) SetBootData(data any) *gomock.Call { return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetBootData", reflect.TypeOf((*MockManagement)(nil).SetBootData), data) } +// SetRPEEnabled mocks base method. +func (m *MockManagement) SetRPEEnabled(enabled bool) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "SetRPEEnabled", enabled) + ret0, _ := ret[0].(error) + return ret0 +} + +// SetRPEEnabled indicates an expected call of SetRPEEnabled. +func (mr *MockManagementMockRecorder) SetRPEEnabled(enabled any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetRPEEnabled", reflect.TypeOf((*MockManagement)(nil).SetRPEEnabled), enabled) +} + +// SendRemoteErase mocks base method. +func (m *MockManagement) SendRemoteErase(eraseMask int) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "SendRemoteErase", eraseMask) + ret0, _ := ret[0].(error) + return ret0 +} + +// SendRemoteErase indicates an expected call of SendRemoteErase. +func (mr *MockManagementMockRecorder) SendRemoteErase(eraseMask any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SendRemoteErase", reflect.TypeOf((*MockManagement)(nil).SendRemoteErase), eraseMask) +} + // SetIPSKVMRedirectionSettingData mocks base method. func (m *MockManagement) SetIPSKVMRedirectionSettingData(data *kvmredirection.KVMRedirectionSettingsRequest) (kvmredirection.Response, error) { m.ctrl.T.Helper() diff --git a/internal/mocks/wsv1_mocks.go b/internal/mocks/wsv1_mocks.go index 3ca7c33e3..ccc312afa 100644 --- a/internal/mocks/wsv1_mocks.go +++ b/internal/mocks/wsv1_mocks.go @@ -270,6 +270,49 @@ func (mr *MockFeatureMockRecorder) GetAuditLog(ctx, startIndex, guid any) *gomoc return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetAuditLog", reflect.TypeOf((*MockFeature)(nil).GetAuditLog), ctx, startIndex, guid) } +// GetBootCapabilities mocks base method. +func (m *MockFeature) GetBootCapabilities(ctx context.Context, guid string) (dto.BootCapabilities, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetBootCapabilities", ctx, guid) + ret0, _ := ret[0].(dto.BootCapabilities) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetBootCapabilities indicates an expected call of GetBootCapabilities. +func (mr *MockFeatureMockRecorder) GetBootCapabilities(ctx, guid any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetBootCapabilities", reflect.TypeOf((*MockFeature)(nil).GetBootCapabilities), ctx, guid) +} + +// SetRPEEnabled mocks base method. +func (m *MockFeature) SetRPEEnabled(ctx context.Context, guid string, enabled bool) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "SetRPEEnabled", ctx, guid, enabled) + ret0, _ := ret[0].(error) + return ret0 +} + +// SetRPEEnabled indicates an expected call of SetRPEEnabled. +func (mr *MockFeatureMockRecorder) SetRPEEnabled(ctx, guid, enabled any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetRPEEnabled", reflect.TypeOf((*MockFeature)(nil).SetRPEEnabled), ctx, guid, enabled) +} + +// SendRemoteErase mocks base method. +func (m *MockFeature) SendRemoteErase(ctx context.Context, guid string, eraseMask int) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "SendRemoteErase", ctx, guid, eraseMask) + ret0, _ := ret[0].(error) + return ret0 +} + +// SendRemoteErase indicates an expected call of SendRemoteErase. +func (mr *MockFeatureMockRecorder) SendRemoteErase(ctx, guid, eraseMask any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SendRemoteErase", reflect.TypeOf((*MockFeature)(nil).SendRemoteErase), ctx, guid, eraseMask) +} + // GetBootSourceSetting mocks base method. func (m *MockFeature) GetBootSourceSetting(ctx context.Context, guid string) ([]dto.BootSources, error) { m.ctrl.T.Helper() diff --git a/internal/usecase/devices/boot.go b/internal/usecase/devices/boot.go new file mode 100644 index 000000000..ec3a36a82 --- /dev/null +++ b/internal/usecase/devices/boot.go @@ -0,0 +1,128 @@ +package devices + +import ( + "context" + + "github.com/device-management-toolkit/console/internal/entity/dto/v1" +) + +func (uc *UseCase) GetBootCapabilities(c context.Context, guid string) (dto.BootCapabilities, error) { + item, err := uc.repo.GetByID(c, guid, "") + if err != nil { + return dto.BootCapabilities{}, err + } + + if item == nil || item.GUID == "" { + return dto.BootCapabilities{}, ErrNotFound + } + + device, err := uc.device.SetupWsmanClient(*item, false, true) + if err != nil { + return dto.BootCapabilities{}, err + } + + capabilities, err := device.GetBootCapabilities() + if err != nil { + return dto.BootCapabilities{}, err + } + + uc.log.Debug("GetBootCapabilities: PlatformErase capability", "guid", guid, "PlatformErase", capabilities.PlatformErase, "supported", capabilities.PlatformErase != 0) + + return dto.BootCapabilities{ + IDER: capabilities.IDER, + SOL: capabilities.SOL, + BIOSReflash: capabilities.BIOSReflash, + BIOSSetup: capabilities.BIOSSetup, + BIOSPause: capabilities.BIOSPause, + ForcePXEBoot: capabilities.ForcePXEBoot, + ForceHardDriveBoot: capabilities.ForceHardDriveBoot, + ForceHardDriveSafeModeBoot: capabilities.ForceHardDriveSafeModeBoot, + ForceDiagnosticBoot: capabilities.ForceDiagnosticBoot, + ForceCDorDVDBoot: capabilities.ForceCDorDVDBoot, + VerbosityScreenBlank: capabilities.VerbosityScreenBlank, + PowerButtonLock: capabilities.PowerButtonLock, + ResetButtonLock: capabilities.ResetButtonLock, + KeyboardLock: capabilities.KeyboardLock, + SleepButtonLock: capabilities.SleepButtonLock, + UserPasswordBypass: capabilities.UserPasswordBypass, + ForcedProgressEvents: capabilities.ForcedProgressEvents, + VerbosityVerbose: capabilities.VerbosityVerbose, + VerbosityQuiet: capabilities.VerbosityQuiet, + ConfigurationDataReset: capabilities.ConfigurationDataReset, + BIOSSecureBoot: capabilities.BIOSSecureBoot, + SecureErase: capabilities.SecureErase, + ForceWinREBoot: capabilities.ForceWinREBoot, + ForceUEFILocalPBABoot: capabilities.ForceUEFILocalPBABoot, + ForceUEFIHTTPSBoot: capabilities.ForceUEFIHTTPSBoot, + AMTSecureBootControl: capabilities.AMTSecureBootControl, + UEFIWiFiCoExistenceAndProfileShare: capabilities.UEFIWiFiCoExistenceAndProfileShare, + PlatformErase: capabilities.PlatformErase, + }, nil +} + +func (uc *UseCase) SetRPEEnabled(c context.Context, guid string, enabled bool) error { + item, err := uc.repo.GetByID(c, guid, "") + if err != nil { + return err + } + + if item == nil || item.GUID == "" { + return ErrNotFound + } + + device, err := uc.device.SetupWsmanClient(*item, false, true) + if err != nil { + return err + } + + capabilities, err := device.GetBootCapabilities() + if err != nil { + return err + } + + if capabilities.PlatformErase == 0 { + return ValidationError{}.Wrap("SetRPEEnabled", "check boot capabilities", "device does not support Remote Platform Erase") + } + + return device.SetRPEEnabled(enabled) +} + +func (uc *UseCase) SendRemoteErase(c context.Context, guid string, eraseMask int) error { + item, err := uc.repo.GetByID(c, guid, "") + if err != nil { + return err + } + + if item == nil || item.GUID == "" { + return ErrNotFound + } + + device, err := uc.device.SetupWsmanClient(*item, false, true) + if err != nil { + return err + } + + capabilities, err := device.GetBootCapabilities() + if err != nil { + return err + } + + if capabilities.PlatformErase == 0 { + return ValidationError{}.Wrap("SendRemoteErase", "check boot capabilities", "device does not support Remote Platform Erase") + } + + if eraseMask != 0 && (capabilities.PlatformErase&eraseMask) == 0 { + return ValidationError{}.Wrap("SendRemoteErase", "validate erase mask", "requested erase capabilities are not supported by this device") + } + + uc.log.Debug("SendRemoteErase", + "guid", guid, + "eraseMask", eraseMask, + "secureErase", eraseMask&0x01 != 0, + "ecStorage", eraseMask&0x02 != 0, + "storageDrives", eraseMask&0x04 != 0, + "meRegion", eraseMask&0x08 != 0, + ) + + return device.SendRemoteErase(eraseMask) +} diff --git a/internal/usecase/devices/boot_test.go b/internal/usecase/devices/boot_test.go new file mode 100644 index 000000000..6868854f2 --- /dev/null +++ b/internal/usecase/devices/boot_test.go @@ -0,0 +1,552 @@ +package devices_test + +import ( + "context" + "testing" + + "github.com/stretchr/testify/require" + gomock "go.uber.org/mock/gomock" + + "github.com/device-management-toolkit/go-wsman-messages/v2/pkg/wsman/amt/boot" + + "github.com/device-management-toolkit/console/internal/entity" + "github.com/device-management-toolkit/console/internal/entity/dto/v1" + "github.com/device-management-toolkit/console/internal/mocks" + devices "github.com/device-management-toolkit/console/internal/usecase/devices" +) + +func TestGetBootCapabilities(t *testing.T) { + t.Parallel() + + device := &entity.Device{ + GUID: "device-guid-123", + TenantID: "tenant-id-456", + } + + fullCapabilities := boot.BootCapabilitiesResponse{ + IDER: true, + SOL: true, + BIOSReflash: true, + BIOSSetup: false, + BIOSPause: false, + ForcePXEBoot: true, + ForceHardDriveBoot: false, + ForceHardDriveSafeModeBoot: false, + ForceDiagnosticBoot: false, + ForceCDorDVDBoot: false, + VerbosityScreenBlank: false, + PowerButtonLock: false, + ResetButtonLock: false, + KeyboardLock: false, + SleepButtonLock: false, + UserPasswordBypass: false, + ForcedProgressEvents: false, + VerbosityVerbose: false, + VerbosityQuiet: false, + ConfigurationDataReset: false, + BIOSSecureBoot: false, + SecureErase: false, + ForceWinREBoot: false, + ForceUEFILocalPBABoot: false, + ForceUEFIHTTPSBoot: true, + AMTSecureBootControl: false, + UEFIWiFiCoExistenceAndProfileShare: false, + PlatformErase: 1, + } + + expectedDTO := dto.BootCapabilities{ + IDER: true, + SOL: true, + BIOSReflash: true, + ForcePXEBoot: true, + ForceUEFIHTTPSBoot: true, + PlatformErase: 1, + } + + tests := []struct { + name string + manMock func(*mocks.MockWSMAN, *mocks.MockManagement) + repoMock func(*mocks.MockDeviceManagementRepository) + res dto.BootCapabilities + err error + }{ + { + name: "success", + manMock: func(man *mocks.MockWSMAN, man2 *mocks.MockManagement) { + man.EXPECT(). + SetupWsmanClient(gomock.Any(), false, true). + Return(man2, nil) + man2.EXPECT(). + GetBootCapabilities(). + Return(fullCapabilities, nil) + }, + repoMock: func(repo *mocks.MockDeviceManagementRepository) { + repo.EXPECT(). + GetByID(context.Background(), device.GUID, ""). + Return(device, nil) + }, + res: expectedDTO, + err: nil, + }, + { + name: "GetByID returns error", + manMock: func(_ *mocks.MockWSMAN, _ *mocks.MockManagement) {}, + repoMock: func(repo *mocks.MockDeviceManagementRepository) { + repo.EXPECT(). + GetByID(context.Background(), device.GUID, ""). + Return(nil, ErrGeneral) + }, + res: dto.BootCapabilities{}, + err: ErrGeneral, + }, + { + name: "GetByID returns nil device", + manMock: func(_ *mocks.MockWSMAN, _ *mocks.MockManagement) {}, + repoMock: func(repo *mocks.MockDeviceManagementRepository) { + repo.EXPECT(). + GetByID(context.Background(), device.GUID, ""). + Return(nil, nil) + }, + res: dto.BootCapabilities{}, + err: devices.ErrNotFound, + }, + { + name: "GetByID returns device with empty GUID", + manMock: func(_ *mocks.MockWSMAN, _ *mocks.MockManagement) {}, + repoMock: func(repo *mocks.MockDeviceManagementRepository) { + repo.EXPECT(). + GetByID(context.Background(), device.GUID, ""). + Return(&entity.Device{GUID: "", TenantID: "tenant-id-456"}, nil) + }, + res: dto.BootCapabilities{}, + err: devices.ErrNotFound, + }, + { + name: "SetupWsmanClient returns error", + manMock: func(man *mocks.MockWSMAN, _ *mocks.MockManagement) { + man.EXPECT(). + SetupWsmanClient(gomock.Any(), false, true). + Return(nil, ErrGeneral) + }, + repoMock: func(repo *mocks.MockDeviceManagementRepository) { + repo.EXPECT(). + GetByID(context.Background(), device.GUID, ""). + Return(device, nil) + }, + res: dto.BootCapabilities{}, + err: ErrGeneral, + }, + { + name: "GetBootCapabilities wsman call returns error", + manMock: func(man *mocks.MockWSMAN, man2 *mocks.MockManagement) { + man.EXPECT(). + SetupWsmanClient(gomock.Any(), false, true). + Return(man2, nil) + man2.EXPECT(). + GetBootCapabilities(). + Return(boot.BootCapabilitiesResponse{}, ErrGeneral) + }, + repoMock: func(repo *mocks.MockDeviceManagementRepository) { + repo.EXPECT(). + GetByID(context.Background(), device.GUID, ""). + Return(device, nil) + }, + res: dto.BootCapabilities{}, + err: ErrGeneral, + }, + } + + for _, tc := range tests { + tc := tc + + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + useCase, wsmanMock, management, repo := initInfoTest(t) + tc.manMock(wsmanMock, management) + tc.repoMock(repo) + + res, err := useCase.GetBootCapabilities(context.Background(), device.GUID) + require.Equal(t, tc.err, err) + require.Equal(t, tc.res, res) + }) + } +} + +func TestSetRPEEnabled(t *testing.T) { + t.Parallel() + + device := &entity.Device{ + GUID: "device-guid-123", + TenantID: "tenant-id-456", + } + + tests := []struct { + name string + enabled bool + manMock func(*mocks.MockWSMAN, *mocks.MockManagement) + repoMock func(*mocks.MockDeviceManagementRepository) + err error + }{ + { + name: "success - enable RPE on supported device", + enabled: true, + manMock: func(man *mocks.MockWSMAN, man2 *mocks.MockManagement) { + man.EXPECT(). + SetupWsmanClient(gomock.Any(), false, true). + Return(man2, nil) + man2.EXPECT(). + GetBootCapabilities(). + Return(boot.BootCapabilitiesResponse{PlatformErase: 1}, nil) + man2.EXPECT(). + SetRPEEnabled(true). + Return(nil) + }, + repoMock: func(repo *mocks.MockDeviceManagementRepository) { + repo.EXPECT(). + GetByID(context.Background(), device.GUID, ""). + Return(device, nil) + }, + err: nil, + }, + { + name: "success - disable RPE on supported device", + enabled: false, + manMock: func(man *mocks.MockWSMAN, man2 *mocks.MockManagement) { + man.EXPECT(). + SetupWsmanClient(gomock.Any(), false, true). + Return(man2, nil) + man2.EXPECT(). + GetBootCapabilities(). + Return(boot.BootCapabilitiesResponse{PlatformErase: 1}, nil) + man2.EXPECT(). + SetRPEEnabled(false). + Return(nil) + }, + repoMock: func(repo *mocks.MockDeviceManagementRepository) { + repo.EXPECT(). + GetByID(context.Background(), device.GUID, ""). + Return(device, nil) + }, + err: nil, + }, + { + name: "device does not support RPE - PlatformErase is 0", + enabled: true, + manMock: func(man *mocks.MockWSMAN, man2 *mocks.MockManagement) { + man.EXPECT(). + SetupWsmanClient(gomock.Any(), false, true). + Return(man2, nil) + man2.EXPECT(). + GetBootCapabilities(). + Return(boot.BootCapabilitiesResponse{PlatformErase: 0}, nil) + }, + repoMock: func(repo *mocks.MockDeviceManagementRepository) { + repo.EXPECT(). + GetByID(context.Background(), device.GUID, ""). + Return(device, nil) + }, + err: devices.ValidationError{}.Wrap("SetRPEEnabled", "check boot capabilities", "device does not support Remote Platform Erase"), + }, + { + name: "GetByID returns error", + enabled: true, + manMock: func(_ *mocks.MockWSMAN, _ *mocks.MockManagement) {}, + repoMock: func(repo *mocks.MockDeviceManagementRepository) { + repo.EXPECT(). + GetByID(context.Background(), device.GUID, ""). + Return(nil, ErrGeneral) + }, + err: ErrGeneral, + }, + { + name: "GetByID returns nil device", + enabled: true, + manMock: func(_ *mocks.MockWSMAN, _ *mocks.MockManagement) {}, + repoMock: func(repo *mocks.MockDeviceManagementRepository) { + repo.EXPECT(). + GetByID(context.Background(), device.GUID, ""). + Return(nil, nil) + }, + err: devices.ErrNotFound, + }, + { + name: "GetByID returns device with empty GUID", + enabled: true, + manMock: func(_ *mocks.MockWSMAN, _ *mocks.MockManagement) {}, + repoMock: func(repo *mocks.MockDeviceManagementRepository) { + repo.EXPECT(). + GetByID(context.Background(), device.GUID, ""). + Return(&entity.Device{GUID: "", TenantID: "tenant-id-456"}, nil) + }, + err: devices.ErrNotFound, + }, + { + name: "SetupWsmanClient returns error", + enabled: true, + manMock: func(man *mocks.MockWSMAN, _ *mocks.MockManagement) { + man.EXPECT(). + SetupWsmanClient(gomock.Any(), false, true). + Return(nil, ErrGeneral) + }, + repoMock: func(repo *mocks.MockDeviceManagementRepository) { + repo.EXPECT(). + GetByID(context.Background(), device.GUID, ""). + Return(device, nil) + }, + err: ErrGeneral, + }, + { + name: "GetBootCapabilities returns error", + enabled: true, + manMock: func(man *mocks.MockWSMAN, man2 *mocks.MockManagement) { + man.EXPECT(). + SetupWsmanClient(gomock.Any(), false, true). + Return(man2, nil) + man2.EXPECT(). + GetBootCapabilities(). + Return(boot.BootCapabilitiesResponse{}, ErrGeneral) + }, + repoMock: func(repo *mocks.MockDeviceManagementRepository) { + repo.EXPECT(). + GetByID(context.Background(), device.GUID, ""). + Return(device, nil) + }, + err: ErrGeneral, + }, + { + name: "SetRPEEnabled wsman call returns error", + enabled: true, + manMock: func(man *mocks.MockWSMAN, man2 *mocks.MockManagement) { + man.EXPECT(). + SetupWsmanClient(gomock.Any(), false, true). + Return(man2, nil) + man2.EXPECT(). + GetBootCapabilities(). + Return(boot.BootCapabilitiesResponse{PlatformErase: 1}, nil) + man2.EXPECT(). + SetRPEEnabled(true). + Return(ErrGeneral) + }, + repoMock: func(repo *mocks.MockDeviceManagementRepository) { + repo.EXPECT(). + GetByID(context.Background(), device.GUID, ""). + Return(device, nil) + }, + err: ErrGeneral, + }, + } + + for _, tc := range tests { + tc := tc + + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + useCase, wsmanMock, management, repo := initInfoTest(t) + tc.manMock(wsmanMock, management) + tc.repoMock(repo) + + err := useCase.SetRPEEnabled(context.Background(), device.GUID, tc.enabled) + require.Equal(t, tc.err, err) + }) + } +} + +func TestSendRemoteErase(t *testing.T) { + t.Parallel() + + device := &entity.Device{ + GUID: "device-guid-123", + TenantID: "tenant-id-456", + } + + tests := []struct { + name string + eraseMask int + manMock func(*mocks.MockWSMAN, *mocks.MockManagement) + repoMock func(*mocks.MockDeviceManagementRepository) + err error + }{ + { + name: "success - eraseMask 0 erases all", + eraseMask: 0, + manMock: func(man *mocks.MockWSMAN, man2 *mocks.MockManagement) { + man.EXPECT(). + SetupWsmanClient(gomock.Any(), false, true). + Return(man2, nil) + man2.EXPECT(). + GetBootCapabilities(). + Return(boot.BootCapabilitiesResponse{PlatformErase: 3}, nil) + man2.EXPECT(). + SendRemoteErase(0). + Return(nil) + }, + repoMock: func(repo *mocks.MockDeviceManagementRepository) { + repo.EXPECT(). + GetByID(context.Background(), device.GUID, ""). + Return(device, nil) + }, + err: nil, + }, + { + name: "success - specific supported eraseMask", + eraseMask: 2, + manMock: func(man *mocks.MockWSMAN, man2 *mocks.MockManagement) { + man.EXPECT(). + SetupWsmanClient(gomock.Any(), false, true). + Return(man2, nil) + man2.EXPECT(). + GetBootCapabilities(). + Return(boot.BootCapabilitiesResponse{PlatformErase: 3}, nil) + man2.EXPECT(). + SendRemoteErase(2). + Return(nil) + }, + repoMock: func(repo *mocks.MockDeviceManagementRepository) { + repo.EXPECT(). + GetByID(context.Background(), device.GUID, ""). + Return(device, nil) + }, + err: nil, + }, + { + name: "device does not support RPE - PlatformErase is 0", + eraseMask: 0, + manMock: func(man *mocks.MockWSMAN, man2 *mocks.MockManagement) { + man.EXPECT(). + SetupWsmanClient(gomock.Any(), false, true). + Return(man2, nil) + man2.EXPECT(). + GetBootCapabilities(). + Return(boot.BootCapabilitiesResponse{PlatformErase: 0}, nil) + }, + repoMock: func(repo *mocks.MockDeviceManagementRepository) { + repo.EXPECT(). + GetByID(context.Background(), device.GUID, ""). + Return(device, nil) + }, + err: devices.ValidationError{}.Wrap("SendRemoteErase", "check boot capabilities", "device does not support Remote Platform Erase"), + }, + { + name: "eraseMask not supported by device capabilities", + eraseMask: 4, + manMock: func(man *mocks.MockWSMAN, man2 *mocks.MockManagement) { + man.EXPECT(). + SetupWsmanClient(gomock.Any(), false, true). + Return(man2, nil) + man2.EXPECT(). + GetBootCapabilities(). + Return(boot.BootCapabilitiesResponse{PlatformErase: 3}, nil) + }, + repoMock: func(repo *mocks.MockDeviceManagementRepository) { + repo.EXPECT(). + GetByID(context.Background(), device.GUID, ""). + Return(device, nil) + }, + err: devices.ValidationError{}.Wrap("SendRemoteErase", "validate erase mask", "requested erase capabilities are not supported by this device"), + }, + { + name: "GetByID returns error", + eraseMask: 0, + manMock: func(_ *mocks.MockWSMAN, _ *mocks.MockManagement) {}, + repoMock: func(repo *mocks.MockDeviceManagementRepository) { + repo.EXPECT(). + GetByID(context.Background(), device.GUID, ""). + Return(nil, ErrGeneral) + }, + err: ErrGeneral, + }, + { + name: "GetByID returns nil device", + eraseMask: 0, + manMock: func(_ *mocks.MockWSMAN, _ *mocks.MockManagement) {}, + repoMock: func(repo *mocks.MockDeviceManagementRepository) { + repo.EXPECT(). + GetByID(context.Background(), device.GUID, ""). + Return(nil, nil) + }, + err: devices.ErrNotFound, + }, + { + name: "GetByID returns device with empty GUID", + eraseMask: 0, + manMock: func(_ *mocks.MockWSMAN, _ *mocks.MockManagement) {}, + repoMock: func(repo *mocks.MockDeviceManagementRepository) { + repo.EXPECT(). + GetByID(context.Background(), device.GUID, ""). + Return(&entity.Device{GUID: "", TenantID: "tenant-id-456"}, nil) + }, + err: devices.ErrNotFound, + }, + { + name: "SetupWsmanClient returns error", + eraseMask: 0, + manMock: func(man *mocks.MockWSMAN, _ *mocks.MockManagement) { + man.EXPECT(). + SetupWsmanClient(gomock.Any(), false, true). + Return(nil, ErrGeneral) + }, + repoMock: func(repo *mocks.MockDeviceManagementRepository) { + repo.EXPECT(). + GetByID(context.Background(), device.GUID, ""). + Return(device, nil) + }, + err: ErrGeneral, + }, + { + name: "GetBootCapabilities returns error", + eraseMask: 0, + manMock: func(man *mocks.MockWSMAN, man2 *mocks.MockManagement) { + man.EXPECT(). + SetupWsmanClient(gomock.Any(), false, true). + Return(man2, nil) + man2.EXPECT(). + GetBootCapabilities(). + Return(boot.BootCapabilitiesResponse{}, ErrGeneral) + }, + repoMock: func(repo *mocks.MockDeviceManagementRepository) { + repo.EXPECT(). + GetByID(context.Background(), device.GUID, ""). + Return(device, nil) + }, + err: ErrGeneral, + }, + { + name: "SendRemoteErase wsman call returns error", + eraseMask: 0, + manMock: func(man *mocks.MockWSMAN, man2 *mocks.MockManagement) { + man.EXPECT(). + SetupWsmanClient(gomock.Any(), false, true). + Return(man2, nil) + man2.EXPECT(). + GetBootCapabilities(). + Return(boot.BootCapabilitiesResponse{PlatformErase: 3}, nil) + man2.EXPECT(). + SendRemoteErase(0). + Return(ErrGeneral) + }, + repoMock: func(repo *mocks.MockDeviceManagementRepository) { + repo.EXPECT(). + GetByID(context.Background(), device.GUID, ""). + Return(device, nil) + }, + err: ErrGeneral, + }, + } + + for _, tc := range tests { + tc := tc + + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + useCase, wsmanMock, management, repo := initInfoTest(t) + tc.manMock(wsmanMock, management) + tc.repoMock(repo) + + err := useCase.SendRemoteErase(context.Background(), device.GUID, tc.eraseMask) + require.Equal(t, tc.err, err) + }) + } +} diff --git a/internal/usecase/devices/features.go b/internal/usecase/devices/features.go index ba572058f..f0a1a00ce 100644 --- a/internal/usecase/devices/features.go +++ b/internal/usecase/devices/features.go @@ -96,6 +96,11 @@ func (uc *UseCase) GetFeatures(c context.Context, guid string) (settingsResults settingsResults.HTTPSBootSupported = settingsResultsV2.HTTPSBootSupported settingsResults.WinREBootSupported = settingsResultsV2.WinREBootSupported settingsResults.LocalPBABootSupported = settingsResultsV2.LocalPBABootSupported + settingsResults.RemoteEraseEnabled = settingsResultsV2.RemoteEraseEnabled + settingsResults.RemoteEraseSupported = settingsResultsV2.RemoteEraseSupported + settingsResults.PlatformEraseCaps = settingsResultsV2.PlatformEraseCaps + + uc.log.Debug("GetFeatures: RemoteErase (PlatformErase) support", "guid", guid, "RemoteErase", settingsResultsV2.RemoteEraseEnabled) return settingsResults, settingsResultsV2, nil } @@ -178,6 +183,9 @@ func getOneClickRecoverySettings(settingsResultsV2 *dtov2.Features, device wsman settingsResultsV2.HTTPSBootSupported = isHTTPSBootSupported settingsResultsV2.WinREBootSupported = isWinREBootSupported settingsResultsV2.LocalPBABootSupported = isLocalPBABootSupported + settingsResultsV2.RemoteEraseEnabled = ocrData.bootData.PlatformErase + settingsResultsV2.RemoteEraseSupported = ocrData.capabilities.PlatformErase != 0 + settingsResultsV2.PlatformEraseCaps = ocrData.capabilities.PlatformErase return nil } @@ -235,33 +243,61 @@ func (uc *UseCase) SetFeatures(c context.Context, guid string, features dto.Feat settingsResults.UserConsent = features.UserConsent settingsResultsV2.UserConsent = features.UserConsent - // Configure OCR settings - requestedState := 0 - if features.OCR { - requestedState = enabledStateEnabled - } else { - requestedState = enabledStateDisabled + if err := setRPE(features.EnablePlatformErase, &settingsResultsV2, device); err != nil { + return settingsResults, settingsResultsV2, err } - _, err = device.BootServiceStateChange(requestedState) - if err == nil { - // Get OCR settings - err = getOneClickRecoverySettings(&settingsResultsV2, device) - if err != nil { - return dto.Features{}, dtov2.Features{}, err - } - - settingsResults.OCR = settingsResultsV2.OCR - settingsResults.HTTPSBootSupported = settingsResultsV2.HTTPSBootSupported - settingsResults.WinREBootSupported = settingsResultsV2.WinREBootSupported - settingsResults.LocalPBABootSupported = settingsResultsV2.LocalPBABootSupported + settingsResults.RemoteEraseEnabled = settingsResultsV2.RemoteEraseEnabled + if err := setOCRFeatures(features.OCR, &settingsResultsV2, device); err != nil { return settingsResults, settingsResultsV2, err } + settingsResults.OCR = settingsResultsV2.OCR + settingsResults.HTTPSBootSupported = settingsResultsV2.HTTPSBootSupported + settingsResults.WinREBootSupported = settingsResultsV2.WinREBootSupported + settingsResults.LocalPBABootSupported = settingsResultsV2.LocalPBABootSupported + settingsResults.RemoteEraseEnabled = settingsResultsV2.RemoteEraseEnabled + settingsResults.RemoteEraseSupported = settingsResultsV2.RemoteEraseSupported + settingsResults.PlatformEraseCaps = settingsResultsV2.PlatformEraseCaps + return settingsResults, settingsResultsV2, nil } +func setRPE(enableRemoteErase bool, settingsResultsV2 *dtov2.Features, device wsman.Management) error { + bootCapabilities, err := device.GetBootCapabilities() + if err != nil { + return err + } + + if bootCapabilities.PlatformErase != 0 { + if err := device.SetRPEEnabled(enableRemoteErase); err != nil { + return err + } + } + + settingsResultsV2.RemoteEraseEnabled = enableRemoteErase && bootCapabilities.PlatformErase != 0 + settingsResultsV2.RemoteEraseSupported = bootCapabilities.PlatformErase != 0 + settingsResultsV2.PlatformEraseCaps = bootCapabilities.PlatformErase + + return nil +} + +func setOCRFeatures(enableOCR bool, settingsResultsV2 *dtov2.Features, device wsman.Management) error { + requestedState := enabledStateDisabled + if enableOCR { + requestedState = enabledStateEnabled + } + + _, err := device.BootServiceStateChange(requestedState) + if err != nil { + // BootServiceStateChange failing is non-fatal (device may not support OCR) + return nil + } + + return getOneClickRecoverySettings(settingsResultsV2, device) +} + func handleAMTKVMError(err error, results *dtov2.Features) bool { amtErr := &amterror.AMTError{} if errors.As(err, &amtErr) { diff --git a/internal/usecase/devices/features_test.go b/internal/usecase/devices/features_test.go index 3c2143259..c3cfb6dfc 100644 --- a/internal/usecase/devices/features_test.go +++ b/internal/usecase/devices/features_test.go @@ -316,7 +316,7 @@ func TestGetFeatures(t *testing.T) { HTTPSBootSupported: true, WinREBootSupported: false, LocalPBABootSupported: false, - RemoteErase: false, + RemoteEraseEnabled: false, }, resV2: dtov2.Features{ UserConsent: "kvm", @@ -1216,7 +1216,7 @@ func TestSetFeatures(t *testing.T) { HTTPSBootSupported: true, WinREBootSupported: false, LocalPBABootSupported: false, - RemoteErase: false, + RemoteEraseEnabled: false, } featureSetDisabledOCRResult := dto.Features{ @@ -1229,7 +1229,7 @@ func TestSetFeatures(t *testing.T) { HTTPSBootSupported: true, WinREBootSupported: false, LocalPBABootSupported: false, - RemoteErase: false, + RemoteEraseEnabled: false, } featureSetV2DisabledOCR := dtov2.Features{ @@ -1243,7 +1243,16 @@ func TestSetFeatures(t *testing.T) { HTTPSBootSupported: true, WinREBootSupported: false, LocalPBABootSupported: false, - RemoteErase: false, + RemoteEraseEnabled: false, + } + + featureSetWithRPE := dto.Features{ + UserConsent: "kvm", + EnableSOL: true, + EnableIDER: true, + EnableKVM: true, + OCR: true, + EnablePlatformErase: true, } failGetByIDResult := dto.Features{} @@ -1331,6 +1340,9 @@ func TestSetFeatures(t *testing.T) { OptInState: 0, }). Return(nil) + man2.EXPECT(). + GetBootCapabilities(). + Return(boot.BootCapabilitiesResponse{}, nil) man2.EXPECT(). BootServiceStateChange(32769). // OCR enabled Return(cimBoot.BootService{}, nil) @@ -1385,7 +1397,7 @@ func TestSetFeatures(t *testing.T) { HTTPSBootSupported: true, WinREBootSupported: false, LocalPBABootSupported: false, - RemoteErase: false, + RemoteEraseEnabled: false, }, resV2: featureSetV2, err: nil, @@ -1442,6 +1454,9 @@ func TestSetFeatures(t *testing.T) { OptInState: 0, }). Return(nil) + man2.EXPECT(). + GetBootCapabilities(). + Return(boot.BootCapabilitiesResponse{}, nil) man2.EXPECT(). BootServiceStateChange(32768). Return(cimBoot.BootService{}, nil) @@ -1596,6 +1611,9 @@ func TestSetFeatures(t *testing.T) { OptInState: 0, }). Return(nil) + man2.EXPECT(). + GetBootCapabilities(). + Return(boot.BootCapabilitiesResponse{}, nil) man2.EXPECT(). BootServiceStateChange(32769). Return(cimBoot.BootService{}, nil) @@ -1650,7 +1668,7 @@ func TestSetFeatures(t *testing.T) { HTTPSBootSupported: true, WinREBootSupported: false, LocalPBABootSupported: false, - RemoteErase: false, + RemoteEraseEnabled: false, }, resV2: dtov2.Features{ UserConsent: "kvm", @@ -1813,6 +1831,9 @@ func TestSetFeatures(t *testing.T) { OptInState: 0, }). Return(nil) + man2.EXPECT(). + GetBootCapabilities(). + Return(boot.BootCapabilitiesResponse{}, nil) man2.EXPECT(). BootServiceStateChange(32769). Return(cimBoot.BootService{}, ErrGeneral) @@ -1891,6 +1912,9 @@ func TestSetFeatures(t *testing.T) { OptInState: 0, }). Return(nil) + man2.EXPECT(). + GetBootCapabilities(). + Return(boot.BootCapabilitiesResponse{}, nil) man2.EXPECT(). BootServiceStateChange(32769). Return(cimBoot.BootService{}, nil) @@ -2028,6 +2052,255 @@ func TestSetFeatures(t *testing.T) { }, err: ErrGeneral, }, + { + name: "setRPE fails on GetBootCapabilities error", + action: 0, + manMock: func(man *mocks.MockWSMAN, man2 *mocks.MockManagement) { + man.EXPECT(). + SetupWsmanClient(gomock.Any(), false, true). + Return(man2, nil) + man2.EXPECT(). + RequestAMTRedirectionServiceStateChange(true, true). + Return(redirection.EnableIDERAndSOL, 1, nil) + man2.EXPECT(). + SetKVMRedirection(true). + Return(1, nil) + man2.EXPECT(). + GetAMTRedirectionService(). + Return(redirection.Response{ + Body: redirection.Body{ + GetAndPutResponse: redirection.RedirectionResponse{ + EnabledState: 32771, + ListenerEnabled: true, + }, + }, + }, nil) + man2.EXPECT(). + SetAMTRedirectionService(&redirection.RedirectionRequest{ + EnabledState: redirection.EnabledState(redirection.EnableIDERAndSOL), + ListenerEnabled: true, + }). + Return(redirection.Response{ + Body: redirection.Body{ + GetAndPutResponse: redirection.RedirectionResponse{ + EnabledState: 32771, + ListenerEnabled: true, + }, + }, + }, nil) + man2.EXPECT(). + GetIPSOptInService(). + Return(optin.Response{ + Body: optin.Body{ + GetAndPutResponse: optin.OptInServiceResponse{ + OptInRequired: 1, + OptInState: 0, + }, + }, + }, nil) + man2.EXPECT(). + SetIPSOptInService(optin.OptInServiceRequest{ + OptInRequired: 1, + OptInState: 0, + }). + Return(nil) + man2.EXPECT(). + GetBootCapabilities(). + Return(boot.BootCapabilitiesResponse{}, ErrGeneral) + }, + repoMock: func(repo *mocks.MockDeviceManagementRepository) { + repo.EXPECT(). + GetByID(context.Background(), device.GUID, ""). + Return(device, nil) + }, + res: dto.Features{ + EnableSOL: true, + EnableIDER: true, + EnableKVM: true, + Redirection: true, + }, + resV2: dtov2.Features{ + EnableSOL: true, + EnableIDER: true, + EnableKVM: true, + Redirection: true, + KVMAvailable: true, + }, + err: ErrGeneral, + }, + { + name: "setRPE succeeds with PlatformErase supported and enabled", + action: 0, + manMock: func(man *mocks.MockWSMAN, man2 *mocks.MockManagement) { + man.EXPECT(). + SetupWsmanClient(gomock.Any(), false, true). + Return(man2, nil) + man2.EXPECT(). + RequestAMTRedirectionServiceStateChange(true, true). + Return(redirection.EnableIDERAndSOL, 1, nil) + man2.EXPECT(). + SetKVMRedirection(true). + Return(1, nil) + man2.EXPECT(). + GetAMTRedirectionService(). + Return(redirection.Response{ + Body: redirection.Body{ + GetAndPutResponse: redirection.RedirectionResponse{ + EnabledState: 32771, + ListenerEnabled: true, + }, + }, + }, nil) + man2.EXPECT(). + SetAMTRedirectionService(&redirection.RedirectionRequest{ + EnabledState: redirection.EnabledState(redirection.EnableIDERAndSOL), + ListenerEnabled: true, + }). + Return(redirection.Response{ + Body: redirection.Body{ + GetAndPutResponse: redirection.RedirectionResponse{ + EnabledState: 32771, + ListenerEnabled: true, + }, + }, + }, nil) + man2.EXPECT(). + GetIPSOptInService(). + Return(optin.Response{ + Body: optin.Body{ + GetAndPutResponse: optin.OptInServiceResponse{ + OptInRequired: 1, + OptInState: 0, + }, + }, + }, nil) + man2.EXPECT(). + SetIPSOptInService(optin.OptInServiceRequest{ + OptInRequired: 1, + OptInState: 0, + }). + Return(nil) + man2.EXPECT(). + GetBootCapabilities(). + Return(boot.BootCapabilitiesResponse{PlatformErase: 3}, nil) + man2.EXPECT(). + SetRPEEnabled(true). + Return(nil) + // BootServiceStateChange fails (non-fatal), so getOneClickRecoverySettings + // is skipped and the RPE fields set by setRPE are preserved. + man2.EXPECT(). + BootServiceStateChange(32769). + Return(cimBoot.BootService{}, ErrGeneral) + }, + repoMock: func(repo *mocks.MockDeviceManagementRepository) { + repo.EXPECT(). + GetByID(context.Background(), device.GUID, ""). + Return(device, nil) + }, + res: dto.Features{ + UserConsent: "kvm", + EnableSOL: true, + EnableIDER: true, + EnableKVM: true, + Redirection: true, + OCR: false, + RemoteEraseEnabled: true, + RemoteEraseSupported: true, + PlatformEraseCaps: 3, + }, + resV2: dtov2.Features{ + UserConsent: "kvm", + EnableSOL: true, + EnableIDER: true, + EnableKVM: true, + Redirection: true, + KVMAvailable: true, + OCR: false, + RemoteEraseEnabled: true, + RemoteEraseSupported: true, + PlatformEraseCaps: 3, + }, + err: nil, + }, + { + name: "setRPE fails on SetRPEEnabled error", + action: 0, + manMock: func(man *mocks.MockWSMAN, man2 *mocks.MockManagement) { + man.EXPECT(). + SetupWsmanClient(gomock.Any(), false, true). + Return(man2, nil) + man2.EXPECT(). + RequestAMTRedirectionServiceStateChange(true, true). + Return(redirection.EnableIDERAndSOL, 1, nil) + man2.EXPECT(). + SetKVMRedirection(true). + Return(1, nil) + man2.EXPECT(). + GetAMTRedirectionService(). + Return(redirection.Response{ + Body: redirection.Body{ + GetAndPutResponse: redirection.RedirectionResponse{ + EnabledState: 32771, + ListenerEnabled: true, + }, + }, + }, nil) + man2.EXPECT(). + SetAMTRedirectionService(&redirection.RedirectionRequest{ + EnabledState: redirection.EnabledState(redirection.EnableIDERAndSOL), + ListenerEnabled: true, + }). + Return(redirection.Response{ + Body: redirection.Body{ + GetAndPutResponse: redirection.RedirectionResponse{ + EnabledState: 32771, + ListenerEnabled: true, + }, + }, + }, nil) + man2.EXPECT(). + GetIPSOptInService(). + Return(optin.Response{ + Body: optin.Body{ + GetAndPutResponse: optin.OptInServiceResponse{ + OptInRequired: 1, + OptInState: 0, + }, + }, + }, nil) + man2.EXPECT(). + SetIPSOptInService(optin.OptInServiceRequest{ + OptInRequired: 1, + OptInState: 0, + }). + Return(nil) + man2.EXPECT(). + GetBootCapabilities(). + Return(boot.BootCapabilitiesResponse{PlatformErase: 3}, nil) + man2.EXPECT(). + SetRPEEnabled(true). + Return(ErrGeneral) + }, + repoMock: func(repo *mocks.MockDeviceManagementRepository) { + repo.EXPECT(). + GetByID(context.Background(), device.GUID, ""). + Return(device, nil) + }, + res: dto.Features{ + EnableSOL: true, + EnableIDER: true, + EnableKVM: true, + Redirection: true, + }, + resV2: dtov2.Features{ + EnableSOL: true, + EnableIDER: true, + EnableKVM: true, + Redirection: true, + KVMAvailable: true, + }, + err: ErrGeneral, + }, } for _, tc := range tests { @@ -2042,11 +2315,16 @@ func TestSetFeatures(t *testing.T) { tc.repoMock(repo) - // Use the appropriate input for the OCR disabled test + // Use the appropriate input for tests that require non-default features var inputFeatures dto.Features - if tc.name == "success with OCR disabled" { + + switch tc.name { + case "success with OCR disabled": inputFeatures = featureSetDisabledOCR - } else { + case "setRPE succeeds with PlatformErase supported and enabled", + "setRPE fails on SetRPEEnabled error": + inputFeatures = featureSetWithRPE + default: inputFeatures = featureSet } diff --git a/internal/usecase/devices/interfaces.go b/internal/usecase/devices/interfaces.go index 1fe76e625..2ba88afc1 100644 --- a/internal/usecase/devices/interfaces.go +++ b/internal/usecase/devices/interfaces.go @@ -70,6 +70,9 @@ type ( GetHardwareInfo(ctx context.Context, guid string) (dto.HardwareInfo, error) GetPowerState(ctx context.Context, guid string) (dto.PowerState, error) GetPowerCapabilities(ctx context.Context, guid string) (dto.PowerCapabilities, error) + GetBootCapabilities(ctx context.Context, guid string) (dto.BootCapabilities, error) + SetRPEEnabled(ctx context.Context, guid string, enabled bool) error + SendRemoteErase(ctx context.Context, guid string, eraseMask int) error GetGeneralSettings(ctx context.Context, guid string) (dto.GeneralSettings, error) CancelUserConsent(ctx context.Context, guid string) (dto.UserConsentMessage, error) GetUserConsentCode(ctx context.Context, guid string) (dto.UserConsentMessage, error) diff --git a/internal/usecase/devices/wsman/interfaces.go b/internal/usecase/devices/wsman/interfaces.go index 7ff7a5abf..3b3113b58 100644 --- a/internal/usecase/devices/wsman/interfaces.go +++ b/internal/usecase/devices/wsman/interfaces.go @@ -46,6 +46,7 @@ type Management interface { GetIPSPowerManagementService() (ipspower.PowerManagementService, error) RequestOSPowerSavingStateChange(osPowerSavingState ipspower.OSPowerSavingState) (ipspower.PowerActionResponse, error) GetPowerCapabilities() (boot.BootCapabilitiesResponse, error) + GetBootCapabilities() (boot.BootCapabilitiesResponse, error) GetGeneralSettings() (interface{}, error) CancelUserConsentRequest() (optin.Response, error) GetUserConsentCode() (optin.Response, error) @@ -72,4 +73,6 @@ type Management interface { SetIPSKVMRedirectionSettingData(data *kvmredirection.KVMRedirectionSettingsRequest) (kvmredirection.Response, error) DeleteCertificate(instanceID string) error SetLinkPreference(linkPreference, timeout uint32) (int, error) + SetRPEEnabled(enabled bool) error + SendRemoteErase(eraseMask int) error } diff --git a/internal/usecase/devices/wsman/message.go b/internal/usecase/devices/wsman/message.go index a7c10c024..64d19a07e 100644 --- a/internal/usecase/devices/wsman/message.go +++ b/internal/usecase/devices/wsman/message.go @@ -681,6 +681,106 @@ func (c *ConnectionEntry) GetPowerCapabilities() (boot.BootCapabilitiesResponse, return response.Body.BootCapabilitiesGetResponse, nil } +func (c *ConnectionEntry) GetBootCapabilities() (boot.BootCapabilitiesResponse, error) { + response, err := c.WsmanMessages.AMT.BootCapabilities.Get() + if err != nil { + return boot.BootCapabilitiesResponse{}, err + } + + return response.Body.BootCapabilitiesGetResponse, nil +} + +func (c *ConnectionEntry) SetRPEEnabled(enabled bool) error { + bootData, err := c.WsmanMessages.AMT.BootSettingData.Get() + if err != nil { + return err + } + + current := bootData.Body.BootSettingDataGetResponse + + _, err = c.WsmanMessages.AMT.BootSettingData.Put(boot.BootSettingDataRequest{ + BIOSLastStatus: current.BIOSLastStatus, + BIOSPause: current.BIOSPause, + BIOSSetup: current.BIOSSetup, + BootMediaIndex: current.BootMediaIndex, + BootguardStatus: current.BootguardStatus, + ConfigurationDataReset: current.ConfigurationDataReset, + ElementName: current.ElementName, + EnforceSecureBoot: current.EnforceSecureBoot, + FirmwareVerbosity: current.FirmwareVerbosity, + ForcedProgressEvents: current.ForcedProgressEvents, + IDERBootDevice: current.IDERBootDevice, + InstanceID: current.InstanceID, + LockKeyboard: current.LockKeyboard, + LockPowerButton: current.LockPowerButton, + LockResetButton: current.LockResetButton, + LockSleepButton: current.LockSleepButton, + OptionsCleared: current.OptionsCleared, + OwningEntity: current.OwningEntity, + PlatformErase: enabled, + ReflashBIOS: current.ReflashBIOS, + SecureErase: current.SecureErase, + UseIDER: current.UseIDER, + UseSOL: current.UseSOL, + UseSafeMode: current.UseSafeMode, + UserPasswordBypass: current.UserPasswordBypass, + }) + + return err +} + +func (c *ConnectionEntry) SendRemoteErase(eraseMask int) error { + bootData, err := c.WsmanMessages.AMT.BootSettingData.Get() + if err != nil { + return err + } + + current := bootData.Body.BootSettingDataGetResponse + + // Step 1: Set AMT_BootSettingData.PlatformErase to arm RPE on the next boot. + _, err = c.WsmanMessages.AMT.BootSettingData.Put(boot.BootSettingDataRequest{ + BIOSLastStatus: current.BIOSLastStatus, + BIOSPause: current.BIOSPause, + BIOSSetup: current.BIOSSetup, + BootMediaIndex: current.BootMediaIndex, + BootguardStatus: current.BootguardStatus, + ConfigurationDataReset: current.ConfigurationDataReset, + ElementName: current.ElementName, + EnforceSecureBoot: current.EnforceSecureBoot, + FirmwareVerbosity: current.FirmwareVerbosity, + ForcedProgressEvents: current.ForcedProgressEvents, + IDERBootDevice: current.IDERBootDevice, + InstanceID: current.InstanceID, + LockKeyboard: current.LockKeyboard, + LockPowerButton: current.LockPowerButton, + LockResetButton: current.LockResetButton, + LockSleepButton: current.LockSleepButton, + OptionsCleared: current.OptionsCleared, + OwningEntity: current.OwningEntity, + PlatformErase: eraseMask != 0, + ReflashBIOS: current.ReflashBIOS, + SecureErase: current.SecureErase, + UseIDER: current.UseIDER, + UseSOL: current.UseSOL, + UseSafeMode: current.UseSafeMode, + UserPasswordBypass: current.UserPasswordBypass, + }) + if err != nil { + return err + } + + // Step 3: Activate the boot configuration via CIM_BootService.SetBootConfigRole. + _, err = c.WsmanMessages.CIM.BootService.SetBootConfigRole("Intel(r) AMT: Boot Configuration 0", 1) + if err != nil { + return err + } + + // Step 4: Reset the device so AMT executes the RPE flow on the next boot. + _, err = c.WsmanMessages.CIM.PowerManagementService.RequestPowerStateChange(power.MasterBusReset) + + return err +} + func (c *ConnectionEntry) GetGeneralSettings() (interface{}, error) { response, err := c.WsmanMessages.AMT.GeneralSettings.Get() if err != nil { diff --git a/internal/usecase/profiles/usecase.go b/internal/usecase/profiles/usecase.go index 81d20f00a..a8de30103 100644 --- a/internal/usecase/profiles/usecase.go +++ b/internal/usecase/profiles/usecase.go @@ -326,7 +326,7 @@ func (uc *UseCase) BuildConfigurationObject(profileName string, data *entity.Pro Wired: config.Wired{ DHCPEnabled: data.DHCPEnabled, IPSyncEnabled: data.IPSyncEnabled, - SharedStaticIP: !data.DHCPEnabled, + SharedStaticIP: false, }, Wireless: config.Wireless{ WiFiSyncEnabled: data.LocalWiFiSyncEnabled, diff --git a/internal/usecase/profiles/usecase_test.go b/internal/usecase/profiles/usecase_test.go index 3bb442b8b..428667602 100644 --- a/internal/usecase/profiles/usecase_test.go +++ b/internal/usecase/profiles/usecase_test.go @@ -942,78 +942,6 @@ func TestBuildConfigurationObject(t *testing.T) { }, }, }, - { - name: "static IP mode sets SharedStaticIP to true", - profile: &entity.Profile{ - ProfileName: "test-profile-static", - Tags: "static", - DHCPEnabled: false, - IPSyncEnabled: true, - Activation: "acmactivate", - AMTPassword: "testAMTPassword", - MEBXPassword: "testMEBXPassword", - TLSMode: 0, - KVMEnabled: true, - SOLEnabled: false, - IDEREnabled: false, - UserConsent: "All", - }, - domain: &entity.Domain{ - ProvisioningCert: "testCert", - ProvisioningCertPassword: "testCertPwd", - }, - wifi: []config.WirelessProfile{}, - expected: config.Configuration{ - Name: "test-profile-static", - Tags: []string{"static"}, - Configuration: config.RemoteManagement{ - GeneralSettings: config.GeneralSettings{ - SharedFQDN: false, - NetworkInterfaceEnabled: 0, - PingResponseEnabled: false, - }, - Network: config.Network{ - Wired: config.Wired{ - DHCPEnabled: false, - IPSyncEnabled: true, - SharedStaticIP: true, - }, - Wireless: config.Wireless{ - Profiles: []config.WirelessProfile{}, - }, - }, - Redirection: config.Redirection{ - Enabled: true, - Services: config.Services{ - KVM: true, - SOL: false, - IDER: false, - }, - UserConsent: "All", - }, - TLS: config.TLS{ - MutualAuthentication: false, - Enabled: false, - AllowNonTLS: false, - }, - EnterpriseAssistant: config.EnterpriseAssistant{ - URL: "http://test.com:8080", - Username: "username", - Password: "password", - }, - AMTSpecific: config.AMTSpecific{ - ControlMode: "acmactivate", - AdminPassword: "testAMTPassword", - MEBXPassword: "testMEBXPassword", - ProvisioningCert: "testCert", - ProvisioningCertPwd: "testCertPwd", - CIRA: config.CIRA{ - EnvironmentDetection: []string{}, - }, - }, - }, - }, - }, } for _, tc := range tests { diff --git a/internal/usecase/sqldb/device_test.go b/internal/usecase/sqldb/device_test.go index 1f31250ee..aae8602e7 100644 --- a/internal/usecase/sqldb/device_test.go +++ b/internal/usecase/sqldb/device_test.go @@ -17,6 +17,24 @@ import ( "github.com/device-management-toolkit/console/pkg/db" ) +const BuilderErrorTestName = "Builder error" + +// alwaysErrFormat is a squirrel PlaceholderFormat that always fails ReplacePlaceholders, +// causing builder.ToSql() to return an error. +type alwaysErrFormat struct{} + +func (alwaysErrFormat) ReplacePlaceholders(_ string) (string, error) { + return "", errors.New("placeholder error") +} + +func createSQLConfigWithBuilderError(dbConn *sql.DB) *db.SQL { + return &db.SQL{ + Builder: squirrel.StatementBuilder.PlaceholderFormat(alwaysErrFormat{}), + Pool: dbConn, + IsEmbedded: true, + } +} + var ( crthash = "certhash" Certhash = &crthash @@ -1179,6 +1197,14 @@ func TestDeviceRepo_UpdateConnectionStatus(t *testing.T) { err: &sqldb.DatabaseError{}, verify: func(_ *testing.T, _ *sql.DB) {}, }, + { + name: BuilderErrorTestName, + setup: func(_ *sql.DB) {}, + guid: "guid1", + status: true, + err: &sqldb.DatabaseError{}, + verify: func(_ *testing.T, _ *sql.DB) {}, + }, } for _, tc := range tests { @@ -1191,7 +1217,12 @@ func TestDeviceRepo_UpdateConnectionStatus(t *testing.T) { tc.setup(dbConn) - sqlConfig := CreateSQLConfig(dbConn, tc.name == QueryExecutionErrorTestName) + var sqlConfig *db.SQL + if tc.name == BuilderErrorTestName { + sqlConfig = createSQLConfigWithBuilderError(dbConn) + } else { + sqlConfig = CreateSQLConfig(dbConn, tc.name == QueryExecutionErrorTestName) + } mockLog := mocks.NewMockLogger(nil) repo := sqldb.NewDeviceRepo(sqlConfig, mockLog) @@ -1258,6 +1289,13 @@ func TestDeviceRepo_UpdateLastSeen(t *testing.T) { err: &sqldb.DatabaseError{}, verify: func(_ *testing.T, _ *sql.DB) {}, }, + { + name: BuilderErrorTestName, + setup: func(_ *sql.DB) {}, + guid: "guid1", + err: &sqldb.DatabaseError{}, + verify: func(_ *testing.T, _ *sql.DB) {}, + }, } for _, tc := range tests { @@ -1270,7 +1308,12 @@ func TestDeviceRepo_UpdateLastSeen(t *testing.T) { tc.setup(dbConn) - sqlConfig := CreateSQLConfig(dbConn, tc.name == QueryExecutionErrorTestName) + var sqlConfig *db.SQL + if tc.name == BuilderErrorTestName { + sqlConfig = createSQLConfigWithBuilderError(dbConn) + } else { + sqlConfig = CreateSQLConfig(dbConn, tc.name == QueryExecutionErrorTestName) + } mockLog := mocks.NewMockLogger(nil) repo := sqldb.NewDeviceRepo(sqlConfig, mockLog)