diff --git a/integration-test/collections/console_mps_apis.postman_collection.json b/integration-test/collections/console_mps_apis.postman_collection.json index 2f5fb3dff..2dcb982e2 100644 --- a/integration-test/collections/console_mps_apis.postman_collection.json +++ b/integration-test/collections/console_mps_apis.postman_collection.json @@ -515,6 +515,70 @@ }, "response": [] }, + { + "name": "Send Advanced Power Action - EnforceSecureBoot false in CCM", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// This test validates the CCM restriction for EnforceSecureBoot\r", + "// When a device is in Client Control Mode (CCM), EnforceSecureBoot cannot be turned off\r", + "// Expected: 400 Bad Request with error message about CCM restriction\r", + "// Note: This test requires a device in CCM mode to properly validate\r", + "\r", + "pm.test(\"Status code is 400 or 404\", function () {\r", + " // 400 = CCM restriction error (expected for CCM device)\r", + " // 404 = Device not found (if no device configured)\r", + " pm.expect(pm.response.code).to.be.oneOf([400, 404]);\r", + "});\r", + "\r", + "pm.test(\"Response contains appropriate error message\", function () {\r", + " var jsonData = pm.response.json();\r", + " if (pm.response.code === 400) {\r", + " pm.expect(jsonData.error).to.include(\"EnforceSecureBoot\");\r", + " pm.expect(jsonData.error).to.include(\"CCM\");\r", + " } else if (pm.response.code === 404) {\r", + " pm.expect(jsonData.error).to.eq(\"Error not found\");\r", + " }\r", + "});\r", + "" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\r\n \"action\": 105,\r\n \"useSOL\": false,\r\n \"bootDetails\": {\r\n \"url\": \"https://example.com/boot.efi\",\r\n \"enforceSecureBoot\": false\r\n }\r\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{protocol}}://{{host}}/api/v1/amt/power/bootoptions/{{deviceId}}", + "protocol": "{{protocol}}", + "host": [ + "{{host}}" + ], + "path": [ + "api", + "v1", + "amt", + "power", + "bootoptions", + "{{deviceId}}" + ] + } + }, + "response": [] + }, { "name": "Get AMT Features", "event": [ diff --git a/internal/controller/httpapi/v1/error.go b/internal/controller/httpapi/v1/error.go index 218891d78..8fdb80fb4 100644 --- a/internal/controller/httpapi/v1/error.go +++ b/internal/controller/httpapi/v1/error.go @@ -29,6 +29,7 @@ func ErrorResponse(c *gin.Context, err error) { NotUniqueErr sqldb.NotUniqueError amtErr devices.AMTError notSupportedErr devices.NotSupportedError + validationErr devices.ValidationError certExpErr domains.CertExpirationError certPasswordErr domains.CertPasswordError netErr net.Error @@ -49,6 +50,9 @@ func ErrorResponse(c *gin.Context, err error) { dbErrorHandle(c, dbErr) case errors.As(err, &amtErr): amtErrorHandle(c, amtErr) + case errors.As(err, &validationErr): + msg := validationErr.Console.FriendlyMessage() + c.AbortWithStatusJSON(http.StatusBadRequest, response{Error: msg, Message: msg}) case errors.As(err, ¬SupportedErr): msg := notSupportedErr.Console.FriendlyMessage() c.AbortWithStatusJSON(http.StatusNotImplemented, response{Error: msg, Message: msg}) diff --git a/internal/entity/dto/v1/bootsetting.go b/internal/entity/dto/v1/bootsetting.go index 65eee4715..1736f7233 100644 --- a/internal/entity/dto/v1/bootsetting.go +++ b/internal/entity/dto/v1/bootsetting.go @@ -5,7 +5,7 @@ type BootDetails struct { Username string `json:"username" example:"admin"` Password string `json:"password" example:"password"` BootPath string `json:"bootPath" example:"\\OemPba.efi"` - EnforceSecureBoot bool `json:"enforceSecureBoot" example:"true"` + EnforceSecureBoot *bool `json:"enforceSecureBoot,omitempty" example:"true"` } type BootSetting struct { diff --git a/internal/usecase/devices/power.go b/internal/usecase/devices/power.go index 0e0305e1c..ab7151903 100644 --- a/internal/usecase/devices/power.go +++ b/internal/usecase/devices/power.go @@ -8,6 +8,7 @@ import ( "strings" "github.com/device-management-toolkit/go-wsman-messages/v2/pkg/wsman/amt/boot" + "github.com/device-management-toolkit/go-wsman-messages/v2/pkg/wsman/amt/setupandconfiguration" cimBoot "github.com/device-management-toolkit/go-wsman-messages/v2/pkg/wsman/cim/boot" "github.com/device-management-toolkit/go-wsman-messages/v2/pkg/wsman/cim/power" "github.com/device-management-toolkit/go-wsman-messages/v2/pkg/wsman/cim/software" @@ -244,6 +245,18 @@ func (uc *UseCase) SetBootOptions(c context.Context, guid string, bootSetting dt return power.PowerActionResponse{}, err } + // Validate EnforceSecureBoot restriction in CCM + if bootSetting.BootDetails.EnforceSecureBoot != nil && !*bootSetting.BootDetails.EnforceSecureBoot { + setupConfig, err := device.GetSetupAndConfiguration() + if err != nil { + return power.PowerActionResponse{}, err + } + + if len(setupConfig) > 0 && setupConfig[0].ProvisioningMode == setupandconfiguration.ClientControlMode { + return power.PowerActionResponse{}, ValidationError{}.Wrap("SetBootOptions", "validate provisioning mode", "EnforceSecureBoot cannot be turned off in CCM") + } + } + bootData, err := device.GetBootData() if err != nil { return power.PowerActionResponse{}, err @@ -325,7 +338,8 @@ func determineBootDevice(bootSetting dto.BootSetting, newData *boot.BootSettingD return err } - setUEFIBootSettings(newData, bootSetting.BootDetails.EnforceSecureBoot, params, typeLengthValueBuffer) + enforceSecureBoot := getEnforceSecureBoot(bootSetting.BootDetails.EnforceSecureBoot, newData.EnforceSecureBoot) + setUEFIBootSettings(newData, enforceSecureBoot, params, typeLengthValueBuffer) case BootActionPBA, BootActionPowerOnPBA, BootActionWinREBoot, BootActionPowerOnWinREBoot: if bootSetting.BootDetails.BootPath == "" { return ErrValidationUseCase @@ -336,7 +350,8 @@ func determineBootDevice(bootSetting dto.BootSetting, newData *boot.BootSettingD return err } - setUEFIBootSettings(newData, bootSetting.BootDetails.EnforceSecureBoot, params, typeLengthValueBuffer) + enforceSecureBoot := getEnforceSecureBoot(bootSetting.BootDetails.EnforceSecureBoot, newData.EnforceSecureBoot) + setUEFIBootSettings(newData, enforceSecureBoot, params, typeLengthValueBuffer) case BootActionResetToIDERCDROM, BootActionPowerOnIDERCDROM: newData.IDERBootDevice = 1 default: @@ -346,6 +361,19 @@ func determineBootDevice(bootSetting dto.BootSetting, newData *boot.BootSettingD return nil } +// getEnforceSecureBoot returns the EnforceSecureBoot value from the request if provided, +// otherwise falls back to the current device value. +func getEnforceSecureBoot(requestValue *bool, currentValue bool) bool { + if requestValue != nil { + return *requestValue + } + + return currentValue +} + +// setUEFIBootSettings expects enforceSecureBoot to be a fully resolved value. +// Callers should resolve any optional request value (for example, via getEnforceSecureBoot) +// before invoking this function, as it no longer accepts a *bool. func setUEFIBootSettings(newData *boot.BootSettingDataRequest, enforceSecureBoot bool, params int, typeLengthValueBuffer []byte) { newData.BIOSLastStatus = nil newData.UseIDER = false diff --git a/internal/usecase/devices/power_test.go b/internal/usecase/devices/power_test.go index bbd547eb7..93945914c 100644 --- a/internal/usecase/devices/power_test.go +++ b/internal/usecase/devices/power_test.go @@ -10,6 +10,7 @@ import ( gomock "go.uber.org/mock/gomock" "github.com/device-management-toolkit/go-wsman-messages/v2/pkg/wsman/amt/boot" + "github.com/device-management-toolkit/go-wsman-messages/v2/pkg/wsman/amt/setupandconfiguration" cimBoot "github.com/device-management-toolkit/go-wsman-messages/v2/pkg/wsman/cim/boot" "github.com/device-management-toolkit/go-wsman-messages/v2/pkg/wsman/cim/power" "github.com/device-management-toolkit/go-wsman-messages/v2/pkg/wsman/cim/service" @@ -747,6 +748,205 @@ func TestSetBootOptions(t *testing.T) { } } +func TestSetBootOptions_CCMRestriction(t *testing.T) { + t.Parallel() + + bootResponse := boot.BootSettingDataResponse{ + BIOSLastStatus: []uint16{2, 0}, + EnforceSecureBoot: true, + ElementName: "Intel(r) AMT Boot Configuration Settings", + InstanceID: "Intel(r) AMT:BootSettingData 0", + OwningEntity: "Intel(r) AMT", + } + + device := &entity.Device{ + GUID: "device-guid-123", + TenantID: "tenant-id-456", + } + + enforceSecureBootFalse := false + enforceSecureBootTrue := true + + powerActionRes := power.PowerActionResponse{ReturnValue: 5} + + tests := []struct { + name string + bootSetting dto.BootSetting + manMock func(*mocks.MockWSMAN, *mocks.MockManagement) + repoMock func(*mocks.MockDeviceManagementRepository) + wantErr error + }{ + { + name: "CCM restriction - EnforceSecureBoot false in CCM returns error", + bootSetting: dto.BootSetting{ + Action: 400, + UseSOL: true, + BootDetails: dto.BootDetails{ + EnforceSecureBoot: &enforceSecureBootFalse, + }, + }, + manMock: func(man *mocks.MockWSMAN, hmm *mocks.MockManagement) { + man.EXPECT(). + SetupWsmanClient(gomock.Any(), false, true). + Return(hmm, nil) + hmm.EXPECT(). + GetSetupAndConfiguration(). + Return([]setupandconfiguration.SetupAndConfigurationServiceResponse{ + {ProvisioningMode: setupandconfiguration.ClientControlMode}, + }, nil) + }, + repoMock: func(repo *mocks.MockDeviceManagementRepository) { + repo.EXPECT(). + GetByID(context.Background(), device.GUID, ""). + Return(device, nil) + }, + wantErr: devices.ValidationError{}.Wrap("SetBootOptions", "validate provisioning mode", "EnforceSecureBoot cannot be turned off in CCM"), + }, + { + name: "ACM mode - EnforceSecureBoot false allowed", + bootSetting: dto.BootSetting{ + Action: 400, + UseSOL: true, + BootDetails: dto.BootDetails{ + EnforceSecureBoot: &enforceSecureBootFalse, + }, + }, + manMock: func(man *mocks.MockWSMAN, hmm *mocks.MockManagement) { + man.EXPECT(). + SetupWsmanClient(gomock.Any(), false, true). + Return(hmm, nil) + hmm.EXPECT(). + GetSetupAndConfiguration(). + Return([]setupandconfiguration.SetupAndConfigurationServiceResponse{ + {ProvisioningMode: setupandconfiguration.AdminControlMode}, + }, nil) + hmm.EXPECT(). + GetBootData(). + Return(bootResponse, nil) + hmm.EXPECT(). + ChangeBootOrder(""). + Return(cimBoot.ChangeBootOrder_OUTPUT{}, nil) + hmm.EXPECT(). + SetBootData(gomock.Any()). + Return(nil, nil) + hmm.EXPECT(). + SetBootConfigRole(1). + Return(powerActionRes, nil) + hmm.EXPECT(). + ChangeBootOrder(string(cimBoot.PXE)). + Return(cimBoot.ChangeBootOrder_OUTPUT{}, nil) + hmm.EXPECT(). + SendPowerAction(10). + Return(powerActionRes, nil) + }, + repoMock: func(repo *mocks.MockDeviceManagementRepository) { + repo.EXPECT(). + GetByID(context.Background(), device.GUID, ""). + Return(device, nil) + }, + wantErr: nil, + }, + { + name: "CCM mode - EnforceSecureBoot true allowed", + bootSetting: dto.BootSetting{ + Action: 400, + UseSOL: true, + BootDetails: dto.BootDetails{ + EnforceSecureBoot: &enforceSecureBootTrue, + }, + }, + manMock: func(man *mocks.MockWSMAN, hmm *mocks.MockManagement) { + man.EXPECT(). + SetupWsmanClient(gomock.Any(), false, true). + Return(hmm, nil) + hmm.EXPECT(). + GetBootData(). + Return(bootResponse, nil) + hmm.EXPECT(). + ChangeBootOrder(""). + Return(cimBoot.ChangeBootOrder_OUTPUT{}, nil) + hmm.EXPECT(). + SetBootData(gomock.Any()). + Return(nil, nil) + hmm.EXPECT(). + SetBootConfigRole(1). + Return(powerActionRes, nil) + hmm.EXPECT(). + ChangeBootOrder(string(cimBoot.PXE)). + Return(cimBoot.ChangeBootOrder_OUTPUT{}, nil) + hmm.EXPECT(). + SendPowerAction(10). + Return(powerActionRes, nil) + }, + repoMock: func(repo *mocks.MockDeviceManagementRepository) { + repo.EXPECT(). + GetByID(context.Background(), device.GUID, ""). + Return(device, nil) + }, + wantErr: nil, + }, + { + name: "EnforceSecureBoot not provided - no CCM check", + bootSetting: dto.BootSetting{ + Action: 400, + UseSOL: true, + BootDetails: dto.BootDetails{ + EnforceSecureBoot: nil, + }, + }, + manMock: func(man *mocks.MockWSMAN, hmm *mocks.MockManagement) { + man.EXPECT(). + SetupWsmanClient(gomock.Any(), false, true). + Return(hmm, nil) + hmm.EXPECT(). + GetBootData(). + Return(bootResponse, nil) + hmm.EXPECT(). + ChangeBootOrder(""). + Return(cimBoot.ChangeBootOrder_OUTPUT{}, nil) + hmm.EXPECT(). + SetBootData(gomock.Any()). + Return(nil, nil) + hmm.EXPECT(). + SetBootConfigRole(1). + Return(powerActionRes, nil) + hmm.EXPECT(). + ChangeBootOrder(string(cimBoot.PXE)). + Return(cimBoot.ChangeBootOrder_OUTPUT{}, nil) + hmm.EXPECT(). + SendPowerAction(10). + Return(powerActionRes, nil) + }, + repoMock: func(repo *mocks.MockDeviceManagementRepository) { + repo.EXPECT(). + GetByID(context.Background(), device.GUID, ""). + Return(device, nil) + }, + wantErr: nil, + }, + } + + for _, tc := range tests { + tc := tc + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + useCase, wsmanMock, management, repo := initPowerTest(t) + tc.manMock(wsmanMock, management) + tc.repoMock(repo) + + _, err := useCase.SetBootOptions(context.Background(), device.GUID, tc.bootSetting) + + if tc.wantErr != nil { + require.Error(t, err) + require.ErrorIs(t, err, tc.wantErr) + } else { + require.NoError(t, err) + } + }) + } +} + func TestGetBootSourceSetting(t *testing.T) { t.Parallel()