From 3177b2fdb75dff9d0ca76df5df9d210076228876 Mon Sep 17 00:00:00 2001 From: cm Date: Fri, 1 Jul 2022 10:57:29 +0200 Subject: [PATCH] AB#2032 Write IDs to disk and read when verifying (#212) * AB#2032 Write IDs to disk and read when verifying * Update CHANGELOG.md * update changelog * update changelog * cli verify: prefer flag values * Rename fid file Co-authored-by: Thomas Tendyck --- CHANGELOG.md | 1 + cli/internal/cmd/details.go | 7 ++++ cli/internal/cmd/init.go | 5 +++ cli/internal/cmd/init_test.go | 17 +++++++++ cli/internal/cmd/validargs.go | 4 +++ cli/internal/cmd/validargs_test.go | 5 +++ cli/internal/cmd/verify.go | 57 ++++++++++++++++++++++-------- cli/internal/cmd/verify_test.go | 46 +++++++++++++++++++++++- internal/constants/constants.go | 1 + 9 files changed, 128 insertions(+), 15 deletions(-) create mode 100644 cli/internal/cmd/details.go diff --git a/CHANGELOG.md b/CHANGELOG.md index 930eca021..732bed769 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - GCP-native Kubernetes load balancing ### Changed +- Create `constellation-id.json` when initializing the cluster to save the cluster's unique ID and the owner ID to disk. Verifying will read this file back to use the values for the verification. This is overriden by specifying the command line arguments. ### Removed diff --git a/cli/internal/cmd/details.go b/cli/internal/cmd/details.go new file mode 100644 index 000000000..2ee388965 --- /dev/null +++ b/cli/internal/cmd/details.go @@ -0,0 +1,7 @@ +package cmd + +type clusterIDFile struct { + ClusterID string + OwnerID string + Endpoint string +} diff --git a/cli/internal/cmd/init.go b/cli/internal/cmd/init.go index d81f43500..79c4f5803 100644 --- a/cli/internal/cmd/init.go +++ b/cli/internal/cmd/init.go @@ -267,6 +267,11 @@ func (r activationResult) writeOutput(wr io.Writer, fileHandler file.Handler) er return fmt.Errorf("write kubeconfig: %w", err) } + idFile := clusterIDFile{ClusterID: r.clusterID, OwnerID: r.ownerID, Endpoint: r.coordinatorPubIP} + if err := fileHandler.WriteJSON(constants.IDsFileName, idFile, file.OptNone); err != nil { + return fmt.Errorf("writing Constellation id file: %w", err) + } + fmt.Fprintln(wr, "You can now connect to your cluster by executing:") fmt.Fprintf(wr, "\twg-quick up ./%s\n", constants.WGQuickConfigFilename) fmt.Fprintf(wr, "\texport KUBECONFIG=\"$PWD/%s\"\n", constants.AdminConfFilename) diff --git a/cli/internal/cmd/init_test.go b/cli/internal/cmd/init_test.go index 9152324e5..fd8753355 100644 --- a/cli/internal/cmd/init_test.go +++ b/cli/internal/cmd/init_test.go @@ -4,6 +4,7 @@ import ( "bytes" "context" "encoding/base64" + "encoding/json" "errors" "strconv" "strings" @@ -331,6 +332,13 @@ func TestWriteOutput(t *testing.T) { coordinatorPubIP: "baz-qq", kubeconfig: "foo-bar-baz-qq", } + + expectedIdFile := clusterIDFile{ + Endpoint: result.coordinatorPubIP, + ClusterID: result.clusterID, + OwnerID: result.ownerID, + } + var out bytes.Buffer testFs := afero.NewMemMapFs() fileHandler := file.NewHandler(testFs) @@ -340,11 +348,20 @@ func TestWriteOutput(t *testing.T) { assert.Contains(out.String(), result.clientVpnIP) assert.Contains(out.String(), result.coordinatorPubIP) assert.Contains(out.String(), result.coordinatorPubKey) + assert.Contains(out.String(), result.clusterID) + assert.Contains(out.String(), result.ownerID) afs := afero.Afero{Fs: testFs} adminConf, err := afs.ReadFile(constants.AdminConfFilename) assert.NoError(err) assert.Equal(result.kubeconfig, string(adminConf)) + + idsFile, err := afs.ReadFile(constants.IDsFileName) + assert.NoError(err) + var testIdFile clusterIDFile + err = json.Unmarshal(idsFile, &testIdFile) + assert.NoError(err) + assert.Equal(expectedIdFile, testIdFile) } func TestIpsToEndpoints(t *testing.T) { diff --git a/cli/internal/cmd/validargs.go b/cli/internal/cmd/validargs.go index a3e83d1a5..521f9b6db 100644 --- a/cli/internal/cmd/validargs.go +++ b/cli/internal/cmd/validargs.go @@ -58,6 +58,10 @@ func validInstanceTypeForProvider(cmd *cobra.Command, insType string, provider c } func validateEndpoint(endpoint string, defaultPort int) (string, error) { + if endpoint == "" { + return "", errors.New("endpoint is empty") + } + _, _, err := net.SplitHostPort(endpoint) if err == nil { return endpoint, nil diff --git a/cli/internal/cmd/validargs_test.go b/cli/internal/cmd/validargs_test.go index f9ebd9b5c..5dd89aea2 100644 --- a/cli/internal/cmd/validargs_test.go +++ b/cli/internal/cmd/validargs_test.go @@ -65,6 +65,11 @@ func TestValidateEndpoint(t *testing.T) { defaultPort: 3, wantResult: "foo:3", }, + "empty endpoint": { + endpoint: "", + defaultPort: 3, + wantErr: true, + }, "invalid endpoint": { endpoint: "foo:2:2", defaultPort: 3, diff --git a/cli/internal/cmd/verify.go b/cli/internal/cmd/verify.go index 5a08e501c..225f31812 100644 --- a/cli/internal/cmd/verify.go +++ b/cli/internal/cmd/verify.go @@ -5,6 +5,7 @@ import ( "context" "errors" "fmt" + "io/fs" "net" "github.com/edgelesssys/constellation/cli/internal/cloudcmd" @@ -25,7 +26,9 @@ func NewVerifyCmd() *cobra.Command { cmd := &cobra.Command{ Use: "verify {aws|azure|gcp}", Short: "Verify the confidential properties of a Constellation cluster", - Long: "Verify the confidential properties of a Constellation cluster.", + Long: `Verify the confidential properties of a Constellation cluster. + +If arguments are not specified, values are read from ` + constants.IDsFileName + `.`, Args: cobra.MatchAll( cobra.ExactArgs(1), isCloudProvider(0), @@ -35,8 +38,7 @@ func NewVerifyCmd() *cobra.Command { } cmd.Flags().String("owner-id", "", "verify using the owner identity derived from the master secret") cmd.Flags().String("unique-id", "", "verify using the unique cluster identity") - cmd.Flags().StringP("node-endpoint", "e", "", "endpoint of the node to verify, passed as HOST[:PORT] (required)") - must(cmd.MarkFlagRequired("node-endpoint")) + cmd.Flags().StringP("node-endpoint", "e", "", "endpoint of the node to verify, passed as HOST[:PORT]") return cmd } @@ -50,7 +52,7 @@ func runVerify(cmd *cobra.Command, args []string) error { func verify( cmd *cobra.Command, provider cloudprovider.Provider, fileHandler file.Handler, verifyClient verifyClient, ) error { - flags, err := parseVerifyFlags(cmd) + flags, err := parseVerifyFlags(cmd, fileHandler) if err != nil { return err } @@ -97,7 +99,11 @@ func verify( return nil } -func parseVerifyFlags(cmd *cobra.Command) (verifyFlags, error) { +func parseVerifyFlags(cmd *cobra.Command, fileHandler file.Handler) (verifyFlags, error) { + configPath, err := cmd.Flags().GetString("config") + if err != nil { + return verifyFlags{}, fmt.Errorf("parsing config path argument: %w", err) + } ownerID, err := cmd.Flags().GetString("owner-id") if err != nil { return verifyFlags{}, fmt.Errorf("parsing owner-id argument: %w", err) @@ -106,22 +112,37 @@ func parseVerifyFlags(cmd *cobra.Command) (verifyFlags, error) { if err != nil { return verifyFlags{}, fmt.Errorf("parsing unique-id argument: %w", err) } - if ownerID == "" && clusterID == "" { - return verifyFlags{}, errors.New("neither owner-id nor unique-id provided to verify the cluster") - } - endpoint, err := cmd.Flags().GetString("node-endpoint") if err != nil { return verifyFlags{}, fmt.Errorf("parsing node-endpoint argument: %w", err) } - endpoint, err = validateEndpoint(endpoint, constants.VerifyServiceNodePortGRPC) - if err != nil { - return verifyFlags{}, fmt.Errorf("validating endpoint argument: %w", err) + + // Get empty values from ID file + emptyEndpoint := endpoint == "" + emptyIDs := ownerID == "" && clusterID == "" + if emptyEndpoint || emptyIDs { + if details, err := readIds(fileHandler); err == nil { + if emptyEndpoint { + cmd.Printf("Using endpoint from %q. Specify --node-endpoint to override this.\n", constants.IDsFileName) + endpoint = details.Endpoint + } + if emptyIDs { + cmd.Printf("Using IDs from %q. Specify --owner-id and/or --unique-id to override this.\n", constants.IDsFileName) + ownerID = details.OwnerID + clusterID = details.ClusterID + } + } else if !errors.Is(err, fs.ErrNotExist) { + return verifyFlags{}, err + } } - configPath, err := cmd.Flags().GetString("config") + // Validate + if ownerID == "" && clusterID == "" { + return verifyFlags{}, errors.New("neither owner-id nor unique-id provided to verify the cluster") + } + endpoint, err = validateEndpoint(endpoint, constants.CoordinatorPort) if err != nil { - return verifyFlags{}, fmt.Errorf("parsing config path argument: %w", err) + return verifyFlags{}, fmt.Errorf("validating endpoint argument: %w", err) } return verifyFlags{ @@ -139,6 +160,14 @@ type verifyFlags struct { configPath string } +func readIds(fileHandler file.Handler) (clusterIDFile, error) { + det := clusterIDFile{} + if err := fileHandler.ReadJSON(constants.IDsFileName, &det); err != nil { + return clusterIDFile{}, fmt.Errorf("reading cluster ids: %w", err) + } + return det, nil +} + // verifyCompletion handles the completion of CLI arguments. It is frequently called // while the user types arguments of the command to suggest completion. func verifyCompletion(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { diff --git a/cli/internal/cmd/verify_test.go b/cli/internal/cmd/verify_test.go index e184f773a..f9a25a297 100644 --- a/cli/internal/cmd/verify_test.go +++ b/cli/internal/cmd/verify_test.go @@ -61,11 +61,13 @@ func TestVerify(t *testing.T) { testCases := map[string]struct { setupFs func(*require.Assertions) afero.Fs provider cloudprovider.Provider - protoClient verifyClient + protoClient *stubVerifyClient nodeEndpointFlag string configFlag string ownerIDFlag string clusterIDFlag string + idFile *clusterIDFile + wantEndpoint string wantErr bool }{ "gcp": { @@ -74,6 +76,7 @@ func TestVerify(t *testing.T) { nodeEndpointFlag: "192.0.2.1:1234", ownerIDFlag: zeroBase64, protoClient: &stubVerifyClient{}, + wantEndpoint: "192.0.2.1:1234", }, "azure": { setupFs: func(require *require.Assertions) afero.Fs { return afero.NewMemMapFs() }, @@ -81,6 +84,7 @@ func TestVerify(t *testing.T) { nodeEndpointFlag: "192.0.2.1:1234", ownerIDFlag: zeroBase64, protoClient: &stubVerifyClient{}, + wantEndpoint: "192.0.2.1:1234", }, "default port": { setupFs: func(require *require.Assertions) afero.Fs { return afero.NewMemMapFs() }, @@ -88,6 +92,31 @@ func TestVerify(t *testing.T) { nodeEndpointFlag: "192.0.2.1", ownerIDFlag: zeroBase64, protoClient: &stubVerifyClient{}, + wantEndpoint: "192.0.2.1:9000", + }, + "endpoint not set": { + setupFs: func(require *require.Assertions) afero.Fs { return afero.NewMemMapFs() }, + provider: cloudprovider.GCP, + ownerIDFlag: zeroBase64, + protoClient: &stubVerifyClient{}, + wantErr: true, + }, + "endpoint from id file": { + setupFs: func(require *require.Assertions) afero.Fs { return afero.NewMemMapFs() }, + provider: cloudprovider.GCP, + ownerIDFlag: zeroBase64, + protoClient: &stubVerifyClient{}, + idFile: &clusterIDFile{Endpoint: "192.0.2.1:1234"}, + wantEndpoint: "192.0.2.1:1234", + }, + "override endpoint from details file": { + setupFs: func(require *require.Assertions) afero.Fs { return afero.NewMemMapFs() }, + provider: cloudprovider.GCP, + nodeEndpointFlag: "192.0.2.2:1234", + ownerIDFlag: zeroBase64, + protoClient: &stubVerifyClient{}, + idFile: &clusterIDFile{Endpoint: "192.0.2.1:1234"}, + wantEndpoint: "192.0.2.2:1234", }, "invalid endpoint": { setupFs: func(require *require.Assertions) afero.Fs { return afero.NewMemMapFs() }, @@ -103,6 +132,14 @@ func TestVerify(t *testing.T) { nodeEndpointFlag: "192.0.2.1:1234", wantErr: true, }, + "use owner id from id file": { + setupFs: func(require *require.Assertions) afero.Fs { return afero.NewMemMapFs() }, + provider: cloudprovider.GCP, + nodeEndpointFlag: "192.0.2.1:1234", + protoClient: &stubVerifyClient{}, + idFile: &clusterIDFile{OwnerID: zeroBase64}, + wantEndpoint: "192.0.2.1:1234", + }, "config file not existing": { setupFs: func(require *require.Assertions) afero.Fs { return afero.NewMemMapFs() }, provider: cloudprovider.GCP, @@ -153,6 +190,10 @@ func TestVerify(t *testing.T) { } fileHandler := file.NewHandler(tc.setupFs(require)) + if tc.idFile != nil { + require.NoError(fileHandler.WriteJSON(constants.IDsFileName, tc.idFile, file.OptNone)) + } + err := verify(cmd, tc.provider, fileHandler, tc.protoClient) if tc.wantErr { @@ -160,6 +201,7 @@ func TestVerify(t *testing.T) { } else { assert.NoError(err) assert.Contains(out.String(), "OK") + assert.Equal(tc.wantEndpoint, tc.protoClient.endpoint) } }) } @@ -284,9 +326,11 @@ func TestVerifyClient(t *testing.T) { type stubVerifyClient struct { verifyErr error + endpoint string } func (c *stubVerifyClient) Verify(ctx context.Context, endpoint string, req *verifyproto.GetAttestationRequest, validator atls.Validator) error { + c.endpoint = endpoint return c.verifyErr } diff --git a/internal/constants/constants.go b/internal/constants/constants.go index e25e3dc06..4518ea312 100644 --- a/internal/constants/constants.go +++ b/internal/constants/constants.go @@ -50,6 +50,7 @@ const ( // StateFilename = "constellation-state.json" + IDsFileName = "constellation-id.json" ConfigFilename = "constellation-conf.yaml" DebugdConfigFilename = "cdbg-conf.yaml" AdminConfFilename = "constellation-admin.conf"