From ef25e7978aefc842fe9313481ab6f64169e90ad0 Mon Sep 17 00:00:00 2001 From: "Loh, Shao Boon" Date: Tue, 7 Apr 2026 01:55:24 +0000 Subject: [PATCH] feat: add wireless profile management API --- .../console_mps_apis.postman_collection.json | 97 +++ internal/controller/httpapi/router.go | 30 +- .../controller/httpapi/v1/devicemanagement.go | 2 + internal/controller/httpapi/v1/wifiprofile.go | 44 ++ .../controller/httpapi/v1/wifiprofile_test.go | 148 ++++ internal/controller/httpapi/v1/wifistate.go | 2 +- .../controller/httpapi/v1/wifistate_test.go | 2 +- .../controller/openapi/devicemanagement.go | 38 + internal/controller/ws/v1/interface.go | 3 + internal/entity/dto/v1/wifiprofile.go | 149 ++++ internal/entity/dto/v1/wifiprofile_test.go | 341 ++++++++ internal/mocks/devicemanagement_mocks.go | 31 + internal/mocks/wsman_mocks.go | 77 ++ internal/mocks/wsv1_mocks.go | 31 + internal/usecase/devices/interfaces.go | 3 + internal/usecase/devices/wifiprofile.go | 539 +++++++++++++ .../devices/wifiprofile_private_test.go | 671 ++++++++++++++++ internal/usecase/devices/wifiprofile_test.go | 742 ++++++++++++++++++ internal/usecase/devices/wifistate.go | 2 +- internal/usecase/devices/wifistate_test.go | 4 +- internal/usecase/devices/wsman/interfaces.go | 8 + 21 files changed, 2949 insertions(+), 15 deletions(-) create mode 100644 internal/controller/httpapi/v1/wifiprofile.go create mode 100644 internal/controller/httpapi/v1/wifiprofile_test.go create mode 100644 internal/entity/dto/v1/wifiprofile.go create mode 100644 internal/entity/dto/v1/wifiprofile_test.go create mode 100644 internal/usecase/devices/wifiprofile.go create mode 100644 internal/usecase/devices/wifiprofile_private_test.go create mode 100644 internal/usecase/devices/wifiprofile_test.go diff --git a/integration-test/collections/console_mps_apis.postman_collection.json b/integration-test/collections/console_mps_apis.postman_collection.json index e7b29ebfc..0fef04838 100644 --- a/integration-test/collections/console_mps_apis.postman_collection.json +++ b/integration-test/collections/console_mps_apis.postman_collection.json @@ -913,6 +913,103 @@ } }, "response": [] + }, + { + "name": "Get Wireless Profiles", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Status code is 404\", function () {\r", + " pm.response.to.have.status(404);\r", + "});\r", + "\r", + "pm.test(\"Device should not be found\", function () {\r", + " var jsonData = pm.response.json();\r", + " pm.expect(jsonData.error).to.eq(\"Error not found\")\r", + "});\r", + "" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "{{protocol}}://{{host}}/api/v1/amt/networkSettings/wireless/profile/{{deviceId}}", + "protocol": "{{protocol}}", + "host": [ + "{{host}}" + ], + "path": [ + "api", + "v1", + "amt", + "networkSettings", + "wireless", + "profile", + "{{deviceId}}" + ] + } + }, + "response": [] + }, + { + "name": "Apply Wireless Profiles", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Status code is 404\", function () {\r", + " pm.response.to.have.status(404);\r", + "});\r", + "\r", + "pm.test(\"Device should not be found\", function () {\r", + " var jsonData = pm.response.json();\r", + " pm.expect(jsonData.error).to.eq(\"Error not found\")\r", + "});\r", + "" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\r\n \"profiles\": [\r\n {\r\n \"profileName\": \"Home\",\r\n \"ssid\": \"HomeSSID\",\r\n \"password\": \"P@ssword123\",\r\n \"authenticationMethod\": \"WPA2PSK\",\r\n \"encryptionMethod\": \"CCMP\",\r\n \"priority\": 1\r\n }\r\n ]\r\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{protocol}}://{{host}}/api/v1/amt/networkSettings/wireless/profile/{{deviceId}}", + "protocol": "{{protocol}}", + "host": [ + "{{host}}" + ], + "path": [ + "api", + "v1", + "amt", + "networkSettings", + "wireless", + "profile", + "{{deviceId}}" + ] + } + }, + "response": [] } ] }, diff --git a/internal/controller/httpapi/router.go b/internal/controller/httpapi/router.go index 80c69578d..6485a20a4 100644 --- a/internal/controller/httpapi/router.go +++ b/internal/controller/httpapi/router.go @@ -62,16 +62,7 @@ func NewRouter(handler *gin.Engine, l logger.Interface, t usecase.Usecases, cfg protected = handler.Group("/api", login.JWTAuthMiddleware()) } - // Register custom validators once - if v, ok := binding.Validator.Engine().(*validator.Validate); ok { - if err := v.RegisterValidation("alphanumhyphenunderscore", dto.ValidateAlphaNumHyphenUnderscore); err != nil { - l.Error("failed to register custom validation: " + err.Error()) - } - - if err := v.RegisterValidation("wifistate", dto.ValidateWirelessState); err != nil { - l.Error("failed to register custom validation: " + err.Error()) - } - } + registerCustomValidators(l) // Routers h2 := protected.Group("/v1") @@ -95,3 +86,22 @@ func NewRouter(handler *gin.Engine, l logger.Interface, t usecase.Usecases, cfg v2.NewAmtRoutes(h3, t.Devices, l) } } + +func registerCustomValidators(l logger.Interface) { + v, ok := binding.Validator.Engine().(*validator.Validate) + if !ok { + return + } + + registerValidation := func(tag string, validationFunc validator.Func) { + if err := v.RegisterValidation(tag, validationFunc); err != nil { + l.Error("failed to register custom validation: " + err.Error()) + } + } + + registerValidation("alphanumhyphenunderscore", dto.ValidateAlphaNumHyphenUnderscore) + registerValidation("wifistate", dto.ValidateWirelessState) + registerValidation("wirelessprofile", dto.ValidateWirelessProfile) + registerValidation("wirelessprofilescount", dto.ValidateWirelessProfilesCount) + registerValidation("wirelessprofilesuniquepriorities", dto.ValidateWirelessProfilesUniquePriorities) +} diff --git a/internal/controller/httpapi/v1/devicemanagement.go b/internal/controller/httpapi/v1/devicemanagement.go index 0ca12e54d..a20d9405e 100644 --- a/internal/controller/httpapi/v1/devicemanagement.go +++ b/internal/controller/httpapi/v1/devicemanagement.go @@ -52,6 +52,8 @@ func NewAmtRoutes(handler *gin.RouterGroup, d devices.Feature, amt amtexplorer.F h.GET("networkSettings/:guid", r.getNetworkSettings) h.GET("networkSettings/wireless/state/:guid", r.getWirelessState) h.POST("networkSettings/wireless/state/:guid", r.requestWirelessStateChange) + h.GET("networkSettings/wireless/profile/:guid", r.getWirelessProfiles) + h.POST("networkSettings/wireless/profile/:guid", r.applyWirelessProfiles) h.GET("explorer", r.getCallList) h.GET("explorer/:guid/:call", r.executeCall) diff --git a/internal/controller/httpapi/v1/wifiprofile.go b/internal/controller/httpapi/v1/wifiprofile.go new file mode 100644 index 000000000..a25216403 --- /dev/null +++ b/internal/controller/httpapi/v1/wifiprofile.go @@ -0,0 +1,44 @@ +package v1 + +import ( + "net/http" + + "github.com/gin-gonic/gin" + + "github.com/device-management-toolkit/console/internal/entity/dto/v1" +) + +func (r *deviceManagementRoutes) getWirelessProfiles(c *gin.Context) { + guid := c.Param("guid") + + response, err := r.d.GetWirelessProfiles(c.Request.Context(), guid) + if err != nil { + r.l.Error(err, "http - v1 - getWirelessProfiles") + ErrorResponse(c, err) + + return + } + + c.JSON(http.StatusOK, response) +} + +func (r *deviceManagementRoutes) applyWirelessProfiles(c *gin.Context) { + guid := c.Param("guid") + + var req dto.WirelessProfilesApplyRequest + if err := c.ShouldBindJSON(&req); err != nil { + ErrorResponse(c, err) + + return + } + + response, err := r.d.ApplyWirelessProfiles(c.Request.Context(), guid, req) + if err != nil { + r.l.Error(err, "http - v1 - applyWirelessProfiles") + ErrorResponse(c, err) + + return + } + + c.JSON(http.StatusOK, response) +} diff --git a/internal/controller/httpapi/v1/wifiprofile_test.go b/internal/controller/httpapi/v1/wifiprofile_test.go new file mode 100644 index 000000000..7c6fc4c1f --- /dev/null +++ b/internal/controller/httpapi/v1/wifiprofile_test.go @@ -0,0 +1,148 @@ +package v1 + +import ( + "bytes" + "context" + "encoding/json" + "net/http" + "net/http/httptest" + "testing" + + "github.com/stretchr/testify/require" + + "github.com/device-management-toolkit/go-wsman-messages/v2/pkg/config" + + "github.com/device-management-toolkit/console/internal/entity/dto/v1" + "github.com/device-management-toolkit/console/internal/mocks" +) + +type wiFiProfileRouteTest struct { + name string + method string + url string + mock func(*mocks.MockDeviceManagementFeature) + requestBody interface{} + rawBody string + response interface{} + expectedCode int +} + +func TestWiFiProfileRoutes(t *testing.T) { + t.Parallel() + + request := dto.WirelessProfilesApplyRequest{ + Profiles: []config.WirelessProfile{{ + ProfileName: "office", + SSID: "CorpNet", + Priority: 1, + AuthenticationMethod: "WPA2PSK", + EncryptionMethod: "CCMP", + Password: "password123", + }}, + } + + expectedProfiles := []config.WirelessProfile{{ + ProfileName: "office", + SSID: "CorpNet", + Priority: 1, + AuthenticationMethod: "WPA2PSK", + EncryptionMethod: "CCMP", + }} + + tests := []wiFiProfileRouteTest{ + { + name: "get wireless profiles", + method: http.MethodGet, + url: "/api/v1/amt/networkSettings/wireless/profile/device-guid", + mock: func(feature *mocks.MockDeviceManagementFeature) { + feature.EXPECT().GetWirelessProfiles(context.Background(), "device-guid").Return(expectedProfiles, nil) + }, + response: expectedProfiles, + expectedCode: http.StatusOK, + }, + { + name: "get wireless profiles - service failure", + method: http.MethodGet, + url: "/api/v1/amt/networkSettings/wireless/profile/device-guid", + mock: func(feature *mocks.MockDeviceManagementFeature) { + feature.EXPECT().GetWirelessProfiles(context.Background(), "device-guid").Return(nil, ErrGeneral) + }, + expectedCode: http.StatusInternalServerError, + }, + { + name: "apply wireless profiles", + method: http.MethodPost, + url: "/api/v1/amt/networkSettings/wireless/profile/device-guid", + requestBody: request, + mock: func(feature *mocks.MockDeviceManagementFeature) { + feature.EXPECT().ApplyWirelessProfiles(context.Background(), "device-guid", request).Return(expectedProfiles, nil) + }, + response: expectedProfiles, + expectedCode: http.StatusOK, + }, + { + name: "apply wireless profiles - bind failure", + method: http.MethodPost, + url: "/api/v1/amt/networkSettings/wireless/profile/device-guid", + rawBody: `{"profiles":`, + expectedCode: http.StatusInternalServerError, + }, + { + name: "apply wireless profiles - service failure", + method: http.MethodPost, + url: "/api/v1/amt/networkSettings/wireless/profile/device-guid", + requestBody: request, + mock: func(feature *mocks.MockDeviceManagementFeature) { + feature.EXPECT().ApplyWirelessProfiles(context.Background(), "device-guid", request).Return(nil, ErrGeneral) + }, + expectedCode: http.StatusInternalServerError, + }, + } + + for _, tc := range tests { + tc := tc + + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + feature, engine := deviceManagementTest(t) + + if tc.mock != nil { + tc.mock(feature) + } + + var req *http.Request + + var err error + + switch tc.method { + case http.MethodPost, http.MethodPatch: + if tc.rawBody != "" { + req, err = http.NewRequestWithContext(context.Background(), tc.method, tc.url, bytes.NewBufferString(tc.rawBody)) + } else { + payload, marshalErr := json.Marshal(tc.requestBody) + require.NoError(t, marshalErr) + + req, err = http.NewRequestWithContext(context.Background(), tc.method, tc.url, bytes.NewBuffer(payload)) + } + + req.Header.Set("Content-Type", "application/json") + default: + req, err = http.NewRequestWithContext(context.Background(), tc.method, tc.url, 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, marshalErr := json.Marshal(tc.response) + require.NoError(t, marshalErr) + require.Equal(t, string(jsonBytes), w.Body.String()) + } + }) + } +} diff --git a/internal/controller/httpapi/v1/wifistate.go b/internal/controller/httpapi/v1/wifistate.go index dd0471c07..8959c7d83 100644 --- a/internal/controller/httpapi/v1/wifistate.go +++ b/internal/controller/httpapi/v1/wifistate.go @@ -8,7 +8,7 @@ import ( "github.com/device-management-toolkit/go-wsman-messages/v2/pkg/wsman/cim/wifi" - dto "github.com/device-management-toolkit/console/internal/entity/dto/v1" + "github.com/device-management-toolkit/console/internal/entity/dto/v1" "github.com/device-management-toolkit/console/internal/usecase/devices/wsman" "github.com/device-management-toolkit/console/pkg/consoleerrors" ) diff --git a/internal/controller/httpapi/v1/wifistate_test.go b/internal/controller/httpapi/v1/wifistate_test.go index 76eaee671..029016fb9 100644 --- a/internal/controller/httpapi/v1/wifistate_test.go +++ b/internal/controller/httpapi/v1/wifistate_test.go @@ -8,7 +8,7 @@ import ( "github.com/gin-gonic/gin" "github.com/stretchr/testify/require" - gomock "go.uber.org/mock/gomock" + "go.uber.org/mock/gomock" "github.com/device-management-toolkit/go-wsman-messages/v2/pkg/wsman/cim/wifi" diff --git a/internal/controller/openapi/devicemanagement.go b/internal/controller/openapi/devicemanagement.go index 4d1087f77..61a54ab12 100644 --- a/internal/controller/openapi/devicemanagement.go +++ b/internal/controller/openapi/devicemanagement.go @@ -5,6 +5,8 @@ import ( "github.com/go-fuego/fuego" + "github.com/device-management-toolkit/go-wsman-messages/v2/pkg/config" + "github.com/device-management-toolkit/console/internal/entity/dto/v1" ) @@ -118,6 +120,22 @@ func (f *FuegoAdapter) registerNetworkAndFeatureRoutes() { protectedRouteOptions(), ) + fuego.Get(f.server, "/api/v1/amt/networkSettings/wireless/profile/{guid}", f.getWirelessProfiles, + fuego.OptionTags("Device Management"), + fuego.OptionSummary("Get Wireless Profiles"), + fuego.OptionDescription("Retrieve configured wireless profiles for a device"), + fuego.OptionPath("guid", "Device GUID"), + protectedRouteOptions(), + ) + + fuego.Post(f.server, "/api/v1/amt/networkSettings/wireless/profile/{guid}", f.applyWirelessProfiles, + fuego.OptionTags("Device Management"), + fuego.OptionSummary("Apply Wireless Profiles"), + fuego.OptionDescription("Apply wireless profiles to a device"), + fuego.OptionPath("guid", "Device GUID"), + protectedRouteOptions(), + ) + fuego.Post(f.server, "/api/v1/amt/network/linkPreference/{guid}", f.setLinkPreference, fuego.OptionTags("Device Management"), fuego.OptionSummary("Set Link Preference"), @@ -413,6 +431,26 @@ func (f *FuegoAdapter) requestWirelessStateChange(c fuego.ContextWithBody[dto.Wi return dto.WirelessStateResponse(req), nil } +func (f *FuegoAdapter) getWirelessProfiles(_ fuego.ContextNoBody) ([]config.WirelessProfile, error) { + return []config.WirelessProfile{{ + ProfileName: "OfficeWiFi", + SSID: "OfficeSSID", + Priority: 1, + AuthenticationMethod: "WPA2PSK", + EncryptionMethod: "CCMP", + Password: "password123", + }}, nil +} + +func (f *FuegoAdapter) applyWirelessProfiles(c fuego.ContextWithBody[dto.WirelessProfilesApplyRequest]) ([]config.WirelessProfile, error) { + req, err := c.Body() + if err != nil { + return nil, err + } + + return req.Profiles, nil +} + func (f *FuegoAdapter) setLinkPreference(c fuego.ContextWithBody[dto.LinkPreferenceRequest]) (dto.LinkPreferenceResponse, error) { _, err := c.Body() if err != nil { diff --git a/internal/controller/ws/v1/interface.go b/internal/controller/ws/v1/interface.go index a915ee00c..c969057c5 100644 --- a/internal/controller/ws/v1/interface.go +++ b/internal/controller/ws/v1/interface.go @@ -7,6 +7,7 @@ import ( "github.com/gin-gonic/gin" "github.com/gorilla/websocket" + "github.com/device-management-toolkit/go-wsman-messages/v2/pkg/config" "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/wifi" @@ -61,6 +62,8 @@ type Feature interface { GetNetworkSettings(c context.Context, guid string) (dto.NetworkSettings, error) RequestWirelessStateChange(c context.Context, guid string, requestedState wifi.RequestedState) (wifi.RequestedState, error) GetWirelessState(c context.Context, guid string) (wifi.EnabledState, error) + GetWirelessProfiles(c context.Context, guid string) ([]config.WirelessProfile, error) + ApplyWirelessProfiles(c context.Context, guid string, req dto.WirelessProfilesApplyRequest) ([]config.WirelessProfile, error) GetCertificates(c context.Context, guid string) (dto.SecuritySettings, error) GetTLSSettingData(c context.Context, guid string) ([]dto.SettingDataResponse, error) GetDiskInfo(c context.Context, guid string) (dto.DiskInfo, error) diff --git a/internal/entity/dto/v1/wifiprofile.go b/internal/entity/dto/v1/wifiprofile.go new file mode 100644 index 000000000..7d61cb6b2 --- /dev/null +++ b/internal/entity/dto/v1/wifiprofile.go @@ -0,0 +1,149 @@ +package dto + +import ( + "regexp" + + "github.com/go-playground/validator/v10" + + "github.com/device-management-toolkit/go-wsman-messages/v2/pkg/config" + "github.com/device-management-toolkit/go-wsman-messages/v2/pkg/wsman/cim/wifi" +) + +const ( + maxWirelessProfiles = 8 + maxWirelessProfilePriority = 255 +) + +// WirelessProfilesApplyRequest carries the desired wireless profile list for one device. +type WirelessProfilesApplyRequest struct { + Profiles []config.WirelessProfile `json:"profiles" binding:"wirelessprofilescount,wirelessprofilesuniquepriorities,dive,wirelessprofile"` +} + +var reAlphaNumWirelessProfileName = regexp.MustCompile("^[a-zA-Z0-9]+$") + +var supportedWirelessAuthenticationMethods = map[wifi.AuthenticationMethod]struct{}{ + wifi.AuthenticationMethodWPAPSK: {}, + wifi.AuthenticationMethodWPAIEEE8021x: {}, + wifi.AuthenticationMethodWPA2PSK: {}, + wifi.AuthenticationMethodWPA2IEEE8021x: {}, +} + +var supportedWirelessEncryptionMethods = map[wifi.EncryptionMethod]struct{}{ + wifi.EncryptionMethodTKIP: {}, + wifi.EncryptionMethodCCMP: {}, +} + +// ValidateWirelessProfile validates one shared config.WirelessProfile payload. +var ValidateWirelessProfile validator.Func = func(fl validator.FieldLevel) bool { + profile, ok := fl.Field().Interface().(config.WirelessProfile) + if !ok { + return false + } + + if !isValidWirelessProfileBase(profile) { + return false + } + + authMethod, ok := parseSupportedAuthenticationMethod(profile.AuthenticationMethod) + if !ok { + return false + } + + if !isSupportedEncryptionMethod(profile.EncryptionMethod) { + return false + } + + return hasValidWirelessProfileCredentials(profile, authMethod) +} + +func isValidWirelessProfileBase(profile config.WirelessProfile) bool { + if profile.ProfileName == "" || !reAlphaNumWirelessProfileName.MatchString(profile.ProfileName) { + return false + } + + if profile.SSID == "" || profile.Priority <= 0 || profile.Priority > maxWirelessProfilePriority { + return false + } + + return true +} + +func parseSupportedAuthenticationMethod(authenticationMethod string) (wifi.AuthenticationMethod, bool) { + authMethod, ok := wifi.ParseAuthenticationMethod(authenticationMethod) + if !ok { + return 0, false + } + + _, supported := supportedWirelessAuthenticationMethods[authMethod] + + return authMethod, supported +} + +func isSupportedEncryptionMethod(encryptionMethod string) bool { + parsedEncryptionMethod, ok := wifi.ParseEncryptionMethod(encryptionMethod) + if !ok { + return false + } + + _, supported := supportedWirelessEncryptionMethods[parsedEncryptionMethod] + + return supported +} + +func hasValidWirelessProfileCredentials(profile config.WirelessProfile, authMethod wifi.AuthenticationMethod) bool { + if isPSKAuthenticationMethod(authMethod) { + return profile.Password != "" && profile.IEEE8021x == nil + } + + if isIEEE8021xAuthenticationMethod(authMethod) { + return hasValidIEEE8021xCredentials(profile) + } + + return false +} + +func isPSKAuthenticationMethod(authMethod wifi.AuthenticationMethod) bool { + return authMethod == wifi.AuthenticationMethodWPAPSK || authMethod == wifi.AuthenticationMethodWPA2PSK +} + +func isIEEE8021xAuthenticationMethod(authMethod wifi.AuthenticationMethod) bool { + return authMethod == wifi.AuthenticationMethodWPAIEEE8021x || authMethod == wifi.AuthenticationMethodWPA2IEEE8021x +} + +func hasValidIEEE8021xCredentials(profile config.WirelessProfile) bool { + if profile.IEEE8021x == nil || profile.Password != "" { + return false + } + + return profile.IEEE8021x.AuthenticationProtocol == 0 || profile.IEEE8021x.AuthenticationProtocol == 2 +} + +// ValidateWirelessProfilesCount enforces the max number of profiles in one request. +var ValidateWirelessProfilesCount validator.Func = func(fl validator.FieldLevel) bool { + profiles, ok := fl.Field().Interface().([]config.WirelessProfile) + if !ok { + return false + } + + return len(profiles) <= maxWirelessProfiles +} + +// ValidateWirelessProfilesUniquePriorities enforces unique priority per request payload. +var ValidateWirelessProfilesUniquePriorities validator.Func = func(fl validator.FieldLevel) bool { + profiles, ok := fl.Field().Interface().([]config.WirelessProfile) + if !ok { + return false + } + + seen := make(map[int]struct{}, len(profiles)) + for i := range profiles { + priority := profiles[i].Priority + if _, exists := seen[priority]; exists { + return false + } + + seen[priority] = struct{}{} + } + + return true +} diff --git a/internal/entity/dto/v1/wifiprofile_test.go b/internal/entity/dto/v1/wifiprofile_test.go new file mode 100644 index 000000000..0972298b6 --- /dev/null +++ b/internal/entity/dto/v1/wifiprofile_test.go @@ -0,0 +1,341 @@ +package dto + +import ( + "testing" + + "github.com/go-playground/validator/v10" + "github.com/stretchr/testify/require" + + "github.com/device-management-toolkit/go-wsman-messages/v2/pkg/config" + "github.com/device-management-toolkit/go-wsman-messages/v2/pkg/wsman/cim/wifi" +) + +func TestValidateWirelessProfile(t *testing.T) { + t.Parallel() + + validate := validator.New() + require.NoError(t, validate.RegisterValidation("wirelessprofile", ValidateWirelessProfile)) + + type profileWrapper struct { + Profile config.WirelessProfile `validate:"wirelessprofile"` + } + + tests := []struct { + name string + profile config.WirelessProfile + wantErr bool + }{ + { + name: "valid psk profile", + profile: config.WirelessProfile{ + ProfileName: "Office", + SSID: "CorpNet", + Priority: 1, + AuthenticationMethod: "WPA2PSK", + EncryptionMethod: "CCMP", + Password: "password123", + }, + wantErr: false, + }, + { + name: "valid ieee8021x profile", + profile: config.WirelessProfile{ + ProfileName: "OfficeEAP", + SSID: "CorpNet", + Priority: 1, + AuthenticationMethod: "WPA2IEEE8021x", + EncryptionMethod: "CCMP", + IEEE8021x: &config.IEEE8021x{ + AuthenticationProtocol: 0, + Username: "user", + }, + }, + wantErr: false, + }, + { + name: "invalid profile name", + profile: config.WirelessProfile{ + ProfileName: "office-net", + SSID: "CorpNet", + Priority: 1, + AuthenticationMethod: "WPA2PSK", + EncryptionMethod: "CCMP", + Password: "password123", + }, + wantErr: true, + }, + { + name: "invalid auth method", + profile: config.WirelessProfile{ + ProfileName: "Office", + SSID: "CorpNet", + Priority: 1, + AuthenticationMethod: "OpenSystem", + EncryptionMethod: "CCMP", + Password: "password123", + }, + wantErr: true, + }, + { + name: "auth parse failure", + profile: config.WirelessProfile{ + ProfileName: "Office", + SSID: "CorpNet", + Priority: 1, + AuthenticationMethod: "NotRealAuthMethod", + EncryptionMethod: "CCMP", + Password: "password123", + }, + wantErr: true, + }, + { + name: "invalid encryption method", + profile: config.WirelessProfile{ + ProfileName: "Office", + SSID: "CorpNet", + Priority: 1, + AuthenticationMethod: "WPA2PSK", + EncryptionMethod: "WEP", + Password: "password123", + }, + wantErr: true, + }, + { + name: "encryption parse failure", + profile: config.WirelessProfile{ + ProfileName: "Office", + SSID: "CorpNet", + Priority: 1, + AuthenticationMethod: "WPA2PSK", + EncryptionMethod: "NotRealEncryptionMethod", + Password: "password123", + }, + wantErr: true, + }, + { + name: "invalid ssid priority guard", + profile: config.WirelessProfile{ + ProfileName: "Office", + SSID: "", + Priority: 1, + AuthenticationMethod: "WPA2PSK", + EncryptionMethod: "CCMP", + Password: "password123", + }, + wantErr: true, + }, + { + name: "invalid priority out of range", + profile: config.WirelessProfile{ + ProfileName: "Office", + SSID: "CorpNet", + Priority: 256, + AuthenticationMethod: "WPA2PSK", + EncryptionMethod: "CCMP", + Password: "password123", + }, + wantErr: true, + }, + { + name: "psk auth missing password", + profile: config.WirelessProfile{ + ProfileName: "Office", + SSID: "CorpNet", + Priority: 1, + AuthenticationMethod: "WPA2PSK", + EncryptionMethod: "CCMP", + }, + wantErr: true, + }, + { + name: "ieee8021x auth missing settings", + profile: config.WirelessProfile{ + ProfileName: "OfficeEAP", + SSID: "CorpNet", + Priority: 1, + AuthenticationMethod: "WPA2IEEE8021x", + EncryptionMethod: "CCMP", + }, + wantErr: true, + }, + { + name: "ieee8021x invalid authentication protocol", + profile: config.WirelessProfile{ + ProfileName: "OfficeEAP", + SSID: "CorpNet", + Priority: 1, + AuthenticationMethod: "WPA2IEEE8021x", + EncryptionMethod: "CCMP", + IEEE8021x: &config.IEEE8021x{ + AuthenticationProtocol: 1, + Username: "user", + }, + }, + wantErr: true, + }, + } + + for _, tc := range tests { + tc := tc + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + err := validate.Struct(profileWrapper{Profile: tc.profile}) + if tc.wantErr { + require.Error(t, err) + + return + } + + require.NoError(t, err) + }) + } +} + +func TestValidateWirelessProfileTypeAssertionFailure(t *testing.T) { + t.Parallel() + + validate := validator.New() + require.NoError(t, validate.RegisterValidation("wirelessprofile", ValidateWirelessProfile)) + + type wrongProfileWrapper struct { + Profile interface{} `validate:"wirelessprofile"` + } + + err := validate.Struct(wrongProfileWrapper{Profile: "not-a-wireless-profile"}) + require.Error(t, err) +} + +func TestHasValidWirelessProfileCredentialsUnsupportedAuthMethod(t *testing.T) { + t.Parallel() + + profile := config.WirelessProfile{ + ProfileName: "Office", + SSID: "CorpNet", + Priority: 1, + } + + require.False(t, hasValidWirelessProfileCredentials(profile, wifi.AuthenticationMethod(255))) +} + +func TestWirelessProfilesApplyRequestValidation(t *testing.T) { + t.Parallel() + + validate := validator.New() + validate.SetTagName("binding") + require.NoError(t, validate.RegisterValidation("wirelessprofile", ValidateWirelessProfile)) + require.NoError(t, validate.RegisterValidation("wirelessprofilescount", ValidateWirelessProfilesCount)) + require.NoError(t, validate.RegisterValidation("wirelessprofilesuniquepriorities", ValidateWirelessProfilesUniquePriorities)) + + t.Run("valid request", func(t *testing.T) { + t.Parallel() + + req := WirelessProfilesApplyRequest{ + Profiles: []config.WirelessProfile{ + { + ProfileName: "Office", + SSID: "CorpNet", + Priority: 1, + AuthenticationMethod: "WPA2PSK", + EncryptionMethod: "CCMP", + Password: "password123", + }, + }, + } + + require.NoError(t, validate.Struct(req)) + }) + + t.Run("invalid request", func(t *testing.T) { + t.Parallel() + + req := WirelessProfilesApplyRequest{ + Profiles: []config.WirelessProfile{ + { + ProfileName: "office-net", + SSID: "", + Priority: 0, + AuthenticationMethod: "bad-auth", + EncryptionMethod: "bad-encryption", + }, + }, + } + + require.Error(t, validate.Struct(req)) + }) + + t.Run("too many profiles", func(t *testing.T) { + t.Parallel() + + profiles := make([]config.WirelessProfile, 0, 9) + for i := 0; i < 9; i++ { + profiles = append(profiles, config.WirelessProfile{ + ProfileName: "Office", + SSID: "CorpNet", + Priority: 1, + AuthenticationMethod: "WPA2PSK", + EncryptionMethod: "CCMP", + Password: "password123", + }) + } + + req := WirelessProfilesApplyRequest{Profiles: profiles} + + require.Error(t, validate.Struct(req)) + }) + + t.Run("duplicate priorities", func(t *testing.T) { + t.Parallel() + + req := WirelessProfilesApplyRequest{ + Profiles: []config.WirelessProfile{ + { + ProfileName: "Office", + SSID: "CorpNetA", + Priority: 10, + AuthenticationMethod: "WPA2PSK", + EncryptionMethod: "CCMP", + Password: "password123", + }, + { + ProfileName: "Guest", + SSID: "CorpNetB", + Priority: 10, + AuthenticationMethod: "WPA2PSK", + EncryptionMethod: "CCMP", + Password: "password456", + }, + }, + } + + require.Error(t, validate.Struct(req)) + }) +} + +func TestValidateWirelessProfilesCountTypeAssertionFailure(t *testing.T) { + t.Parallel() + + validate := validator.New() + require.NoError(t, validate.RegisterValidation("wirelessprofilescount", ValidateWirelessProfilesCount)) + + type wrongCountWrapper struct { + Profiles interface{} `validate:"wirelessprofilescount"` + } + + err := validate.Struct(wrongCountWrapper{Profiles: []string{"invalid"}}) + require.Error(t, err) +} + +func TestValidateWirelessProfilesUniquePrioritiesTypeAssertionFailure(t *testing.T) { + t.Parallel() + + validate := validator.New() + require.NoError(t, validate.RegisterValidation("wirelessprofilesuniquepriorities", ValidateWirelessProfilesUniquePriorities)) + + type wrongPrioritiesWrapper struct { + Profiles interface{} `validate:"wirelessprofilesuniquepriorities"` + } + + err := validate.Struct(wrongPrioritiesWrapper{Profiles: []string{"invalid"}}) + require.Error(t, err) +} diff --git a/internal/mocks/devicemanagement_mocks.go b/internal/mocks/devicemanagement_mocks.go index 998721050..c3893c1a0 100644 --- a/internal/mocks/devicemanagement_mocks.go +++ b/internal/mocks/devicemanagement_mocks.go @@ -18,6 +18,7 @@ import ( v2 "github.com/device-management-toolkit/console/internal/entity/dto/v2" devices "github.com/device-management-toolkit/console/internal/usecase/devices" wsman "github.com/device-management-toolkit/console/internal/usecase/devices/wsman" + config "github.com/device-management-toolkit/go-wsman-messages/v2/pkg/config" wsman0 "github.com/device-management-toolkit/go-wsman-messages/v2/pkg/wsman" power "github.com/device-management-toolkit/go-wsman-messages/v2/pkg/wsman/cim/power" wifi "github.com/device-management-toolkit/go-wsman-messages/v2/pkg/wsman/cim/wifi" @@ -478,6 +479,21 @@ func (mr *MockDeviceManagementFeatureMockRecorder) AddCertificate(c, guid, certI return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AddCertificate", reflect.TypeOf((*MockDeviceManagementFeature)(nil).AddCertificate), c, guid, certInfo) } +// ApplyWirelessProfiles mocks base method. +func (m *MockDeviceManagementFeature) ApplyWirelessProfiles(c context.Context, guid string, req dto.WirelessProfilesApplyRequest) ([]config.WirelessProfile, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ApplyWirelessProfiles", c, guid, req) + ret0, _ := ret[0].([]config.WirelessProfile) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// ApplyWirelessProfiles indicates an expected call of ApplyWirelessProfiles. +func (mr *MockDeviceManagementFeatureMockRecorder) ApplyWirelessProfiles(c, guid, req any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ApplyWirelessProfiles", reflect.TypeOf((*MockDeviceManagementFeature)(nil).ApplyWirelessProfiles), c, guid, req) +} + // CancelUserConsent mocks base method. func (m *MockDeviceManagementFeature) CancelUserConsent(ctx context.Context, guid string) (dto.UserConsentMessage, error) { m.ctrl.T.Helper() @@ -897,6 +913,21 @@ func (mr *MockDeviceManagementFeatureMockRecorder) GetVersion(ctx, guid any) *go return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetVersion", reflect.TypeOf((*MockDeviceManagementFeature)(nil).GetVersion), ctx, guid) } +// GetWirelessProfiles mocks base method. +func (m *MockDeviceManagementFeature) GetWirelessProfiles(c context.Context, guid string) ([]config.WirelessProfile, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetWirelessProfiles", c, guid) + ret0, _ := ret[0].([]config.WirelessProfile) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetWirelessProfiles indicates an expected call of GetWirelessProfiles. +func (mr *MockDeviceManagementFeatureMockRecorder) GetWirelessProfiles(c, guid any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetWirelessProfiles", reflect.TypeOf((*MockDeviceManagementFeature)(nil).GetWirelessProfiles), c, guid) +} + // GetWirelessState mocks base method. func (m *MockDeviceManagementFeature) GetWirelessState(c context.Context, guid string) (wifi.EnabledState, error) { m.ctrl.T.Helper() diff --git a/internal/mocks/wsman_mocks.go b/internal/mocks/wsman_mocks.go index efadc96fe..e819f2227 100644 --- a/internal/mocks/wsman_mocks.go +++ b/internal/mocks/wsman_mocks.go @@ -22,10 +22,13 @@ import ( redirection "github.com/device-management-toolkit/go-wsman-messages/v2/pkg/wsman/amt/redirection" setupandconfiguration "github.com/device-management-toolkit/go-wsman-messages/v2/pkg/wsman/amt/setupandconfiguration" tls0 "github.com/device-management-toolkit/go-wsman-messages/v2/pkg/wsman/amt/tls" + wifiportconfiguration "github.com/device-management-toolkit/go-wsman-messages/v2/pkg/wsman/amt/wifiportconfiguration" boot0 "github.com/device-management-toolkit/go-wsman-messages/v2/pkg/wsman/cim/boot" concrete "github.com/device-management-toolkit/go-wsman-messages/v2/pkg/wsman/cim/concrete" credential "github.com/device-management-toolkit/go-wsman-messages/v2/pkg/wsman/cim/credential" + ieee8021x "github.com/device-management-toolkit/go-wsman-messages/v2/pkg/wsman/cim/ieee8021x" kvm "github.com/device-management-toolkit/go-wsman-messages/v2/pkg/wsman/cim/kvm" + models "github.com/device-management-toolkit/go-wsman-messages/v2/pkg/wsman/cim/models" power "github.com/device-management-toolkit/go-wsman-messages/v2/pkg/wsman/cim/power" service "github.com/device-management-toolkit/go-wsman-messages/v2/pkg/wsman/cim/service" software "github.com/device-management-toolkit/go-wsman-messages/v2/pkg/wsman/cim/software" @@ -77,6 +80,21 @@ func (mr *MockManagementMockRecorder) AddClientCert(clientCert any) *gomock.Call return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AddClientCert", reflect.TypeOf((*MockManagement)(nil).AddClientCert), clientCert) } +// AddPrivateKey mocks base method. +func (m *MockManagement) AddPrivateKey(privateKey string) (string, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "AddPrivateKey", privateKey) + ret0, _ := ret[0].(string) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// AddPrivateKey indicates an expected call of AddPrivateKey. +func (mr *MockManagementMockRecorder) AddPrivateKey(privateKey any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AddPrivateKey", reflect.TypeOf((*MockManagement)(nil).AddPrivateKey), privateKey) +} + // AddTrustedRootCert mocks base method. func (m *MockManagement) AddTrustedRootCert(caCert string) (string, error) { m.ctrl.T.Helper() @@ -92,6 +110,21 @@ func (mr *MockManagementMockRecorder) AddTrustedRootCert(caCert any) *gomock.Cal return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AddTrustedRootCert", reflect.TypeOf((*MockManagement)(nil).AddTrustedRootCert), caCert) } +// AddWiFiSettings mocks base method. +func (m *MockManagement) AddWiFiSettings(wifiEndpointSettings wifi.WiFiEndpointSettingsRequest, ieee8021xSettings models.IEEE8021xSettings, wifiEndpoint, clientCredential, caCredential string) (wifiportconfiguration.Response, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "AddWiFiSettings", wifiEndpointSettings, ieee8021xSettings, wifiEndpoint, clientCredential, caCredential) + ret0, _ := ret[0].(wifiportconfiguration.Response) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// AddWiFiSettings indicates an expected call of AddWiFiSettings. +func (mr *MockManagementMockRecorder) AddWiFiSettings(wifiEndpointSettings, ieee8021xSettings, wifiEndpoint, clientCredential, caCredential any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AddWiFiSettings", reflect.TypeOf((*MockManagement)(nil).AddWiFiSettings), wifiEndpointSettings, ieee8021xSettings, wifiEndpoint, clientCredential, caCredential) +} + // BootServiceStateChange mocks base method. func (m *MockManagement) BootServiceStateChange(requestedState int) (boot0.BootService, error) { m.ctrl.T.Helper() @@ -180,6 +213,20 @@ func (mr *MockManagementMockRecorder) DeleteCertificate(instanceID any) *gomock. return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteCertificate", reflect.TypeOf((*MockManagement)(nil).DeleteCertificate), instanceID) } +// DeleteWiFiSetting mocks base method. +func (m *MockManagement) DeleteWiFiSetting(instanceID string) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "DeleteWiFiSetting", instanceID) + ret0, _ := ret[0].(error) + return ret0 +} + +// DeleteWiFiSetting indicates an expected call of DeleteWiFiSetting. +func (mr *MockManagementMockRecorder) DeleteWiFiSetting(instanceID any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteWiFiSetting", reflect.TypeOf((*MockManagement)(nil).DeleteWiFiSetting), instanceID) +} + // EnumerateWiFiPort mocks base method. func (m *MockManagement) EnumerateWiFiPort() (wifi.Response, error) { m.ctrl.T.Helper() @@ -300,6 +347,21 @@ func (mr *MockManagementMockRecorder) GetCIMBootSourceSetting() *gomock.Call { return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetCIMBootSourceSetting", reflect.TypeOf((*MockManagement)(nil).GetCIMBootSourceSetting)) } +// GetCIMIEEE8021xSettings mocks base method. +func (m *MockManagement) GetCIMIEEE8021xSettings() (ieee8021x.Response, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetCIMIEEE8021xSettings") + ret0, _ := ret[0].(ieee8021x.Response) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetCIMIEEE8021xSettings indicates an expected call of GetCIMIEEE8021xSettings. +func (mr *MockManagementMockRecorder) GetCIMIEEE8021xSettings() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetCIMIEEE8021xSettings", reflect.TypeOf((*MockManagement)(nil).GetCIMIEEE8021xSettings)) +} + // GetCertificates mocks base method. func (m *MockManagement) GetCertificates() (wsman.Certificates, error) { m.ctrl.T.Helper() @@ -600,6 +662,21 @@ func (mr *MockManagementMockRecorder) GetUserConsentCode() *gomock.Call { return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetUserConsentCode", reflect.TypeOf((*MockManagement)(nil).GetUserConsentCode)) } +// GetWiFiSettings mocks base method. +func (m *MockManagement) GetWiFiSettings() ([]wifi.WiFiEndpointSettingsResponse, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetWiFiSettings") + ret0, _ := ret[0].([]wifi.WiFiEndpointSettingsResponse) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetWiFiSettings indicates an expected call of GetWiFiSettings. +func (mr *MockManagementMockRecorder) GetWiFiSettings() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetWiFiSettings", reflect.TypeOf((*MockManagement)(nil).GetWiFiSettings)) +} + // PullWiFiPort mocks base method. func (m *MockManagement) PullWiFiPort(enumerationContext string) (wifi.Response, error) { m.ctrl.T.Helper() diff --git a/internal/mocks/wsv1_mocks.go b/internal/mocks/wsv1_mocks.go index ae71e3438..5862e2b7b 100644 --- a/internal/mocks/wsv1_mocks.go +++ b/internal/mocks/wsv1_mocks.go @@ -16,6 +16,7 @@ import ( dto "github.com/device-management-toolkit/console/internal/entity/dto/v1" v2 "github.com/device-management-toolkit/console/internal/entity/dto/v2" + config "github.com/device-management-toolkit/go-wsman-messages/v2/pkg/config" power "github.com/device-management-toolkit/go-wsman-messages/v2/pkg/wsman/cim/power" wifi "github.com/device-management-toolkit/go-wsman-messages/v2/pkg/wsman/cim/wifi" gin "github.com/gin-gonic/gin" @@ -139,6 +140,21 @@ func (mr *MockFeatureMockRecorder) AddCertificate(c, guid, certInfo any) *gomock return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AddCertificate", reflect.TypeOf((*MockFeature)(nil).AddCertificate), c, guid, certInfo) } +// ApplyWirelessProfiles mocks base method. +func (m *MockFeature) ApplyWirelessProfiles(c context.Context, guid string, req dto.WirelessProfilesApplyRequest) ([]config.WirelessProfile, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ApplyWirelessProfiles", c, guid, req) + ret0, _ := ret[0].([]config.WirelessProfile) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// ApplyWirelessProfiles indicates an expected call of ApplyWirelessProfiles. +func (mr *MockFeatureMockRecorder) ApplyWirelessProfiles(c, guid, req any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ApplyWirelessProfiles", reflect.TypeOf((*MockFeature)(nil).ApplyWirelessProfiles), c, guid, req) +} + // CancelUserConsent mocks base method. func (m *MockFeature) CancelUserConsent(ctx context.Context, guid string) (dto.UserConsentMessage, error) { m.ctrl.T.Helper() @@ -558,6 +574,21 @@ func (mr *MockFeatureMockRecorder) GetVersion(ctx, guid any) *gomock.Call { return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetVersion", reflect.TypeOf((*MockFeature)(nil).GetVersion), ctx, guid) } +// GetWirelessProfiles mocks base method. +func (m *MockFeature) GetWirelessProfiles(c context.Context, guid string) ([]config.WirelessProfile, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetWirelessProfiles", c, guid) + ret0, _ := ret[0].([]config.WirelessProfile) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetWirelessProfiles indicates an expected call of GetWirelessProfiles. +func (mr *MockFeatureMockRecorder) GetWirelessProfiles(c, guid any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetWirelessProfiles", reflect.TypeOf((*MockFeature)(nil).GetWirelessProfiles), c, guid) +} + // GetWirelessState mocks base method. func (m *MockFeature) GetWirelessState(c context.Context, guid string) (wifi.EnabledState, error) { m.ctrl.T.Helper() diff --git a/internal/usecase/devices/interfaces.go b/internal/usecase/devices/interfaces.go index 6b3931b6c..59676ca61 100644 --- a/internal/usecase/devices/interfaces.go +++ b/internal/usecase/devices/interfaces.go @@ -5,6 +5,7 @@ import ( "github.com/gorilla/websocket" + "github.com/device-management-toolkit/go-wsman-messages/v2/pkg/config" "github.com/device-management-toolkit/go-wsman-messages/v2/pkg/wsman" "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/wifi" @@ -83,6 +84,8 @@ type ( GetNetworkSettings(c context.Context, guid string) (dto.NetworkSettings, error) RequestWirelessStateChange(c context.Context, guid string, requestedState wifi.RequestedState) (wifi.RequestedState, error) GetWirelessState(c context.Context, guid string) (wifi.EnabledState, error) + GetWirelessProfiles(c context.Context, guid string) ([]config.WirelessProfile, error) + ApplyWirelessProfiles(c context.Context, guid string, req dto.WirelessProfilesApplyRequest) ([]config.WirelessProfile, error) GetCertificates(c context.Context, guid string) (dto.SecuritySettings, error) GetTLSSettingData(c context.Context, guid string) ([]dto.SettingDataResponse, error) GetDiskInfo(c context.Context, guid string) (dto.DiskInfo, error) diff --git a/internal/usecase/devices/wifiprofile.go b/internal/usecase/devices/wifiprofile.go new file mode 100644 index 000000000..20d5adcff --- /dev/null +++ b/internal/usecase/devices/wifiprofile.go @@ -0,0 +1,539 @@ +package devices + +import ( + "context" + "errors" + "fmt" + "strings" + "time" + + "github.com/device-management-toolkit/go-wsman-messages/v2/pkg/config" + "github.com/device-management-toolkit/go-wsman-messages/v2/pkg/wsman/cim/concrete" + cimIEEE8021x "github.com/device-management-toolkit/go-wsman-messages/v2/pkg/wsman/cim/ieee8021x" + "github.com/device-management-toolkit/go-wsman-messages/v2/pkg/wsman/cim/models" + "github.com/device-management-toolkit/go-wsman-messages/v2/pkg/wsman/cim/wifi" + + "github.com/device-management-toolkit/console/internal/entity/dto/v1" + "github.com/device-management-toolkit/console/internal/usecase/devices/wsman" +) + +const ( + defaultWiFiEndpoint = "WiFi Endpoint 0" + instanceIDPrefixUserSettings = "Intel(r) AMT:WiFi Endpoint User Settings" + instanceIDFormatWiFiEndpoint = "Intel(r) AMT:WiFi Endpoint Settings %s" + instanceIDFormatIEEE8021x = "Intel(r) AMT:IEEE 802.1x Settings %s" + resourceCIMWiFiEndpointSettings = "CIM_WiFiEndpointSettings" + resourceCIMIEEE8021xSettings = "CIM_IEEE8021xSettings" + selectorNameInstanceID = "InstanceID" +) + +var ( + errInvalidAuthenticationMethod = errors.New("invalid authentication method") + errInvalidEncryptionMethod = errors.New("invalid encryption method") +) + +type IEEE8021xCertHandles struct { + ClientCertHandle string + RootCertHandle string +} + +type preparedWirelessProfile struct { + wifiRequest wifi.WiFiEndpointSettingsRequest + ieee8021xRequest models.IEEE8021xSettings + certHandles *IEEE8021xCertHandles +} + +func (uc *UseCase) GetWirelessProfiles(c context.Context, guid string) ([]config.WirelessProfile, error) { + device, err := uc.setupWirelessProfileManagement(c, guid) + if err != nil { + return nil, err + } + + return getWirelessProfilesFromDevice(device) +} + +func (uc *UseCase) ApplyWirelessProfiles(c context.Context, guid string, req dto.WirelessProfilesApplyRequest) ([]config.WirelessProfile, error) { + device, err := uc.setupWirelessProfileManagement(c, guid) + if err != nil { + return nil, err + } + + preparedProfiles, needsPauseBeforeApply, err := prepareWirelessProfilesForApply(device, req.Profiles) + if err != nil { + return nil, err + } + + // pause to allow amt to handle certs + if needsPauseBeforeApply { + if err := waitForAMTCertificateHandling(c, time.Second); err != nil { + return nil, err + } + } + + if err := clearWirelessProfiles(device); err != nil { + return nil, err + } + + if err := applyPreparedWirelessProfiles(device, preparedProfiles); err != nil { + return nil, err + } + + return req.Profiles, nil +} + +func (uc *UseCase) setupWirelessProfileManagement(c context.Context, guid string) (wsman.Management, error) { + item, err := uc.repo.GetByID(c, guid, "") + if err != nil { + return nil, err + } + + if item == nil || item.GUID == "" { + return nil, ErrNotFound + } + + device, err := uc.device.SetupWsmanClient(c, *item, false, true) + if err != nil { + return nil, err + } + + return device, nil +} + +func prepareWirelessProfilesForApply(device wsman.Management, profiles []config.WirelessProfile) ([]preparedWirelessProfile, bool, error) { + preparedProfiles := make([]preparedWirelessProfile, 0, len(profiles)) + needsPauseBeforeApply := false + + for i := range profiles { + profile := profiles[i] + + wifiRequest, err := toWiFiEndpointSettingsRequest(profile) + if err != nil { + return nil, false, err + } + + prepared := preparedWirelessProfile{ + wifiRequest: wifiRequest, + ieee8021xRequest: models.IEEE8021xSettings{}, + certHandles: &IEEE8021xCertHandles{}, + } + + if profile.IEEE8021x != nil { + prepared.ieee8021xRequest = toIEEE8021xSettingsRequest(profile) + + certHandles, pauseBeforeAdd, certErr := configureIEEE8021xCertificates( + device, + profile.IEEE8021x.PrivateKey, + profile.IEEE8021x.ClientCert, + profile.IEEE8021x.CACert, + ) + if certErr != nil { + return nil, false, certErr + } + + prepared.certHandles = certHandles + + needsPauseBeforeApply = needsPauseBeforeApply || pauseBeforeAdd + } + + preparedProfiles = append(preparedProfiles, prepared) + } + + return preparedProfiles, needsPauseBeforeApply, nil +} + +func applyPreparedWirelessProfiles(device wsman.Management, preparedProfiles []preparedWirelessProfile) error { + for i := range preparedProfiles { + prepared := preparedProfiles[i] + + _, err := device.AddWiFiSettings( + prepared.wifiRequest, + prepared.ieee8021xRequest, + defaultWiFiEndpoint, + prepared.certHandles.ClientCertHandle, + prepared.certHandles.RootCertHandle, + ) + if err != nil { + return err + } + } + + return nil +} + +func clearWirelessProfiles(device wsman.Management) error { + settings, err := device.GetWiFiSettings() + if err != nil { + return err + } + + for i := range settings { + setting := settings[i] + if setting.InstanceID == "" || isUserSettingsInstanceID(setting.InstanceID) { + continue + } + + if err := device.DeleteWiFiSetting(setting.InstanceID); err != nil { + return err + } + } + + return nil +} + +func getWirelessProfilesFromDevice(device wsman.Management) ([]config.WirelessProfile, error) { + settings, err := device.GetWiFiSettings() + if err != nil { + return nil, err + } + + ieee8021xResponse, err := device.GetCIMIEEE8021xSettings() + if err != nil { + return nil, err + } + + concreteDependencies, err := device.GetConcreteDependencies() + if err != nil { + return nil, err + } + + ieee8021xByID := indexIEEE8021xSettings(ieee8021xResponse.Body.PullResponse.IEEE8021xSettingsItems) + ieee8021xByProfileName := indexIEEE8021xSettingsByProfileName(ieee8021xResponse.Body.PullResponse.IEEE8021xSettingsItems) + associatedIEEE8021xByWiFiID := mapAssociatedIEEE8021xByWiFiID(concreteDependencies) + + profiles := make([]config.WirelessProfile, 0, len(settings)) + for i := range settings { + setting := settings[i] + if setting.InstanceID == "" || isUserSettingsInstanceID(setting.InstanceID) { + continue + } + + profile := wifiSettingToConfig(setting) + if ieee8021xSettings, found := findAssociatedIEEE8021xSettings(setting, associatedIEEE8021xByWiFiID, ieee8021xByID, ieee8021xByProfileName); found { + profile.IEEE8021x = ieee8021xSettingToConfig(ieee8021xSettings) + } + + profiles = append(profiles, profile) + } + + return profiles, nil +} + +func indexIEEE8021xSettings(settings []cimIEEE8021x.IEEE8021xSettingsResponse) map[string]cimIEEE8021x.IEEE8021xSettingsResponse { + indexed := make(map[string]cimIEEE8021x.IEEE8021xSettingsResponse, len(settings)) + for i := range settings { + setting := settings[i] + if setting.InstanceID == "" { + continue + } + + indexed[setting.InstanceID] = setting + } + + return indexed +} + +func indexIEEE8021xSettingsByProfileName(settings []cimIEEE8021x.IEEE8021xSettingsResponse) map[string]cimIEEE8021x.IEEE8021xSettingsResponse { + indexed := make(map[string]cimIEEE8021x.IEEE8021xSettingsResponse, len(settings)) + for i := range settings { + setting := settings[i] + if setting.ElementName == "" { + continue + } + + indexed[normalizeAssociationKey(setting.ElementName)] = setting + } + + return indexed +} + +func mapAssociatedIEEE8021xByWiFiID(dependencies []concrete.ConcreteDependency) map[string]string { + associated := map[string]string{} + + for i := range dependencies { + dependency := dependencies[i] + + wifiEndpointReference, ieee8021xReference, found := dependencyReferencesForWiFi8021x(dependency) + if !found { + continue + } + + wifiID, hasWiFiID := associationReferenceInstanceID(wifiEndpointReference) + + ieee8021xID, hasIEEE8021xID := associationReferenceInstanceID(ieee8021xReference) + if !hasWiFiID || !hasIEEE8021xID { + continue + } + + associated[wifiID] = ieee8021xID + } + + return associated +} + +func dependencyReferencesForWiFi8021x(dependency concrete.ConcreteDependency) (wifiEndpointReference, ieee8021xReference models.AssociationReference, found bool) { + antecedentURI := dependency.Antecedent.ReferenceParameters.ResourceURI + dependentURI := dependency.Dependent.ReferenceParameters.ResourceURI + + if isAssociationResource(antecedentURI, resourceCIMWiFiEndpointSettings) && isAssociationResource(dependentURI, resourceCIMIEEE8021xSettings) { + return dependency.Antecedent, dependency.Dependent, true + } + + if isAssociationResource(antecedentURI, resourceCIMIEEE8021xSettings) && isAssociationResource(dependentURI, resourceCIMWiFiEndpointSettings) { + return dependency.Dependent, dependency.Antecedent, true + } + + return wifiEndpointReference, ieee8021xReference, found +} + +func isAssociationResource(resourceURI, resourceName string) bool { + return strings.HasSuffix(strings.ToLower(resourceURI), strings.ToLower(resourceName)) +} + +func associationReferenceInstanceID(reference models.AssociationReference) (string, bool) { + selectors := reference.ReferenceParameters.SelectorSet.Selectors + for i := range selectors { + selector := selectors[i] + if !strings.EqualFold(selector.Name, selectorNameInstanceID) { + continue + } + + if selector.Text == "" { + return "", false + } + + return selector.Text, true + } + + return "", false +} + +func findAssociatedIEEE8021xSettings( + setting wifi.WiFiEndpointSettingsResponse, + associatedIEEE8021xByWiFiID map[string]string, + ieee8021xByID map[string]cimIEEE8021x.IEEE8021xSettingsResponse, + ieee8021xByProfileName map[string]cimIEEE8021x.IEEE8021xSettingsResponse, +) (cimIEEE8021x.IEEE8021xSettingsResponse, bool) { + if ieee8021xID, found := associatedIEEE8021xByWiFiID[setting.InstanceID]; found { + ieee8021xSettings, exists := ieee8021xByID[ieee8021xID] + if exists { + return ieee8021xSettings, true + } + } + + if setting.ElementName == "" { + return cimIEEE8021x.IEEE8021xSettingsResponse{}, false + } + + fallbackIEEE8021xID := fmt.Sprintf(instanceIDFormatIEEE8021x, setting.ElementName) + if ieee8021xSettings, found := ieee8021xByID[fallbackIEEE8021xID]; found { + return ieee8021xSettings, true + } + + ieee8021xSettings, found := ieee8021xByProfileName[normalizeAssociationKey(setting.ElementName)] + + return ieee8021xSettings, found +} + +func normalizeAssociationKey(value string) string { + return strings.ToLower(strings.TrimSpace(value)) +} + +func isUserSettingsInstanceID(instanceID string) bool { + return strings.HasPrefix(instanceID, instanceIDPrefixUserSettings) +} + +func waitForAMTCertificateHandling(c context.Context, delay time.Duration) error { + timer := time.NewTimer(delay) + + defer func() { + if !timer.Stop() { + select { + case <-timer.C: + default: + } + } + }() + + select { + case <-timer.C: + return nil + case <-c.Done(): + return c.Err() + } +} + +func toWiFiEndpointSettingsRequest(req config.WirelessProfile) (wifi.WiFiEndpointSettingsRequest, error) { + authMethod, ok := wifi.ParseAuthenticationMethod(req.AuthenticationMethod) + if !ok { + return wifi.WiFiEndpointSettingsRequest{}, fmt.Errorf("%w %q for profile %q", errInvalidAuthenticationMethod, req.AuthenticationMethod, req.ProfileName) + } + + encryptionMethod, ok := wifi.ParseEncryptionMethod(req.EncryptionMethod) + if !ok { + return wifi.WiFiEndpointSettingsRequest{}, fmt.Errorf("%w %q for profile %q", errInvalidEncryptionMethod, req.EncryptionMethod, req.ProfileName) + } + + return wifi.WiFiEndpointSettingsRequest{ + ElementName: req.ProfileName, + InstanceID: fmt.Sprintf(instanceIDFormatWiFiEndpoint, req.ProfileName), + AuthenticationMethod: authMethod, + EncryptionMethod: encryptionMethod, + SSID: req.SSID, + Priority: req.Priority, + PSKPassPhrase: req.Password, + }, nil +} + +func toIEEE8021xSettingsRequest(req config.WirelessProfile) models.IEEE8021xSettings { + if req.IEEE8021x == nil { + return models.IEEE8021xSettings{} + } + + return models.IEEE8021xSettings{ + ElementName: req.ProfileName, + InstanceID: fmt.Sprintf(instanceIDFormatIEEE8021x, req.ProfileName), + AuthenticationProtocol: models.AuthenticationProtocol(req.IEEE8021x.AuthenticationProtocol), + Username: req.IEEE8021x.Username, + Password: req.IEEE8021x.Password, + } +} + +func wifiSettingToConfig(setting wifi.WiFiEndpointSettingsResponse) config.WirelessProfile { + return config.WirelessProfile{ + ProfileName: setting.ElementName, + SSID: setting.SSID, + AuthenticationMethod: setting.AuthenticationMethod.String(), + EncryptionMethod: setting.EncryptionMethod.String(), + Priority: setting.Priority, + } +} + +func ieee8021xSettingToConfig(setting cimIEEE8021x.IEEE8021xSettingsResponse) *config.IEEE8021x { + return &config.IEEE8021x{ + Username: setting.Username, + Password: setting.Password, + AuthenticationProtocol: setting.AuthenticationProtocol, + } +} + +func configureIEEE8021xCertificates( + device wsman.Management, + privateKey, clientCert, caCert string, +) (*IEEE8021xCertHandles, bool, error) { + handles := &IEEE8021xCertHandles{} + + certs, err := device.GetCertificates() + if err != nil { + return nil, false, err + } + + addedCredentials := false + + if privateKey != "" { + var added bool + + _, certs, added, err = resolveOrAddCredentialHandle(certs, privateKey, findExistingPrivateKeyHandle, device.AddPrivateKey, device.GetCertificates) + if err != nil { + return nil, false, err + } + + addedCredentials = addedCredentials || added + } + + if clientCert != "" { + var added bool + + handles.ClientCertHandle, certs, added, err = resolveOrAddCredentialHandle(certs, clientCert, findExistingClientCertHandle, device.AddClientCert, device.GetCertificates) + if err != nil { + return nil, false, err + } + + addedCredentials = addedCredentials || added + } + + if caCert != "" { + var added bool + + handles.RootCertHandle, _, added, err = resolveOrAddCredentialHandle(certs, caCert, findExistingTrustedRootCertHandle, device.AddTrustedRootCert, device.GetCertificates) + if err != nil { + return nil, false, err + } + + addedCredentials = addedCredentials || added + } + + return handles, addedCredentials, nil +} + +type ( + credentialHandleFinder func(certs wsman.Certificates, credential string) (string, bool) + credentialHandleAdder func(credential string) (string, error) + certsRefresher func() (wsman.Certificates, error) +) + +func resolveOrAddCredentialHandle(certs wsman.Certificates, credential string, find credentialHandleFinder, add credentialHandleAdder, refresh certsRefresher) (handle string, updatedCerts wsman.Certificates, added bool, err error) { + updatedCerts = certs + + if credential == "" { + return "", updatedCerts, false, nil + } + + handle, found := find(updatedCerts, credential) + if found { + return handle, updatedCerts, false, nil + } + + handle, addErr := add(credential) + if addErr == nil { + return handle, updatedCerts, true, nil + } + + if !strings.Contains(strings.ToLower(addErr.Error()), "already exists") { + return "", updatedCerts, false, addErr + } + + updatedCerts, err = refresh() + if err != nil { + return "", updatedCerts, false, err + } + + handle, found = find(updatedCerts, credential) + if !found { + return "", updatedCerts, false, addErr + } + + return handle, updatedCerts, false, nil +} + +func findExistingPrivateKeyHandle(certs wsman.Certificates, privateKey string) (string, bool) { + for i := range certs.PublicPrivateKeyPairResponse.PublicPrivateKeyPairItems { + item := certs.PublicPrivateKeyPairResponse.PublicPrivateKeyPairItems[i] + if item.DERKey == privateKey { + return item.InstanceID, true + } + } + + return "", false +} + +func findExistingClientCertHandle(certs wsman.Certificates, clientCert string) (string, bool) { + for i := range certs.PublicKeyCertificateResponse.PublicKeyCertificateItems { + item := certs.PublicKeyCertificateResponse.PublicKeyCertificateItems[i] + if item.X509Certificate == clientCert && !item.TrustedRootCertificate { + return item.InstanceID, true + } + } + + return "", false +} + +func findExistingTrustedRootCertHandle(certs wsman.Certificates, caCert string) (string, bool) { + for i := range certs.PublicKeyCertificateResponse.PublicKeyCertificateItems { + item := certs.PublicKeyCertificateResponse.PublicKeyCertificateItems[i] + if item.X509Certificate == caCert && item.TrustedRootCertificate { + return item.InstanceID, true + } + } + + return "", false +} diff --git a/internal/usecase/devices/wifiprofile_private_test.go b/internal/usecase/devices/wifiprofile_private_test.go new file mode 100644 index 000000000..ba83576b0 --- /dev/null +++ b/internal/usecase/devices/wifiprofile_private_test.go @@ -0,0 +1,671 @@ +package devices + +import ( + "errors" + "testing" + + "github.com/stretchr/testify/require" + + "github.com/device-management-toolkit/go-wsman-messages/v2/pkg/config" + "github.com/device-management-toolkit/go-wsman-messages/v2/pkg/wsman/amt/publickey" + "github.com/device-management-toolkit/go-wsman-messages/v2/pkg/wsman/amt/publicprivate" + "github.com/device-management-toolkit/go-wsman-messages/v2/pkg/wsman/cim/concrete" + cimIEEE8021x "github.com/device-management-toolkit/go-wsman-messages/v2/pkg/wsman/cim/ieee8021x" + "github.com/device-management-toolkit/go-wsman-messages/v2/pkg/wsman/cim/models" + "github.com/device-management-toolkit/go-wsman-messages/v2/pkg/wsman/cim/wifi" + + "github.com/device-management-toolkit/console/internal/usecase/devices/wsman" +) + +const testNewHandle = "new-handle" + +func wifiProfileTestCertificates() wsman.Certificates { + return wsman.Certificates{ + PublicPrivateKeyPairResponse: publicprivate.RefinedPullResponse{ + PublicPrivateKeyPairItems: []publicprivate.RefinedPublicPrivateKeyPair{{ + InstanceID: "pk-handle", + DERKey: "private-key", + }}, + }, + PublicKeyCertificateResponse: publickey.RefinedPullResponse{ + PublicKeyCertificateItems: []publickey.RefinedPublicKeyCertificateResponse{ + {InstanceID: "client-handle", X509Certificate: "client-cert", TrustedRootCertificate: false}, + {InstanceID: "root-handle", X509Certificate: "ca-cert", TrustedRootCertificate: true}, + }, + }, + } +} + +func TestWiFiProfileTransformers(t *testing.T) { + t.Parallel() + + t.Run("toWiFiEndpointSettingsRequest", func(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + profile config.WirelessProfile + res wifi.WiFiEndpointSettingsRequest + err string + }{ + { + name: "success", + profile: config.WirelessProfile{ + ProfileName: "Office", + SSID: "OfficeSSID", + Password: "P@ssword", + AuthenticationMethod: "WPA2PSK", + EncryptionMethod: "CCMP", + Priority: 5, + }, + res: wifi.WiFiEndpointSettingsRequest{ + ElementName: "Office", + InstanceID: "Intel(r) AMT:WiFi Endpoint Settings Office", + AuthenticationMethod: wifi.AuthenticationMethodWPA2PSK, + EncryptionMethod: wifi.EncryptionMethodCCMP, + SSID: "OfficeSSID", + Priority: 5, + PSKPassPhrase: "P@ssword", + }, + }, + { + name: "invalid authentication method", + profile: config.WirelessProfile{ + ProfileName: "Office", + AuthenticationMethod: "INVALID", + EncryptionMethod: "CCMP", + }, + err: "invalid authentication method \"INVALID\" for profile \"Office\"", + }, + { + name: "invalid encryption method", + profile: config.WirelessProfile{ + ProfileName: "Office", + AuthenticationMethod: "WPA2PSK", + EncryptionMethod: "INVALID", + }, + err: "invalid encryption method \"INVALID\" for profile \"Office\"", + }, + } + + for _, tc := range tests { + tc := tc + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + res, err := toWiFiEndpointSettingsRequest(tc.profile) + if tc.err != "" { + require.EqualError(t, err, tc.err) + require.Equal(t, wifi.WiFiEndpointSettingsRequest{}, res) + + return + } + + require.NoError(t, err) + require.Equal(t, tc.res, res) + }) + } + }) + + t.Run("toIEEE8021xSettingsRequest", func(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + profile config.WirelessProfile + res models.IEEE8021xSettings + }{ + { + name: "empty", + profile: config.WirelessProfile{}, + res: models.IEEE8021xSettings{}, + }, + { + name: "success", + profile: config.WirelessProfile{ + ProfileName: "SecureProfile", + IEEE8021x: &config.IEEE8021x{ + AuthenticationProtocol: 2, + Username: "user", + Password: "secret", + }, + }, + res: models.IEEE8021xSettings{ + ElementName: "SecureProfile", + InstanceID: "Intel(r) AMT:IEEE 802.1x Settings SecureProfile", + AuthenticationProtocol: models.AuthenticationProtocol(2), + Username: "user", + Password: "secret", + }, + }, + } + + for _, tc := range tests { + tc := tc + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + res := toIEEE8021xSettingsRequest(tc.profile) + require.Equal(t, tc.res, res) + }) + } + }) + + t.Run("wifiSettingToConfig", func(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + setting wifi.WiFiEndpointSettingsResponse + res config.WirelessProfile + }{ + { + name: "success", + setting: wifi.WiFiEndpointSettingsResponse{ + ElementName: "ProfileB", + SSID: "SSID-B", + AuthenticationMethod: wifi.AuthenticationMethodWPAIEEE8021x, + EncryptionMethod: wifi.EncryptionMethodTKIP, + Priority: 3, + }, + res: config.WirelessProfile{ + ProfileName: "ProfileB", + SSID: "SSID-B", + AuthenticationMethod: "WPAIEEE8021x", + EncryptionMethod: "TKIP", + Priority: 3, + }, + }, + } + + for _, tc := range tests { + tc := tc + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + res := wifiSettingToConfig(tc.setting) + require.Equal(t, tc.res, res) + }) + } + }) +} + +func TestWiFiProfileFindExistingHandles(t *testing.T) { + t.Parallel() + + certs := wifiProfileTestCertificates() + + tests := []struct { + name string + finder credentialHandleFinder + credential string + handle string + found bool + }{ + { + name: "private key found", + finder: findExistingPrivateKeyHandle, + credential: "private-key", + handle: "pk-handle", + found: true, + }, + { + name: "private key missing", + finder: findExistingPrivateKeyHandle, + credential: "missing-private", + handle: "", + found: false, + }, + { + name: "client cert found", + finder: findExistingClientCertHandle, + credential: "client-cert", + handle: "client-handle", + found: true, + }, + { + name: "client cert missing", + finder: findExistingClientCertHandle, + credential: "ca-cert", + handle: "", + found: false, + }, + { + name: "trusted root cert found", + finder: findExistingTrustedRootCertHandle, + credential: "ca-cert", + handle: "root-handle", + found: true, + }, + { + name: "trusted root cert missing", + finder: findExistingTrustedRootCertHandle, + credential: "client-cert", + handle: "", + found: false, + }, + } + + for _, tc := range tests { + tc := tc + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + handle, found := tc.finder(certs, tc.credential) + require.Equal(t, tc.found, found) + require.Equal(t, tc.handle, handle) + }) + } +} + +func TestWiFiProfileResolveOrAddCredentialHandle(t *testing.T) { + t.Parallel() + + baseCerts := wifiProfileTestCertificates() + errGeneral := errors.New("general error") + errAlreadyExists := errors.New("ALREADY EXISTS") + + refreshed := wsman.Certificates{ + PublicKeyCertificateResponse: publickey.RefinedPullResponse{ + PublicKeyCertificateItems: []publickey.RefinedPublicKeyCertificateResponse{{ + InstanceID: testNewHandle, + X509Certificate: "new-client", + TrustedRootCertificate: false, + }}, + }, + } + + tests := []struct { + name string + credential string + certs wsman.Certificates + find credentialHandleFinder + add credentialHandleAdder + refresh certsRefresher + handle string + resCerts wsman.Certificates + added bool + err error + }{ + { + name: "empty credential", + credential: "", + certs: baseCerts, + find: findExistingClientCertHandle, + add: func(_ string) (string, error) { + return "", nil + }, + refresh: func() (wsman.Certificates, error) { + return wsman.Certificates{}, nil + }, + handle: "", + resCerts: baseCerts, + added: false, + err: nil, + }, + { + name: "existing handle is returned", + credential: "client-cert", + certs: baseCerts, + find: findExistingClientCertHandle, + add: func(_ string) (string, error) { + return "", errors.New("unexpected add call") + }, + refresh: func() (wsman.Certificates, error) { + return wsman.Certificates{}, errors.New("unexpected refresh call") + }, + handle: "client-handle", + resCerts: baseCerts, + added: false, + err: nil, + }, + { + name: "add succeeds", + credential: "new-client", + certs: baseCerts, + find: findExistingClientCertHandle, + add: func(_ string) (string, error) { + return testNewHandle, nil + }, + refresh: func() (wsman.Certificates, error) { + return wsman.Certificates{}, errors.New("unexpected refresh call") + }, + handle: testNewHandle, + resCerts: baseCerts, + added: true, + err: nil, + }, + { + name: "non already exists add error", + credential: "new-client", + certs: baseCerts, + find: findExistingClientCertHandle, + add: func(_ string) (string, error) { + return "", errGeneral + }, + refresh: func() (wsman.Certificates, error) { + return wsman.Certificates{}, nil + }, + handle: "", + resCerts: baseCerts, + added: false, + err: errGeneral, + }, + { + name: "already exists refresh resolves", + credential: "new-client", + certs: baseCerts, + find: findExistingClientCertHandle, + add: func(_ string) (string, error) { + return "", errAlreadyExists + }, + refresh: func() (wsman.Certificates, error) { + return refreshed, nil + }, + handle: testNewHandle, + resCerts: refreshed, + added: false, + err: nil, + }, + { + name: "already exists refresh fails", + credential: "new-client", + certs: baseCerts, + find: findExistingClientCertHandle, + add: func(_ string) (string, error) { + return "", errAlreadyExists + }, + refresh: func() (wsman.Certificates, error) { + return wsman.Certificates{}, errGeneral + }, + handle: "", + resCerts: wsman.Certificates{}, + added: false, + err: errGeneral, + }, + { + name: "already exists refresh missing handle", + credential: "new-client", + certs: baseCerts, + find: findExistingClientCertHandle, + add: func(_ string) (string, error) { + return "", errAlreadyExists + }, + refresh: func() (wsman.Certificates, error) { + return baseCerts, nil + }, + handle: "", + resCerts: baseCerts, + added: false, + err: errAlreadyExists, + }, + } + + for _, tc := range tests { + tc := tc + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + handle, certs, added, err := resolveOrAddCredentialHandle(tc.certs, tc.credential, tc.find, tc.add, tc.refresh) + require.Equal(t, tc.handle, handle) + require.Equal(t, tc.resCerts, certs) + require.Equal(t, tc.added, added) + + if tc.err != nil { + require.IsType(t, tc.err, err) + + return + } + + require.NoError(t, err) + }) + } +} + +func TestWiFiProfileIndexIEEE8021xSettings(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + settings []cimIEEE8021x.IEEE8021xSettingsResponse + expectedNames map[string]string + }{ + { + name: "skips empty instance id", + settings: []cimIEEE8021x.IEEE8021xSettingsResponse{ + {InstanceID: "", ElementName: "skip"}, + {InstanceID: "id-1", ElementName: "keep"}, + }, + expectedNames: map[string]string{"id-1": "keep"}, + }, + } + + for _, tc := range tests { + tc := tc + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + indexed := indexIEEE8021xSettings(tc.settings) + require.Len(t, indexed, len(tc.expectedNames)) + + for id, expectedName := range tc.expectedNames { + require.Equal(t, expectedName, indexed[id].ElementName) + } + }) + } +} + +func TestWiFiProfileDependencyReferencesForWiFi8021x(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + dependency concrete.ConcreteDependency + expectedFound bool + expectedWiFiURI string + expectedIEEEURI string + }{ + { + name: "forward mapping", + dependency: concrete.ConcreteDependency{ + Antecedent: models.AssociationReference{ReferenceParameters: models.ReferenceParametersNoNamespace{ResourceURI: "http://schemas.dmtf.org/wbem/wscim/1/cim-schema/2/CIM_WiFiEndpointSettings"}}, + Dependent: models.AssociationReference{ReferenceParameters: models.ReferenceParametersNoNamespace{ResourceURI: "http://schemas.dmtf.org/wbem/wscim/1/cim-schema/2/CIM_IEEE8021xSettings"}}, + }, + expectedFound: true, + expectedWiFiURI: "http://schemas.dmtf.org/wbem/wscim/1/cim-schema/2/CIM_WiFiEndpointSettings", + expectedIEEEURI: "http://schemas.dmtf.org/wbem/wscim/1/cim-schema/2/CIM_IEEE8021xSettings", + }, + { + name: "reverse mapping", + dependency: concrete.ConcreteDependency{ + Antecedent: models.AssociationReference{ReferenceParameters: models.ReferenceParametersNoNamespace{ResourceURI: "http://schemas.dmtf.org/wbem/wscim/1/cim-schema/2/CIM_IEEE8021xSettings"}}, + Dependent: models.AssociationReference{ReferenceParameters: models.ReferenceParametersNoNamespace{ResourceURI: "http://schemas.dmtf.org/wbem/wscim/1/cim-schema/2/CIM_WiFiEndpointSettings"}}, + }, + expectedFound: true, + expectedWiFiURI: "http://schemas.dmtf.org/wbem/wscim/1/cim-schema/2/CIM_WiFiEndpointSettings", + expectedIEEEURI: "http://schemas.dmtf.org/wbem/wscim/1/cim-schema/2/CIM_IEEE8021xSettings", + }, + { + name: "no match", + dependency: concrete.ConcreteDependency{ + Antecedent: models.AssociationReference{ReferenceParameters: models.ReferenceParametersNoNamespace{ResourceURI: "http://schemas.dmtf.org/wbem/wscim/1/cim-schema/2/CIM_ComputerSystem"}}, + Dependent: models.AssociationReference{ReferenceParameters: models.ReferenceParametersNoNamespace{ResourceURI: "http://schemas.dmtf.org/wbem/wscim/1/cim-schema/2/CIM_IEEE8021xSettings"}}, + }, + expectedFound: false, + }, + } + + for _, tc := range tests { + tc := tc + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + wifiRef, ieeeRef, found := dependencyReferencesForWiFi8021x(tc.dependency) + require.Equal(t, tc.expectedFound, found) + + if !tc.expectedFound { + return + } + + require.Equal(t, tc.expectedWiFiURI, wifiRef.ReferenceParameters.ResourceURI) + require.Equal(t, tc.expectedIEEEURI, ieeeRef.ReferenceParameters.ResourceURI) + }) + } +} + +func TestWiFiProfileAssociationReferenceInstanceID(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + reference models.AssociationReference + expected string + found bool + }{ + { + name: "found", + reference: models.AssociationReference{ReferenceParameters: models.ReferenceParametersNoNamespace{SelectorSet: models.SelectorNoNamespace{Selectors: []models.SelectorResponse{{Name: "InstanceID", Text: "id-1"}}}}}, + expected: "id-1", + found: true, + }, + { + name: "empty instance id", + reference: models.AssociationReference{ReferenceParameters: models.ReferenceParametersNoNamespace{SelectorSet: models.SelectorNoNamespace{Selectors: []models.SelectorResponse{{Name: "InstanceID", Text: ""}}}}}, + expected: "", + found: false, + }, + { + name: "missing selector", + reference: models.AssociationReference{ReferenceParameters: models.ReferenceParametersNoNamespace{SelectorSet: models.SelectorNoNamespace{Selectors: []models.SelectorResponse{{Name: "Name", Text: "x"}}}}}, + expected: "", + found: false, + }, + } + + for _, tc := range tests { + tc := tc + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + id, ok := associationReferenceInstanceID(tc.reference) + require.Equal(t, tc.found, ok) + require.Equal(t, tc.expected, id) + }) + } +} + +func TestWiFiProfileMapAssociatedIEEE8021xByWiFiID(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + dependencies []concrete.ConcreteDependency + expected map[string]string + }{ + { + name: "skips incomplete references", + dependencies: []concrete.ConcreteDependency{ + { + Antecedent: models.AssociationReference{ReferenceParameters: models.ReferenceParametersNoNamespace{ResourceURI: "http://schemas.dmtf.org/wbem/wscim/1/cim-schema/2/CIM_WiFiEndpointSettings", SelectorSet: models.SelectorNoNamespace{Selectors: []models.SelectorResponse{{Name: "InstanceID", Text: "wifi-1"}}}}}, + Dependent: models.AssociationReference{ReferenceParameters: models.ReferenceParametersNoNamespace{ResourceURI: "http://schemas.dmtf.org/wbem/wscim/1/cim-schema/2/CIM_IEEE8021xSettings", SelectorSet: models.SelectorNoNamespace{Selectors: []models.SelectorResponse{{Name: "InstanceID", Text: "ieee-1"}}}}}, + }, + { + Antecedent: models.AssociationReference{ReferenceParameters: models.ReferenceParametersNoNamespace{ResourceURI: "http://schemas.dmtf.org/wbem/wscim/1/cim-schema/2/CIM_IEEE8021xSettings", SelectorSet: models.SelectorNoNamespace{Selectors: []models.SelectorResponse{{Name: "InstanceID", Text: "ieee-2"}}}}}, + Dependent: models.AssociationReference{ReferenceParameters: models.ReferenceParametersNoNamespace{ResourceURI: "http://schemas.dmtf.org/wbem/wscim/1/cim-schema/2/CIM_WiFiEndpointSettings", SelectorSet: models.SelectorNoNamespace{Selectors: []models.SelectorResponse{{Name: "InstanceID", Text: "wifi-2"}}}}}, + }, + { + Antecedent: models.AssociationReference{ReferenceParameters: models.ReferenceParametersNoNamespace{ResourceURI: "http://schemas.dmtf.org/wbem/wscim/1/cim-schema/2/CIM_WiFiEndpointSettings", SelectorSet: models.SelectorNoNamespace{Selectors: []models.SelectorResponse{{Name: "InstanceID", Text: ""}}}}}, + Dependent: models.AssociationReference{ReferenceParameters: models.ReferenceParametersNoNamespace{ResourceURI: "http://schemas.dmtf.org/wbem/wscim/1/cim-schema/2/CIM_IEEE8021xSettings", SelectorSet: models.SelectorNoNamespace{Selectors: []models.SelectorResponse{{Name: "InstanceID", Text: "ieee-skip"}}}}}, + }, + { + Antecedent: models.AssociationReference{ReferenceParameters: models.ReferenceParametersNoNamespace{ResourceURI: "http://schemas.dmtf.org/wbem/wscim/1/cim-schema/2/CIM_ComputerSystem"}}, + Dependent: models.AssociationReference{ReferenceParameters: models.ReferenceParametersNoNamespace{ResourceURI: "http://schemas.dmtf.org/wbem/wscim/1/cim-schema/2/CIM_IEEE8021xSettings"}}, + }, + }, + expected: map[string]string{ + "wifi-1": "ieee-1", + "wifi-2": "ieee-2", + }, + }, + } + + for _, tc := range tests { + tc := tc + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + mapped := mapAssociatedIEEE8021xByWiFiID(tc.dependencies) + require.Equal(t, tc.expected, mapped) + }) + } +} + +func TestWiFiProfileFindAssociatedIEEE8021xSettings(t *testing.T) { + t.Parallel() + + ieeeByID := map[string]cimIEEE8021x.IEEE8021xSettingsResponse{ + "assoc-id": {InstanceID: "assoc-id", Username: "assoc-user"}, + "Intel(r) AMT:IEEE 802.1x Settings CorpFallback": {InstanceID: "Intel(r) AMT:IEEE 802.1x Settings CorpFallback", Username: "fallback-user"}, + } + ieeeByName := map[string]cimIEEE8021x.IEEE8021xSettingsResponse{ + "corpname": {ElementName: "CorpName", Username: "name-user"}, + } + + tests := []struct { + name string + setting wifi.WiFiEndpointSettingsResponse + associatedMap map[string]string + byID map[string]cimIEEE8021x.IEEE8021xSettingsResponse + byName map[string]cimIEEE8021x.IEEE8021xSettingsResponse + expectedUser string + found bool + }{ + { + name: "direct association match", + setting: wifi.WiFiEndpointSettingsResponse{InstanceID: "wifi-1", ElementName: "Ignored"}, + associatedMap: map[string]string{"wifi-1": "assoc-id"}, + byID: ieeeByID, + byName: ieeeByName, + expectedUser: "assoc-user", + found: true, + }, + { + name: "fallback by generated instance id", + setting: wifi.WiFiEndpointSettingsResponse{ElementName: "CorpFallback"}, + associatedMap: map[string]string{}, + byID: ieeeByID, + byName: ieeeByName, + expectedUser: "fallback-user", + found: true, + }, + { + name: "fallback by normalized profile name", + setting: wifi.WiFiEndpointSettingsResponse{ElementName: " CorpName "}, + associatedMap: map[string]string{}, + byID: map[string]cimIEEE8021x.IEEE8021xSettingsResponse{}, + byName: ieeeByName, + expectedUser: "name-user", + found: true, + }, + { + name: "no element name means not found", + setting: wifi.WiFiEndpointSettingsResponse{ElementName: ""}, + associatedMap: map[string]string{}, + byID: map[string]cimIEEE8021x.IEEE8021xSettingsResponse{}, + byName: map[string]cimIEEE8021x.IEEE8021xSettingsResponse{}, + expectedUser: "", + found: false, + }, + } + + for _, tc := range tests { + tc := tc + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + res, found := findAssociatedIEEE8021xSettings(tc.setting, tc.associatedMap, tc.byID, tc.byName) + require.Equal(t, tc.found, found) + require.Equal(t, tc.expectedUser, res.Username) + }) + } +} diff --git a/internal/usecase/devices/wifiprofile_test.go b/internal/usecase/devices/wifiprofile_test.go new file mode 100644 index 000000000..430fef078 --- /dev/null +++ b/internal/usecase/devices/wifiprofile_test.go @@ -0,0 +1,742 @@ +package devices_test + +import ( + "context" + "fmt" + "testing" + + "github.com/stretchr/testify/require" + "go.uber.org/mock/gomock" + + "github.com/device-management-toolkit/go-wsman-messages/v2/pkg/config" + "github.com/device-management-toolkit/go-wsman-messages/v2/pkg/wsman/amt/publickey" + "github.com/device-management-toolkit/go-wsman-messages/v2/pkg/wsman/amt/publicprivate" + "github.com/device-management-toolkit/go-wsman-messages/v2/pkg/wsman/amt/wifiportconfiguration" + "github.com/device-management-toolkit/go-wsman-messages/v2/pkg/wsman/cim/concrete" + cimIEEE8021x "github.com/device-management-toolkit/go-wsman-messages/v2/pkg/wsman/cim/ieee8021x" + "github.com/device-management-toolkit/go-wsman-messages/v2/pkg/wsman/cim/models" + "github.com/device-management-toolkit/go-wsman-messages/v2/pkg/wsman/cim/wifi" + + "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" + "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/logger" +) + +const ( + testWiFiEndpoint = "WiFi Endpoint 0" + testUserSettingsInstanceIDPrefix = "Intel(r) AMT:WiFi Endpoint User Settings" +) + +type applyWirelessProfilesTestCase struct { + name string + ctx context.Context + req dto.WirelessProfilesApplyRequest + manMock func(*mocks.MockWSMAN, *mocks.MockManagement) + repoMock func(*mocks.MockDeviceManagementRepository) + res []config.WirelessProfile + err error + errString bool +} + +func initWiFiProfileTest(t *testing.T) (*devices.UseCase, *mocks.MockWSMAN, *mocks.MockManagement, *mocks.MockDeviceManagementRepository) { + t.Helper() + + mockCtl := gomock.NewController(t) + + repo := mocks.NewMockDeviceManagementRepository(mockCtl) + wsmanMock := mocks.NewMockWSMAN(mockCtl) + wsmanMock.EXPECT().Worker().Return().AnyTimes() + + management := mocks.NewMockManagement(mockCtl) + log := logger.New("error") + u := devices.New(repo, wsmanMock, mocks.NewMockRedirection(mockCtl), log, mocks.MockCrypto{}) + + return u, wsmanMock, management, repo +} + +func expectedWiFiRequest(profile config.WirelessProfile) wifi.WiFiEndpointSettingsRequest { + authMethod, ok := wifi.ParseAuthenticationMethod(profile.AuthenticationMethod) + if !ok { + panic(fmt.Sprintf("invalid authentication method in test profile: %q", profile.AuthenticationMethod)) + } + + encryptionMethod, ok := wifi.ParseEncryptionMethod(profile.EncryptionMethod) + if !ok { + panic(fmt.Sprintf("invalid encryption method in test profile: %q", profile.EncryptionMethod)) + } + + return wifi.WiFiEndpointSettingsRequest{ + ElementName: profile.ProfileName, + InstanceID: fmt.Sprintf("Intel(r) AMT:WiFi Endpoint Settings %s", profile.ProfileName), + AuthenticationMethod: authMethod, + EncryptionMethod: encryptionMethod, + SSID: profile.SSID, + Priority: profile.Priority, + PSKPassPhrase: profile.Password, + } +} + +func TestGetWirelessProfiles(t *testing.T) { + t.Parallel() + + device := &entity.Device{GUID: "device-guid-123"} + tests := []struct { + name string + manMock func(*mocks.MockWSMAN, *mocks.MockManagement) + repoMock func(*mocks.MockDeviceManagementRepository) + res []config.WirelessProfile + err error + }{ + { + name: "success filters endpoint user settings by instance id", + manMock: func(man *mocks.MockWSMAN, man2 *mocks.MockManagement) { + gomock.InOrder( + man.EXPECT().SetupWsmanClient(gomock.Any(), gomock.Any(), false, true).Return(man2, nil), + man2.EXPECT().GetWiFiSettings().Return([]wifi.WiFiEndpointSettingsResponse{ + {ElementName: "Endpoint User Settings", InstanceID: testUserSettingsInstanceIDPrefix + " Profile"}, + { + ElementName: "Corp", + InstanceID: "Intel(r) AMT:WiFi Endpoint Settings Corp", + SSID: "CorpSSID", + AuthenticationMethod: wifi.AuthenticationMethodWPA2PSK, + EncryptionMethod: wifi.EncryptionMethodCCMP, + Priority: 2, + }, + }, nil), + man2.EXPECT().GetCIMIEEE8021xSettings().Return(cimIEEE8021x.Response{}, nil), + man2.EXPECT().GetConcreteDependencies().Return([]concrete.ConcreteDependency{}, nil), + ) + }, + repoMock: func(repo *mocks.MockDeviceManagementRepository) { + repo.EXPECT().GetByID(context.Background(), device.GUID, "").Return(device, nil) + }, + res: []config.WirelessProfile{{ + ProfileName: "Corp", + SSID: "CorpSSID", + AuthenticationMethod: "WPA2PSK", + EncryptionMethod: "CCMP", + Priority: 2, + }}, + err: nil, + }, + { + name: "success maps associated ieee8021x", + manMock: func(man *mocks.MockWSMAN, man2 *mocks.MockManagement) { + gomock.InOrder( + man.EXPECT().SetupWsmanClient(gomock.Any(), gomock.Any(), false, true).Return(man2, nil), + man2.EXPECT().GetWiFiSettings().Return([]wifi.WiFiEndpointSettingsResponse{ + { + ElementName: "Corp", + InstanceID: "Intel(r) AMT:WiFi Endpoint Settings Corp", + SSID: "CorpSSID", + AuthenticationMethod: wifi.AuthenticationMethodWPA2IEEE8021x, + EncryptionMethod: wifi.EncryptionMethodCCMP, + Priority: 1, + }, + }, nil), + man2.EXPECT().GetCIMIEEE8021xSettings().Return(cimIEEE8021x.Response{Body: cimIEEE8021x.Body{PullResponse: cimIEEE8021x.PullResponse{IEEE8021xSettingsItems: []cimIEEE8021x.IEEE8021xSettingsResponse{{ + InstanceID: "Intel(r) AMT:IEEE 802.1x Settings Corp", + AuthenticationProtocol: 2, + Username: "corp-user", + Password: "corp-pass", + }}}}}, nil), + man2.EXPECT().GetConcreteDependencies().Return([]concrete.ConcreteDependency{{ + Antecedent: models.AssociationReference{ReferenceParameters: models.ReferenceParametersNoNamespace{ResourceURI: "http://schemas.dmtf.org/wbem/wscim/1/cim-schema/2/CIM_WiFiEndpointSettings", SelectorSet: models.SelectorNoNamespace{Selectors: []models.SelectorResponse{{Name: "InstanceID", Text: "Intel(r) AMT:WiFi Endpoint Settings Corp"}}}}}, + Dependent: models.AssociationReference{ReferenceParameters: models.ReferenceParametersNoNamespace{ResourceURI: "http://schemas.dmtf.org/wbem/wscim/1/cim-schema/2/CIM_IEEE8021xSettings", SelectorSet: models.SelectorNoNamespace{Selectors: []models.SelectorResponse{{Name: "InstanceID", Text: "Intel(r) AMT:IEEE 802.1x Settings Corp"}}}}}, + }}, nil), + ) + }, + repoMock: func(repo *mocks.MockDeviceManagementRepository) { + repo.EXPECT().GetByID(context.Background(), device.GUID, "").Return(device, nil) + }, + res: []config.WirelessProfile{{ + ProfileName: "Corp", + SSID: "CorpSSID", + AuthenticationMethod: "WPA2IEEE8021x", + EncryptionMethod: "CCMP", + Priority: 1, + IEEE8021x: &config.IEEE8021x{ + AuthenticationProtocol: 2, + Username: "corp-user", + Password: "corp-pass", + }, + }}, + err: nil, + }, + { + name: "success maps ieee8021x by profile name fallback", + manMock: func(man *mocks.MockWSMAN, man2 *mocks.MockManagement) { + gomock.InOrder( + man.EXPECT().SetupWsmanClient(gomock.Any(), gomock.Any(), false, true).Return(man2, nil), + man2.EXPECT().GetWiFiSettings().Return([]wifi.WiFiEndpointSettingsResponse{ + { + ElementName: "CorpEAP2", + InstanceID: "Intel(r) AMT:WiFi Endpoint Settings CorpEAP2", + SSID: "CorpNet2", + AuthenticationMethod: wifi.AuthenticationMethodWPA2IEEE8021x, + EncryptionMethod: wifi.EncryptionMethodCCMP, + Priority: 4, + }, + }, nil), + man2.EXPECT().GetCIMIEEE8021xSettings().Return(cimIEEE8021x.Response{Body: cimIEEE8021x.Body{PullResponse: cimIEEE8021x.PullResponse{IEEE8021xSettingsItems: []cimIEEE8021x.IEEE8021xSettingsResponse{{ + ElementName: "CorpEAP2", + InstanceID: "Intel(r) AMT:IEEE 802.1x Settings CorpEAP2", + AuthenticationProtocol: 2, + Username: "corp-user", + Password: "corp-pass", + }}}}}, nil), + man2.EXPECT().GetConcreteDependencies().Return([]concrete.ConcreteDependency{}, nil), + ) + }, + repoMock: func(repo *mocks.MockDeviceManagementRepository) { + repo.EXPECT().GetByID(context.Background(), device.GUID, "").Return(device, nil) + }, + res: []config.WirelessProfile{{ + ProfileName: "CorpEAP2", + SSID: "CorpNet2", + AuthenticationMethod: "WPA2IEEE8021x", + EncryptionMethod: "CCMP", + Priority: 4, + IEEE8021x: &config.IEEE8021x{ + AuthenticationProtocol: 2, + Username: "corp-user", + Password: "corp-pass", + }, + }}, + err: nil, + }, + { + name: "GetByID fails", + manMock: nil, + repoMock: func(repo *mocks.MockDeviceManagementRepository) { + repo.EXPECT().GetByID(context.Background(), device.GUID, "").Return(nil, ErrGeneral) + }, + res: nil, + err: devices.ErrGeneral, + }, + { + name: "device not found", + manMock: nil, + repoMock: func(repo *mocks.MockDeviceManagementRepository) { + repo.EXPECT().GetByID(context.Background(), device.GUID, "").Return(nil, nil) + }, + res: nil, + err: devices.ErrNotFound, + }, + { + name: "device GUID empty", + manMock: nil, + repoMock: func(repo *mocks.MockDeviceManagementRepository) { + repo.EXPECT().GetByID(context.Background(), device.GUID, "").Return(&entity.Device{}, nil) + }, + res: nil, + err: devices.ErrNotFound, + }, + { + name: "SetupWsmanClient fails", + manMock: func(man *mocks.MockWSMAN, _ *mocks.MockManagement) { + man.EXPECT().SetupWsmanClient(gomock.Any(), gomock.Any(), false, true).Return(nil, ErrGeneral) + }, + repoMock: func(repo *mocks.MockDeviceManagementRepository) { + repo.EXPECT().GetByID(context.Background(), device.GUID, "").Return(device, nil) + }, + res: nil, + err: devices.ErrGeneral, + }, + { + name: "GetWiFiSettings fails", + manMock: func(man *mocks.MockWSMAN, man2 *mocks.MockManagement) { + man.EXPECT().SetupWsmanClient(gomock.Any(), gomock.Any(), false, true).Return(man2, nil) + man2.EXPECT().GetWiFiSettings().Return(nil, ErrGeneral) + }, + repoMock: func(repo *mocks.MockDeviceManagementRepository) { + repo.EXPECT().GetByID(context.Background(), device.GUID, "").Return(device, nil) + }, + res: nil, + err: devices.ErrGeneral, + }, + { + name: "GetCIMIEEE8021xSettings fails", + manMock: func(man *mocks.MockWSMAN, man2 *mocks.MockManagement) { + gomock.InOrder( + man.EXPECT().SetupWsmanClient(gomock.Any(), gomock.Any(), false, true).Return(man2, nil), + man2.EXPECT().GetWiFiSettings().Return([]wifi.WiFiEndpointSettingsResponse{}, nil), + man2.EXPECT().GetCIMIEEE8021xSettings().Return(cimIEEE8021x.Response{}, ErrGeneral), + ) + }, + repoMock: func(repo *mocks.MockDeviceManagementRepository) { + repo.EXPECT().GetByID(context.Background(), device.GUID, "").Return(device, nil) + }, + res: nil, + err: devices.ErrGeneral, + }, + { + name: "GetConcreteDependencies fails", + manMock: func(man *mocks.MockWSMAN, man2 *mocks.MockManagement) { + gomock.InOrder( + man.EXPECT().SetupWsmanClient(gomock.Any(), gomock.Any(), false, true).Return(man2, nil), + man2.EXPECT().GetWiFiSettings().Return([]wifi.WiFiEndpointSettingsResponse{}, nil), + man2.EXPECT().GetCIMIEEE8021xSettings().Return(cimIEEE8021x.Response{}, nil), + man2.EXPECT().GetConcreteDependencies().Return(nil, ErrGeneral), + ) + }, + repoMock: func(repo *mocks.MockDeviceManagementRepository) { + repo.EXPECT().GetByID(context.Background(), device.GUID, "").Return(device, nil) + }, + res: nil, + err: devices.ErrGeneral, + }, + } + + for _, tc := range tests { + tc := tc + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + useCase, wsmanMock, management, repo := initWiFiProfileTest(t) + + if tc.manMock != nil { + tc.manMock(wsmanMock, management) + } + + if tc.repoMock != nil { + tc.repoMock(repo) + } + + res, err := useCase.GetWirelessProfiles(context.Background(), device.GUID) + require.Equal(t, tc.res, res) + + if tc.err != nil { + require.IsType(t, tc.err, err) + + return + } + + require.NoError(t, err) + }) + } +} + +func TestApplyWirelessProfiles(t *testing.T) { + t.Parallel() + + device := &entity.Device{GUID: "device-guid-123"} + tests := []applyWirelessProfilesTestCase{ + { + name: "success with mixed profiles", + ctx: context.Background(), + req: dto.WirelessProfilesApplyRequest{Profiles: []config.WirelessProfile{ + { + ProfileName: "Home", + SSID: "HomeSSID", + Priority: 7, + Password: "password", + AuthenticationMethod: "WPA2PSK", + EncryptionMethod: "CCMP", + }, + { + ProfileName: "Corp", + SSID: "CorpSSID", + Priority: 3, + AuthenticationMethod: "WPA2IEEE8021x", + EncryptionMethod: "CCMP", + IEEE8021x: &config.IEEE8021x{ + AuthenticationProtocol: 2, + Username: "corp-user", + Password: "corp-pass", + PrivateKey: "new-private", + ClientCert: "new-client", + CACert: "new-ca", + }, + }, + }}, + manMock: func(man *mocks.MockWSMAN, man2 *mocks.MockManagement) { + certs := wsman.Certificates{ + PublicPrivateKeyPairResponse: publicprivate.RefinedPullResponse{ + PublicPrivateKeyPairItems: []publicprivate.RefinedPublicPrivateKeyPair{{ + InstanceID: "existing-private", + DERKey: "other-private", + }}, + }, + PublicKeyCertificateResponse: publickey.RefinedPullResponse{ + PublicKeyCertificateItems: []publickey.RefinedPublicKeyCertificateResponse{{ + InstanceID: "existing-client", + X509Certificate: "other-client", + TrustedRootCertificate: false, + }}, + }, + } + + req := dto.WirelessProfilesApplyRequest{Profiles: []config.WirelessProfile{ + { + ProfileName: "Home", + SSID: "HomeSSID", + Priority: 7, + Password: "password", + AuthenticationMethod: "WPA2PSK", + EncryptionMethod: "CCMP", + }, + { + ProfileName: "Corp", + SSID: "CorpSSID", + Priority: 3, + AuthenticationMethod: "WPA2IEEE8021x", + EncryptionMethod: "CCMP", + IEEE8021x: &config.IEEE8021x{ + AuthenticationProtocol: 2, + Username: "corp-user", + Password: "corp-pass", + PrivateKey: "new-private", + ClientCert: "new-client", + CACert: "new-ca", + }, + }, + }} + + gomock.InOrder( + man.EXPECT().SetupWsmanClient(gomock.Any(), gomock.Any(), false, true).Return(man2, nil), + man2.EXPECT().GetCertificates().Return(certs, nil), + man2.EXPECT().AddPrivateKey("new-private").Return("private-handle", nil), + man2.EXPECT().AddClientCert("new-client").Return("client-handle", nil), + man2.EXPECT().AddTrustedRootCert("new-ca").Return("root-handle", nil), + man2.EXPECT().GetWiFiSettings().Return([]wifi.WiFiEndpointSettingsResponse{ + {InstanceID: "", ElementName: "ignore-empty-instance"}, + {InstanceID: testUserSettingsInstanceIDPrefix + " Profile", ElementName: "Endpoint User Settings"}, + {InstanceID: "old-profile", ElementName: "Old"}, + }, nil), + man2.EXPECT().DeleteWiFiSetting("old-profile").Return(nil), + man2.EXPECT().AddWiFiSettings(expectedWiFiRequest(req.Profiles[0]), models.IEEE8021xSettings{}, testWiFiEndpoint, "", "").Return(wifiportconfiguration.Response{}, nil), + man2.EXPECT().AddWiFiSettings( + expectedWiFiRequest(req.Profiles[1]), + models.IEEE8021xSettings{ + ElementName: "Corp", + InstanceID: "Intel(r) AMT:IEEE 802.1x Settings Corp", + AuthenticationProtocol: models.AuthenticationProtocol(2), + Username: "corp-user", + Password: "corp-pass", + }, + testWiFiEndpoint, + "client-handle", + "root-handle", + ).Return(wifiportconfiguration.Response{}, nil), + ) + }, + repoMock: func(repo *mocks.MockDeviceManagementRepository) { + repo.EXPECT().GetByID(context.Background(), device.GUID, "").Return(device, nil) + }, + res: []config.WirelessProfile{ + { + ProfileName: "Home", + SSID: "HomeSSID", + Priority: 7, + Password: "password", + AuthenticationMethod: "WPA2PSK", + EncryptionMethod: "CCMP", + }, + { + ProfileName: "Corp", + SSID: "CorpSSID", + Priority: 3, + AuthenticationMethod: "WPA2IEEE8021x", + EncryptionMethod: "CCMP", + IEEE8021x: &config.IEEE8021x{ + AuthenticationProtocol: 2, + Username: "corp-user", + Password: "corp-pass", + PrivateKey: "new-private", + ClientCert: "new-client", + CACert: "new-ca", + }, + }, + }, + err: nil, + }, + { + name: "invalid authentication method fails fast", + ctx: context.Background(), + req: dto.WirelessProfilesApplyRequest{Profiles: []config.WirelessProfile{{ + ProfileName: "Home", + SSID: "HomeSSID", + Password: "password", + AuthenticationMethod: "INVALID", + EncryptionMethod: "CCMP", + }}}, + manMock: func(man *mocks.MockWSMAN, man2 *mocks.MockManagement) { + man.EXPECT().SetupWsmanClient(gomock.Any(), gomock.Any(), false, true).Return(man2, nil) + }, + repoMock: func(repo *mocks.MockDeviceManagementRepository) { + repo.EXPECT().GetByID(context.Background(), device.GUID, "").Return(device, nil) + }, + res: nil, + err: fmt.Errorf("invalid authentication method %q for profile %q", "INVALID", "Home"), + errString: true, + }, + { + name: "canceled context returns before apply delay completes", + ctx: func() context.Context { + ctx, cancel := context.WithCancel(context.Background()) + cancel() + + return ctx + }(), + req: dto.WirelessProfilesApplyRequest{Profiles: []config.WirelessProfile{{ + ProfileName: "Corp", + SSID: "CorpSSID", + AuthenticationMethod: "WPA2IEEE8021x", + EncryptionMethod: "CCMP", + IEEE8021x: &config.IEEE8021x{ + PrivateKey: "new-private", + }, + }}}, + manMock: func(man *mocks.MockWSMAN, man2 *mocks.MockManagement) { + gomock.InOrder( + man.EXPECT().SetupWsmanClient(gomock.Any(), gomock.Any(), false, true).Return(man2, nil), + man2.EXPECT().GetCertificates().Return(wsman.Certificates{}, nil), + man2.EXPECT().AddPrivateKey("new-private").Return("private-handle", nil), + ) + }, + repoMock: func(repo *mocks.MockDeviceManagementRepository) { + repo.EXPECT().GetByID(gomock.Any(), device.GUID, "").Return(device, nil) + }, + res: nil, + err: context.Canceled, + }, + { + name: "setup fails", + ctx: context.Background(), + req: dto.WirelessProfilesApplyRequest{}, + manMock: nil, + repoMock: func(repo *mocks.MockDeviceManagementRepository) { + repo.EXPECT().GetByID(context.Background(), device.GUID, "").Return(nil, ErrGeneral) + }, + res: nil, + err: devices.ErrGeneral, + }, + { + name: "certificate read fails", + ctx: context.Background(), + req: dto.WirelessProfilesApplyRequest{Profiles: []config.WirelessProfile{{ + ProfileName: "Corp", + SSID: "CorpSSID", + AuthenticationMethod: "WPA2IEEE8021x", + EncryptionMethod: "CCMP", + IEEE8021x: &config.IEEE8021x{ + ClientCert: "client", + }, + }}}, + manMock: func(man *mocks.MockWSMAN, man2 *mocks.MockManagement) { + man.EXPECT().SetupWsmanClient(gomock.Any(), gomock.Any(), false, true).Return(man2, nil) + man2.EXPECT().GetCertificates().Return(wsman.Certificates{}, ErrGeneral) + }, + repoMock: func(repo *mocks.MockDeviceManagementRepository) { + repo.EXPECT().GetByID(context.Background(), device.GUID, "").Return(device, nil) + }, + res: nil, + err: devices.ErrGeneral, + }, + { + name: "private key add fails", + ctx: context.Background(), + req: dto.WirelessProfilesApplyRequest{Profiles: []config.WirelessProfile{{ + ProfileName: "Corp", + SSID: "CorpSSID", + AuthenticationMethod: "WPA2IEEE8021x", + EncryptionMethod: "CCMP", + IEEE8021x: &config.IEEE8021x{PrivateKey: "pk"}, + }}}, + manMock: func(man *mocks.MockWSMAN, man2 *mocks.MockManagement) { + gomock.InOrder( + man.EXPECT().SetupWsmanClient(gomock.Any(), gomock.Any(), false, true).Return(man2, nil), + man2.EXPECT().GetCertificates().Return(wsman.Certificates{}, nil), + man2.EXPECT().AddPrivateKey("pk").Return("", ErrGeneral), + ) + }, + repoMock: func(repo *mocks.MockDeviceManagementRepository) { + repo.EXPECT().GetByID(context.Background(), device.GUID, "").Return(device, nil) + }, + res: nil, + err: devices.ErrGeneral, + }, + { + name: "client cert add fails", + ctx: context.Background(), + req: dto.WirelessProfilesApplyRequest{Profiles: []config.WirelessProfile{{ + ProfileName: "Corp", + SSID: "CorpSSID", + AuthenticationMethod: "WPA2IEEE8021x", + EncryptionMethod: "CCMP", + IEEE8021x: &config.IEEE8021x{ClientCert: "client"}, + }}}, + manMock: func(man *mocks.MockWSMAN, man2 *mocks.MockManagement) { + gomock.InOrder( + man.EXPECT().SetupWsmanClient(gomock.Any(), gomock.Any(), false, true).Return(man2, nil), + man2.EXPECT().GetCertificates().Return(wsman.Certificates{}, nil), + man2.EXPECT().AddClientCert("client").Return("", ErrGeneral), + ) + }, + repoMock: func(repo *mocks.MockDeviceManagementRepository) { + repo.EXPECT().GetByID(context.Background(), device.GUID, "").Return(device, nil) + }, + res: nil, + err: devices.ErrGeneral, + }, + { + name: "ca cert add fails", + ctx: context.Background(), + req: dto.WirelessProfilesApplyRequest{Profiles: []config.WirelessProfile{{ + ProfileName: "Corp", + SSID: "CorpSSID", + AuthenticationMethod: "WPA2IEEE8021x", + EncryptionMethod: "CCMP", + IEEE8021x: &config.IEEE8021x{CACert: "ca"}, + }}}, + manMock: func(man *mocks.MockWSMAN, man2 *mocks.MockManagement) { + gomock.InOrder( + man.EXPECT().SetupWsmanClient(gomock.Any(), gomock.Any(), false, true).Return(man2, nil), + man2.EXPECT().GetCertificates().Return(wsman.Certificates{}, nil), + man2.EXPECT().AddTrustedRootCert("ca").Return("", ErrGeneral), + ) + }, + repoMock: func(repo *mocks.MockDeviceManagementRepository) { + repo.EXPECT().GetByID(context.Background(), device.GUID, "").Return(device, nil) + }, + res: nil, + err: devices.ErrGeneral, + }, + { + name: "clear profiles fails", + ctx: context.Background(), + req: dto.WirelessProfilesApplyRequest{Profiles: []config.WirelessProfile{{ + ProfileName: "Home", + SSID: "HomeSSID", + Password: "password", + AuthenticationMethod: "WPA2PSK", + EncryptionMethod: "CCMP", + }}}, + manMock: func(man *mocks.MockWSMAN, man2 *mocks.MockManagement) { + man.EXPECT().SetupWsmanClient(gomock.Any(), gomock.Any(), false, true).Return(man2, nil) + man2.EXPECT().GetWiFiSettings().Return(nil, ErrGeneral) + }, + repoMock: func(repo *mocks.MockDeviceManagementRepository) { + repo.EXPECT().GetByID(context.Background(), device.GUID, "").Return(device, nil) + }, + res: nil, + err: devices.ErrGeneral, + }, + { + name: "delete profile fails", + ctx: context.Background(), + req: dto.WirelessProfilesApplyRequest{Profiles: []config.WirelessProfile{{ + ProfileName: "Home", + SSID: "HomeSSID", + Password: "password", + AuthenticationMethod: "WPA2PSK", + EncryptionMethod: "CCMP", + }}}, + manMock: func(man *mocks.MockWSMAN, man2 *mocks.MockManagement) { + gomock.InOrder( + man.EXPECT().SetupWsmanClient(gomock.Any(), gomock.Any(), false, true).Return(man2, nil), + man2.EXPECT().GetWiFiSettings().Return([]wifi.WiFiEndpointSettingsResponse{{ + InstanceID: "profile-1", + ElementName: "Profile1", + }}, nil), + man2.EXPECT().DeleteWiFiSetting("profile-1").Return(ErrGeneral), + ) + }, + repoMock: func(repo *mocks.MockDeviceManagementRepository) { + repo.EXPECT().GetByID(context.Background(), device.GUID, "").Return(device, nil) + }, + res: nil, + err: devices.ErrGeneral, + }, + { + name: "add wifi settings fails", + ctx: context.Background(), + req: dto.WirelessProfilesApplyRequest{Profiles: []config.WirelessProfile{{ + ProfileName: "Home", + SSID: "HomeSSID", + Priority: 13, + Password: "password", + AuthenticationMethod: "WPA2PSK", + EncryptionMethod: "CCMP", + }}}, + manMock: func(man *mocks.MockWSMAN, man2 *mocks.MockManagement) { + req := dto.WirelessProfilesApplyRequest{Profiles: []config.WirelessProfile{{ + ProfileName: "Home", + SSID: "HomeSSID", + Priority: 13, + Password: "password", + AuthenticationMethod: "WPA2PSK", + EncryptionMethod: "CCMP", + }}} + + gomock.InOrder( + man.EXPECT().SetupWsmanClient(gomock.Any(), gomock.Any(), false, true).Return(man2, nil), + man2.EXPECT().GetWiFiSettings().Return([]wifi.WiFiEndpointSettingsResponse{}, nil), + man2.EXPECT().AddWiFiSettings( + expectedWiFiRequest(req.Profiles[0]), + models.IEEE8021xSettings{}, + testWiFiEndpoint, + "", + "", + ).Return(wifiportconfiguration.Response{}, ErrGeneral), + ) + }, + repoMock: func(repo *mocks.MockDeviceManagementRepository) { + repo.EXPECT().GetByID(context.Background(), device.GUID, "").Return(device, nil) + }, + res: nil, + err: devices.ErrGeneral, + }, + } + + for _, tc := range tests { + tc := tc + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + runApplyWirelessProfilesCase(t, tc, device) + }) + } +} + +func runApplyWirelessProfilesCase(t *testing.T, tc applyWirelessProfilesTestCase, device *entity.Device) { + t.Helper() + + useCase, wsmanMock, management, repo := initWiFiProfileTest(t) + + if tc.manMock != nil { + tc.manMock(wsmanMock, management) + } + + if tc.repoMock != nil { + tc.repoMock(repo) + } + + ctx := tc.ctx + if ctx == nil { + ctx = context.Background() + } + + res, err := useCase.ApplyWirelessProfiles(ctx, device.GUID, tc.req) + require.Equal(t, tc.res, res) + + if tc.err != nil { + require.Error(t, err) + + if tc.errString { + require.Equal(t, tc.err.Error(), err.Error()) + + return + } + + require.IsType(t, tc.err, err) + + return + } + + require.NoError(t, err) +} diff --git a/internal/usecase/devices/wifistate.go b/internal/usecase/devices/wifistate.go index 7588ca29d..b2a44ecbb 100644 --- a/internal/usecase/devices/wifistate.go +++ b/internal/usecase/devices/wifistate.go @@ -5,7 +5,7 @@ import ( "github.com/device-management-toolkit/go-wsman-messages/v2/pkg/wsman/cim/wifi" - wsman "github.com/device-management-toolkit/console/internal/usecase/devices/wsman" + "github.com/device-management-toolkit/console/internal/usecase/devices/wsman" ) func (uc *UseCase) RequestWirelessStateChange(c context.Context, guid string, requestedState wifi.RequestedState) (wifi.RequestedState, error) { diff --git a/internal/usecase/devices/wifistate_test.go b/internal/usecase/devices/wifistate_test.go index d1d45abe5..4fb75d615 100644 --- a/internal/usecase/devices/wifistate_test.go +++ b/internal/usecase/devices/wifistate_test.go @@ -6,14 +6,14 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - gomock "go.uber.org/mock/gomock" + "go.uber.org/mock/gomock" "github.com/device-management-toolkit/go-wsman-messages/v2/pkg/wsman/cim/wifi" "github.com/device-management-toolkit/go-wsman-messages/v2/pkg/wsman/common" "github.com/device-management-toolkit/console/internal/entity" "github.com/device-management-toolkit/console/internal/mocks" - devices "github.com/device-management-toolkit/console/internal/usecase/devices" + "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/logger" ) diff --git a/internal/usecase/devices/wsman/interfaces.go b/internal/usecase/devices/wsman/interfaces.go index f229f38c7..929f09cc1 100644 --- a/internal/usecase/devices/wsman/interfaces.go +++ b/internal/usecase/devices/wsman/interfaces.go @@ -11,10 +11,13 @@ import ( "github.com/device-management-toolkit/go-wsman-messages/v2/pkg/wsman/amt/redirection" "github.com/device-management-toolkit/go-wsman-messages/v2/pkg/wsman/amt/setupandconfiguration" "github.com/device-management-toolkit/go-wsman-messages/v2/pkg/wsman/amt/tls" + "github.com/device-management-toolkit/go-wsman-messages/v2/pkg/wsman/amt/wifiportconfiguration" 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/concrete" "github.com/device-management-toolkit/go-wsman-messages/v2/pkg/wsman/cim/credential" + cimIEEE8021x "github.com/device-management-toolkit/go-wsman-messages/v2/pkg/wsman/cim/ieee8021x" "github.com/device-management-toolkit/go-wsman-messages/v2/pkg/wsman/cim/kvm" + "github.com/device-management-toolkit/go-wsman-messages/v2/pkg/wsman/cim/models" "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" "github.com/device-management-toolkit/go-wsman-messages/v2/pkg/wsman/cim/software" @@ -60,9 +63,14 @@ type Management interface { GetAuditLog(startIndex int) (auditlog.Response, error) GetEventLog(startIndex, maxReadRecords int) (messagelog.GetRecordsResponse, error) GetNetworkSettings() (NetworkResults, error) + GetWiFiSettings() ([]wifi.WiFiEndpointSettingsResponse, error) + GetCIMIEEE8021xSettings() (cimIEEE8021x.Response, error) + DeleteWiFiSetting(instanceID string) error + AddWiFiSettings(wifiEndpointSettings wifi.WiFiEndpointSettingsRequest, ieee8021xSettings models.IEEE8021xSettings, wifiEndpoint, clientCredential, caCredential string) (wifiportconfiguration.Response, error) EnumerateWiFiPort() (wifi.Response, error) PullWiFiPort(enumerationContext string) (wifi.Response, error) WiFiRequestStateChange(requestedState wifi.RequestedState) error + AddPrivateKey(privateKey string) (string, error) GetCertificates() (Certificates, error) GetTLSSettingData() ([]tls.SettingDataResponse, error) GetCredentialRelationships() (credential.Items, error)