From 164a5b7f2891e326b6b7067d62fffdda9a911fbc Mon Sep 17 00:00:00 2001 From: "Lim, Choon Keat" Date: Thu, 15 Oct 2026 16:10:33 +0800 Subject: [PATCH] feat: return a 409 error when a device reaches max 8 wireless profiles --- .../console_mps_apis.postman_collection.json | 50 +++++++++- internal/controller/httpapi/v1/wifiprofile.go | 9 ++ .../controller/httpapi/v1/wifiprofile_test.go | 11 +++ .../controller/openapi/devicemanagement.go | 4 +- internal/usecase/devices/wifiprofile.go | 28 ++++++ internal/usecase/devices/wifiprofile_test.go | 99 +++++++++++++++++++ 6 files changed, 198 insertions(+), 3 deletions(-) diff --git a/integration-test/collections/console_mps_apis.postman_collection.json b/integration-test/collections/console_mps_apis.postman_collection.json index 510059e21..68f131143 100644 --- a/integration-test/collections/console_mps_apis.postman_collection.json +++ b/integration-test/collections/console_mps_apis.postman_collection.json @@ -1419,9 +1419,55 @@ "profile", "{{deviceId}}" ] + }, + "description": "Adds a wireless profile to an AMT device.\n\nThe test script expects 404 because the integration test run has no device.\n\nOther responses:\n- 204 No Content: the profile was added.\n- 409 Conflict: a profile with the same name or priority already exists, or the device already stores the maximum of 8 wireless profiles. To reproduce the limit case, add 8 profiles with different names and priorities to the device first, then send a 9th. AMT \"User Settings\" entries do not count toward the limit. See the saved example \"409 Conflict - maximum of 8 wireless profiles reached\"." + }, + "response": [ + { + "name": "409 Conflict - maximum of 8 wireless profiles reached", + "originalRequest": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\r\n \"profileName\": \"Profile9\",\r\n \"ssid\": \"Profile9SSID\",\r\n \"priority\": 9,\r\n \"authenticationMethod\": \"WPA2PSK\",\r\n \"encryptionMethod\": \"CCMP\",\r\n \"password\": \"password123\"\r\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{protocol}}://{{host}}/api/v1/amt/networkSettings/wireless/profile/123e4567-e89b-12d3-a456-426614174000", + "protocol": "{{protocol}}", + "host": [ + "{{host}}" + ], + "path": [ + "api", + "v1", + "amt", + "networkSettings", + "wireless", + "profile", + "123e4567-e89b-12d3-a456-426614174000" + ] + }, + "description": "Prerequisite: the device already stores 8 wireless profiles (profiles 1 to 8, with priorities other than 9). This request adds a 9th." + }, + "status": "Conflict", + "code": 409, + "_postman_previewlanguage": "json", + "header": [ + { + "key": "Content-Type", + "value": "application/json; charset=utf-8" + } + ], + "cookie": [], + "body": "{\n \"error\": \"Add Wireless Profile failed for guid: 123e4567-e89b-12d3-a456-426614174000. - device already has the maximum of 8 wireless profiles; delete a profile before adding a new one\"\n}" } - }, - "response": [] + ] }, { "name": "Add Duplicate Wireless Profile", diff --git a/internal/controller/httpapi/v1/wifiprofile.go b/internal/controller/httpapi/v1/wifiprofile.go index 36e06da7d..211a966b6 100644 --- a/internal/controller/httpapi/v1/wifiprofile.go +++ b/internal/controller/httpapi/v1/wifiprofile.go @@ -8,6 +8,7 @@ import ( "github.com/device-management-toolkit/console/internal/controller/httpapi/middleware" "github.com/device-management-toolkit/console/internal/entity/dto/v1" + "github.com/device-management-toolkit/console/internal/usecase/devices" "github.com/device-management-toolkit/console/internal/usecase/devices/wsman" "github.com/device-management-toolkit/console/pkg/consoleerrors" ) @@ -54,6 +55,14 @@ func (r *deviceManagementRoutes) addWirelessProfile(c *gin.Context) { return } + if errors.Is(err, devices.ErrWirelessProfileLimitReached) { + c.JSON(http.StatusConflict, gin.H{ + errorKey: "Add Wireless Profile failed for guid: " + guid + ". - " + err.Error(), + }) + + return + } + ErrorResponse(c, err) return diff --git a/internal/controller/httpapi/v1/wifiprofile_test.go b/internal/controller/httpapi/v1/wifiprofile_test.go index f9d4289db..049c8ddb3 100644 --- a/internal/controller/httpapi/v1/wifiprofile_test.go +++ b/internal/controller/httpapi/v1/wifiprofile_test.go @@ -17,6 +17,7 @@ import ( dto "github.com/device-management-toolkit/console/internal/entity/dto/v1" "github.com/device-management-toolkit/console/internal/mocks" + "github.com/device-management-toolkit/console/internal/usecase/devices" "github.com/device-management-toolkit/console/internal/usecase/devices/wsman" ) @@ -112,6 +113,16 @@ func TestWiFiProfileRoutes(t *testing.T) { //nolint:gocognit // table-driven HTT }, expectedCode: http.StatusNotFound, }, + { + name: "add wireless profile - profile limit reached", + method: http.MethodPost, + url: "/api/v1/amt/networkSettings/wireless/profile/device-guid", + requestBody: request, + mock: func(feature *mocks.MockDeviceManagementFeature) { + feature.EXPECT().AddWirelessProfile(context.Background(), "device-guid", "", request).Return(devices.ErrWirelessProfileLimitReached) + }, + expectedCode: http.StatusConflict, + }, { name: "update wireless profile", method: http.MethodPatch, diff --git a/internal/controller/openapi/devicemanagement.go b/internal/controller/openapi/devicemanagement.go index 1424420c4..7d913f85b 100644 --- a/internal/controller/openapi/devicemanagement.go +++ b/internal/controller/openapi/devicemanagement.go @@ -150,7 +150,9 @@ func (f *FuegoAdapter) registerNetworkRoutes() { f.server, "/api/v1/amt/networkSettings/wireless/profile/{guid}", f.addWirelessProfile, fuego.OptionTags("Device Management"), fuego.OptionSummary("Create Wireless Profile"), - fuego.OptionDescription("Create a wireless profile on a device. "+wirelessProfileRequestRules), + fuego.OptionDescription("Create a wireless profile on a device. "+wirelessProfileRequestRules+". "+ + "A device stores at most 8 wireless profiles; adding one more, or a profile whose name or priority "+ + "already exists, is rejected with 409 Conflict"), fuego.OptionPath("guid", "Device GUID"), fuego.OptionDefaultStatusCode(http.StatusNoContent), protectedRouteOptions(), diff --git a/internal/usecase/devices/wifiprofile.go b/internal/usecase/devices/wifiprofile.go index 792e4a2b8..48b8a4cf3 100644 --- a/internal/usecase/devices/wifiprofile.go +++ b/internal/usecase/devices/wifiprofile.go @@ -26,11 +26,17 @@ const ( resourceCIMWiFiEndpointSettings = "CIM_WiFiEndpointSettings" resourceCIMIEEE8021xSettings = "CIM_IEEE8021xSettings" selectorNameInstanceID = "InstanceID" + // MaxWirelessProfiles is the number of user-defined wireless profiles AMT can store. + MaxWirelessProfiles = 8 ) var ( errInvalidAuthenticationMethod = errors.New("invalid authentication method") errInvalidEncryptionMethod = errors.New("invalid encryption method") + + // ErrWirelessProfileLimitReached is returned when a device already stores + // MaxWirelessProfiles wireless profiles. The HTTP layer maps it to 409 Conflict. + ErrWirelessProfileLimitReached = errors.New("device already has the maximum of 8 wireless profiles; delete a profile before adding a new one") ) type IEEE8021xCertHandles struct { @@ -81,6 +87,12 @@ func (uc *UseCase) AddWirelessProfile(c context.Context, guid, tenantID string, return wirelessProfilePriorityAlreadyExists(profile.Priority) } + // Check the limit before any certificate is added, so a rejected request + // leaves nothing behind on the device. + if countWirelessProfiles(settings) >= MaxWirelessProfiles { + return ErrWirelessProfileLimitReached + } + preparedProfile, needsPauseBeforeApply, err := prepareWirelessProfileForApply(device, profile) if err != nil { return err @@ -260,6 +272,22 @@ func findWirelessSettingByProfileName(settings []wifi.WiFiEndpointSettingsRespon return wifi.WiFiEndpointSettingsResponse{}, false } +// countWirelessProfiles counts the user-defined profiles, skipping the +// "User Settings" entries that AMT reports alongside them. +func countWirelessProfiles(settings []wifi.WiFiEndpointSettingsResponse) int { + count := 0 + + for i := range settings { + if settings[i].InstanceID == "" || isUserSettingsInstanceID(settings[i].InstanceID) { + continue + } + + count++ + } + + return count +} + func findWirelessSettingByPriority(settings []wifi.WiFiEndpointSettingsResponse, priority int) (wifi.WiFiEndpointSettingsResponse, bool) { for i := range settings { setting := settings[i] diff --git a/internal/usecase/devices/wifiprofile_test.go b/internal/usecase/devices/wifiprofile_test.go index a255ef938..4da92697a 100644 --- a/internal/usecase/devices/wifiprofile_test.go +++ b/internal/usecase/devices/wifiprofile_test.go @@ -536,6 +536,105 @@ func TestAddWirelessProfile(t *testing.T) { //nolint:gocognit // table-driven co } } +// wirelessProfileSettings returns count user-defined profiles with priorities +// starting at 2, so they do not clash with a request that uses priority 1. +func wirelessProfileSettings(count int) []wifi.WiFiEndpointSettingsResponse { + settings := make([]wifi.WiFiEndpointSettingsResponse, 0, count) + + for i := 0; i < count; i++ { + name := fmt.Sprintf("Profile%d", i) + settings = append(settings, wifi.WiFiEndpointSettingsResponse{ + InstanceID: "Intel(r) AMT:WiFi Endpoint Settings " + name, + ElementName: name, + Priority: i + 2, + }) + } + + return settings +} + +func TestAddWirelessProfileLimit(t *testing.T) { + t.Parallel() + + device := &entity.Device{GUID: "device-guid-123"} + profile := config.WirelessProfile{ + ProfileName: "Home", + SSID: "HomeSSID", + Priority: 1, + Password: "password", + AuthenticationMethod: "WPA2PSK", + EncryptionMethod: "CCMP", + } + userSettings := wifi.WiFiEndpointSettingsResponse{InstanceID: "Intel(r) AMT:WiFi Endpoint User Settings 1", ElementName: "User", Priority: 1} + + tests := []struct { + name string + profile config.WirelessProfile + settings []wifi.WiFiEndpointSettingsResponse + expectAdd bool + wantErrIs error + }{ + { + name: "one below the limit is added", + profile: profile, + settings: wirelessProfileSettings(devices.MaxWirelessProfiles - 1), + expectAdd: true, + }, + { + name: "user settings entries do not count toward the limit", + profile: config.WirelessProfile{ProfileName: "Home", SSID: "HomeSSID", Priority: 20, Password: "password", AuthenticationMethod: "WPA2PSK", EncryptionMethod: "CCMP"}, + settings: append(wirelessProfileSettings(devices.MaxWirelessProfiles-1), userSettings), + expectAdd: true, + }, + { + name: "at the limit is rejected", + profile: profile, + settings: wirelessProfileSettings(devices.MaxWirelessProfiles), + wantErrIs: devices.ErrWirelessProfileLimitReached, + }, + { + name: "at the limit is rejected before 802.1x certificates are added", + profile: config.WirelessProfile{ + ProfileName: "Corp", + SSID: "CorpSSID", + Priority: 1, + AuthenticationMethod: "WPA2IEEE8021x", + EncryptionMethod: "CCMP", + IEEE8021x: &config.IEEE8021x{PrivateKey: "new-private"}, + }, + settings: wirelessProfileSettings(devices.MaxWirelessProfiles), + wantErrIs: devices.ErrWirelessProfileLimitReached, + }, + } + + for _, tc := range tests { + tc := tc + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + useCase, wsmanMock, management, repo := initWiFiProfileTest(t) + + repo.EXPECT().GetByID(context.Background(), device.GUID, device.TenantID).Return(device, nil) + wsmanMock.EXPECT().SetupWsmanClient(gomock.Any(), gomock.Any(), false, true).Return(management, nil) + expectWiFiPortPresent(management) + management.EXPECT().GetWiFiSettings().Return(tc.settings, nil) + + if tc.expectAdd { + management.EXPECT().AddWiFiSettings(expectedWiFiRequest(tc.profile), models.IEEE8021xSettings{}, testWiFiEndpoint, "", "").Return(wifiportconfiguration.Response{}, nil) + } + + err := useCase.AddWirelessProfile(context.Background(), device.GUID, device.TenantID, tc.profile) + if tc.wantErrIs != nil { + require.ErrorIs(t, err, tc.wantErrIs) + + return + } + + require.NoError(t, err) + }) + } +} + func TestDeleteWirelessProfile(t *testing.T) { t.Parallel()