diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 25164a53b..550acee45 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -140,9 +140,9 @@ jobs: with: go-version: ${{ matrix.go-version }} - name: build go - run: go build ./cmd/app/main.go + run: go build ./cmd/app - name: build noui (headless) - run: CGO_ENABLED=0 go build -tags=noui -o console-headless ./cmd/app/main.go + run: CGO_ENABLED=0 go build -tags=noui -o console-headless ./cmd/app - name: Install Test Converter and run tests run: | export GOPATH="$HOME/go/" diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index d4168beff..da250906a 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -103,28 +103,28 @@ jobs: # Cross-compile all platform binaries from a single runner using CGO_ENABLED=0 # Static Go binaries are cross-platform compatible, so we can build all targets from Linux - name: Build Linux x64 - run: CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -ldflags "-s -w -X 'github.com/device-management-toolkit/console/internal/app.Version=${{ needs.prepare.outputs.version }}'" -trimpath -o dist/linux/console_linux_x64 ./cmd/app/main.go + run: CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -ldflags "-s -w -X 'github.com/device-management-toolkit/console/internal/app.Version=${{ needs.prepare.outputs.version }}'" -trimpath -o dist/linux/console_linux_x64 ./cmd/app - name: Build Linux x64 headless - run: CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -tags=noui -ldflags "-s -w -X 'github.com/device-management-toolkit/console/internal/app.Version=${{ needs.prepare.outputs.version }}'" -trimpath -o dist/linux/console_linux_x64_headless ./cmd/app/main.go + run: CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -tags=noui -ldflags "-s -w -X 'github.com/device-management-toolkit/console/internal/app.Version=${{ needs.prepare.outputs.version }}'" -trimpath -o dist/linux/console_linux_x64_headless ./cmd/app - name: Build Linux arm64 - run: CGO_ENABLED=0 GOOS=linux GOARCH=arm64 go build -ldflags "-s -w -X 'github.com/device-management-toolkit/console/internal/app.Version=${{ needs.prepare.outputs.version }}'" -trimpath -o dist/linux/console_linux_arm64 ./cmd/app/main.go + run: CGO_ENABLED=0 GOOS=linux GOARCH=arm64 go build -ldflags "-s -w -X 'github.com/device-management-toolkit/console/internal/app.Version=${{ needs.prepare.outputs.version }}'" -trimpath -o dist/linux/console_linux_arm64 ./cmd/app - name: Build Linux arm64 headless - run: CGO_ENABLED=0 GOOS=linux GOARCH=arm64 go build -tags=noui -ldflags "-s -w -X 'github.com/device-management-toolkit/console/internal/app.Version=${{ needs.prepare.outputs.version }}'" -trimpath -o dist/linux/console_linux_arm64_headless ./cmd/app/main.go + run: CGO_ENABLED=0 GOOS=linux GOARCH=arm64 go build -tags=noui -ldflags "-s -w -X 'github.com/device-management-toolkit/console/internal/app.Version=${{ needs.prepare.outputs.version }}'" -trimpath -o dist/linux/console_linux_arm64_headless ./cmd/app - name: Build Windows x64 - run: CGO_ENABLED=0 GOOS=windows GOARCH=amd64 go build -ldflags "-s -w -X 'github.com/device-management-toolkit/console/internal/app.Version=${{ needs.prepare.outputs.version }}'" -trimpath -o dist/windows/console_windows_x64.exe ./cmd/app/main.go + run: CGO_ENABLED=0 GOOS=windows GOARCH=amd64 go build -ldflags "-s -w -X 'github.com/device-management-toolkit/console/internal/app.Version=${{ needs.prepare.outputs.version }}'" -trimpath -o dist/windows/console_windows_x64.exe ./cmd/app - name: Build Windows x64 headless - run: CGO_ENABLED=0 GOOS=windows GOARCH=amd64 go build -tags=noui -ldflags "-s -w -X 'github.com/device-management-toolkit/console/internal/app.Version=${{ needs.prepare.outputs.version }}'" -trimpath -o dist/windows/console_windows_x64_headless.exe ./cmd/app/main.go + run: CGO_ENABLED=0 GOOS=windows GOARCH=amd64 go build -tags=noui -ldflags "-s -w -X 'github.com/device-management-toolkit/console/internal/app.Version=${{ needs.prepare.outputs.version }}'" -trimpath -o dist/windows/console_windows_x64_headless.exe ./cmd/app - name: Build macOS arm64 - run: CGO_ENABLED=0 GOOS=darwin GOARCH=arm64 go build -ldflags "-s -w -X 'github.com/device-management-toolkit/console/internal/app.Version=${{ needs.prepare.outputs.version }}'" -trimpath -o dist/darwin/console_mac_arm64 ./cmd/app/main.go + run: CGO_ENABLED=0 GOOS=darwin GOARCH=arm64 go build -ldflags "-s -w -X 'github.com/device-management-toolkit/console/internal/app.Version=${{ needs.prepare.outputs.version }}'" -trimpath -o dist/darwin/console_mac_arm64 ./cmd/app - name: Build macOS arm64 headless - run: CGO_ENABLED=0 GOOS=darwin GOARCH=arm64 go build -tags=noui -ldflags "-s -w -X 'github.com/device-management-toolkit/console/internal/app.Version=${{ needs.prepare.outputs.version }}'" -trimpath -o dist/darwin/console_mac_arm64_headless ./cmd/app/main.go + run: CGO_ENABLED=0 GOOS=darwin GOARCH=arm64 go build -tags=noui -ldflags "-s -w -X 'github.com/device-management-toolkit/console/internal/app.Version=${{ needs.prepare.outputs.version }}'" -trimpath -o dist/darwin/console_mac_arm64_headless ./cmd/app # Cache all build artifacts in a single step - uses: actions/cache@1bd1e32a3bdc45362d1e726936510720a7c30a57 # v4.2.0 @@ -246,7 +246,7 @@ jobs: - name: Generate OpenAPI specification if: steps.semantic-release.outputs.new_release_published == 'true' && steps.check-openapi-changes.outputs.changed == 'true' run: | - GIN_MODE=debug go run ./cmd/app/main.go + GIN_MODE=debug go run ./cmd/app - name: Verify OpenAPI spec was generated run: | diff --git a/Dockerfile b/Dockerfile index 35e57501b..d9292eaa7 100644 --- a/Dockerfile +++ b/Dockerfile @@ -7,14 +7,14 @@ ARG BUILD_TAGS="" # Step 1: Modules caching -FROM golang:1.25.6-alpine@sha256:98e6cffc31ccc44c7c15d83df1d69891efee8115a5bb7ede2bf30a38af3e3c92 AS modules +FROM golang:1.25.7-alpine@sha256:f6751d823c26342f9506c03797d2527668d095b0a15f1862cddb4d927a7a4ced AS modules COPY go.mod go.sum /modules/ WORKDIR /modules RUN apk add --no-cache git RUN go mod download # Step 2: Builder -FROM golang:1.25.6-alpine@sha256:98e6cffc31ccc44c7c15d83df1d69891efee8115a5bb7ede2bf30a38af3e3c92 AS builder +FROM golang:1.25.7-alpine@sha256:f6751d823c26342f9506c03797d2527668d095b0a15f1862cddb4d927a7a4ced AS builder # Build tags control dependencies: # - Default (no tags): Full build with UI # - noui: Excludes web UI assets @@ -37,10 +37,12 @@ RUN mkdir -p /.config/device-management-toolkit # Step 3: Final - Use scratch for all builds (all are fully static with pure Go) FROM scratch ENV TMPDIR=/tmp -COPY --from=builder /app/tmp /tmp -COPY --from=builder /app/config /config +ENV XDG_CONFIG_HOME=/.config +COPY --chown=65534:65534 --from=builder /app/tmp /tmp +COPY --chown=65534:65534 --from=builder /app/config /config COPY --from=builder /app/internal/app/migrations /migrations COPY --from=builder /bin/app /app COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ -COPY --from=builder /.config/device-management-toolkit /.config/device-management-toolkit +COPY --chown=65534:65534 --from=builder /.config/device-management-toolkit /.config/device-management-toolkit +USER 65534:65534 CMD ["/app"] \ No newline at end of file diff --git a/SECURITY.md b/SECURITY.md index 01914f0b8..a4b23c343 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -1,5 +1,5 @@ # Security Policy +Intel is committed to rapidly addressing security vulnerabilities affecting our customers and providing clear guidance on the solution, impact, severity and mitigation. ## Reporting a Vulnerability - -Please report any security vulnerabilities in this project utilizing the Github's guidelines [here](https://docs.github.com/en/code-security/security-advisories/guidance-on-reporting-and-writing/privately-reporting-a-security-vulnerability#privately-reporting-a-security-vulnerability). +Please report any security vulnerabilities in this project utilizing the guidelines [here](https://www.intel.com/content/www/us/en/security-center/vulnerability-handling-guidelines.html). diff --git a/cmd/app/browser.go b/cmd/app/browser.go new file mode 100644 index 000000000..93411a206 --- /dev/null +++ b/cmd/app/browser.go @@ -0,0 +1,57 @@ +//go:build !noui + +package main + +import ( + "context" + "os/exec" + "runtime" + + "github.com/device-management-toolkit/console/config" +) + +func launchBrowser(cfg *config.Config) { + scheme := "http" + if cfg.TLS.Enabled { + scheme = "https" + } + + if err := openBrowser(scheme+"://localhost:"+cfg.Port, runtime.GOOS); err != nil { + panic(err) + } +} + +// CommandExecutor is an interface to allow for mocking exec.Command in tests. +type CommandExecutor interface { + Execute(name string, arg ...string) error +} + +// RealCommandExecutor is a real implementation of CommandExecutor. +type RealCommandExecutor struct{} + +func (e *RealCommandExecutor) Execute(name string, arg ...string) error { + return exec.CommandContext(context.Background(), name, arg...).Start() +} + +// Global command executor, can be replaced in tests. +var cmdExecutor CommandExecutor = &RealCommandExecutor{} + +func openBrowser(url, currentOS string) error { + var cmd string + + var args []string + + switch currentOS { + case "darwin": + cmd = "open" + args = []string{url} + case "windows": + cmd = "cmd" + args = []string{"/c", "start", url} + default: + cmd = "xdg-open" + args = []string{url} + } + + return cmdExecutor.Execute(cmd, args...) +} diff --git a/cmd/app/browser_noui.go b/cmd/app/browser_noui.go new file mode 100644 index 000000000..6396a4136 --- /dev/null +++ b/cmd/app/browser_noui.go @@ -0,0 +1,8 @@ +//go:build noui + +package main + +import "github.com/device-management-toolkit/console/config" + +// launchBrowser is a no-op in noui builds; there is no UI to open. +func launchBrowser(_ *config.Config) {} diff --git a/cmd/app/browser_test.go b/cmd/app/browser_test.go new file mode 100644 index 000000000..c60dde30a --- /dev/null +++ b/cmd/app/browser_test.go @@ -0,0 +1,53 @@ +//go:build !noui + +package main + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" +) + +type MockCommandExecutor struct { + mock.Mock +} + +func (m *MockCommandExecutor) Execute(name string, arg ...string) error { + args := m.Called(name, arg) + + return args.Error(0) +} + +func TestOpenBrowserWindows(t *testing.T) { //nolint:paralleltest // cannot have simultaneous tests modifying executor. + mockCmdExecutor := new(MockCommandExecutor) + cmdExecutor = mockCmdExecutor + + mockCmdExecutor.On("Execute", "cmd", []string{"/c", "start", "http://localhost:8080"}).Return(nil) + + err := openBrowser("http://localhost:8080", "windows") + assert.NoError(t, err) + mockCmdExecutor.AssertExpectations(t) +} + +func TestOpenBrowserDarwin(t *testing.T) { //nolint:paralleltest // cannot have simultaneous tests modifying executor. + mockCmdExecutor := new(MockCommandExecutor) + cmdExecutor = mockCmdExecutor + + mockCmdExecutor.On("Execute", "open", []string{"http://localhost:8080"}).Return(nil) + + err := openBrowser("http://localhost:8080", "darwin") + assert.NoError(t, err) + mockCmdExecutor.AssertExpectations(t) +} + +func TestOpenBrowserLinux(t *testing.T) { //nolint:paralleltest // cannot have simultaneous tests modifying executor. + mockCmdExecutor := new(MockCommandExecutor) + cmdExecutor = mockCmdExecutor + + mockCmdExecutor.On("Execute", "xdg-open", []string{"http://localhost:8080"}).Return(nil) + + err := openBrowser("http://localhost:8080", "ubuntu") + assert.NoError(t, err) + mockCmdExecutor.AssertExpectations(t) +} diff --git a/cmd/app/main.go b/cmd/app/main.go index 8d09b2323..8466dc696 100644 --- a/cmd/app/main.go +++ b/cmd/app/main.go @@ -1,13 +1,10 @@ package main import ( - "context" "errors" "fmt" "log" "os" - "os/exec" - "runtime" "github.com/device-management-toolkit/go-wsman-messages/v2/pkg/security" @@ -30,7 +27,9 @@ var ( var ( initializeConfigFunc = config.NewConfig initializeAppFunc = app.Init - runAppFunc = app.Run + runAppFunc = func(cfg *config.Config, log logger.Interface) { + app.Run(cfg, log) + } // NewGeneratorFunc allows tests to inject a fake OpenAPI generator. NewGeneratorFunc = func(u usecase.Usecases, l logger.Interface) interface { GenerateSpec() ([]byte, error) @@ -63,9 +62,11 @@ func main() { log.Fatalf("CIRA certificate setup error: %s", err) } + l := logger.New(cfg.Level) + handleEncryptionKey(cfg) - handleDebugMode(cfg) - runAppFunc(cfg) + handleDebugMode(cfg, l) + runAppFunc(cfg, l) } func setupCIRACertificates(cfg *config.Config, secretsClient security.Storager) error { @@ -86,29 +87,15 @@ func setupCIRACertificates(cfg *config.Config, secretsClient security.Storager) return nil } -func handleDebugMode(cfg *config.Config) { +func handleDebugMode(cfg *config.Config, l logger.Interface) { if os.Getenv("GIN_MODE") != "debug" { go launchBrowser(cfg) } else { - if err := handleOpenAPIGeneration(); err != nil { - log.Fatalf("Failed to generate OpenAPI spec: %s", err) - } - } -} - -func launchBrowser(cfg *config.Config) { - scheme := "http" - if cfg.TLS.Enabled { - scheme = "https" - } - - if err := openBrowser(scheme+"://localhost:"+cfg.Port, runtime.GOOS); err != nil { - panic(err) + handleOpenAPIGeneration(l) } } -func handleOpenAPIGeneration() error { - l := logger.New("info") +func handleOpenAPIGeneration(l logger.Interface) { usecases := usecase.Usecases{} // Create OpenAPI generator @@ -117,17 +104,19 @@ func handleOpenAPIGeneration() error { // Generate specification spec, err := generator.GenerateSpec() if err != nil { - return err + l.Warn("Failed to generate OpenAPI spec: %s", err) + + return } // Save to file if err := generator.SaveSpec(spec, "doc/openapi.json"); err != nil { - return err - } + l.Warn("Failed to save OpenAPI spec: %s", err) - log.Println("OpenAPI specification generated at doc/openapi.json") + return + } - return nil + l.Info("OpenAPI specification generated at doc/openapi.json") } func handleSecretsConfig(cfg *config.Config) (security.Storager, error) { @@ -299,38 +288,3 @@ func handleKeyNotFound(toolkitCrypto security.Crypto, _, _ security.Storager) st return toolkitCrypto.GenerateKey() } - -// CommandExecutor is an interface to allow for mocking exec.Command in tests. -type CommandExecutor interface { - Execute(name string, arg ...string) error -} - -// RealCommandExecutor is a real implementation of CommandExecutor. -type RealCommandExecutor struct{} - -func (e *RealCommandExecutor) Execute(name string, arg ...string) error { - return exec.CommandContext(context.Background(), name, arg...).Start() -} - -// Global command executor, can be replaced in tests. -var cmdExecutor CommandExecutor = &RealCommandExecutor{} - -func openBrowser(url, currentOS string) error { - var cmd string - - var args []string - - switch currentOS { - case "darwin": - cmd = "open" - args = []string{url} - case "windows": - cmd = "cmd" - args = []string{"/c", "start", url} - default: - cmd = "xdg-open" - args = []string{url} - } - - return cmdExecutor.Execute(cmd, args...) -} diff --git a/cmd/app/main_test.go b/cmd/app/main_test.go index ce75169d5..7f0900ced 100644 --- a/cmd/app/main_test.go +++ b/cmd/app/main_test.go @@ -17,29 +17,19 @@ import ( "github.com/device-management-toolkit/console/pkg/logger" ) -type MockCommandExecutor struct { - mock.Mock -} - -func (m *MockCommandExecutor) Execute(name string, arg ...string) error { - args := m.Called(name, arg) - - return args.Error(0) -} - func TestMainFunction(_ *testing.T) { //nolint:paralleltest // cannot have simultaneous tests modifying env variables. os.Setenv("GIN_MODE", "debug") // Mock functions initializeConfigFunc = func() (*config.Config, error) { - return &config.Config{HTTP: config.HTTP{Port: "8080"}, App: config.App{EncryptionKey: "test"}}, nil + return &config.Config{HTTP: config.HTTP{Port: "8080"}, App: config.App{EncryptionKey: "test"}, Log: config.Log{Level: "info"}}, nil } initializeAppFunc = func(_ *config.Config) error { return nil } - runAppFunc = func(_ *config.Config) {} + runAppFunc = func(_ *config.Config, _ logger.Interface) {} // Mock certificate functions loadOrGenerateRootCertFunc = func(_ security.Storager, _ bool, _, _, _ string, _ bool) (*x509.Certificate, *rsa.PrivateKey, error) { @@ -54,39 +44,6 @@ func TestMainFunction(_ *testing.T) { //nolint:paralleltest // cannot have simul main() } -func TestOpenBrowserWindows(t *testing.T) { //nolint:paralleltest // cannot have simultaneous tests modifying executor. - mockCmdExecutor := new(MockCommandExecutor) - cmdExecutor = mockCmdExecutor - - mockCmdExecutor.On("Execute", "cmd", []string{"/c", "start", "http://localhost:8080"}).Return(nil) - - err := openBrowser("http://localhost:8080", "windows") - assert.NoError(t, err) - mockCmdExecutor.AssertExpectations(t) -} - -func TestOpenBrowserDarwin(t *testing.T) { //nolint:paralleltest // cannot have simultaneous tests modifying executor. - mockCmdExecutor := new(MockCommandExecutor) - cmdExecutor = mockCmdExecutor - - mockCmdExecutor.On("Execute", "open", []string{"http://localhost:8080"}).Return(nil) - - err := openBrowser("http://localhost:8080", "darwin") - assert.NoError(t, err) - mockCmdExecutor.AssertExpectations(t) -} - -func TestOpenBrowserLinux(t *testing.T) { //nolint:paralleltest // cannot have simultaneous tests modifying executor. - mockCmdExecutor := new(MockCommandExecutor) - cmdExecutor = mockCmdExecutor - - mockCmdExecutor.On("Execute", "xdg-open", []string{"http://localhost:8080"}).Return(nil) - - err := openBrowser("http://localhost:8080", "ubuntu") - assert.NoError(t, err) - mockCmdExecutor.AssertExpectations(t) -} - type MockGenerator struct { mock.Mock } @@ -126,8 +83,7 @@ func TestHandleOpenAPIGeneration_Success(t *testing.T) { mockGen.On("GenerateSpec").Return(expectedSpec, nil) mockGen.On("SaveSpec", expectedSpec, "doc/openapi.json").Return(nil) - err := handleOpenAPIGeneration() - assert.NoError(t, err) + handleOpenAPIGeneration(logger.New("info")) mockGen.AssertExpectations(t) } @@ -145,8 +101,7 @@ func TestHandleOpenAPIGeneration_GenerateFails(t *testing.T) { mockGen.On("GenerateSpec").Return([]byte(nil), assert.AnError) - err := handleOpenAPIGeneration() - assert.Error(t, err) + handleOpenAPIGeneration(logger.New("info")) mockGen.AssertExpectations(t) } diff --git a/go.mod b/go.mod index 94a6cb102..523014425 100644 --- a/go.mod +++ b/go.mod @@ -8,7 +8,7 @@ require ( github.com/Masterminds/squirrel v1.5.4 github.com/coreos/go-oidc/v3 v3.17.0 github.com/getkin/kin-openapi v0.133.0 - github.com/device-management-toolkit/go-wsman-messages/v2 v2.36.1 + github.com/device-management-toolkit/go-wsman-messages/v2 v2.36.2 github.com/gin-contrib/cors v1.7.6 github.com/gin-contrib/pprof v1.5.3 github.com/gin-gonic/gin v1.11.0 @@ -24,9 +24,10 @@ require ( github.com/prometheus/client_golang v1.23.2 github.com/rs/zerolog v1.34.0 github.com/stretchr/testify v1.11.1 + github.com/zsais/go-gin-prometheus v1.0.3 go.uber.org/mock v0.6.0 gopkg.in/yaml.v2 v2.4.0 - modernc.org/sqlite v1.44.3 + modernc.org/sqlite v1.46.1 software.sslmate.com/src/go-pkcs12 v0.7.0 ) @@ -49,7 +50,6 @@ require ( github.com/hashicorp/go-secure-stdlib/strutil v0.1.2 // indirect github.com/hashicorp/go-sockaddr v1.0.7 // indirect github.com/hashicorp/hcl v1.0.1-vault-7 // indirect - github.com/kr/text v0.2.0 // indirect github.com/mitchellh/go-homedir v1.1.0 // indirect github.com/mitchellh/mapstructure v1.5.0 // indirect github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 // indirect @@ -109,7 +109,7 @@ require ( github.com/prometheus/common v0.66.1 // indirect github.com/prometheus/procfs v0.16.1 // indirect github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect - github.com/sirupsen/logrus v1.9.3 // indirect + github.com/sirupsen/logrus v1.9.4 // indirect github.com/stretchr/objx v0.5.2 // indirect github.com/twitchyliquid64/golang-asm v0.15.1 // indirect github.com/ugorji/go/codec v1.3.0 // indirect diff --git a/go.sum b/go.sum index dfe1cda05..7bbce51a1 100644 --- a/go.sum +++ b/go.sum @@ -32,15 +32,14 @@ github.com/containerd/errdefs/pkg v0.3.0/go.mod h1:NJw6s9HwNuRhnjJhM7pylWwMyAkmC github.com/coreos/go-oidc/v3 v3.17.0 h1:hWBGaQfbi0iVviX4ibC7bk8OKT5qNr4klBaCHVNvehc= github.com/coreos/go-oidc/v3 v3.17.0/go.mod h1:wqPbKFrVnE90vty060SB40FCJ8fTHTxSwyXJqZH+sI8= github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= -github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/danieljoos/wincred v1.2.2 h1:774zMFJrqaeYCK2W57BgAem/MLi6mtSE47MB6BOJ0i0= github.com/danieljoos/wincred v1.2.2/go.mod h1:w7w4Utbrz8lqeMbDAK0lkNJUv5sAOkFi7nd/ogr0Uh8= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/device-management-toolkit/go-wsman-messages/v2 v2.36.1 h1:tNl4nbfcfrmhjgngAEhyUVQRp4Y6sXqaMaLISCl9VVY= -github.com/device-management-toolkit/go-wsman-messages/v2 v2.36.1/go.mod h1:qlR1/nrV6QPse0918YSJ2bjMY8VNkm6qnNU0ioqQEsQ= +github.com/device-management-toolkit/go-wsman-messages/v2 v2.36.2 h1:kzIogc4os3TAJ8hxMZRj8GlYaFZE5ux2w3bFLPiLjX8= +github.com/device-management-toolkit/go-wsman-messages/v2 v2.36.2/go.mod h1:UZyI13zG1c1NK7b10ilHNIqDqZVk+0hNRcNqZozk2H0= github.com/dhui/dktest v0.4.6 h1:+DPKyScKSEp3VLtbMDHcUq6V5Lm5zfZZVb0Sk7Ahom4= github.com/dhui/dktest v0.4.6/go.mod h1:JHTSYDtKkvFNFHJKqCzVzqXecyv+tKt8EzceOmQOgbU= github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk= @@ -211,10 +210,10 @@ github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A= github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= -github.com/oapi-codegen/runtime v1.1.2 h1:P2+CubHq8fO4Q6fV1tqDBZHCwpVpvPg7oKiYzQgXIyI= -github.com/oapi-codegen/runtime v1.1.2/go.mod h1:SK9X900oXmPWilYR5/WKPzt3Kqxn/uS/+lbpREv+eCg= github.com/ncruces/go-strftime v1.0.0 h1:HMFp8mLCTPp341M/ZnA4qaf7ZlsbTc+miZjCLOFAw7w= github.com/ncruces/go-strftime v1.0.0/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls= +github.com/oapi-codegen/runtime v1.1.2 h1:P2+CubHq8fO4Q6fV1tqDBZHCwpVpvPg7oKiYzQgXIyI= +github.com/oapi-codegen/runtime v1.1.2/go.mod h1:SK9X900oXmPWilYR5/WKPzt3Kqxn/uS/+lbpREv+eCg= github.com/oasdiff/yaml v0.0.0-20250309154309-f31be36b4037 h1:G7ERwszslrBzRxj//JalHPu/3yz+De2J+4aLtSRlHiY= github.com/oasdiff/yaml v0.0.0-20250309154309-f31be36b4037/go.mod h1:2bpvgLBZEtENV5scfDFEtB/5+1M4hkQhDQrccEJ/qGw= github.com/oasdiff/yaml3 v0.0.0-20250309153720-d2182401db90 h1:bQx3WeLcUWy+RletIKwUIt4x3t8n2SxavmoclizMb8c= @@ -253,8 +252,9 @@ github.com/rs/zerolog v1.34.0 h1:k43nTLIwcTVQAncfCw4KZ2VY6ukYoZaBPNOE8txlOeY= github.com/rs/zerolog v1.34.0/go.mod h1:bJsvje4Z08ROH4Nhs5iH600c3IkWhwp44iRc54W6wYQ= github.com/ryanuber/go-glob v1.0.0 h1:iQh3xXAumdQ+4Ufa5b25cRpC5TYKlno6hsv6Cb3pkBk= github.com/ryanuber/go-glob v1.0.0/go.mod h1:807d1WSdnB0XRJzKNil9Om6lcp/3a0v4qIHxIXzX/Yc= -github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= -github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= +github.com/sirupsen/logrus v1.9.4 h1:TsZE7l11zFCLZnZ+teH4Umoq5BhEIfIzfRDZ1Uzql2w= +github.com/sirupsen/logrus v1.9.4/go.mod h1:ftWc9WdOfJ0a92nsE2jF5u5ZwH8Bv2zdeOC42RjbV2g= +github.com/spkg/bom v0.0.0-20160624110644-59b7046e48ad h1:fiWzISvDn0Csy5H0iwgAuJGQTUpVfEMJJd4nRFXogbc= github.com/spkg/bom v0.0.0-20160624110644-59b7046e48ad/go.mod h1:qLr4V1qq6nMqFKkMo8ZTx3f+BZEkzsRUY10Xsm2mwU0= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= @@ -283,6 +283,8 @@ github.com/woodsbury/decimal128 v1.4.0 h1:xJATj7lLu4f2oObouMt2tgGiElE5gO6mSWUjQs github.com/woodsbury/decimal128 v1.4.0/go.mod h1:BP46FUrVjVhdTbKT+XuQh2xfQaGki9LMIRJSFuh6THU= github.com/zalando/go-keyring v0.2.6 h1:r7Yc3+H+Ux0+M72zacZoItR3UDxeWfKTcabvkI8ua9s= github.com/zalando/go-keyring v0.2.6/go.mod h1:2TCrxYrbUNYfNS/Kgy/LSrkSQzZ5UPVH85RwfczwvcI= +github.com/zsais/go-gin-prometheus v1.0.3 h1:NIYXItaoGNiyDWXqrIzfQHWcRnen+iwgAw4sX/UieiM= +github.com/zsais/go-gin-prometheus v1.0.3/go.mod h1:avQI7yOKIhpOi4QJxFZdmZb47AEjmS4MTC4Z6PsNmiA= go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 h1:F7Jx+6hwnZ41NSFTO5q4LYDtJRXBf2PD0rNBkeB/lus= @@ -313,7 +315,6 @@ golang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI= golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU= golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= -golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -357,8 +358,8 @@ modernc.org/opt v0.1.4 h1:2kNGMRiUjrp4LcaPuLY2PzUfqM/w9N23quVwhKt5Qm8= modernc.org/opt v0.1.4/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns= modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w= modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE= -modernc.org/sqlite v1.44.3 h1:+39JvV/HWMcYslAwRxHb8067w+2zowvFOUrOWIy9PjY= -modernc.org/sqlite v1.44.3/go.mod h1:CzbrU2lSB1DKUusvwGz7rqEKIq+NUd8GWuBBZDs9/nA= +modernc.org/sqlite v1.46.1 h1:eFJ2ShBLIEnUWlLy12raN0Z1plqmFX9Qe3rjQTKt6sU= +modernc.org/sqlite v1.46.1/go.mod h1:CzbrU2lSB1DKUusvwGz7rqEKIq+NUd8GWuBBZDs9/nA= modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0= modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A= modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y= diff --git a/integration-test/collections/console_mps_apis.postman_collection.json b/integration-test/collections/console_mps_apis.postman_collection.json index 2f5fb3dff..2dcb982e2 100644 --- a/integration-test/collections/console_mps_apis.postman_collection.json +++ b/integration-test/collections/console_mps_apis.postman_collection.json @@ -515,6 +515,70 @@ }, "response": [] }, + { + "name": "Send Advanced Power Action - EnforceSecureBoot false in CCM", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// This test validates the CCM restriction for EnforceSecureBoot\r", + "// When a device is in Client Control Mode (CCM), EnforceSecureBoot cannot be turned off\r", + "// Expected: 400 Bad Request with error message about CCM restriction\r", + "// Note: This test requires a device in CCM mode to properly validate\r", + "\r", + "pm.test(\"Status code is 400 or 404\", function () {\r", + " // 400 = CCM restriction error (expected for CCM device)\r", + " // 404 = Device not found (if no device configured)\r", + " pm.expect(pm.response.code).to.be.oneOf([400, 404]);\r", + "});\r", + "\r", + "pm.test(\"Response contains appropriate error message\", function () {\r", + " var jsonData = pm.response.json();\r", + " if (pm.response.code === 400) {\r", + " pm.expect(jsonData.error).to.include(\"EnforceSecureBoot\");\r", + " pm.expect(jsonData.error).to.include(\"CCM\");\r", + " } else if (pm.response.code === 404) {\r", + " pm.expect(jsonData.error).to.eq(\"Error not found\");\r", + " }\r", + "});\r", + "" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\r\n \"action\": 105,\r\n \"useSOL\": false,\r\n \"bootDetails\": {\r\n \"url\": \"https://example.com/boot.efi\",\r\n \"enforceSecureBoot\": false\r\n }\r\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{protocol}}://{{host}}/api/v1/amt/power/bootoptions/{{deviceId}}", + "protocol": "{{protocol}}", + "host": [ + "{{host}}" + ], + "path": [ + "api", + "v1", + "amt", + "power", + "bootoptions", + "{{deviceId}}" + ] + } + }, + "response": [] + }, { "name": "Get AMT Features", "event": [ diff --git a/internal/app/app.go b/internal/app/app.go index de0ec8a3b..c2c770afd 100644 --- a/internal/app/app.go +++ b/internal/app/app.go @@ -32,8 +32,7 @@ var CertStore security.Storager var Version = "DEVELOPMENT" // Run creates objects via constructors. -func Run(cfg *config.Config) { - log := logger.New(cfg.Level) +func Run(cfg *config.Config, log logger.Interface) { cfg.Version = Version log.Info("app - Run - version: " + cfg.Version) // route standard and Gin logs through our JSON logger diff --git a/internal/app/app_test.go b/internal/app/app_test.go index 8a52daf2b..11358a93e 100644 --- a/internal/app/app_test.go +++ b/internal/app/app_test.go @@ -9,6 +9,7 @@ import ( "github.com/device-management-toolkit/console/config" "github.com/device-management-toolkit/console/internal/app" "github.com/device-management-toolkit/console/internal/mocks" + "github.com/device-management-toolkit/console/pkg/logger" ) func TestRun(t *testing.T) { @@ -45,7 +46,7 @@ func TestRun(t *testing.T) { cfg: cfg, expectFunc: func(_ *testing.T) { go func() { - app.Run(cfg) + app.Run(cfg, logger.New("info")) }() }, }, diff --git a/internal/controller/httpapi/router.go b/internal/controller/httpapi/router.go index 68652847e..a49871f09 100644 --- a/internal/controller/httpapi/router.go +++ b/internal/controller/httpapi/router.go @@ -6,6 +6,7 @@ import ( "github.com/gin-gonic/gin" "github.com/prometheus/client_golang/prometheus/promhttp" + ginprometheus "github.com/zsais/go-gin-prometheus" "github.com/device-management-toolkit/console/config" v1 "github.com/device-management-toolkit/console/internal/controller/httpapi/v1" @@ -28,6 +29,13 @@ func NewRouter(handler *gin.Engine, l logger.Interface, t usecase.Usecases, cfg l.Fatal("Failed to initialize redfish: " + err.Error()) } + // Add Prometheus middleware for automatic HTTP metrics + // Don't automatically register /metrics endpoint - we have our own + p := ginprometheus.NewPrometheus("gin") + p.MetricsPath = "" + // Use middleware function directly without calling Use() which would register conflicting routes + handler.Use(p.HandlerFunc()) + // Initialize Fuego adapter fuegoAdapter := openapi.NewFuegoAdapter(t, l) fuegoAdapter.RegisterRoutes() diff --git a/internal/controller/httpapi/v1/error.go b/internal/controller/httpapi/v1/error.go index 218891d78..8fdb80fb4 100644 --- a/internal/controller/httpapi/v1/error.go +++ b/internal/controller/httpapi/v1/error.go @@ -29,6 +29,7 @@ func ErrorResponse(c *gin.Context, err error) { NotUniqueErr sqldb.NotUniqueError amtErr devices.AMTError notSupportedErr devices.NotSupportedError + validationErr devices.ValidationError certExpErr domains.CertExpirationError certPasswordErr domains.CertPasswordError netErr net.Error @@ -49,6 +50,9 @@ func ErrorResponse(c *gin.Context, err error) { dbErrorHandle(c, dbErr) case errors.As(err, &amtErr): amtErrorHandle(c, amtErr) + case errors.As(err, &validationErr): + msg := validationErr.Console.FriendlyMessage() + c.AbortWithStatusJSON(http.StatusBadRequest, response{Error: msg, Message: msg}) case errors.As(err, ¬SupportedErr): msg := notSupportedErr.Console.FriendlyMessage() c.AbortWithStatusJSON(http.StatusNotImplemented, response{Error: msg, Message: msg}) diff --git a/internal/controller/httpapi/v1/login.go b/internal/controller/httpapi/v1/login.go index 5b0bdb46e..630333ad7 100644 --- a/internal/controller/httpapi/v1/login.go +++ b/internal/controller/httpapi/v1/login.go @@ -2,6 +2,8 @@ package v1 import ( "context" + "errors" + "fmt" "net/http" "strings" "time" @@ -15,7 +17,10 @@ import ( "github.com/device-management-toolkit/console/pkg/consoleerrors" ) -var ErrLogin = consoleerrors.CreateConsoleError("LoginHandler") +var ( + ErrLogin = consoleerrors.CreateConsoleError("LoginHandler") + ErrUnexpectedSigningMethod = errors.New("unexpected signing method") +) type LoginRoute struct { Config *config.Config @@ -99,11 +104,17 @@ func (lr LoginRoute) JWTAuthMiddleware() gin.HandlerFunc { if err != nil { c.JSON(http.StatusUnauthorized, gin.H{"error": "invalid access token"}) c.Abort() + + return } } else { claims := &jwt.MapClaims{} - token, err := jwt.ParseWithClaims(tokenString, claims, func(_ *jwt.Token) (interface{}, error) { + token, err := jwt.ParseWithClaims(tokenString, claims, func(token *jwt.Token) (interface{}, error) { + if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok { + return nil, fmt.Errorf("%w: %v", ErrUnexpectedSigningMethod, token.Header["alg"]) + } + return []byte(lr.Config.JWTKey), nil }) diff --git a/internal/controller/httpapi/v1/profiles.go b/internal/controller/httpapi/v1/profiles.go index 958c99284..442815e49 100644 --- a/internal/controller/httpapi/v1/profiles.go +++ b/internal/controller/httpapi/v1/profiles.go @@ -27,6 +27,7 @@ func NewProfileRoutes(handler *gin.RouterGroup, t profiles.Feature, l logger.Int if v, ok := binding.Validator.Engine().(*validator.Validate); ok { _ = v.RegisterValidation("genpasswordwone", dto.ValidateAMTPassOrGenRan) _ = v.RegisterValidation("ciraortls", dto.ValidateCIRAOrTLS) + _ = v.RegisterValidation("wifidhcp", dto.ValidateWiFiDHCP) } } diff --git a/internal/controller/tcp/cira/tunnel.go b/internal/controller/tcp/cira/tunnel.go index 57fcc4f2f..fe308dfbb 100644 --- a/internal/controller/tcp/cira/tunnel.go +++ b/internal/controller/tcp/cira/tunnel.go @@ -28,6 +28,7 @@ const ( weakCipherSuiteCount = 3 keepAliveInterval = 30 keepAliveTimeout = 90 + apfSessionTimeout = 3 * time.Second ) var ( @@ -145,8 +146,10 @@ func (s *Server) handleConnection(conn net.Conn) { conn: conn, tlsConn: tlsConn, handler: NewAPFHandler(s.devices, s.log), - session: &apf.Session{}, - log: s.log, + session: &apf.Session{ + Timer: time.NewTimer(apfSessionTimeout), + }, + log: s.log, } ctx.processor = apf.NewProcessor(ctx.handler) @@ -162,6 +165,17 @@ func (ctx *connectionContext) cleanup() { delete(wsman.Connections, deviceID) mu.Unlock() } + + // Stop and clean up the session timer + if ctx.session != nil && ctx.session.Timer != nil { + if !ctx.session.Timer.Stop() { + // Drain the channel if the timer fired + select { + case <-ctx.session.Timer.C: + default: + } + } + } } func (s *Server) processConnection(ctx *connectionContext) { diff --git a/internal/controller/tcp/cira/tunnel_test.go b/internal/controller/tcp/cira/tunnel_test.go new file mode 100644 index 000000000..ccafa2aba --- /dev/null +++ b/internal/controller/tcp/cira/tunnel_test.go @@ -0,0 +1,183 @@ +package cira + +import ( + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/device-management-toolkit/go-wsman-messages/v2/pkg/apf" + + "github.com/device-management-toolkit/console/internal/usecase/devices/wsman" + "github.com/device-management-toolkit/console/pkg/logger" +) + +type cleanupTestCase struct { + name string + setupSession func() *apf.Session + authenticated bool + deviceID string + wantPanic bool + wantTimerStopped bool +} + +var cleanupTests = []cleanupTestCase{ + { + name: "cleanup with nil session", + setupSession: func() *apf.Session { return nil }, + authenticated: false, + deviceID: "", + wantPanic: false, + wantTimerStopped: false, + }, + { + name: "cleanup with session but nil timer", + setupSession: func() *apf.Session { + return &apf.Session{Timer: nil} + }, + authenticated: false, + deviceID: "", + wantPanic: false, + wantTimerStopped: false, + }, + { + name: "cleanup with timer that stops successfully", + setupSession: func() *apf.Session { + return &apf.Session{Timer: time.NewTimer(1 * time.Hour)} + }, + authenticated: false, + deviceID: "", + wantPanic: false, + wantTimerStopped: true, + }, + { + name: "cleanup with timer that fails to stop should drain channel", + setupSession: func() *apf.Session { + timer := time.NewTimer(1 * time.Nanosecond) + + time.Sleep(2 * time.Millisecond) + + return &apf.Session{Timer: timer} + }, + authenticated: false, + deviceID: "", + wantPanic: false, + wantTimerStopped: false, + }, + { + name: "cleanup with timer stop failure and empty channel hits default case", + setupSession: func() *apf.Session { + timer := time.NewTimer(100 * time.Nanosecond) + + time.Sleep(2 * time.Millisecond) + + select { + case <-timer.C: + default: + } + + return &apf.Session{Timer: timer} + }, + authenticated: false, + deviceID: "", + wantPanic: false, + wantTimerStopped: false, + }, + { + name: "cleanup with authenticated connection removes from connections map", + setupSession: func() *apf.Session { + return &apf.Session{Timer: time.NewTimer(10 * time.Second)} + }, + authenticated: true, + deviceID: "test-device", + wantPanic: false, + wantTimerStopped: true, + }, +} + +func TestConnectionContext_cleanup(t *testing.T) { + t.Parallel() + + for _, tt := range cleanupTests { + tt := tt // capture range variable + + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + runCleanupTest(t, tt) + }) + } +} + +func runCleanupTest(t *testing.T, tt cleanupTestCase) { + t.Helper() + + // Setup + session := tt.setupSession() + ctx := setupConnectionContext(t, session, tt.authenticated, tt.deviceID) + + setupConnectionsMap(t, tt.authenticated, tt.deviceID) + + // Execute + require.NotPanics(t, func() { + ctx.cleanup() + }) + + // Verify + verifyTimerState(t, session, tt.wantTimerStopped) + verifyConnectionRemoved(t, tt.authenticated, tt.deviceID) +} + +func setupConnectionContext(t *testing.T, session *apf.Session, authenticated bool, deviceID string) *connectionContext { + t.Helper() + + // Create a proper APFHandler with mock deviceID + log := logger.New("error") + handler := NewAPFHandler(nil, log) // devices.Feature can be nil for cleanup test + handler.deviceID = deviceID // Set deviceID directly for test + + return &connectionContext{ + session: session, + authenticated: authenticated, + handler: handler, + } +} + +func setupConnectionsMap(t *testing.T, authenticated bool, deviceID string) { + t.Helper() + + if authenticated && deviceID != "" { + mu.Lock() + + wsman.Connections[deviceID] = &wsman.ConnectionEntry{} + + mu.Unlock() + } +} + +func verifyTimerState(t *testing.T, session *apf.Session, wantTimerStopped bool) { + t.Helper() + + if wantTimerStopped && session != nil && session.Timer != nil { + select { + case <-session.Timer.C: + // Timer was stopped and channel was drained, or timer expired naturally + default: + // Timer was stopped before it could fire + } + } +} + +func verifyConnectionRemoved(t *testing.T, authenticated bool, deviceID string) { + t.Helper() + + if authenticated && deviceID != "" { + mu.Lock() + + _, exists := wsman.Connections[deviceID] + + mu.Unlock() + + assert.False(t, exists, "Connection should be removed from map") + } +} diff --git a/internal/controller/ws/v1/redirect.go b/internal/controller/ws/v1/redirect.go index d917bb9b2..c197062f8 100644 --- a/internal/controller/ws/v1/redirect.go +++ b/internal/controller/ws/v1/redirect.go @@ -2,7 +2,10 @@ package v1 import ( "compress/flate" + "errors" + "fmt" "net/http" + "time" "github.com/gin-gonic/gin" "github.com/golang-jwt/jwt/v5" @@ -13,6 +16,8 @@ import ( "github.com/device-management-toolkit/console/pkg/logger" ) +var ErrUnexpectedSigningMethod = errors.New("unexpected signing method") + type RedirectRoutes struct { d devices.Feature l logger.Interface @@ -41,7 +46,11 @@ func (r *RedirectRoutes) websocketHandler(c *gin.Context) { claims := &jwt.MapClaims{} - token, err := jwt.ParseWithClaims(tokenString, claims, func(_ *jwt.Token) (interface{}, error) { + token, err := jwt.ParseWithClaims(tokenString, claims, func(token *jwt.Token) (interface{}, error) { + if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok { + return nil, fmt.Errorf("%w: %v", ErrUnexpectedSigningMethod, token.Header["alg"]) + } + return []byte(config.ConsoleConfig.JWTKey), nil }) @@ -59,7 +68,13 @@ func (r *RedirectRoutes) websocketHandler(c *gin.Context) { upgrader.Subprotocols = []string{tokenString} } + // KVM_TIMING: Measure WebSocket upgrade duration + upgradeStart := time.Now() conn, err := r.u.Upgrade(c.Writer, c.Request, nil) + upgradeDuration := time.Since(upgradeStart) + devices.RecordWebsocketUpgrade(upgradeDuration) + r.l.Debug("KVM_TIMING: WebSocket upgrade", "duration_ms", upgradeDuration.Milliseconds()) + if err != nil { http.Error(c.Writer, "Could not open websocket connection", http.StatusInternalServerError) @@ -77,7 +92,13 @@ func (r *RedirectRoutes) websocketHandler(c *gin.Context) { r.l.Info("Websocket connection opened") + // KVM_TIMING: Measure total connection time + totalStart := time.Now() err = r.d.Redirect(c, conn, c.Query("host"), c.Query("mode")) + totalDuration := time.Since(totalStart) + devices.RecordTotalConnection(totalDuration, c.Query("mode")) + r.l.Debug("KVM_TIMING: Total connection time", "duration_ms", totalDuration.Milliseconds(), "mode", c.Query("mode")) + if err != nil { r.l.Error(err, "http - devices - v1 - redirect") errorResponse(c, http.StatusInternalServerError, "redirect failed") diff --git a/internal/controller/ws/v1/redirect_test.go b/internal/controller/ws/v1/redirect_test.go index 5a1db8239..e942a0080 100644 --- a/internal/controller/ws/v1/redirect_test.go +++ b/internal/controller/ws/v1/redirect_test.go @@ -65,21 +65,26 @@ func TestWebSocketHandler(t *testing.T) { //nolint:paralleltest // logging libra Upgrade(gomock.Any(), gomock.Any(), nil). Return(nil, tc.upgraderError) mockLogger.EXPECT().Debug("failed to cast Upgrader to *websocket.Upgrader") + mockLogger.EXPECT().Debug("KVM_TIMING: WebSocket upgrade", "duration_ms", gomock.Any()) } else { mockUpgrader.EXPECT(). Upgrade(gomock.Any(), gomock.Any(), nil). Return(&websocket.Conn{}, nil) mockLogger.EXPECT().Debug("failed to cast Upgrader to *websocket.Upgrader") + mockLogger.EXPECT().Debug("KVM_TIMING: WebSocket upgrade", "duration_ms", gomock.Any()) mockLogger.EXPECT().Info("Websocket connection opened") - if tc.redirectError != nil { - mockLogger.EXPECT().Error(tc.redirectError, "http - devices - v1 - redirect") - } - mockFeature.EXPECT(). Redirect(gomock.Any(), gomock.Any(), "someHost", "someMode"). Return(tc.redirectError) + + // Total connection time is always logged after Redirect completes + mockLogger.EXPECT().Debug("KVM_TIMING: Total connection time", "duration_ms", gomock.Any(), "mode", "someMode") + + if tc.redirectError != nil { + mockLogger.EXPECT().Error(tc.redirectError, "http - devices - v1 - redirect") + } } r := gin.Default() diff --git a/internal/entity/dto/v1/bootsetting.go b/internal/entity/dto/v1/bootsetting.go index 65eee4715..1736f7233 100644 --- a/internal/entity/dto/v1/bootsetting.go +++ b/internal/entity/dto/v1/bootsetting.go @@ -5,7 +5,7 @@ type BootDetails struct { Username string `json:"username" example:"admin"` Password string `json:"password" example:"password"` BootPath string `json:"bootPath" example:"\\OemPba.efi"` - EnforceSecureBoot bool `json:"enforceSecureBoot" example:"true"` + EnforceSecureBoot *bool `json:"enforceSecureBoot,omitempty" example:"true"` } type BootSetting struct { diff --git a/internal/entity/dto/v1/profile.go b/internal/entity/dto/v1/profile.go index 2b8376414..8c371f3b0 100644 --- a/internal/entity/dto/v1/profile.go +++ b/internal/entity/dto/v1/profile.go @@ -21,7 +21,7 @@ type Profile struct { DHCPEnabled bool `json:"dhcpEnabled" example:"true"` IPSyncEnabled bool `json:"ipSyncEnabled" example:"true"` LocalWiFiSyncEnabled bool `json:"localWifiSyncEnabled" example:"true"` - WiFiConfigs []ProfileWiFiConfigs `json:"wifiConfigs,omitempty" binding:"excluded_if=DHCPEnabled false,dive"` + WiFiConfigs []ProfileWiFiConfigs `json:"wifiConfigs,omitempty" binding:"wifidhcp,dive"` TenantID string `json:"tenantId" example:"abc123"` TLSMode int `json:"tlsMode,omitempty" binding:"omitempty,min=1,max=4,ciraortls" example:"1"` TLSCerts *TLSCerts `json:"tlsCerts,omitempty"` @@ -66,6 +66,18 @@ var ValidateUserConsent validator.Func = func(fl validator.FieldLevel) bool { return userConsent == "none" || userConsent == "kvm" || userConsent == "all" } +var ValidateWiFiDHCP validator.Func = func(fl validator.FieldLevel) bool { + dhcpEnabled := fl.Parent().FieldByName("DHCPEnabled").Bool() + wifiConfigs := fl.Field() + + // If WiFiConfigs has items and DHCP is disabled, fail validation + if wifiConfigs.Len() > 0 && !dhcpEnabled { + return false + } + + return true +} + type ProfileCountResponse struct { Count int `json:"totalCount"` Data []Profile `json:"data"` diff --git a/internal/entity/dto/v1/profile_test.go b/internal/entity/dto/v1/profile_test.go new file mode 100644 index 000000000..42d03190c --- /dev/null +++ b/internal/entity/dto/v1/profile_test.go @@ -0,0 +1,123 @@ +package dto + +import ( + "testing" + + "github.com/go-playground/validator/v10" + "github.com/stretchr/testify/assert" +) + +func TestValidateWiFiDHCP(t *testing.T) { + t.Parallel() + + validate := validator.New() + err := validate.RegisterValidation("wifidhcp", ValidateWiFiDHCP) + assert.NoError(t, err) + + tests := []struct { + name string + dhcpEnabled bool + wifiConfigs []ProfileWiFiConfigs + wantErr bool + }{ + { + name: "valid with WiFi configs and DHCP enabled", + dhcpEnabled: true, + wifiConfigs: []ProfileWiFiConfigs{ + { + Priority: 1, + WirelessProfileName: "MyWiFiProfile", + ProfileName: "MyProfile", + TenantID: "tenant1", + }, + }, + wantErr: false, + }, + { + name: "invalid with WiFi configs and DHCP disabled", + dhcpEnabled: false, + wifiConfigs: []ProfileWiFiConfigs{ + { + Priority: 1, + WirelessProfileName: "MyWiFiProfile", + ProfileName: "MyProfile", + TenantID: "tenant1", + }, + }, + wantErr: true, + }, + { + name: "valid with no WiFi configs and DHCP enabled", + dhcpEnabled: true, + wifiConfigs: []ProfileWiFiConfigs{}, + wantErr: false, + }, + { + name: "valid with no WiFi configs and DHCP disabled", + dhcpEnabled: false, + wifiConfigs: []ProfileWiFiConfigs{}, + wantErr: false, + }, + { + name: "valid with multiple WiFi configs and DHCP enabled", + dhcpEnabled: true, + wifiConfigs: []ProfileWiFiConfigs{ + { + Priority: 1, + WirelessProfileName: "WiFiProfile1", + ProfileName: "Profile1", + TenantID: "tenant1", + }, + { + Priority: 2, + WirelessProfileName: "WiFiProfile2", + ProfileName: "Profile2", + TenantID: "tenant1", + }, + }, + wantErr: false, + }, + { + name: "invalid with multiple WiFi configs and DHCP disabled", + dhcpEnabled: false, + wifiConfigs: []ProfileWiFiConfigs{ + { + Priority: 1, + WirelessProfileName: "WiFiProfile1", + ProfileName: "Profile1", + TenantID: "tenant1", + }, + { + Priority: 2, + WirelessProfileName: "WiFiProfile2", + ProfileName: "Profile2", + TenantID: "tenant1", + }, + }, + wantErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + type testStruct struct { + DHCPEnabled bool `validate:"omitempty"` + WiFiConfigs []ProfileWiFiConfigs `validate:"wifidhcp"` + } + + s := testStruct{ + DHCPEnabled: tt.dhcpEnabled, + WiFiConfigs: tt.wifiConfigs, + } + err := validate.Struct(s) + + if tt.wantErr { + assert.Error(t, err) + } else { + assert.NoError(t, err) + } + }) + } +} diff --git a/internal/mocks/devicemanagement_mocks.go b/internal/mocks/devicemanagement_mocks.go index 79b05eecf..ee3694979 100644 --- a/internal/mocks/devicemanagement_mocks.go +++ b/internal/mocks/devicemanagement_mocks.go @@ -237,11 +237,12 @@ func (mr *MockRedirectionMockRecorder) RedirectSend(ctx, deviceConnection, messa } // SetupWsmanClient mocks base method. -func (m *MockRedirection) SetupWsmanClient(device entity.Device, isRedirection, logMessages bool) wsman0.Messages { +func (m *MockRedirection) SetupWsmanClient(device entity.Device, isRedirection, logMessages bool) (wsman0.Messages, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "SetupWsmanClient", device, isRedirection, logMessages) ret0, _ := ret[0].(wsman0.Messages) - return ret0 + ret1, _ := ret[1].(error) + return ret0, ret1 } // SetupWsmanClient indicates an expected call of SetupWsmanClient. diff --git a/internal/usecase/amtexplorer/wsman.go b/internal/usecase/amtexplorer/wsman.go index fecb17ab0..460a56047 100644 --- a/internal/usecase/amtexplorer/wsman.go +++ b/internal/usecase/amtexplorer/wsman.go @@ -54,7 +54,12 @@ func (g GoWSMANMessages) SetupWsmanClient(device entity.Device, logAMTMessages b clientParams.PinnedCert = *device.CertHash } - clientParams.Password, _ = g.safeRequirements.Decrypt(device.Password) + decryptedPassword, err := g.safeRequirements.Decrypt(device.Password) + if err != nil { + return nil, err + } + + clientParams.Password = decryptedPassword connectionsMu.Lock() defer connectionsMu.Unlock() diff --git a/internal/usecase/ciraconfigs/usecase.go b/internal/usecase/ciraconfigs/usecase.go index c58cc0cb1..6f05cc5c0 100644 --- a/internal/usecase/ciraconfigs/usecase.go +++ b/internal/usecase/ciraconfigs/usecase.go @@ -89,7 +89,10 @@ func (uc *UseCase) Delete(ctx context.Context, configName, tenantID string) erro } func (uc *UseCase) Update(ctx context.Context, d *dto.CIRAConfig) (*dto.CIRAConfig, error) { - d1 := uc.dtoToEntity(d) + d1, err := uc.dtoToEntity(d) + if err != nil { + return nil, err + } updated, err := uc.repo.Update(ctx, d1) if err != nil { @@ -111,9 +114,12 @@ func (uc *UseCase) Update(ctx context.Context, d *dto.CIRAConfig) (*dto.CIRAConf } func (uc *UseCase) Insert(ctx context.Context, d *dto.CIRAConfig) (*dto.CIRAConfig, error) { - d1 := uc.dtoToEntity(d) + d1, err := uc.dtoToEntity(d) + if err != nil { + return nil, err + } - _, err := uc.repo.Insert(ctx, d1) + _, err = uc.repo.Insert(ctx, d1) if err != nil { return nil, ErrDatabase.Wrap("Insert", "uc.repo.Insert", err) } @@ -129,7 +135,7 @@ func (uc *UseCase) Insert(ctx context.Context, d *dto.CIRAConfig) (*dto.CIRAConf } // convert dto.CIRAConfig to entity.CIRAConfig. -func (uc *UseCase) dtoToEntity(d *dto.CIRAConfig) *entity.CIRAConfig { +func (uc *UseCase) dtoToEntity(d *dto.CIRAConfig) (*entity.CIRAConfig, error) { d1 := &entity.CIRAConfig{ ConfigName: d.ConfigName, MPSAddress: d.MPSAddress, @@ -145,10 +151,15 @@ func (uc *UseCase) dtoToEntity(d *dto.CIRAConfig) *entity.CIRAConfig { GenerateRandomPassword: d.GenerateRandomPassword, Version: d.Version, } + + var err error // Encrypt password before storing - d1.Password, _ = uc.safeRequirements.Encrypt(d.Password) + d1.Password, err = uc.safeRequirements.Encrypt(d.Password) + if err != nil { + return nil, ErrCIRAConfigsUseCase.Wrap("dtoToEntity", "failed to encrypt password", err) + } - return d1 + return d1, nil } // convert entity.CIRAConfig to dto.CIRAConfig. diff --git a/internal/usecase/devices/interceptor.go b/internal/usecase/devices/interceptor.go index 1fe06c9a0..faa387ddb 100644 --- a/internal/usecase/devices/interceptor.go +++ b/internal/usecase/devices/interceptor.go @@ -30,6 +30,8 @@ const ( ConnectionTimeout = 5 * time.Minute InactivityTimeout = 30 * time.Second // Close connection if no data for 30 seconds HeartbeatInterval = 30 * time.Second // Check connection health every 30 seconds + SlowReceiveThreshold = 100 // Milliseconds threshold for slow device receive + SlowWriteThreshold = 50 // Milliseconds threshold for slow write operations ) type DeviceConnection struct { @@ -48,7 +50,13 @@ type DeviceConnection struct { } func (uc *UseCase) Redirect(c context.Context, conn *websocket.Conn, guid, mode string) error { + // KVM_TIMING: Measure device lookup latency + lookupStart := time.Now() device, err := uc.repo.GetByID(c, guid, "") + + RecordDeviceLookup(time.Since(lookupStart)) + uc.log.Debug("KVM_TIMING: Device lookup", "duration_ms", time.Since(lookupStart).Milliseconds(), "guid", guid) + if err != nil { return err } @@ -64,7 +72,13 @@ func (uc *UseCase) Redirect(c context.Context, conn *websocket.Conn, guid, mode return err } + // KVM_TIMING: Measure connection setup latency + connectStart := time.Now() err = uc.redirection.RedirectConnect(c, deviceConnection) + + RecordConnectionSetup(time.Since(connectStart), mode) + uc.log.Debug("KVM_TIMING: Connection setup", "duration_ms", time.Since(connectStart).Milliseconds(), "mode", mode, "guid", guid) + if err != nil { deviceConnection.cancel() @@ -111,9 +125,17 @@ func (uc *UseCase) getOrCreateConnection(c context.Context, conn *websocket.Conn } func (uc *UseCase) createNewConnection(c context.Context, conn *websocket.Conn, key string, device *entity.Device) (*DeviceConnection, error) { - wsmanConnection := uc.redirection.SetupWsmanClient(*device, true, true) + wsmanConnection, err := uc.redirection.SetupWsmanClient(*device, true, true) + if err != nil { + return nil, err + } + + decryptedPassword, err := uc.safeRequirements.Decrypt(device.Password) + if err != nil { + return nil, err + } - device.Password, _ = uc.safeRequirements.Decrypt(device.Password) + device.Password = decryptedPassword ctx, cancel := context.WithCancel(c) now := time.Now() @@ -212,7 +234,12 @@ func (uc *UseCase) ListenToDevice(deviceConnection *DeviceConnection) { // Measure time blocked waiting for device data recvStart := time.Now() data, err := uc.redirection.RedirectListen(deviceConnection.ctx, deviceConnection) - kvmDeviceReceiveBlockSeconds.WithLabelValues(deviceConnection.Mode).Observe(time.Since(recvStart).Seconds()) + recvDuration := time.Since(recvStart) + kvmDeviceReceiveBlockSeconds.WithLabelValues(deviceConnection.Mode).Observe(recvDuration.Seconds()) + + if recvDuration.Milliseconds() > SlowReceiveThreshold { + uc.log.Debug("KVM_TIMING: Device receive blocked", "duration_ms", recvDuration.Milliseconds(), "mode", deviceConnection.Mode) + } if err != nil { break @@ -241,7 +268,12 @@ func (uc *UseCase) ListenToDevice(deviceConnection *DeviceConnection) { err = conn.WriteMessage(websocket.BinaryMessage, toSend) - kvmDeviceToBrowserWriteSeconds.WithLabelValues(deviceConnection.Mode).Observe(time.Since(start).Seconds()) + writeDuration := time.Since(start) + kvmDeviceToBrowserWriteSeconds.WithLabelValues(deviceConnection.Mode).Observe(writeDuration.Seconds()) + + if writeDuration.Milliseconds() > SlowWriteThreshold { + uc.log.Debug("KVM_TIMING: Device to browser write slow", "duration_ms", writeDuration.Milliseconds(), "mode", deviceConnection.Mode, "bytes", len(toSend)) + } if err != nil { if websocket.IsUnexpectedCloseError(err, websocket.CloseGoingAway, websocket.CloseAbnormalClosure) { @@ -273,7 +305,12 @@ func (uc *UseCase) ListenToBrowser(deviceConnection *DeviceConnection) { readStart := time.Now() _, msg, err := deviceConnection.Conn.ReadMessage() - kvmBrowserReadBlockSeconds.WithLabelValues(deviceConnection.Mode).Observe(time.Since(readStart).Seconds()) + readDuration := time.Since(readStart) + kvmBrowserReadBlockSeconds.WithLabelValues(deviceConnection.Mode).Observe(readDuration.Seconds()) + + if readDuration.Milliseconds() > SlowReceiveThreshold { + uc.log.Debug("KVM_TIMING: Browser read blocked", "duration_ms", readDuration.Milliseconds(), "mode", deviceConnection.Mode) + } if err != nil { if websocket.IsUnexpectedCloseError(err, websocket.CloseGoingAway, websocket.CloseAbnormalClosure) { @@ -300,7 +337,12 @@ func (uc *UseCase) ListenToBrowser(deviceConnection *DeviceConnection) { kvmBrowserToDeviceMessages.WithLabelValues(deviceConnection.Mode).Inc() // Send the message to the TCP Connection on the device err = uc.redirection.RedirectSend(deviceConnection.ctx, deviceConnection, toSend) - kvmBrowserToDeviceSendSeconds.WithLabelValues(deviceConnection.Mode).Observe(time.Since(start).Seconds()) + sendDuration := time.Since(start) + kvmBrowserToDeviceSendSeconds.WithLabelValues(deviceConnection.Mode).Observe(sendDuration.Seconds()) + + if sendDuration.Milliseconds() > SlowWriteThreshold { + uc.log.Debug("KVM_TIMING: Browser to device send slow", "duration_ms", sendDuration.Milliseconds(), "mode", deviceConnection.Mode, "bytes", len(toSend)) + } if err != nil { _ = fmt.Errorf("interceptor - listenToBrowser - error sending message to device: %w", err) diff --git a/internal/usecase/devices/interceptor_test.go b/internal/usecase/devices/interceptor_test.go index e91f7f73d..d8f3825f3 100644 --- a/internal/usecase/devices/interceptor_test.go +++ b/internal/usecase/devices/interceptor_test.go @@ -66,7 +66,7 @@ func TestRedirect(t *testing.T) { Username: "user", Password: "pass", }, nil) - mockRedir.EXPECT().SetupWsmanClient(gomock.Any(), true, true).Return(wsman.Messages{}) + mockRedir.EXPECT().SetupWsmanClient(gomock.Any(), true, true).Return(wsman.Messages{}, nil) mockRedir.EXPECT().RedirectConnect(gomock.Any(), gomock.Any()).Return(ErrInterceptorGeneral) }, expectedErr: ErrInterceptorGeneral, @@ -138,7 +138,7 @@ func TestRedirectSuccessfulFlow(t *testing.T) { // Mock successful flow up to RedirectConnect, then fail to avoid goroutines mockRepo.EXPECT().GetByID(gomock.Any(), testGUID, "").Return(device, nil) - mockRedirection.EXPECT().SetupWsmanClient(*device, true, true).Return(wsman.Messages{}) + mockRedirection.EXPECT().SetupWsmanClient(*device, true, true).Return(wsman.Messages{}, nil) // Return error to avoid starting problematic goroutines but still test the flow mockRedirection.EXPECT().RedirectConnect(gomock.Any(), gomock.Any()).Return(ErrConnectionFailed) @@ -211,7 +211,7 @@ func TestRedirectConnectionReuse(t *testing.T) { // First call - create new connection but fail at connect to avoid goroutines mockRepo.EXPECT().GetByID(gomock.Any(), testGUID, "").Return(device, nil) - mockRedirection.EXPECT().SetupWsmanClient(*device, true, true).Return(wsman.Messages{}) + mockRedirection.EXPECT().SetupWsmanClient(*device, true, true).Return(wsman.Messages{}, nil) mockRedirection.EXPECT().RedirectConnect(gomock.Any(), gomock.Any()).Return(ErrFirstConnectionFailed) err := uc.Redirect(context.Background(), mockConn, testGUID, testMode) @@ -219,7 +219,7 @@ func TestRedirectConnectionReuse(t *testing.T) { // Second call - also fail to avoid goroutines but test reuse logic mockRepo.EXPECT().GetByID(gomock.Any(), testGUID, "").Return(device, nil) - mockRedirection.EXPECT().SetupWsmanClient(*device, true, true).Return(wsman.Messages{}) + mockRedirection.EXPECT().SetupWsmanClient(*device, true, true).Return(wsman.Messages{}, nil) mockRedirection.EXPECT().RedirectConnect(gomock.Any(), gomock.Any()).Return(ErrSecondConnectionFailed) err = uc.Redirect(context.Background(), mockConn, testGUID, testMode) @@ -305,7 +305,7 @@ func TestRedirectWithErrorScenarios(t *testing.T) { device := &entity.Device{GUID: testGUID, Username: "user", Password: "pass"} mockRepo.EXPECT().GetByID(gomock.Any(), testGUID, "").Return(device, nil) - mockRedir.EXPECT().SetupWsmanClient(*device, true, true).Return(wsman.Messages{}) + mockRedir.EXPECT().SetupWsmanClient(*device, true, true).Return(wsman.Messages{}, nil) mockRedir.EXPECT().RedirectConnect(gomock.Any(), gomock.Any()).Return(ErrConnectionFailed) }, expectedErr: "connection failed", @@ -383,7 +383,7 @@ func TestRedirectConnectionFlowCoverage(t *testing.T) { device := &entity.Device{GUID: "test-device", Username: "user", Password: "pass"} mockRepo.EXPECT().GetByID(gomock.Any(), "test-device", "").Return(device, nil) - mockRedir.EXPECT().SetupWsmanClient(*device, true, true).Return(wsman.Messages{}) + mockRedir.EXPECT().SetupWsmanClient(*device, true, true).Return(wsman.Messages{}, nil) // Return error to avoid starting goroutines, but still exercise connection creation mockRedir.EXPECT().RedirectConnect(gomock.Any(), gomock.Any()).Return(ErrTestError) }, diff --git a/internal/usecase/devices/interfaces.go b/internal/usecase/devices/interfaces.go index 207df2508..f99c07462 100644 --- a/internal/usecase/devices/interfaces.go +++ b/internal/usecase/devices/interfaces.go @@ -28,7 +28,7 @@ type ( } Redirection interface { - SetupWsmanClient(device entity.Device, isRedirection, logMessages bool) wsman.Messages + SetupWsmanClient(device entity.Device, isRedirection, logMessages bool) (wsman.Messages, error) RedirectConnect(ctx context.Context, deviceConnection *DeviceConnection) error RedirectClose(ctx context.Context, deviceConnection *DeviceConnection) error RedirectListen(ctx context.Context, deviceConnection *DeviceConnection) ([]byte, error) diff --git a/internal/usecase/devices/metrics.go b/internal/usecase/devices/metrics.go index 849306626..b350ecd2d 100644 --- a/internal/usecase/devices/metrics.go +++ b/internal/usecase/devices/metrics.go @@ -1,6 +1,8 @@ package devices import ( + "time" + "github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus/promauto" ) @@ -92,4 +94,73 @@ var ( }, []string{"mode"}, ) + + // KVM Connection Performance Metrics. + kvmDeviceLookupSeconds = promauto.NewHistogram( + prometheus.HistogramOpts{ + Name: "kvm_device_lookup_seconds", + Help: "Time to look up device from database during KVM connection (KVM_TIMING)", + Buckets: []float64{0.001, 0.002, 0.005, 0.01, 0.02, 0.05, 0.1, 0.2, 0.5, 1, 2}, + }, + ) + + kvmConnectionSetupSeconds = promauto.NewHistogramVec( + prometheus.HistogramOpts{ + Name: "kvm_connection_setup_seconds", + Help: "Time to establish TCP connection to device during KVM setup (KVM_TIMING)", + Buckets: []float64{0.01, 0.02, 0.05, 0.1, 0.2, 0.5, 1, 2, 5, 10}, + }, + []string{"mode"}, + ) + + kvmWebsocketUpgradeSeconds = promauto.NewHistogram( + prometheus.HistogramOpts{ + Name: "kvm_websocket_upgrade_seconds", + Help: "Time to upgrade HTTP connection to WebSocket for KVM (KVM_TIMING)", + Buckets: []float64{0.001, 0.002, 0.005, 0.01, 0.02, 0.05, 0.1, 0.2, 0.5, 1}, + }, + ) + + kvmTotalConnectionSeconds = promauto.NewHistogramVec( + prometheus.HistogramOpts{ + Name: "kvm_total_connection_seconds", + Help: "Total time from request to ready KVM connection (KVM_TIMING)", + Buckets: []float64{0.1, 0.2, 0.5, 1, 2, 5, 10, 20, 30}, + }, + []string{"mode"}, + ) + + kvmConsentCodeWaitSeconds = promauto.NewHistogramVec( + prometheus.HistogramOpts{ + Name: "kvm_consent_code_wait_seconds", + Help: "Time spent waiting for consent code handling during KVM setup (KVM_TIMING)", + Buckets: []float64{0.01, 0.05, 0.1, 0.2, 0.5, 1, 2, 5, 10, 30, 60}, + }, + []string{"mode"}, + ) ) + +// RecordWebsocketUpgrade records the WebSocket upgrade duration metric. +func RecordWebsocketUpgrade(duration time.Duration) { + kvmWebsocketUpgradeSeconds.Observe(duration.Seconds()) +} + +// RecordTotalConnection records the total KVM connection time metric. +func RecordTotalConnection(duration time.Duration, mode string) { + kvmTotalConnectionSeconds.WithLabelValues(mode).Observe(duration.Seconds()) +} + +// RecordConsentCodeWait records the consent code wait time metric. +func RecordConsentCodeWait(duration time.Duration, mode string) { + kvmConsentCodeWaitSeconds.WithLabelValues(mode).Observe(duration.Seconds()) +} + +// RecordDeviceLookup records the device lookup duration metric. +func RecordDeviceLookup(duration time.Duration) { + kvmDeviceLookupSeconds.Observe(duration.Seconds()) +} + +// RecordConnectionSetup records the TCP connection setup duration metric. +func RecordConnectionSetup(duration time.Duration, mode string) { + kvmConnectionSetupSeconds.WithLabelValues(mode).Observe(duration.Seconds()) +} diff --git a/internal/usecase/devices/power.go b/internal/usecase/devices/power.go index 0e0305e1c..ab7151903 100644 --- a/internal/usecase/devices/power.go +++ b/internal/usecase/devices/power.go @@ -8,6 +8,7 @@ import ( "strings" "github.com/device-management-toolkit/go-wsman-messages/v2/pkg/wsman/amt/boot" + "github.com/device-management-toolkit/go-wsman-messages/v2/pkg/wsman/amt/setupandconfiguration" cimBoot "github.com/device-management-toolkit/go-wsman-messages/v2/pkg/wsman/cim/boot" "github.com/device-management-toolkit/go-wsman-messages/v2/pkg/wsman/cim/power" "github.com/device-management-toolkit/go-wsman-messages/v2/pkg/wsman/cim/software" @@ -244,6 +245,18 @@ func (uc *UseCase) SetBootOptions(c context.Context, guid string, bootSetting dt return power.PowerActionResponse{}, err } + // Validate EnforceSecureBoot restriction in CCM + if bootSetting.BootDetails.EnforceSecureBoot != nil && !*bootSetting.BootDetails.EnforceSecureBoot { + setupConfig, err := device.GetSetupAndConfiguration() + if err != nil { + return power.PowerActionResponse{}, err + } + + if len(setupConfig) > 0 && setupConfig[0].ProvisioningMode == setupandconfiguration.ClientControlMode { + return power.PowerActionResponse{}, ValidationError{}.Wrap("SetBootOptions", "validate provisioning mode", "EnforceSecureBoot cannot be turned off in CCM") + } + } + bootData, err := device.GetBootData() if err != nil { return power.PowerActionResponse{}, err @@ -325,7 +338,8 @@ func determineBootDevice(bootSetting dto.BootSetting, newData *boot.BootSettingD return err } - setUEFIBootSettings(newData, bootSetting.BootDetails.EnforceSecureBoot, params, typeLengthValueBuffer) + enforceSecureBoot := getEnforceSecureBoot(bootSetting.BootDetails.EnforceSecureBoot, newData.EnforceSecureBoot) + setUEFIBootSettings(newData, enforceSecureBoot, params, typeLengthValueBuffer) case BootActionPBA, BootActionPowerOnPBA, BootActionWinREBoot, BootActionPowerOnWinREBoot: if bootSetting.BootDetails.BootPath == "" { return ErrValidationUseCase @@ -336,7 +350,8 @@ func determineBootDevice(bootSetting dto.BootSetting, newData *boot.BootSettingD return err } - setUEFIBootSettings(newData, bootSetting.BootDetails.EnforceSecureBoot, params, typeLengthValueBuffer) + enforceSecureBoot := getEnforceSecureBoot(bootSetting.BootDetails.EnforceSecureBoot, newData.EnforceSecureBoot) + setUEFIBootSettings(newData, enforceSecureBoot, params, typeLengthValueBuffer) case BootActionResetToIDERCDROM, BootActionPowerOnIDERCDROM: newData.IDERBootDevice = 1 default: @@ -346,6 +361,19 @@ func determineBootDevice(bootSetting dto.BootSetting, newData *boot.BootSettingD return nil } +// getEnforceSecureBoot returns the EnforceSecureBoot value from the request if provided, +// otherwise falls back to the current device value. +func getEnforceSecureBoot(requestValue *bool, currentValue bool) bool { + if requestValue != nil { + return *requestValue + } + + return currentValue +} + +// setUEFIBootSettings expects enforceSecureBoot to be a fully resolved value. +// Callers should resolve any optional request value (for example, via getEnforceSecureBoot) +// before invoking this function, as it no longer accepts a *bool. func setUEFIBootSettings(newData *boot.BootSettingDataRequest, enforceSecureBoot bool, params int, typeLengthValueBuffer []byte) { newData.BIOSLastStatus = nil newData.UseIDER = false diff --git a/internal/usecase/devices/power_test.go b/internal/usecase/devices/power_test.go index bbd547eb7..93945914c 100644 --- a/internal/usecase/devices/power_test.go +++ b/internal/usecase/devices/power_test.go @@ -10,6 +10,7 @@ import ( gomock "go.uber.org/mock/gomock" "github.com/device-management-toolkit/go-wsman-messages/v2/pkg/wsman/amt/boot" + "github.com/device-management-toolkit/go-wsman-messages/v2/pkg/wsman/amt/setupandconfiguration" cimBoot "github.com/device-management-toolkit/go-wsman-messages/v2/pkg/wsman/cim/boot" "github.com/device-management-toolkit/go-wsman-messages/v2/pkg/wsman/cim/power" "github.com/device-management-toolkit/go-wsman-messages/v2/pkg/wsman/cim/service" @@ -747,6 +748,205 @@ func TestSetBootOptions(t *testing.T) { } } +func TestSetBootOptions_CCMRestriction(t *testing.T) { + t.Parallel() + + bootResponse := boot.BootSettingDataResponse{ + BIOSLastStatus: []uint16{2, 0}, + EnforceSecureBoot: true, + ElementName: "Intel(r) AMT Boot Configuration Settings", + InstanceID: "Intel(r) AMT:BootSettingData 0", + OwningEntity: "Intel(r) AMT", + } + + device := &entity.Device{ + GUID: "device-guid-123", + TenantID: "tenant-id-456", + } + + enforceSecureBootFalse := false + enforceSecureBootTrue := true + + powerActionRes := power.PowerActionResponse{ReturnValue: 5} + + tests := []struct { + name string + bootSetting dto.BootSetting + manMock func(*mocks.MockWSMAN, *mocks.MockManagement) + repoMock func(*mocks.MockDeviceManagementRepository) + wantErr error + }{ + { + name: "CCM restriction - EnforceSecureBoot false in CCM returns error", + bootSetting: dto.BootSetting{ + Action: 400, + UseSOL: true, + BootDetails: dto.BootDetails{ + EnforceSecureBoot: &enforceSecureBootFalse, + }, + }, + manMock: func(man *mocks.MockWSMAN, hmm *mocks.MockManagement) { + man.EXPECT(). + SetupWsmanClient(gomock.Any(), false, true). + Return(hmm, nil) + hmm.EXPECT(). + GetSetupAndConfiguration(). + Return([]setupandconfiguration.SetupAndConfigurationServiceResponse{ + {ProvisioningMode: setupandconfiguration.ClientControlMode}, + }, nil) + }, + repoMock: func(repo *mocks.MockDeviceManagementRepository) { + repo.EXPECT(). + GetByID(context.Background(), device.GUID, ""). + Return(device, nil) + }, + wantErr: devices.ValidationError{}.Wrap("SetBootOptions", "validate provisioning mode", "EnforceSecureBoot cannot be turned off in CCM"), + }, + { + name: "ACM mode - EnforceSecureBoot false allowed", + bootSetting: dto.BootSetting{ + Action: 400, + UseSOL: true, + BootDetails: dto.BootDetails{ + EnforceSecureBoot: &enforceSecureBootFalse, + }, + }, + manMock: func(man *mocks.MockWSMAN, hmm *mocks.MockManagement) { + man.EXPECT(). + SetupWsmanClient(gomock.Any(), false, true). + Return(hmm, nil) + hmm.EXPECT(). + GetSetupAndConfiguration(). + Return([]setupandconfiguration.SetupAndConfigurationServiceResponse{ + {ProvisioningMode: setupandconfiguration.AdminControlMode}, + }, nil) + hmm.EXPECT(). + GetBootData(). + Return(bootResponse, nil) + hmm.EXPECT(). + ChangeBootOrder(""). + Return(cimBoot.ChangeBootOrder_OUTPUT{}, nil) + hmm.EXPECT(). + SetBootData(gomock.Any()). + Return(nil, nil) + hmm.EXPECT(). + SetBootConfigRole(1). + Return(powerActionRes, nil) + hmm.EXPECT(). + ChangeBootOrder(string(cimBoot.PXE)). + Return(cimBoot.ChangeBootOrder_OUTPUT{}, nil) + hmm.EXPECT(). + SendPowerAction(10). + Return(powerActionRes, nil) + }, + repoMock: func(repo *mocks.MockDeviceManagementRepository) { + repo.EXPECT(). + GetByID(context.Background(), device.GUID, ""). + Return(device, nil) + }, + wantErr: nil, + }, + { + name: "CCM mode - EnforceSecureBoot true allowed", + bootSetting: dto.BootSetting{ + Action: 400, + UseSOL: true, + BootDetails: dto.BootDetails{ + EnforceSecureBoot: &enforceSecureBootTrue, + }, + }, + manMock: func(man *mocks.MockWSMAN, hmm *mocks.MockManagement) { + man.EXPECT(). + SetupWsmanClient(gomock.Any(), false, true). + Return(hmm, nil) + hmm.EXPECT(). + GetBootData(). + Return(bootResponse, nil) + hmm.EXPECT(). + ChangeBootOrder(""). + Return(cimBoot.ChangeBootOrder_OUTPUT{}, nil) + hmm.EXPECT(). + SetBootData(gomock.Any()). + Return(nil, nil) + hmm.EXPECT(). + SetBootConfigRole(1). + Return(powerActionRes, nil) + hmm.EXPECT(). + ChangeBootOrder(string(cimBoot.PXE)). + Return(cimBoot.ChangeBootOrder_OUTPUT{}, nil) + hmm.EXPECT(). + SendPowerAction(10). + Return(powerActionRes, nil) + }, + repoMock: func(repo *mocks.MockDeviceManagementRepository) { + repo.EXPECT(). + GetByID(context.Background(), device.GUID, ""). + Return(device, nil) + }, + wantErr: nil, + }, + { + name: "EnforceSecureBoot not provided - no CCM check", + bootSetting: dto.BootSetting{ + Action: 400, + UseSOL: true, + BootDetails: dto.BootDetails{ + EnforceSecureBoot: nil, + }, + }, + manMock: func(man *mocks.MockWSMAN, hmm *mocks.MockManagement) { + man.EXPECT(). + SetupWsmanClient(gomock.Any(), false, true). + Return(hmm, nil) + hmm.EXPECT(). + GetBootData(). + Return(bootResponse, nil) + hmm.EXPECT(). + ChangeBootOrder(""). + Return(cimBoot.ChangeBootOrder_OUTPUT{}, nil) + hmm.EXPECT(). + SetBootData(gomock.Any()). + Return(nil, nil) + hmm.EXPECT(). + SetBootConfigRole(1). + Return(powerActionRes, nil) + hmm.EXPECT(). + ChangeBootOrder(string(cimBoot.PXE)). + Return(cimBoot.ChangeBootOrder_OUTPUT{}, nil) + hmm.EXPECT(). + SendPowerAction(10). + Return(powerActionRes, nil) + }, + repoMock: func(repo *mocks.MockDeviceManagementRepository) { + repo.EXPECT(). + GetByID(context.Background(), device.GUID, ""). + Return(device, nil) + }, + wantErr: nil, + }, + } + + for _, tc := range tests { + tc := tc + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + useCase, wsmanMock, management, repo := initPowerTest(t) + tc.manMock(wsmanMock, management) + tc.repoMock(repo) + + _, err := useCase.SetBootOptions(context.Background(), device.GUID, tc.bootSetting) + + if tc.wantErr != nil { + require.Error(t, err) + require.ErrorIs(t, err, tc.wantErr) + } else { + require.NoError(t, err) + } + }) + } +} + func TestGetBootSourceSetting(t *testing.T) { t.Parallel() diff --git a/internal/usecase/devices/redirection.go b/internal/usecase/devices/redirection.go index 8bec141fa..ad5d28c68 100644 --- a/internal/usecase/devices/redirection.go +++ b/internal/usecase/devices/redirection.go @@ -14,7 +14,7 @@ type Redirector struct { SafeRequirements security.Cryptor } -func (g *Redirector) SetupWsmanClient(device entity.Device, isRedirection, logAMTMessages bool) wsman.Messages { +func (g *Redirector) SetupWsmanClient(device entity.Device, isRedirection, logAMTMessages bool) (wsman.Messages, error) { clientParams := client.Parameters{ Target: device.Hostname, Username: device.Username, @@ -29,9 +29,14 @@ func (g *Redirector) SetupWsmanClient(device entity.Device, isRedirection, logAM clientParams.PinnedCert = *device.CertHash } - clientParams.Password, _ = g.SafeRequirements.Decrypt(device.Password) + decryptedPassword, err := g.SafeRequirements.Decrypt(device.Password) + if err != nil { + return wsman.Messages{}, err + } + + clientParams.Password = decryptedPassword - return wsman.NewMessages(clientParams) + return wsman.NewMessages(clientParams), nil } func NewRedirector(safeRequirements security.Cryptor) *Redirector { diff --git a/internal/usecase/devices/redirection_test.go b/internal/usecase/devices/redirection_test.go index 47e5e7cd2..db28025c5 100644 --- a/internal/usecase/devices/redirection_test.go +++ b/internal/usecase/devices/redirection_test.go @@ -28,9 +28,9 @@ func initRedirectionTest(t *testing.T) (*devices.Redirector, *mocks.MockRedirect } type redTest struct { - name string - redMock func(*mocks.MockRedirection) - res any + name string + res any + err error } func TestSetupWsmanClient(t *testing.T) { @@ -44,21 +44,8 @@ func TestSetupWsmanClient(t *testing.T) { tests := []redTest{ { name: "success", - redMock: func(redirect *mocks.MockRedirection) { - redirect.EXPECT(). - SetupWsmanClient(gomock.Any(), false, true). - Return(wsman.Messages{}) - }, - res: wsman.Messages{}, - }, - { - name: "fail", - redMock: func(redirect *mocks.MockRedirection) { - redirect.EXPECT(). - SetupWsmanClient(gomock.Any(), true, true). - Return(wsman.Messages{}) - }, - res: wsman.Messages{}, + res: wsman.Messages{}, + err: nil, }, } @@ -67,17 +54,14 @@ func TestSetupWsmanClient(t *testing.T) { t.Run(tc.name, func(t *testing.T) { t.Parallel() - redirector, redirect, _ := initRedirectionTest(t) - - tc.redMock(redirect) + redirector, _, _ := initRedirectionTest(t) - redirector.SafeRequirements = security.Crypto{ - EncryptionKey: "test", - } + redirector.SafeRequirements = mocks.MockCrypto{} - res := redirector.SetupWsmanClient(*device, true, true) + res, err := redirector.SetupWsmanClient(*device, true, true) require.IsType(t, tc.res, res) + require.Equal(t, tc.err, err) }) } } diff --git a/internal/usecase/devices/wsman/message.go b/internal/usecase/devices/wsman/message.go index 26359e241..2a8fa761e 100644 --- a/internal/usecase/devices/wsman/message.go +++ b/internal/usecase/devices/wsman/message.go @@ -131,7 +131,15 @@ func (g GoWSMANMessages) SetupWsmanClient(device entity.Device, isRedirection, l errChan := make(chan error, 1) // Queue the request requestQueue <- func() { - device.Password, _ = g.safeRequirements.Decrypt(device.Password) + decryptedPassword, err := g.safeRequirements.Decrypt(device.Password) + if err != nil { + errChan <- err + + return + } + + device.Password = decryptedPassword + if device.MPSUsername != "" { if len(Connections) == 0 { errChan <- ErrCIRADeviceNotConnected @@ -424,11 +432,6 @@ func (c *ConnectionEntry) hardwareGets() (GetHWResults, error) { return results, err } - results.ChipResult, err = c.WsmanMessages.CIM.Chip.Get() - if err != nil { - return results, err - } - results.BiosResult, err = c.WsmanMessages.CIM.BIOSElement.Get() if err != nil { return results, err @@ -457,6 +460,16 @@ func (c *ConnectionEntry) hardwarePulls() (PullHWResults, error) { return results, err } + chipEnumerateResult, err := c.WsmanMessages.CIM.Chip.Enumerate() + if err != nil { + return results, err + } + + results.ChipResult, err = c.WsmanMessages.CIM.Chip.Pull(chipEnumerateResult.Body.EnumerateResponse.EnumerationContext) + if err != nil { + return results, err + } + return results, nil } @@ -473,7 +486,7 @@ func (c *ConnectionEntry) GetHardwareInfo() (interface{}, error) { hwResults := HWResults{ ChassisResult: getHWResults.ChassisResult, - ChipResult: getHWResults.ChipResult, + ChipResult: pullHWResults.ChipResult, CardResult: getHWResults.CardResult, PhysicalMemoryResult: pullHWResults.PhysicalMemoryResult, BiosResult: getHWResults.BiosResult, @@ -485,13 +498,13 @@ func (c *ConnectionEntry) GetHardwareInfo() (interface{}, error) { type GetHWResults struct { ChassisResult chassis.Response - ChipResult chip.Response CardResult card.Response BiosResult bios.Response ProcessorResult processor.Response } type PullHWResults struct { PhysicalMemoryResult physical.Response + ChipResult chip.Response } type HWResults struct { ChassisResult chassis.Response diff --git a/internal/usecase/domains/usecase.go b/internal/usecase/domains/usecase.go index 8c06b50a9..5fba47756 100644 --- a/internal/usecase/domains/usecase.go +++ b/internal/usecase/domains/usecase.go @@ -172,7 +172,10 @@ func (uc *UseCase) Delete(ctx context.Context, domainName, tenantID string) erro } func (uc *UseCase) Update(ctx context.Context, d *dto.Domain) (*dto.Domain, error) { - d1 := uc.dtoToEntity(d) + d1, err := uc.dtoToEntity(d) + if err != nil { + return nil, err + } updated, err := uc.repo.Update(ctx, d1) if err != nil { @@ -199,7 +202,11 @@ func (uc *UseCase) Insert(ctx context.Context, d *dto.Domain) (*dto.Domain, erro return nil, err } - d1 := uc.dtoToEntity(d) + d1, err := uc.dtoToEntity(d) + if err != nil { + return nil, err + } + d1.ExpirationDate = cert.NotAfter.Format(time.RFC3339) // Store certificate in Vault (if available) - cert goes to Vault, not DB @@ -267,7 +274,7 @@ func DecryptAndCheckCertExpiration(domain dto.Domain) (*x509.Certificate, error) } // convert dto.Domain to entity.Domain. -func (uc *UseCase) dtoToEntity(d *dto.Domain) *entity.Domain { +func (uc *UseCase) dtoToEntity(d *dto.Domain) (*entity.Domain, error) { d1 := &entity.Domain{ ProfileName: d.ProfileName, DomainSuffix: d.DomainSuffix, @@ -278,9 +285,14 @@ func (uc *UseCase) dtoToEntity(d *dto.Domain) *entity.Domain { Version: d.Version, } - d1.ProvisioningCertPassword, _ = uc.safeRequirements.Encrypt(d.ProvisioningCertPassword) + var err error - return d1 + d1.ProvisioningCertPassword, err = uc.safeRequirements.Encrypt(d.ProvisioningCertPassword) + if err != nil { + return nil, ErrDomainsUseCase.Wrap("dtoToEntity", "failed to encrypt provisioning cert password", err) + } + + return d1, nil } // convert entity.Domain to dto.Domain. diff --git a/internal/usecase/profiles/usecase.go b/internal/usecase/profiles/usecase.go index 3f2adad4a..f8a3caab4 100644 --- a/internal/usecase/profiles/usecase.go +++ b/internal/usecase/profiles/usecase.go @@ -481,9 +481,12 @@ func (uc *UseCase) isWifiProfileExists(ctx context.Context, d *dto.Profile, acti } func (uc *UseCase) Update(ctx context.Context, d *dto.Profile) (*dto.Profile, error) { - d1 := uc.dtoToEntity(d) + d1, err := uc.dtoToEntity(d) + if err != nil { + return nil, err + } - err := uc.isWifiProfileExists(ctx, d, "update") + err = uc.isWifiProfileExists(ctx, d, "update") if err != nil { return nil, err } @@ -530,7 +533,10 @@ func (uc *UseCase) Update(ctx context.Context, d *dto.Profile) (*dto.Profile, er } func (uc *UseCase) Insert(ctx context.Context, d *dto.Profile) (*dto.Profile, error) { - d1 := uc.dtoToEntity(d) + d1, err := uc.dtoToEntity(d) + if err != nil { + return nil, err + } if err := uc.isWifiProfileExists(ctx, d, "insert"); err != nil { return nil, err @@ -612,7 +618,7 @@ func (uc *UseCase) createdProfile(ctx context.Context, d *dto.Profile) (*dto.Pro } // convert dto.Profile to entity.Profile. -func (uc *UseCase) dtoToEntity(d *dto.Profile) *entity.Profile { +func (uc *UseCase) dtoToEntity(d *dto.Profile) (*entity.Profile, error) { // convert []string to comma separated string tags := strings.Join(d.Tags, ", ") @@ -642,10 +648,19 @@ func (uc *UseCase) dtoToEntity(d *dto.Profile) *entity.Profile { UEFIWiFiSyncEnabled: d.UEFIWiFiSyncEnabled, } - d1.AMTPassword, _ = uc.safeRequirements.Encrypt(d.AMTPassword) - d1.MEBXPassword, _ = uc.safeRequirements.Encrypt(d.MEBXPassword) + var err error - return d1 + d1.AMTPassword, err = uc.safeRequirements.Encrypt(d.AMTPassword) + if err != nil { + return nil, ErrProfilesUseCase.Wrap("dtoToEntity", "failed to encrypt AMT password", err) + } + + d1.MEBXPassword, err = uc.safeRequirements.Encrypt(d.MEBXPassword) + if err != nil { + return nil, ErrProfilesUseCase.Wrap("dtoToEntity", "failed to encrypt MEBX password", err) + } + + return d1, nil } // convert entity.Profile to dto.Profile. diff --git a/internal/usecase/wificonfigs/usecase.go b/internal/usecase/wificonfigs/usecase.go index 63c0599dd..c82866d54 100644 --- a/internal/usecase/wificonfigs/usecase.go +++ b/internal/usecase/wificonfigs/usecase.go @@ -106,11 +106,14 @@ func (uc *UseCase) Delete(ctx context.Context, profileName, tenantID string) err } func (uc *UseCase) Update(ctx context.Context, d *dto.WirelessConfig) (*dto.WirelessConfig, error) { - d1 := uc.dtoToEntity(d) + d1, err := uc.dtoToEntity(d) + if err != nil { + return nil, err + } // check if the IEEE profile is exists in the database if d1.IEEE8021xProfileName != nil && *d1.IEEE8021xProfileName != "" { - _, err := uc.ieee.GetByName(ctx, *d1.IEEE8021xProfileName, d.TenantID) + _, err = uc.ieee.GetByName(ctx, *d1.IEEE8021xProfileName, d.TenantID) if err != nil { return nil, err } @@ -136,17 +139,20 @@ func (uc *UseCase) Update(ctx context.Context, d *dto.WirelessConfig) (*dto.Wire } func (uc *UseCase) Insert(ctx context.Context, d *dto.WirelessConfig) (*dto.WirelessConfig, error) { - d1 := uc.dtoToEntity(d) + d1, err := uc.dtoToEntity(d) + if err != nil { + return nil, err + } // check if the IEEE profile is exists in the database if d1.IEEE8021xProfileName != nil && *d1.IEEE8021xProfileName != "" { - _, err := uc.ieee.GetByName(ctx, *d1.IEEE8021xProfileName, d.TenantID) + _, err = uc.ieee.GetByName(ctx, *d1.IEEE8021xProfileName, d.TenantID) if err != nil { return nil, err } } - _, err := uc.repo.Insert(ctx, d1) + _, err = uc.repo.Insert(ctx, d1) if err != nil { return nil, ErrDatabase.Wrap("Insert", "uc.repo.Insert", err) } @@ -162,7 +168,7 @@ func (uc *UseCase) Insert(ctx context.Context, d *dto.WirelessConfig) (*dto.Wire } // convert dto.WirelessConfig to entity.WirelessConfig. -func (uc *UseCase) dtoToEntity(d *dto.WirelessConfig) *entity.WirelessConfig { +func (uc *UseCase) dtoToEntity(d *dto.WirelessConfig) (*entity.WirelessConfig, error) { // convert []int to comma separated string linkPolicy := strings.Trim(strings.Join(strings.Fields(fmt.Sprint(d.LinkPolicy)), ","), "[]") @@ -179,9 +185,14 @@ func (uc *UseCase) dtoToEntity(d *dto.WirelessConfig) *entity.WirelessConfig { Version: d.Version, } - d1.PSKPassphrase, _ = uc.safeRequirements.Encrypt(d.PSKPassphrase) + var err error - return d1 + d1.PSKPassphrase, err = uc.safeRequirements.Encrypt(d.PSKPassphrase) + if err != nil { + return nil, ErrDomainsUseCase.Wrap("dtoToEntity", "failed to encrypt PSK passphrase", err) + } + + return d1, nil } // convert entity.WirelessConfig to dto.WirelessConfig. diff --git a/pkg/httpserver/server.go b/pkg/httpserver/server.go index dba579359..45b1cb934 100644 --- a/pkg/httpserver/server.go +++ b/pkg/httpserver/server.go @@ -55,10 +55,6 @@ func New(handler http.Handler, opts ...Option) *Server { server: httpServer, notify: make(chan error, 1), shutdownTimeout: _defaultShutdownTimeout, - useTLS: false, - certFile: "", - keyFile: "", - log: appLogger.New("info"), } // Custom options diff --git a/pkg/httpserver/server_tls_test.go b/pkg/httpserver/server_tls_test.go index 4701e806f..de767be73 100644 --- a/pkg/httpserver/server_tls_test.go +++ b/pkg/httpserver/server_tls_test.go @@ -16,6 +16,8 @@ import ( "path/filepath" "testing" "time" + + appLogger "github.com/device-management-toolkit/console/pkg/logger" ) // helper to create a basic cert/key pair on disk. @@ -83,7 +85,7 @@ func TestTLS_SelfSigned_GeneratesAndServes(t *testing.T) { //nolint:paralleltest l := newTestListener(t) - s := New(handler, Listener(l), TLS(true, "", "")) + s := New(handler, Listener(l), TLS(true, "", ""), Logger(appLogger.New("info"))) defer func() { _ = s.Shutdown() }() // ensure server is shutdown; ignore error for cleanup