diff --git a/windows-agent/internal/config/config.go b/windows-agent/internal/config/config.go index d1b5fd455..514ebdf14 100644 --- a/windows-agent/internal/config/config.go +++ b/windows-agent/internal/config/config.go @@ -172,10 +172,8 @@ func (c *Config) ProvisioningTasks(ctx context.Context, distroName string) ([]ta if conf, err := c.LandscapeClientConfig(ctx); err != nil { log.Errorf(ctx, "Could not generate provisioning task LandscapeConfigure: %v", err) - } else if landscape, err := tasks.NewLandscapeConfigure(ctx, conf, distroName); err != nil { - log.Errorf(ctx, "Could not generate provisioning task LandscapeConfigure: %v", err) } else { - // Success + landscape := tasks.LandscapeConfigure{Config: conf} taskList = append(taskList, landscape) } diff --git a/windows-agent/internal/proservices/landscape/landscape.go b/windows-agent/internal/proservices/landscape/landscape.go index 9b0642d1a..87315d8eb 100644 --- a/windows-agent/internal/proservices/landscape/landscape.go +++ b/windows-agent/internal/proservices/landscape/landscape.go @@ -3,11 +3,14 @@ package landscape import ( "context" + "crypto/tls" + "crypto/x509" "errors" "fmt" "io/fs" "os" "path/filepath" + "strings" "sync" "sync/atomic" "time" @@ -19,7 +22,9 @@ import ( "github.com/ubuntu/decorate" "google.golang.org/grpc" "google.golang.org/grpc/connectivity" + "google.golang.org/grpc/credentials" "google.golang.org/grpc/credentials/insecure" + "gopkg.in/ini.v1" ) // Client is a client to the landscape service, served remotely. @@ -58,6 +63,7 @@ const cacheFileBase = "landscape.conf" // Config is a configuration provider for ProToken and the Landscape URL. type Config interface { + LandscapeClientConfig(context.Context) (string, error) LandscapeAgentURL(context.Context) (string, error) Subscription(context.Context) (string, config.SubscriptionSource, error) } @@ -198,7 +204,12 @@ func (c *Client) connect(ctx context.Context, address string) (conn *connection, dialCtx, cancel := context.WithTimeout(ctx, 10*time.Second) defer cancel() - grpcConn, err := grpc.DialContext(dialCtx, address, grpc.WithTransportCredentials(insecure.NewCredentials())) + creds, err := c.transportCredentials(ctx) + if err != nil { + return nil, err + } + + grpcConn, err := grpc.DialContext(dialCtx, address, grpc.WithTransportCredentials(creds)) if err != nil { return nil, err } @@ -377,3 +388,56 @@ func (c *Client) getUID() string { func (c *Client) setUID(s string) { c.uid.Store(s) } + +// transportCredentials reads the Landscape client config to check if a SSL public key is specified. +// +// If this credential is not specified, an insecure credential is returned. +// If the credential is specified but erroneous, an error is returned. +func (c *Client) transportCredentials(ctx context.Context) (cred credentials.TransportCredentials, err error) { + defer decorate.OnError(&err, "Landscape credentials") + + conf, err := c.conf.LandscapeClientConfig(ctx) + if err != nil { + return nil, fmt.Errorf("could not obtain Landscape config: %v", err) + } + + if conf == "" { + // No Landscape config: default to insecure + return insecure.NewCredentials(), nil + } + + ini, err := ini.Load(strings.NewReader(conf)) + if err != nil { + return insecure.NewCredentials(), fmt.Errorf("could not parse Landscape config file: %v", err) + } + + const section = "client" + const key = "ssl_public_key" + + sec, err := ini.GetSection(section) + if err != nil { + // No SSL public key provided: default to insecure + return insecure.NewCredentials(), nil + } + + k, err := sec.GetKey(key) + if err != nil { + // No SSL public key provided: default to insecure + return insecure.NewCredentials(), nil + } + + cert, err := os.ReadFile(k.String()) + if err != nil { + return nil, fmt.Errorf("could not load SSL public key file: %v", err) + } + + certPool := x509.NewCertPool() + if ok := certPool.AppendCertsFromPEM(cert); !ok { + return nil, fmt.Errorf("failed to add server CA's certificate: %v", err) + } + + return credentials.NewTLS(&tls.Config{ + RootCAs: certPool, + MinVersion: tls.VersionTLS12, + }), nil +} diff --git a/windows-agent/internal/proservices/landscape/landscape_test.go b/windows-agent/internal/proservices/landscape/landscape_test.go index 664ad7fe4..6ba7f5d35 100644 --- a/windows-agent/internal/proservices/landscape/landscape_test.go +++ b/windows-agent/internal/proservices/landscape/landscape_test.go @@ -2,6 +2,7 @@ package landscape_test import ( "context" + "crypto/tls" "errors" "fmt" "net" @@ -14,6 +15,7 @@ import ( "time" landscapeapi "github.com/canonical/landscape-hostagent-api" + "github.com/canonical/ubuntu-pro-for-windows/common/golden" "github.com/canonical/ubuntu-pro-for-windows/common/wsltestutils" "github.com/canonical/ubuntu-pro-for-windows/mocks/landscape/landscapemockservice" "github.com/canonical/ubuntu-pro-for-windows/windows-agent/internal/config" @@ -27,6 +29,7 @@ import ( wsl "github.com/ubuntu/gowsl" wslmock "github.com/ubuntu/gowsl/mock" "google.golang.org/grpc" + "google.golang.org/grpc/credentials" ) func TestMain(m *testing.M) { @@ -94,10 +97,12 @@ func TestConnect(t *testing.T) { } testCases := map[string]struct { - precancelContext bool - serverNotAvailable bool - landscapeURLErr bool - tokenErr bool + precancelContext bool + serverNotAvailable bool + landscapeURLErr bool + tokenErr bool + requireCertificate bool + breakLandscapeClientConfig bool breakUIDFile bool uid string @@ -105,13 +110,19 @@ func TestConnect(t *testing.T) { wantErr bool wantDistroSkipped bool }{ - "Success in first contact": {}, - "Success in non-first contact": {uid: "123"}, + "Success in first contact": {}, + "Success in non-first contact": {uid: "123"}, + "Success with non-empty config": {}, + "Success with an SSL certificate": {requireCertificate: true}, "Error when the context is cancelled before Connected": {precancelContext: true, wantErr: true}, "Error when the landscape URL cannot be retrieved": {landscapeURLErr: true, wantErr: true}, "Error when the server cannot be reached": {serverNotAvailable: true, wantErr: true}, - "Error when the first-contact SendUpdatedInfo fails ": {tokenErr: true, wantErr: true}, + "Error when the first-contact SendUpdatedInfo fails": {tokenErr: true, wantErr: true}, + "Error when the config cannot be accessed": {breakLandscapeClientConfig: true, wantErr: true}, + "Error when the config cannot be parsed": {wantErr: true}, + "Error when the SSL certificate cannot be read": {wantErr: true}, + "Error when the SSL certificate is not valid": {wantErr: true}, } for name, tc := range testCases { @@ -123,7 +134,7 @@ func TestConnect(t *testing.T) { ctx = wsl.WithMock(ctx, wslmock.New()) } - lis, server, mockService := setUpLandscapeMock(t, ctx, "localhost:") + lis, server, mockService := setUpLandscapeMock(t, ctx, "localhost:", tc.requireCertificate) defer lis.Close() conf := &mockConfig{ @@ -135,7 +146,19 @@ func TestConnect(t *testing.T) { // We trigger an earlier error by erroring out on LandscapeAgentURL landscapeURLErr: tc.landscapeURLErr, + + // We trigger an error when deciding to use a certificate or not + landscapeConfigErr: tc.breakLandscapeClientConfig, + } + + out, err := os.ReadFile(filepath.Join(golden.TestFixturePath(t), "landscape.conf")) + if errors.Is(err, os.ErrNotExist) { + // This fixture is not compulsory + out = []byte{} + err = nil } + require.NoError(t, err, "Setup: could not load landscape config") + conf.landscapeClientConfig = string(out) if !tc.serverNotAvailable { //nolint:errcheck // We don't care about these errors @@ -188,7 +211,7 @@ func TestConnect(t *testing.T) { confFile := filepath.Join(dir, landscape.CacheFileBase) require.FileExists(t, confFile, "Landscape config file should be created after disconnecting") - out, err := os.ReadFile(confFile) + out, err = os.ReadFile(confFile) require.NoError(t, err, "Could not read landscape config file") wantUID := tc.uid @@ -242,7 +265,7 @@ func TestSendUpdatedInfo(t *testing.T) { ctx = wsl.WithMock(ctx, mock) } - lis, server, mockService := setUpLandscapeMock(t, ctx, "localhost:") + lis, server, mockService := setUpLandscapeMock(t, ctx, "localhost:", false) conf := &mockConfig{ proToken: "TOKEN", @@ -408,7 +431,7 @@ func TestAutoReconnection(t *testing.T) { ctx = wsl.WithMock(ctx, mock) } - lis, server, mockService := setUpLandscapeMock(t, ctx, "localhost:") + lis, server, mockService := setUpLandscapeMock(t, ctx, "localhost:", false) defer lis.Close() defer server.Stop() @@ -460,7 +483,7 @@ func TestAutoReconnection(t *testing.T) { }, 5*time.Second, 100*time.Millisecond, "Client should have disconnected after the server is stopped") // Restart server at the same address - lis, server, _ = setUpLandscapeMock(t, ctx, lis.Addr().String()) + lis, server, _ = setUpLandscapeMock(t, ctx, lis.Addr().String(), false) defer lis.Close() //nolint:errcheck // We don't care @@ -541,7 +564,7 @@ func TestReceiveCommands(t *testing.T) { t.Skip("This test can only run with the mock") } - lis, server, service := setUpLandscapeMock(t, ctx, "localhost:") + lis, server, service := setUpLandscapeMock(t, ctx, "localhost:", false) defer lis.Close() //nolint:errcheck // We don't care about these errors @@ -781,14 +804,31 @@ func isAppxInstalled(t *testing.T, appxPackage string) bool { } //nolint:revive // Context goes after testing.T -func setUpLandscapeMock(t *testing.T, ctx context.Context, addr string) (lis net.Listener, server *grpc.Server, service *landscapemockservice.Service) { +func setUpLandscapeMock(t *testing.T, ctx context.Context, addr string, requireCertificate bool) (lis net.Listener, server *grpc.Server, service *landscapemockservice.Service) { t.Helper() var cfg net.ListenConfig lis, err := cfg.Listen(ctx, "tcp", addr) require.NoError(t, err, "Setup: can't listen") - server = grpc.NewServer() + var opts []grpc.ServerOption + if requireCertificate { + certPath := filepath.Join(golden.TestFamilyPath(t), "certificates/cert.pem") + keyPath := filepath.Join(golden.TestFamilyPath(t), "certificates/key.pem") + + serverCert, err := tls.LoadX509KeyPair(certPath, keyPath) + require.NoError(t, err, "Setup: could not load Landscape mock server credentials") + + config := &tls.Config{ + Certificates: []tls.Certificate{serverCert}, + ClientAuth: tls.NoClientCert, + MinVersion: tls.VersionTLS12, + } + + opts = append(opts, grpc.Creds(credentials.NewTLS(config))) + } + + server = grpc.NewServer(opts...) service = landscapemockservice.New() landscapeapi.RegisterLandscapeHostAgentServer(server, service) @@ -796,15 +836,27 @@ func setUpLandscapeMock(t *testing.T, ctx context.Context, addr string) (lis net } type mockConfig struct { - proToken string - landscapeAgentURL string + proToken string + landscapeAgentURL string + landscapeClientConfig string - proTokenErr bool - landscapeURLErr bool + proTokenErr bool + landscapeURLErr bool + landscapeConfigErr bool mu sync.Mutex } +func (m *mockConfig) LandscapeClientConfig(ctx context.Context) (string, error) { + m.mu.Lock() + defer m.mu.Unlock() + + if m.landscapeConfigErr { + return "", errors.New("Mock error") + } + return m.landscapeClientConfig, nil +} + func (m *mockConfig) ProvisioningTasks(ctx context.Context, distroName string) ([]task.Task, error) { return nil, nil } diff --git a/windows-agent/internal/proservices/landscape/testdata/TestConnect/certificates/bad-certificate.pem b/windows-agent/internal/proservices/landscape/testdata/TestConnect/certificates/bad-certificate.pem new file mode 100644 index 000000000..13f344e2e --- /dev/null +++ b/windows-agent/internal/proservices/landscape/testdata/TestConnect/certificates/bad-certificate.pem @@ -0,0 +1 @@ +This is not a valid certificate diff --git a/windows-agent/internal/proservices/landscape/testdata/TestConnect/certificates/cert.pem b/windows-agent/internal/proservices/landscape/testdata/TestConnect/certificates/cert.pem new file mode 100644 index 000000000..7214408e8 --- /dev/null +++ b/windows-agent/internal/proservices/landscape/testdata/TestConnect/certificates/cert.pem @@ -0,0 +1,32 @@ +-----BEGIN CERTIFICATE----- +MIIFdDCCA1ygAwIBAgIUQ2XcjKTRgC0xuZMtwMAa0OvDxRswDQYJKoZIhvcNAQEL +BQAwQTELMAkGA1UEBhMCVVMxEjAQBgNVBAoMCUNhbm9uaWNhbDEeMBwGA1UEAwwV +Q2Fub25pY2FsR3JvdXBMaW1pdGVkMB4XDTIzMTAxMDEwMDcyM1oXDTIzMTEwOTEw +MDcyM1owQTELMAkGA1UEBhMCVVMxEjAQBgNVBAoMCUNhbm9uaWNhbDEeMBwGA1UE +AwwVQ2Fub25pY2FsR3JvdXBMaW1pdGVkMIICIjANBgkqhkiG9w0BAQEFAAOCAg8A +MIICCgKCAgEAzfI1KuyZgkgrIc48UMN5cHNEchOQlWrP9eo9+s2wTb56U7zyIdlq +4azsDXVpdL9ObhI1aLOoIMogRF1ZPgXvCH1JlGTnfjAGtChLUF2+rraUf6HGGESG +A09qFWB+JfeBrYNzRAL3rGFlttuNpW59WQIKAZ9hgKZTmBInjTNFrxMvjrAF9cYX +ebQqN6u29/+c8gH6Rf0mpPYSMahvdRT5IKZvLyaQNwQaT1UxsxSTsxAqsrXQ/o+G +UQcJpUnG3rc49B6jIDdTpiptF5Ey/f9maOjY/txQEKqLO5N04PuW7mDq17i625yJ +HAGI7ukNHQcMhcG475EUJuMLWNooozL0zBp/WAZs1cOSg0MH0TY6Oj2xET8HXoUW +T2gM/XkmWpeX9WmQBfkegbVALuKzDNnUWUYpIfCpvBL8u+9Rew+Dq7vIwmbPUAAf +0KssF+9KSRpDkmqDKZiGwbA6sAKXkLk47JDneeQ9SvANCox4fE0+qh+O2yJGygcO +T8CI0+fVZXdbS6Db5yzn8ZwbNpDw2lBahwpq1aCIwBmJ3P1ksEQXnTsyv3UKQz/F +x/zgSSqZWqgSwqKERYHxjSjSjjAORSNN+XTcKzK9Q9eA2exzw7mI1cR5mztPIRz2 +r2DVTFwLAE/ykR2t31wse5wCLiXno9I0Zm/M2Be9xw0+A4VTbFz1nwkCAwEAAaNk +MGIwHQYDVR0OBBYEFAUAonELhcOl0sZfHnTLTZNkKzmaMB8GA1UdIwQYMBaAFAUA +onELhcOl0sZfHnTLTZNkKzmaMA8GA1UdEwEB/wQFMAMBAf8wDwYDVR0RBAgwBocE +fwAAATANBgkqhkiG9w0BAQsFAAOCAgEAbmUCtPUY9KoEucjLcf3bMmk4asWKTJIr +5kkJL/s7E1WaMudNHovunk4Zx3X62FzJHR6Z04zJjdWxhmdDbE+bO2LrkwsXJNxX +NBbdP7qkSgRUvgigSTu2kc7nAQgDS7dIxyCZyQTC1opaj8t0uuxzqHpG82zzrmQ1 +8Guz1sWpBeBKgwyBM+okFvo4OcZV905hR1+sHzE1aLhoOnpX3uNwFgGh8z8jXc7p +e5FrgoEzaHYZfriioU+Lqf+92nAmtdFtNTP/g2OWunuOhEvYUI+EfPpjqSB19hvi +vD5qnV/euuWbP9mnoGVXmRcg5ZWVqgm6Yh+syRXYDEf8zwkpjod5DOKHVA0oK4hy +1zp5ufVwVETB/rdfUUS8cPXx8HqqtZienHauH/BO5OZTwSNmLrie7l/v1WUgvXBP +5k8p8wxtvXf0JNdTtNFPL82Q5W0f/4GE6PGAet4TxjNVLs9DolnTdBN7isntm9iI +d6BuuQLzNcd9J2p+7qm0Uu8gh7TeNbnBgaJnJiwCtogYMsOGFiimrF3ce3vJz/Qq +tN/od94ffjun+hWgCGQUIPZNFNNOAx7oUQmjQi0ubm/XiCEvNJHBNkvSImTidrrn +sHTu/FLkntCWzHTA8MfA3eZmqsNNUojQvVcSxs01Bwy/UKgYhcpuijRve5HsloSz +IT0QVhzpIdM= +-----END CERTIFICATE----- diff --git a/windows-agent/internal/proservices/landscape/testdata/TestConnect/certificates/generate-certificate.sh b/windows-agent/internal/proservices/landscape/testdata/TestConnect/certificates/generate-certificate.sh new file mode 100644 index 000000000..4c30a2ce8 --- /dev/null +++ b/windows-agent/internal/proservices/landscape/testdata/TestConnect/certificates/generate-certificate.sh @@ -0,0 +1,13 @@ +#!/bin/bash + +openssl req \ + -x509 \ + -newkey rsa:4096 \ + -keyout key.pem \ + -out cert.pem \ + -sha256 \ + -nodes \ + -addext 'subjectAltName = IP:127.0.0.1' \ + -subj "/C=US/O=Canonical/CN=CanonicalGroupLimited" + +echo This is not a valid certificate > bad-certificate.pem \ No newline at end of file diff --git a/windows-agent/internal/proservices/landscape/testdata/TestConnect/certificates/key.pem b/windows-agent/internal/proservices/landscape/testdata/TestConnect/certificates/key.pem new file mode 100644 index 000000000..542bfb686 --- /dev/null +++ b/windows-agent/internal/proservices/landscape/testdata/TestConnect/certificates/key.pem @@ -0,0 +1,52 @@ +-----BEGIN PRIVATE KEY----- +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 +V4lfPNzmMvwoXBp3kxjmSCzEy6TPDQ== +-----END PRIVATE KEY----- diff --git a/windows-agent/internal/proservices/landscape/testdata/TestConnect/error_when_the_config_cannot_be_parsed/landscape.conf b/windows-agent/internal/proservices/landscape/testdata/TestConnect/error_when_the_config_cannot_be_parsed/landscape.conf new file mode 100644 index 000000000..699784501 --- /dev/null +++ b/windows-agent/internal/proservices/landscape/testdata/TestConnect/error_when_the_config_cannot_be_parsed/landscape.conf @@ -0,0 +1,6 @@ +{ + "client": { + "tags": "wsl", + }, + "problem": "This should be an ini file" +} \ No newline at end of file diff --git a/windows-agent/internal/proservices/landscape/testdata/TestConnect/error_when_the_ssl_certificate_cannot_be_read/landscape.conf b/windows-agent/internal/proservices/landscape/testdata/TestConnect/error_when_the_ssl_certificate_cannot_be_read/landscape.conf new file mode 100644 index 000000000..e3d102467 --- /dev/null +++ b/windows-agent/internal/proservices/landscape/testdata/TestConnect/error_when_the_ssl_certificate_cannot_be_read/landscape.conf @@ -0,0 +1,2 @@ +[client] +ssl_public_key = testdata/TestConnect/this_path_does_not_exist/certificate.pem \ No newline at end of file diff --git a/windows-agent/internal/proservices/landscape/testdata/TestConnect/error_when_the_ssl_certificate_is_not_valid/landscape.conf b/windows-agent/internal/proservices/landscape/testdata/TestConnect/error_when_the_ssl_certificate_is_not_valid/landscape.conf new file mode 100644 index 000000000..fa967798f --- /dev/null +++ b/windows-agent/internal/proservices/landscape/testdata/TestConnect/error_when_the_ssl_certificate_is_not_valid/landscape.conf @@ -0,0 +1,2 @@ +[client] +ssl_public_key = testdata/TestConnect/certificates/bad-certificate.pem \ No newline at end of file diff --git a/windows-agent/internal/proservices/landscape/testdata/TestConnect/success_with_an_ssl_certificate/landscape.conf b/windows-agent/internal/proservices/landscape/testdata/TestConnect/success_with_an_ssl_certificate/landscape.conf new file mode 100644 index 000000000..9e0521ef5 --- /dev/null +++ b/windows-agent/internal/proservices/landscape/testdata/TestConnect/success_with_an_ssl_certificate/landscape.conf @@ -0,0 +1,2 @@ +[client] +ssl_public_key = testdata/TestConnect/certificates/cert.pem \ No newline at end of file diff --git a/windows-agent/internal/proservices/landscape/testdata/TestConnect/success_with_non-empty_config/landscape.conf b/windows-agent/internal/proservices/landscape/testdata/TestConnect/success_with_non-empty_config/landscape.conf new file mode 100644 index 000000000..a8a807a95 --- /dev/null +++ b/windows-agent/internal/proservices/landscape/testdata/TestConnect/success_with_non-empty_config/landscape.conf @@ -0,0 +1,3 @@ +[client] +hello=world +tags=wsl \ No newline at end of file diff --git a/windows-agent/internal/tasks/landscape_configure.go b/windows-agent/internal/tasks/landscape_configure.go index 26b2be35c..9f5104e93 100644 --- a/windows-agent/internal/tasks/landscape_configure.go +++ b/windows-agent/internal/tasks/landscape_configure.go @@ -1,16 +1,10 @@ package tasks import ( - "bytes" "context" - "fmt" - "strings" "github.com/canonical/ubuntu-pro-for-windows/windows-agent/internal/distros/task" - log "github.com/canonical/ubuntu-pro-for-windows/windows-agent/internal/grpc/logstreamer" "github.com/canonical/ubuntu-pro-for-windows/wslserviceapi" - "github.com/ubuntu/decorate" - "gopkg.in/ini.v1" ) func init() { @@ -24,48 +18,6 @@ type LandscapeConfigure struct { Config string } -// NewLandscapeConfigure creates a LandscapeConfigure. It overrides the name of the Landscape -// client so it matches the name of the distro. -func NewLandscapeConfigure(ctx context.Context, Config string, distroName string) (conf LandscapeConfigure, err error) { - defer decorate.OnError(&err, "NewLandscapeConfigure error") - - if Config == "" { - // Landscape disablement - return LandscapeConfigure{}, nil - } - - r := strings.NewReader(Config) - data, err := ini.Load(r) - if err != nil { - return LandscapeConfigure{}, fmt.Errorf("could not parse config: %v", err) - } - - const section = "client" - s, err := data.GetSection(section) - if err != nil { - return LandscapeConfigure{}, fmt.Errorf("could not find [%s] section: %v", section, err) - } - - const key = "computer_title" - if s.HasKey(key) { - log.Infof(ctx, "Landscape config contains key %q. Its value will be overridden with %s", key, distroName) - s.DeleteKey(key) - } - - if _, err := s.NewKey(key, distroName); err != nil { - return LandscapeConfigure{}, fmt.Errorf("could not create %q key", key) - } - - w := &bytes.Buffer{} - if _, err := data.WriteTo(w); err != nil { - return LandscapeConfigure{}, fmt.Errorf("could not write modified config: %v", err) - } - - return LandscapeConfigure{ - Config: w.String(), - }, nil -} - // Execute sends the config to the target WSL-Pro-Service so that the distro can be // registered in Landscape. func (t LandscapeConfigure) Execute(ctx context.Context, client wslserviceapi.WSLClient) error { diff --git a/windows-agent/internal/tasks/landscape_configure_test.go b/windows-agent/internal/tasks/landscape_configure_test.go deleted file mode 100644 index 939106ef9..000000000 --- a/windows-agent/internal/tasks/landscape_configure_test.go +++ /dev/null @@ -1,69 +0,0 @@ -package tasks_test - -import ( - "context" - "os" - "path/filepath" - "reflect" - "testing" - - "github.com/canonical/ubuntu-pro-for-windows/common/golden" - "github.com/canonical/ubuntu-pro-for-windows/windows-agent/internal/tasks" - "github.com/stretchr/testify/require" - "gopkg.in/ini.v1" -) - -func TestNewLandscapeConfigure(t *testing.T) { - t.Parallel() - - testCases := map[string]struct { - wrongDistroName bool - - wantErr bool - wantEmptyConfig bool - }{ - "Success enabling": {}, - "Success enabling when the computer_title key already exists": {}, - "Success disabling": {wantEmptyConfig: true}, - - "Error enabling when there is no client section": {wantErr: true}, - "Error enabling when the file cannot be parsed": {wantErr: true}, - } - - for name, tc := range testCases { - tc := tc - t.Run(name, func(t *testing.T) { - t.Parallel() - ctx := context.Background() - - conf, err := os.ReadFile(filepath.Join(golden.TestFixturePath(t), "landscape-client.conf")) - require.NoError(t, err, "Setup: could not load config") - - t.Log(string(conf)) - - const distroName = "TEST_DISTRO_NAME" - task, err := tasks.NewLandscapeConfigure(ctx, string(conf), distroName) - t.Log(task.Config) - if tc.wantErr { - require.Error(t, err, "NewLandscapeConfigure should have returned an error") - return - } - require.NoError(t, err, "NewLandscapeConfigure should have succeeded") - - require.Equal(t, reflect.TypeOf(task).Name(), task.String(), "Task String() does not match the name of the task") - - if tc.wantEmptyConfig { - require.Empty(t, task.Config, "Config was expected to be empty") - return - } - require.NotEmpty(t, task.Config, "Config was not expected to be empty") - - d, err := ini.Load([]byte(task.Config)) - require.NoError(t, err, "could not load config as ini file") - - require.True(t, d.HasSection("client"), "section [client] was expected") - require.True(t, d.Section("client").HasKey("computer_title"), "key computer_title was expected") - require.Equal(t, distroName, d.Section("client").Key("computer_title").Value(), "key computer_title was expected to equal the distro name") - }) - } -} diff --git a/wsl-pro-service/internal/system/landscape.go b/wsl-pro-service/internal/system/landscape.go index 5c752a3a3..cf02bb8b5 100644 --- a/wsl-pro-service/internal/system/landscape.go +++ b/wsl-pro-service/internal/system/landscape.go @@ -1,13 +1,17 @@ package system import ( + "bytes" "context" "fmt" "os" "os/exec" "path/filepath" + "strings" + log "github.com/canonical/ubuntu-pro-for-windows/wsl-pro-service/internal/grpc/logstreamer" "github.com/ubuntu/decorate" + "gopkg.in/ini.v1" ) const ( @@ -19,6 +23,10 @@ func (s *System) LandscapeEnable(ctx context.Context, landscapeConfig string) (e // Decorating here to avoid stuttering the URL (url package prints it as well) defer decorate.OnError(&err, "could not register to landscape") + if landscapeConfig, err = modifyConfig(ctx, s, landscapeConfig); err != nil { + return err + } + if err := s.writeConfig(landscapeConfig); err != nil { return err } @@ -66,3 +74,93 @@ func (s *System) writeConfig(landscapeConfig string) (err error) { return nil } + +// modifyConfig overrides parameters in the configuration to adapt them to the current distro. +func modifyConfig(ctx context.Context, s *System, landscapeConfig string) (string, error) { + if landscapeConfig == "" { + return "", nil + } + + r := strings.NewReader(landscapeConfig) + data, err := ini.Load(r) + if err != nil { + return "", fmt.Errorf("could not parse config: %v", err) + } + + if err := overrideComputerTitle(ctx, s, data); err != nil { + return "", err + } + + if err := overrideSSLCertificate(ctx, s, data); err != nil { + return "", fmt.Errorf("could not override SSL certificate path: %v", err) + } + + w := &bytes.Buffer{} + if _, err := data.WriteTo(w); err != nil { + return "", fmt.Errorf("could not write modified config: %v", err) + } + + return w.String(), nil +} + +// overrideComputerTitle overrides the computer_title field in the Landscape config. +func overrideComputerTitle(ctx context.Context, s *System, data *ini.File) error { + const section = "client" + const key = "computer_title" + + distroName, err := s.wslDistroName(ctx) + if err != nil { + return err + } + + sec, err := data.GetSection(section) + if err != nil { + if sec, err = data.NewSection(section); err != nil { + return fmt.Errorf("could not find nor create section %q: %v", section, err) + } + } + + if sec.HasKey(key) { + log.Infof(ctx, "Landscape config contains key %q. Its value will be overridden with %s", key, distroName) + sec.DeleteKey(key) + } + + if _, err := sec.NewKey(key, distroName); err != nil { + return fmt.Errorf("could not create %q key", key) + } + + return nil +} + +// overrideComputerTitle converts the ssl_public_key field in the Landscape config +// from a Windows path to a Linux path. +func overrideSSLCertificate(ctx context.Context, s *System, data *ini.File) error { + const section = "client" + const key = "ssl_public_key" + + sec, err := data.GetSection(section) + if err != nil { + // No certificate + return nil + } + + k, err := sec.GetKey(key) + if err != nil { + // No certificate + return nil + } + + pathWindows := k.String() + + cmd, args := s.backend.WslpathExecutable("-ua", pathWindows) + //nolint:gosec // In production code, the executable (wslpath) is hardcoded. + out, err := exec.CommandContext(ctx, cmd, args...).CombinedOutput() + if err != nil { + return fmt.Errorf("could not translate SSL certificate path %q to a WSL path: %v: %s", pathWindows, err, out) + } + + pathLinux := s.Path(strings.TrimSpace(string(out))) + k.SetValue(pathLinux) + + return nil +} diff --git a/wsl-pro-service/internal/system/system_test.go b/wsl-pro-service/internal/system/system_test.go index 10c54af0f..b9baf9c0f 100644 --- a/wsl-pro-service/internal/system/system_test.go +++ b/wsl-pro-service/internal/system/system_test.go @@ -181,7 +181,7 @@ func TestLocalAppData(t *testing.T) { require.Equal(t, cmdExePath, *system.CmdExeCache(), "Unexpected path for cmd.exe") // Validating LocalAppData - wantSuffix := `/mnt/c/Users/TestUser/AppData/Local` + wantSuffix := `/mnt/d/Users/TestUser/AppData/Local` require.True(t, strings.HasSuffix(got, wantSuffix), "Unexpected value returned by LocalAppData.\nWant suffix: %s\nGot: %s", wantSuffix, got) }) } @@ -351,13 +351,18 @@ func TestLandscapeEnable(t *testing.T) { testCases := map[string]struct { breakWriteConfig bool breakLandscapeConfig bool + breakWSLPath bool wantErr bool }{ - "Success": {}, - - "Error when the config file cannot be written": {breakWriteConfig: true, wantErr: true}, - "Error when the landscape-config command fails": {breakLandscapeConfig: true, wantErr: true}, + "Success": {}, + "Success overriding computer_title": {}, + "Success overriding the SSL certficate path": {}, + + "Error when the file cannot be parsed": {wantErr: true}, + "Error when the config file cannot be written": {breakWriteConfig: true, wantErr: true}, + "Error when the landscape-config command fails": {breakLandscapeConfig: true, wantErr: true}, + "Error when failing to override the SSL certficate path": {breakWSLPath: true, wantErr: true}, } for name, tc := range testCases { @@ -377,9 +382,14 @@ func TestLandscapeEnable(t *testing.T) { mock.SetControlArg(testutils.LandscapeEnableErr) } - config := "[example]\nnumber = 5\ntext = Lorem ipsum dolot sit amet" + if tc.breakWSLPath { + mock.SetControlArg(testutils.WslpathErr) + } + + config, err := os.ReadFile(filepath.Join(golden.TestFixturePath(t), "landscape.conf")) + require.NoError(t, err, "Setup: could not load fixture") - err := s.LandscapeEnable(ctx, config) + err = s.LandscapeEnable(ctx, string(config)) if tc.wantErr { require.Error(t, err, "LandscapeEnable should have returned an error") return @@ -391,7 +401,12 @@ func TestLandscapeEnable(t *testing.T) { out, err := os.ReadFile(exeProof) require.NoErrorf(t, err, "could not read file %q", exeProof) - require.Equal(t, config, string(out), "Landscape executable did not receive the right config") + // We mock the filesystem, and the mocked filesystem root is not the same between + // runs, so the golden file would never match. This is the solution: + got := strings.ReplaceAll(string(out), mock.FsRoot, "${FILESYSTEM_ROOT}") + + want := golden.LoadWithUpdateFromGolden(t, got) + require.Equal(t, want, got, "Landscape executable did not receive the right config") }) } } diff --git a/wsl-pro-service/internal/system/testdata/TestLandscapeEnable/error_when_failing_to_override_the_ssl_certficate_path/landscape.conf b/wsl-pro-service/internal/system/testdata/TestLandscapeEnable/error_when_failing_to_override_the_ssl_certficate_path/landscape.conf new file mode 100644 index 000000000..f256eee6e --- /dev/null +++ b/wsl-pro-service/internal/system/testdata/TestLandscapeEnable/error_when_failing_to_override_the_ssl_certficate_path/landscape.conf @@ -0,0 +1,3 @@ +[client] +hello = world +ssl_public_key = D:\Users\TestUser\certificate \ No newline at end of file diff --git a/wsl-pro-service/internal/system/testdata/TestLandscapeEnable/error_when_the_config_file_cannot_be_written/landscape.conf b/wsl-pro-service/internal/system/testdata/TestLandscapeEnable/error_when_the_config_file_cannot_be_written/landscape.conf new file mode 100644 index 000000000..41d49280c --- /dev/null +++ b/wsl-pro-service/internal/system/testdata/TestLandscapeEnable/error_when_the_config_file_cannot_be_written/landscape.conf @@ -0,0 +1,2 @@ +[client] +hello = world \ No newline at end of file diff --git a/wsl-pro-service/internal/system/testdata/TestLandscapeEnable/error_when_the_file_cannot_be_parsed/landscape.conf b/wsl-pro-service/internal/system/testdata/TestLandscapeEnable/error_when_the_file_cannot_be_parsed/landscape.conf new file mode 100644 index 000000000..b600b3a46 --- /dev/null +++ b/wsl-pro-service/internal/system/testdata/TestLandscapeEnable/error_when_the_file_cannot_be_parsed/landscape.conf @@ -0,0 +1,3 @@ +{ + "message": "Wait a second, this is not an ini file!" +} \ No newline at end of file diff --git a/wsl-pro-service/internal/system/testdata/TestLandscapeEnable/error_when_the_landscape-config_command_fails/landscape.conf b/wsl-pro-service/internal/system/testdata/TestLandscapeEnable/error_when_the_landscape-config_command_fails/landscape.conf new file mode 100644 index 000000000..41d49280c --- /dev/null +++ b/wsl-pro-service/internal/system/testdata/TestLandscapeEnable/error_when_the_landscape-config_command_fails/landscape.conf @@ -0,0 +1,2 @@ +[client] +hello = world \ No newline at end of file diff --git a/wsl-pro-service/internal/system/testdata/TestLandscapeEnable/golden/success b/wsl-pro-service/internal/system/testdata/TestLandscapeEnable/golden/success new file mode 100644 index 000000000..1a9966055 --- /dev/null +++ b/wsl-pro-service/internal/system/testdata/TestLandscapeEnable/golden/success @@ -0,0 +1,3 @@ +[client] +hello = world +computer_title = TEST_DISTRO diff --git a/wsl-pro-service/internal/system/testdata/TestLandscapeEnable/golden/success_despite_failing_to_override_the_ssl_certficate_path b/wsl-pro-service/internal/system/testdata/TestLandscapeEnable/golden/success_despite_failing_to_override_the_ssl_certficate_path new file mode 100644 index 000000000..3ee8ae0a7 --- /dev/null +++ b/wsl-pro-service/internal/system/testdata/TestLandscapeEnable/golden/success_despite_failing_to_override_the_ssl_certficate_path @@ -0,0 +1,4 @@ +[client] +hello = world +ssl_public_key = D:\Users\TestUser\certificate +computer_title = TEST_DISTRO diff --git a/wsl-pro-service/internal/system/testdata/TestLandscapeEnable/golden/success_overriding_computer_title b/wsl-pro-service/internal/system/testdata/TestLandscapeEnable/golden/success_overriding_computer_title new file mode 100644 index 000000000..1a9966055 --- /dev/null +++ b/wsl-pro-service/internal/system/testdata/TestLandscapeEnable/golden/success_overriding_computer_title @@ -0,0 +1,3 @@ +[client] +hello = world +computer_title = TEST_DISTRO diff --git a/wsl-pro-service/internal/system/testdata/TestLandscapeEnable/golden/success_overriding_the_ssl_certficate_path b/wsl-pro-service/internal/system/testdata/TestLandscapeEnable/golden/success_overriding_the_ssl_certficate_path new file mode 100644 index 000000000..e9563aa4f --- /dev/null +++ b/wsl-pro-service/internal/system/testdata/TestLandscapeEnable/golden/success_overriding_the_ssl_certficate_path @@ -0,0 +1,4 @@ +[client] +hello = world +ssl_public_key = ${FILESYSTEM_ROOT}/mnt/d/Users/TestUser/certificate +computer_title = TEST_DISTRO diff --git a/wsl-pro-service/internal/system/testdata/TestLandscapeEnable/success/landscape.conf b/wsl-pro-service/internal/system/testdata/TestLandscapeEnable/success/landscape.conf new file mode 100644 index 000000000..41d49280c --- /dev/null +++ b/wsl-pro-service/internal/system/testdata/TestLandscapeEnable/success/landscape.conf @@ -0,0 +1,2 @@ +[client] +hello = world \ No newline at end of file diff --git a/wsl-pro-service/internal/system/testdata/TestLandscapeEnable/success_overriding_computer_title/landscape.conf b/wsl-pro-service/internal/system/testdata/TestLandscapeEnable/success_overriding_computer_title/landscape.conf new file mode 100644 index 000000000..974df0251 --- /dev/null +++ b/wsl-pro-service/internal/system/testdata/TestLandscapeEnable/success_overriding_computer_title/landscape.conf @@ -0,0 +1,3 @@ +[client] +computer_title = DEFAULT_COMPUTER_TITLE +hello = world diff --git a/wsl-pro-service/internal/system/testdata/TestLandscapeEnable/success_overriding_the_ssl_certficate_path/landscape.conf b/wsl-pro-service/internal/system/testdata/TestLandscapeEnable/success_overriding_the_ssl_certficate_path/landscape.conf new file mode 100644 index 000000000..f256eee6e --- /dev/null +++ b/wsl-pro-service/internal/system/testdata/TestLandscapeEnable/success_overriding_the_ssl_certficate_path/landscape.conf @@ -0,0 +1,3 @@ +[client] +hello = world +ssl_public_key = D:\Users\TestUser\certificate \ No newline at end of file diff --git a/wsl-pro-service/internal/testutils/mock_executables.go b/wsl-pro-service/internal/testutils/mock_executables.go index 6421491c0..bbbbd03db 100644 --- a/wsl-pro-service/internal/testutils/mock_executables.go +++ b/wsl-pro-service/internal/testutils/mock_executables.go @@ -42,11 +42,12 @@ var ( // defaultWindowsMount is the default path used in tests to set the windows filesystem mount. defaultWindowsMount = "/mnt/d/" - // defaultLocalAppDataDir is the default path used in tests to store Windows agent data. - defaultLocalAppDataDir = filepath.Join(defaultWindowsMount, "/mnt/c/Users/TestUser/AppData/Local/") + // windowsLocalAppDataDir is the default path used in tests to store Windows agent data. + windowsLocalAppDataDir = `D:\Users\TestUser\AppData\Local\` + linuxLocalAppDataDir = "/mnt/d/Users/TestUser/AppData/Local/" - // defaultLocalAppDataDir is the default path used in tests to store the address of the Windows Agent service. - defaultAddrFile = filepath.Join(defaultLocalAppDataDir, common.LocalAppDataDir, common.ListeningPortFileName) + // defaultAddrFile is the default path used in tests to store the address of the Windows Agent service. + defaultAddrFile = filepath.Join(linuxLocalAppDataDir, common.LocalAppDataDir, common.ListeningPortFileName) //go:embed filesystem_defaults/os-release defaultOsReleaseContents []byte @@ -446,7 +447,12 @@ func WslPathMock(t *testing.T) { return exitError } - if argv[1] != defaultLocalAppDataDir { + stdout, ok := map[string]string{ + windowsLocalAppDataDir: linuxLocalAppDataDir, + `D:\Users\TestUser\certificate`: filepath.Join(defaultWindowsMount, "Users/TestUser/certificate"), + }[argv[1]] + + if !ok { fmt.Fprintf(os.Stderr, "Mock not implemented for args %q\n", argv) return exitBadUsage } @@ -456,7 +462,7 @@ func WslPathMock(t *testing.T) { return exitOk } - fmt.Fprintf(os.Stdout, "%s\r\n", defaultLocalAppDataDir) + fmt.Fprintf(os.Stdout, "%s\r\n", stdout) return exitOk default: @@ -497,7 +503,7 @@ func CmdExeMock(t *testing.T) { return exitError } - fmt.Fprintln(os.Stdout, defaultLocalAppDataDir) + fmt.Fprintln(os.Stdout, windowsLocalAppDataDir) return exitOk }) } diff --git a/wsl-pro-service/internal/wslinstanceservice/testdata/TestApplyLandscapeConfig/golden/success_enabling b/wsl-pro-service/internal/wslinstanceservice/testdata/TestApplyLandscapeConfig/golden/success_enabling new file mode 100644 index 000000000..e0016e6c5 --- /dev/null +++ b/wsl-pro-service/internal/wslinstanceservice/testdata/TestApplyLandscapeConfig/golden/success_enabling @@ -0,0 +1,5 @@ +[hello] +world = true + +[client] +computer_title = TEST_DISTRO diff --git a/wsl-pro-service/internal/wslinstanceservice/wslinstanceservice_test.go b/wsl-pro-service/internal/wslinstanceservice/wslinstanceservice_test.go index 4bb1ab77b..92c0e9334 100644 --- a/wsl-pro-service/internal/wslinstanceservice/wslinstanceservice_test.go +++ b/wsl-pro-service/internal/wslinstanceservice/wslinstanceservice_test.go @@ -9,6 +9,7 @@ import ( "time" agentapi "github.com/canonical/ubuntu-pro-for-windows/agentapi/go" + "github.com/canonical/ubuntu-pro-for-windows/common/golden" "github.com/canonical/ubuntu-pro-for-windows/wsl-pro-service/internal/system" "github.com/canonical/ubuntu-pro-for-windows/wsl-pro-service/internal/testutils" "github.com/canonical/ubuntu-pro-for-windows/wsl-pro-service/internal/wslinstanceservice" @@ -188,8 +189,11 @@ func TestApplyLandscapeConfig(t *testing.T) { p := mock.Path("/.landscape-enabled") require.FileExists(t, p, "Landscape executable was not called to enable") out, err := os.ReadFile(p) + + want := golden.LoadWithUpdateFromGolden(t, string(out)) + require.NoError(t, err, "Could not read .landscape-enabled file") - require.Equal(t, config, string(out), "Landscape config does not match expectation") + require.Equal(t, want, string(out), "Landscape config does not match expectation") }) } }