From ef0534c3eea85e1b6dbca947241714777338428d Mon Sep 17 00:00:00 2001 From: Andrew Wilkins Date: Wed, 8 Feb 2017 13:44:43 +0800 Subject: [PATCH] provider/lxd: add interactive auth-type Add the "interactive" auth-type for LXD, which is used in add-credential for interactively adding a credential for a LXD cloud. Currently we only support generating credentials for local LXD; later we will extend this to support generating credentials for remote, untrusted LXD by prompting the user to verify the certificate fingerprint and enter a trust password. --- cmd/juju/backups/restore_test.go | 2 +- provider/lxd/credentials.go | 115 +++++++++++++++++++---- provider/lxd/credentials_test.go | 151 +++++++++++++++++++++++-------- provider/lxd/provider.go | 1 + provider/lxd/provider_test.go | 9 +- provider/lxd/testing_test.go | 4 +- 6 files changed, 219 insertions(+), 63 deletions(-) diff --git a/cmd/juju/backups/restore_test.go b/cmd/juju/backups/restore_test.go index 20f9dd698a3..eec1c24a1ab 100644 --- a/cmd/juju/backups/restore_test.go +++ b/cmd/juju/backups/restore_test.go @@ -283,7 +283,7 @@ func (s *restoreSuite) TestRestoreReboostrapBuiltInProvider(c *gc.C) { c.Assert(args.Cloud, jc.DeepEquals, cloud.Cloud{ Name: "lxd", Type: "lxd", - AuthTypes: []cloud.AuthType{"certificate"}, + AuthTypes: []cloud.AuthType{"certificate", "interactive"}, Regions: []cloud.Region{{Name: "localhost"}}, }) return nil diff --git a/provider/lxd/credentials.go b/provider/lxd/credentials.go index 656bcf64916..75dcf1bdc67 100644 --- a/provider/lxd/credentials.go +++ b/provider/lxd/credentials.go @@ -6,6 +6,8 @@ package lxd import ( + "fmt" + "io" "io/ioutil" "net" "os" @@ -24,6 +26,11 @@ const ( credAttrServerCert = "server-cert" credAttrClientCert = "client-cert" credAttrClientKey = "client-key" + + // interactiveAuthType is a credential auth-type provided as an option to + // "juju add-credential", which takes the user through the process of + // generating a certificate credential. + interactiveAuthType = "interactive" ) // environProviderCredentials implements environs.ProviderCredentials. @@ -37,6 +44,8 @@ type environProviderCredentials struct { // CredentialSchemas is part of the environs.ProviderCredentials interface. func (environProviderCredentials) CredentialSchemas() map[cloud.AuthType]cloud.CredentialSchema { return map[cloud.AuthType]cloud.CredentialSchema{ + interactiveAuthType: {}, + cloud.CertificateAuthType: {{ credAttrServerCert, cloud.CredentialAttr{ @@ -58,26 +67,38 @@ func (environProviderCredentials) CredentialSchemas() map[cloud.AuthType]cloud.C // DetectCredentials is part of the environs.ProviderCredentials interface. func (p environProviderCredentials) DetectCredentials() (*cloud.CloudCredential, error) { - certPEM, keyPEM, err := p.readOrGenerateCert() + raw, err := p.newLocalRawProvider() + if err != nil { + return nil, errors.NewNotFound(err, "failed to connecti to local LXD") + } + + nopLogf := func(string, ...interface{}) {} + certPEM, keyPEM, err := p.readOrGenerateCert(nopLogf) + if err != nil { + return nil, errors.Trace(err) + } + + const credName = "localhost" + label := fmt.Sprintf("LXD credential %q", credName) + certCredential, err := p.finalizeLocalCertificateCredential( + ioutil.Discard, raw, string(certPEM), string(keyPEM), label, + ) if err != nil { return nil, errors.Trace(err) } - certCredential := cloud.NewCredential(cloud.CertificateAuthType, map[string]string{ - credAttrClientCert: string(certPEM), - credAttrClientKey: string(keyPEM), - }) return &cloud.CloudCredential{ - AuthCredentials: map[string]cloud.Credential{"default": certCredential}, + AuthCredentials: map[string]cloud.Credential{credName: *certCredential}, }, nil } -func (p environProviderCredentials) readOrGenerateCert() (certPEM, keyPEM []byte, _ error) { +func (p environProviderCredentials) readOrGenerateCert(logf func(string, ...interface{})) (certPEM, keyPEM []byte, _ error) { // First look in the Juju XDG_DATA dir. This allows the user // to explicitly override the certificates used by the lxc // client if they wish. jujuLXDDir := osenv.JujuXDGDataHomePath("lxd") certPEM, keyPEM, err := readCert(jujuLXDDir) if err == nil { + logf("Loaded client cert/key from %q", jujuLXDDir) return certPEM, keyPEM, nil } else if !os.IsNotExist(err) { return nil, nil, errors.Trace(err) @@ -89,6 +110,7 @@ func (p environProviderCredentials) readOrGenerateCert() (certPEM, keyPEM []byte lxdConfigDir := filepath.Join(utils.Home(), ".config", "lxc") certPEM, keyPEM, err = readCert(lxdConfigDir) if err == nil { + logf("Loaded client cert/key from %q", lxdConfigDir) return certPEM, keyPEM, nil } else if !os.IsNotExist(err) { return nil, nil, errors.Trace(err) @@ -104,6 +126,7 @@ func (p environProviderCredentials) readOrGenerateCert() (certPEM, keyPEM []byte if err := writeCert(jujuLXDDir, certPEM, keyPEM); err != nil { return nil, nil, errors.Trace(err) } + logf("Generating client cert/key in %q", jujuLXDDir) return certPEM, keyPEM, nil } @@ -138,13 +161,41 @@ func writeCert(dir string, certPEM, keyPEM []byte) error { // FinalizeCredential is part of the environs.ProviderCredentials interface. func (p environProviderCredentials) FinalizeCredential(ctx environs.FinalizeCredentialContext, args environs.FinalizeCredentialParams) (*cloud.Credential, error) { + var interactive bool + switch authType := args.Credential.AuthType(); authType { + case interactiveAuthType: + stderr := ctx.GetStderr() + logf := func(s string, args ...interface{}) { + fmt.Fprintf(stderr, s+"\n", args...) + } + certPEM, keyPEM, err := p.readOrGenerateCert(logf) + if err != nil { + return nil, errors.Trace(err) + } + certCredential := cloud.NewCredential(cloud.CertificateAuthType, map[string]string{ + credAttrClientCert: string(certPEM), + credAttrClientKey: string(keyPEM), + }) + certCredential.Label = args.Credential.Label + args.Credential = certCredential + interactive = true + fallthrough + case cloud.CertificateAuthType: + return p.finalizeCertificateCredential(ctx, args, interactive) + default: + return &args.Credential, nil + } +} + +func (p environProviderCredentials) finalizeCertificateCredential( + ctx environs.FinalizeCredentialContext, + args environs.FinalizeCredentialParams, + interactive bool, +) (*cloud.Credential, error) { // Credential detection yields a partial certificate containing just // the client certificate and key. We check if we have a partial // credential, and fill in the server certificate if we can. - - if args.Credential.AuthType() != cloud.CertificateAuthType { - return &args.Credential, nil - } + stderr := ctx.GetStderr() credAttrs := args.Credential.Attributes() if credAttrs[credAttrServerCert] != "" { @@ -174,16 +225,37 @@ func (p environProviderCredentials) FinalizeCredential(ctx environs.FinalizeCred // $HOME/.config/lxc/config.yml to identify the remote by its // endpoint. // - // We may later want to add an "interactive" auth-type - // that users can select when adding credentials to - // go through the interactive server certificate fingerprint - // verification flow. - return &args.Credential, nil + // TODO(axw) for the "interactive" auth-type, we should take + // the user through the server certificate fingerprint + // verification and trust password flow. + prefix := "cannot auto-generate credential for remote LXD" + if interactive { + prefix = "certificate upload for remote LXD unsupported" + } + return nil, errors.Errorf(`%s + +Until support is added for verifying and authenticating to remote LXD hosts, +you must generate the credential on the LXD host, and add the credential to +this client using "juju add-credential localhost". + +See: https://jujucharms.com/docs/stable/clouds-LXD +`, prefix) } raw, err := p.newLocalRawProvider() if err != nil { return nil, errors.Trace(err) } + return p.finalizeLocalCertificateCredential( + stderr, raw, certPEM, keyPEM, + args.Credential.Label, + ) +} + +func (p environProviderCredentials) finalizeLocalCertificateCredential( + output io.Writer, + raw *rawProvider, + certPEM, keyPEM, label string, +) (*cloud.Credential, error) { // Upload the certificate to the server if necessary. clientCert := lxdclient.Cert{ @@ -215,6 +287,8 @@ func (p environProviderCredentials) FinalizeCredential(ctx environs.FinalizeCred // there was a concurrent AddCert by another // process. Carry on. } + fmt.Fprintln(output, "Uploaded certificate to LXD server.") + } else if err != nil { return nil, errors.Annotate(err, "querying certificates") } @@ -224,9 +298,12 @@ func (p environProviderCredentials) FinalizeCredential(ctx environs.FinalizeCred if err != nil { return nil, errors.Annotate(err, "getting server status") } - credAttrs[credAttrServerCert] = serverState.Environment.Certificate - out := cloud.NewCredential(cloud.CertificateAuthType, credAttrs) - out.Label = args.Credential.Label + out := cloud.NewCredential(cloud.CertificateAuthType, map[string]string{ + credAttrClientCert: certPEM, + credAttrClientKey: keyPEM, + credAttrServerCert: serverState.Environment.Certificate, + }) + out.Label = label return &out, nil } diff --git a/provider/lxd/credentials_test.go b/provider/lxd/credentials_test.go index 55fe8a6eb22..9c59b1feb98 100644 --- a/provider/lxd/credentials_test.go +++ b/provider/lxd/credentials_test.go @@ -19,6 +19,7 @@ import ( envtesting "github.com/juju/juju/environs/testing" "github.com/juju/juju/juju/osenv" "github.com/juju/juju/provider/lxd" + coretesting "github.com/juju/juju/testing" ) type credentialsSuite struct { @@ -28,29 +29,33 @@ type credentialsSuite struct { var _ = gc.Suite(&credentialsSuite{}) func (s *credentialsSuite) TestCredentialSchemas(c *gc.C) { - envtesting.AssertProviderAuthTypes(c, s.Provider, "certificate") + envtesting.AssertProviderAuthTypes(c, s.Provider, "interactive", "certificate") } func (s *credentialsSuite) TestDetectCredentialsUsesLXCCert(c *gc.C) { home := c.MkDir() utils.SetHome(home) - s.writeFile(c, filepath.Join(home, ".config/lxc/client.crt"), "client-cert-data") - s.writeFile(c, filepath.Join(home, ".config/lxc/client.key"), "client-key-data") + s.writeFile(c, filepath.Join(home, ".config/lxc/client.crt"), coretesting.CACert+"lxc-client") + s.writeFile(c, filepath.Join(home, ".config/lxc/client.key"), coretesting.CAKey+"lxc-client") + + credential := cloud.NewCredential( + cloud.CertificateAuthType, + map[string]string{ + "client-cert": coretesting.CACert + "lxc-client", + "client-key": coretesting.CAKey + "lxc-client", + "server-cert": "server-cert", + }, + ) + credential.Label = `LXD credential "localhost"` credentials, err := s.Provider.DetectCredentials() c.Assert(err, jc.ErrorIsNil) c.Assert(credentials, jc.DeepEquals, &cloud.CloudCredential{ AuthCredentials: map[string]cloud.Credential{ - "default": cloud.NewCredential( - cloud.CertificateAuthType, - map[string]string{ - "client-cert": "client-cert-data", - "client-key": "client-key-data", - }, - ), + "localhost": credential, }, }) - s.Stub.CheckCallNames(c) + s.Stub.CheckCallNames(c, "CertByFingerprint", "ServerStatus") } func (s *credentialsSuite) TestDetectCredentialsUsesJujuLXDCert(c *gc.C) { @@ -59,25 +64,29 @@ func (s *credentialsSuite) TestDetectCredentialsUsesJujuLXDCert(c *gc.C) { home := c.MkDir() utils.SetHome(home) xdg := osenv.JujuXDGDataHomeDir() - s.writeFile(c, filepath.Join(home, ".config/lxc/client.crt"), "lxc-client-cert-data") - s.writeFile(c, filepath.Join(home, ".config/lxc/client.key"), "lxc-client-key-data") - s.writeFile(c, filepath.Join(xdg, "lxd/client.crt"), "juju-client-cert-data") - s.writeFile(c, filepath.Join(xdg, "lxd/client.key"), "juju-client-key-data") + s.writeFile(c, filepath.Join(home, ".config/lxc/client.crt"), coretesting.CACert+"lxc-client") + s.writeFile(c, filepath.Join(home, ".config/lxc/client.key"), coretesting.CAKey+"lxc-client") + s.writeFile(c, filepath.Join(xdg, "lxd/client.crt"), coretesting.CACert+"juju-client") + s.writeFile(c, filepath.Join(xdg, "lxd/client.key"), coretesting.CAKey+"juju-client") + + credential := cloud.NewCredential( + cloud.CertificateAuthType, + map[string]string{ + "client-cert": coretesting.CACert + "juju-client", + "client-key": coretesting.CAKey + "juju-client", + "server-cert": "server-cert", + }, + ) + credential.Label = `LXD credential "localhost"` credentials, err := s.Provider.DetectCredentials() c.Assert(err, jc.ErrorIsNil) c.Assert(credentials, jc.DeepEquals, &cloud.CloudCredential{ AuthCredentials: map[string]cloud.Credential{ - "default": cloud.NewCredential( - cloud.CertificateAuthType, - map[string]string{ - "client-cert": "juju-client-cert-data", - "client-key": "juju-client-key-data", - }, - ), + "localhost": credential, }, }) - s.Stub.CheckCallNames(c) + s.Stub.CheckCallNames(c, "CertByFingerprint", "ServerStatus") } func (S *credentialsSuite) writeFile(c *gc.C, path, content string) { @@ -88,34 +97,38 @@ func (S *credentialsSuite) writeFile(c *gc.C, path, content string) { } func (s *credentialsSuite) TestDetectCredentialsGeneratesCert(c *gc.C) { + credential := cloud.NewCredential( + cloud.CertificateAuthType, + map[string]string{ + "client-cert": coretesting.CACert + "generated", + "client-key": coretesting.CAKey + "generated", + "server-cert": "server-cert", + }, + ) + credential.Label = `LXD credential "localhost"` + credentials, err := s.Provider.DetectCredentials() c.Assert(err, jc.ErrorIsNil) c.Assert(credentials, jc.DeepEquals, &cloud.CloudCredential{ AuthCredentials: map[string]cloud.Credential{ - "default": cloud.NewCredential( - cloud.CertificateAuthType, - map[string]string{ - "client-cert": "client.crt", - "client-key": "client.key", - }, - ), + "localhost": credential, }, }) - s.Stub.CheckCallNames(c, "GenerateMemCert") + s.Stub.CheckCallNames(c, "GenerateMemCert", "CertByFingerprint", "ServerStatus") // The cert/key pair should have been cached in the juju/lxd dir. xdg := osenv.JujuXDGDataHomeDir() content, err := ioutil.ReadFile(filepath.Join(xdg, "lxd/client.crt")) c.Assert(err, jc.ErrorIsNil) - c.Assert(string(content), gc.Equals, "client.crt") + c.Assert(string(content), gc.Equals, coretesting.CACert+"generated") content, err = ioutil.ReadFile(filepath.Join(xdg, "lxd/client.key")) c.Assert(err, jc.ErrorIsNil) - c.Assert(string(content), gc.Equals, "client.key") + c.Assert(string(content), gc.Equals, coretesting.CAKey+"generated") } func (s *credentialsSuite) TestFinalizeCredentialLocal(c *gc.C) { cert, _ := s.TestingCert(c) - out, err := s.Provider.FinalizeCredential(nil, environs.FinalizeCredentialParams{ + out, err := s.Provider.FinalizeCredential(coretesting.Context(c), environs.FinalizeCredentialParams{ CloudEndpoint: "1.2.3.4", Credential: cloud.NewCredential(cloud.CertificateAuthType, map[string]string{ "client-cert": string(cert.CertPEM), @@ -141,7 +154,7 @@ func (s *credentialsSuite) TestFinalizeCredentialLocal(c *gc.C) { func (s *credentialsSuite) TestFinalizeCredentialLocalAddCert(c *gc.C) { s.Stub.SetErrors(errors.NotFoundf("certificate")) cert, _ := s.TestingCert(c) - out, err := s.Provider.FinalizeCredential(nil, environs.FinalizeCredentialParams{ + out, err := s.Provider.FinalizeCredential(coretesting.Context(c), environs.FinalizeCredentialParams{ CloudEndpoint: "", // skips host lookup Credential: cloud.NewCredential(cloud.CertificateAuthType, map[string]string{ "client-cert": string(cert.CertPEM), @@ -172,7 +185,7 @@ func (s *credentialsSuite) TestFinalizeCredentialLocalAddCertAlreadyThere(c *gc. errors.New("UNIQUE constraint failed: certificates.fingerprint"), ) cert, _ := s.TestingCert(c) - out, err := s.Provider.FinalizeCredential(nil, environs.FinalizeCredentialParams{ + out, err := s.Provider.FinalizeCredential(coretesting.Context(c), environs.FinalizeCredentialParams{ CloudEndpoint: "", // skips host lookup Credential: cloud.NewCredential(cloud.CertificateAuthType, map[string]string{ "client-cert": string(cert.CertPEM), @@ -205,7 +218,7 @@ func (s *credentialsSuite) TestFinalizeCredentialLocalAddCertFatal(c *gc.C) { errors.NotFoundf("certificate"), ) cert, _ := s.TestingCert(c) - _, err := s.Provider.FinalizeCredential(nil, environs.FinalizeCredentialParams{ + _, err := s.Provider.FinalizeCredential(coretesting.Context(c), environs.FinalizeCredentialParams{ CloudEndpoint: "", // skips host lookup Credential: cloud.NewCredential(cloud.CertificateAuthType, map[string]string{ "client-cert": string(cert.CertPEM), @@ -223,10 +236,70 @@ func (s *credentialsSuite) TestFinalizeCredentialNonLocal(c *gc.C) { "client-cert": "foo", "client-key": "bar", }) - out, err := s.Provider.FinalizeCredential(nil, environs.FinalizeCredentialParams{ + _, err := s.Provider.FinalizeCredential(coretesting.Context(c), environs.FinalizeCredentialParams{ CloudEndpoint: "8.8.8.8", Credential: in, }) + c.Assert(err, gc.ErrorMatches, ` +cannot auto-generate credential for remote LXD + +Until support is added for verifying and authenticating to remote LXD hosts, +you must generate the credential on the LXD host, and add the credential to +this client using "juju add-credential localhost". + +See: https://jujucharms.com/docs/stable/clouds-LXD +`[1:]) +} + +func (s *credentialsSuite) TestFinalizeCredentialLocalInteractive(c *gc.C) { + cert, _ := s.TestingCert(c) + home := c.MkDir() + utils.SetHome(home) + s.writeFile(c, filepath.Join(home, ".config/lxc/client.crt"), string(cert.CertPEM)) + s.writeFile(c, filepath.Join(home, ".config/lxc/client.key"), string(cert.KeyPEM)) + + ctx := coretesting.Context(c) + out, err := s.Provider.FinalizeCredential(ctx, environs.FinalizeCredentialParams{ + CloudEndpoint: "1.2.3.4", + Credential: cloud.NewCredential("interactive", map[string]string{}), + }) c.Assert(err, jc.ErrorIsNil) - c.Assert(out, jc.DeepEquals, &in) + + c.Assert(out.AuthType(), gc.Equals, cloud.CertificateAuthType) + c.Assert(out.Attributes(), jc.DeepEquals, map[string]string{ + "client-cert": string(cert.CertPEM), + "client-key": string(cert.KeyPEM), + "server-cert": "server-cert", + }) + s.Stub.CheckCallNames(c, + "LookupHost", + "InterfaceAddrs", + "CertByFingerprint", + "ServerStatus", + ) +} + +func (s *credentialsSuite) TestFinalizeCredentialNonLocalInteractive(c *gc.C) { + cert, _ := s.TestingCert(c) + home := c.MkDir() + utils.SetHome(home) + s.writeFile(c, filepath.Join(home, ".config/lxc/client.crt"), string(cert.CertPEM)) + s.writeFile(c, filepath.Join(home, ".config/lxc/client.key"), string(cert.KeyPEM)) + + // Patch the interface addresses for the calling machine, so + // it appears that we're not on the LXD server host. + s.PatchValue(&s.InterfaceAddrs, []net.Addr{&net.IPNet{IP: net.ParseIP("8.8.8.8")}}) + _, err := s.Provider.FinalizeCredential(coretesting.Context(c), environs.FinalizeCredentialParams{ + CloudEndpoint: "8.8.8.8", + Credential: cloud.NewCredential("interactive", map[string]string{}), + }) + c.Assert(err, gc.ErrorMatches, ` +certificate upload for remote LXD unsupported + +Until support is added for verifying and authenticating to remote LXD hosts, +you must generate the credential on the LXD host, and add the credential to +this client using "juju add-credential localhost". + +See: https://jujucharms.com/docs/stable/clouds-LXD +`[1:]) } diff --git a/provider/lxd/provider.go b/provider/lxd/provider.go index b105714ac7e..ba909c61e85 100644 --- a/provider/lxd/provider.go +++ b/provider/lxd/provider.go @@ -152,6 +152,7 @@ var localhostCloud = cloud.Cloud{ Name: lxdnames.DefaultCloud, Type: lxdnames.ProviderType, AuthTypes: []cloud.AuthType{ + interactiveAuthType, cloud.CertificateAuthType, }, Endpoint: "", diff --git a/provider/lxd/provider_test.go b/provider/lxd/provider_test.go index 93c34f923c5..310f7286586 100644 --- a/provider/lxd/provider_test.go +++ b/provider/lxd/provider_test.go @@ -75,9 +75,12 @@ func (s *providerSuite) TestDetectCloudError(c *gc.C) { func (s *providerSuite) assertLocalhostCloud(c *gc.C, found cloud.Cloud) { c.Assert(found, jc.DeepEquals, cloud.Cloud{ - Name: "localhost", - Type: "lxd", - AuthTypes: []cloud.AuthType{cloud.CertificateAuthType}, + Name: "localhost", + Type: "lxd", + AuthTypes: []cloud.AuthType{ + "interactive", + cloud.CertificateAuthType, + }, Regions: []cloud.Region{{ Name: "localhost", }}, diff --git a/provider/lxd/testing_test.go b/provider/lxd/testing_test.go index 61fb61e7f9f..f8ec403e2f8 100644 --- a/provider/lxd/testing_test.go +++ b/provider/lxd/testing_test.go @@ -351,7 +351,9 @@ func (s *BaseSuite) SetUpTest(c *gc.C) { s.Env.raw = raw s.Provider.generateMemCert = func(client bool) (cert, key []byte, _ error) { s.Stub.AddCall("GenerateMemCert", client) - return []byte("client.crt"), []byte("client.key"), s.Stub.NextErr() + cert = []byte(testing.CACert + "generated") + key = []byte(testing.CAKey + "generated") + return cert, key, s.Stub.NextErr() } s.Provider.newLocalRawProvider = func() (*rawProvider, error) { return raw, nil