From 7030bb11a73501723988628f0fc666e5a424a202 Mon Sep 17 00:00:00 2001 From: George Aristy Date: Wed, 29 Jul 2020 19:11:39 -0400 Subject: [PATCH] feat: #20 bootstrap data retrieve endpoint Signed-off-by: George Aristy --- cmd/auth-rest/startcmd/start.go | 74 +++++++++++++--- cmd/auth-rest/startcmd/start_test.go | 60 +++++++++++++ pkg/restapi/controller_test.go | 2 +- pkg/restapi/operation/models.go | 18 ++++ pkg/restapi/operation/operations.go | 57 +++++++++++- pkg/restapi/operation/operations_test.go | 87 ++++++++++++++++++- .../bdd/fixtures/auth-rest/docker-compose.yml | 2 + 7 files changed, 277 insertions(+), 23 deletions(-) create mode 100644 pkg/restapi/operation/models.go diff --git a/cmd/auth-rest/startcmd/start.go b/cmd/auth-rest/startcmd/start.go index 765697c..2dc2eae 100644 --- a/cmd/auth-rest/startcmd/start.go +++ b/cmd/auth-rest/startcmd/start.go @@ -114,6 +114,19 @@ const ( googleClientSecretEnvKey = "AUTH_REST_GOOGLE_CLIENTSECRET" // nolint:gosec ) +// Bootstrap parameters. +const ( + sdsURLFlagName = "sds-url" + sdsURLFlagUsage = "URL for the Secure Data Storage service." + + " Alternatively, this can be set with the following environment variable: " + sdsURLEnvKey + sdsURLEnvKey = "AUTH_REST_SDS_URL" + + keyServerURLFlagName = "ks-url" + keyServerURLFlagUsage = "URL for the Key Server." + + " Alternatively, this can be set with the following environment variable: " + keyServerURLEnvKey + keyServerURLEnvKey = "AUTH_REST_KEYSERVER_URL" +) + const ( // api healthCheckEndpoint = "/healthcheck" @@ -122,13 +135,14 @@ const ( var logger = log.New("auth-rest") type authRestParameters struct { - hostURL string - logLevel string - databaseType string - databaseURL string - databasePrefix string - tlsParams *tlsParams - oidcParams *oidcParams + hostURL string + logLevel string + databaseType string + databaseURL string + databasePrefix string + tlsParams *tlsParams + oidcParams *oidcParams + bootstrapParams *bootstrapParams } type tlsParams struct { @@ -149,6 +163,11 @@ type oidcProviderParams struct { clientSecret string } +type bootstrapParams struct { + sdsURL string + keyServerURL string +} + type healthCheckResp struct { Status string `json:"status"` CurrentTime time.Time `json:"currentTime"` @@ -233,14 +252,20 @@ func getAuthRestParameters(cmd *cobra.Command) (*authRestParameters, error) { return nil, err } + bootstrapParams, err := getBootstrapParams(cmd) + if err != nil { + return nil, err + } + return &authRestParameters{ - hostURL: hostURL, - tlsParams: tlsParams, - logLevel: loggingLevel, - databaseType: databaseType, - databaseURL: databaseURL, - databasePrefix: databasePrefix, - oidcParams: oidcParams, + hostURL: hostURL, + tlsParams: tlsParams, + logLevel: loggingLevel, + databaseType: databaseType, + databaseURL: databaseURL, + databasePrefix: databasePrefix, + oidcParams: oidcParams, + bootstrapParams: bootstrapParams, }, nil } @@ -293,6 +318,8 @@ func createFlags(startCmd *cobra.Command) { startCmd.Flags().StringP(googleProviderFlagName, "", "", googleProviderFlagUsage) startCmd.Flags().StringP(googleClientIDFlagName, "", "", googleClientIDFlagUsage) startCmd.Flags().StringP(googleClientSecretFlagName, "", "", googleClientSecretFlagUsage) + startCmd.Flags().StringP(sdsURLFlagName, "", "", sdsURLFlagUsage) + startCmd.Flags().StringP(keyServerURLFlagName, "", "", keyServerURLFlagUsage) } func startAuthService(parameters *authRestParameters, srv server) error { @@ -328,6 +355,10 @@ func startAuthService(parameters *authRestParameters, srv server) error { OIDCProviderURL: parameters.oidcParams.google.providerURL, OIDCClientID: parameters.oidcParams.google.clientID, OIDCClientSecret: parameters.oidcParams.google.clientSecret, + BootstrapConfig: &operation.BootstrapConfig{ + SDSURL: parameters.bootstrapParams.sdsURL, + KeyServerURL: parameters.bootstrapParams.keyServerURL, + }, }) if err != nil { return err @@ -391,6 +422,21 @@ func getGoogleOIDCParams(cmd *cobra.Command) (*oidcProviderParams, error) { return params, err } +func getBootstrapParams(cmd *cobra.Command) (*bootstrapParams, error) { + params := &bootstrapParams{} + + var err error + + params.sdsURL, err = cmdutils.GetUserSetVarFromString(cmd, sdsURLFlagName, sdsURLEnvKey, false) + if err != nil { + return nil, err + } + + params.keyServerURL, err = cmdutils.GetUserSetVarFromString(cmd, keyServerURLFlagName, keyServerURLEnvKey, false) + + return params, err +} + func setDefaultLogLevel(userLogLevel string) { logLevel, err := log.ParseLevel(userLogLevel) if err != nil { diff --git a/cmd/auth-rest/startcmd/start_test.go b/cmd/auth-rest/startcmd/start_test.go index 05bf765..3294fc3 100644 --- a/cmd/auth-rest/startcmd/start_test.go +++ b/cmd/auth-rest/startcmd/start_test.go @@ -136,6 +136,52 @@ func TestStartCmdWithMissingArg(t *testing.T) { "Neither host-url (command line flag) nor AUTH_REST_HOST_URL (environment variable) have been set.", err.Error()) }) + + t.Run("missing sds url arg", func(t *testing.T) { + oidcURL := mockOIDCProvider(t) + startCmd := GetStartCmd(&mockServer{}) + + args := []string{ + "--" + hostURLFlagName, "localhost:8080", + "--" + logLevelFlagName, log.ParseString(log.DEBUG), + "--" + databaseTypeFlagName, "mem", + "--" + oidcCallbackURLFlagName, "http://example.com/oauth2/callback", + "--" + googleProviderFlagName, oidcURL, + "--" + googleClientIDFlagName, uuid.New().String(), + "--" + googleClientSecretFlagName, uuid.New().String(), + "--" + keyServerURLFlagName, "http://keyserver.example.com", + } + startCmd.SetArgs(args) + + err := startCmd.Execute() + + require.Error(t, err) + require.Contains(t, err.Error(), sdsURLFlagName) + require.Contains(t, err.Error(), sdsURLEnvKey) + }) + + t.Run("missing keyserver url arg", func(t *testing.T) { + oidcURL := mockOIDCProvider(t) + startCmd := GetStartCmd(&mockServer{}) + + args := []string{ + "--" + hostURLFlagName, "localhost:8080", + "--" + logLevelFlagName, log.ParseString(log.DEBUG), + "--" + databaseTypeFlagName, "mem", + "--" + oidcCallbackURLFlagName, "http://example.com/oauth2/callback", + "--" + googleProviderFlagName, oidcURL, + "--" + googleClientIDFlagName, uuid.New().String(), + "--" + googleClientSecretFlagName, uuid.New().String(), + "--" + sdsURLFlagName, "http://sds.example.com", + } + startCmd.SetArgs(args) + + err := startCmd.Execute() + + require.Error(t, err) + require.Contains(t, err.Error(), keyServerURLFlagName) + require.Contains(t, err.Error(), keyServerURLEnvKey) + }) } func TestStartCmdWithBlankEnvVar(t *testing.T) { @@ -164,6 +210,8 @@ func TestStartCmdValidArgs(t *testing.T) { "--" + googleProviderFlagName, oidcURL, "--" + googleClientIDFlagName, uuid.New().String(), "--" + googleClientSecretFlagName, uuid.New().String(), + "--" + sdsURLFlagName, "http://sds.example.com", + "--" + keyServerURLFlagName, "http://keyserver.example.com", } startCmd.SetArgs(args) @@ -185,6 +233,8 @@ func TestStartCmdValidArgs(t *testing.T) { "--" + googleClientIDFlagName, uuid.New().String(), "--" + googleClientSecretFlagName, uuid.New().String(), "--" + logLevelFlagName, "INVALID", + "--" + sdsURLFlagName, "http://sds.example.com", + "--" + keyServerURLFlagName, "http://keyserver.example.com", } startCmd.SetArgs(args) @@ -207,6 +257,8 @@ func TestStartCmdValidArgs(t *testing.T) { "--" + googleProviderFlagName, oidcURL, "--" + googleClientIDFlagName, uuid.New().String(), "--" + googleClientSecretFlagName, uuid.New().String(), + "--" + sdsURLFlagName, "http://sds.example.com", + "--" + keyServerURLFlagName, "http://keyserver.example.com", } startCmd.SetArgs(args) @@ -231,6 +283,8 @@ func TestStartCmdFailToCreateController(t *testing.T) { "--" + googleProviderFlagName, oidcURL, "--" + googleClientIDFlagName, uuid.New().String(), "--" + googleClientSecretFlagName, uuid.New().String(), + "--" + sdsURLFlagName, "http://sds.example.com", + "--" + keyServerURLFlagName, "http://keyserver.example.com", } startCmd.SetArgs(args) @@ -257,6 +311,8 @@ func TestStartCmdInvalidDatabaseType(t *testing.T) { "--" + googleProviderFlagName, oidcURL, "--" + googleClientIDFlagName, uuid.New().String(), "--" + googleClientSecretFlagName, uuid.New().String(), + "--" + sdsURLFlagName, "http://sds.example.com", + "--" + keyServerURLFlagName, "http://keyserver.example.com", } startCmd.SetArgs(args) @@ -332,6 +388,10 @@ func setEnvVars(t *testing.T) { require.NoError(t, err) err = os.Setenv(googleClientSecretEnvKey, uuid.New().String()) require.NoError(t, err) + err = os.Setenv(sdsURLEnvKey, "http://sds.example.com") + require.NoError(t, err) + err = os.Setenv(keyServerURLEnvKey, "http://keyserver.examepl.com") + require.NoError(t, err) } func unsetEnvVars(t *testing.T) { diff --git a/pkg/restapi/controller_test.go b/pkg/restapi/controller_test.go index cb370cb..75da792 100644 --- a/pkg/restapi/controller_test.go +++ b/pkg/restapi/controller_test.go @@ -43,7 +43,7 @@ func TestController_GetOperations(t *testing.T) { require.NotNil(t, controller) ops := controller.GetOperations() - require.Equal(t, 2, len(ops)) + require.Equal(t, 3, len(ops)) } func config(t *testing.T) *operation.Config { diff --git a/pkg/restapi/operation/models.go b/pkg/restapi/operation/models.go new file mode 100644 index 0000000..af023b4 --- /dev/null +++ b/pkg/restapi/operation/models.go @@ -0,0 +1,18 @@ +/* +Copyright SecureKey Technologies Inc. All Rights Reserved. + +SPDX-License-Identifier: Apache-2.0 +*/ + +package operation + +type createOIDCRequestResponse struct { + Request string `json:"request"` +} + +type bootstrapData struct { + SDSURL string `json:"sdsURL"` + SDSPrimaryVaultID string `json:"sdsPrimaryVaultID"` + KeyServerURL string `json:"keyServerURL"` + KeyStoreIDs []string `json:"keyStoreIDs"` +} diff --git a/pkg/restapi/operation/operations.go b/pkg/restapi/operation/operations.go index 17cedea..1aa1f80 100644 --- a/pkg/restapi/operation/operations.go +++ b/pkg/restapi/operation/operations.go @@ -25,8 +25,9 @@ import ( ) const ( - oauth2GetRequestPath = "/oauth2/request" - oauth2CallbackPath = "/oauth2/callback" + oauth2GetRequestPath = "/oauth2/request" + oauth2CallbackPath = "/oauth2/callback" + bootstrapGetRequestPath = "/bootstrap" // api path params scopeQueryParam = "scope" @@ -117,6 +118,7 @@ type Operation struct { oidcCallbackURL string oauth2ConfigFunc func(...string) oauth2Config bootstrapStore storage.Store + bootstrapConfig *BootstrapConfig } // Config defines configuration for rp operations. @@ -129,10 +131,13 @@ type Config struct { OIDCCallbackURL string TransientStoreProvider storage.Provider StoreProvider storage.Provider + BootstrapConfig *BootstrapConfig } -type createOIDCRequestResponse struct { - Request string `json:"request"` +// BootstrapConfig holds user bootstrap-related config. +type BootstrapConfig struct { + SDSURL string + KeyServerURL string } // New returns rp operation instance. @@ -143,6 +148,7 @@ func New(config *Config) (*Operation, error) { oidcClientID: config.OIDCClientID, oidcClientSecret: config.OIDCClientSecret, oidcCallbackURL: config.OIDCCallbackURL, + bootstrapConfig: config.BootstrapConfig, } // TODO implement retries: https://github.com/trustbloc/hub-auth/issues/45 @@ -340,6 +346,48 @@ func (c *Operation) onboardUser(id string) (*user.Profile, error) { return userProfile, nil } +func (c *Operation) handleBootstrapDataRequest(w http.ResponseWriter, r *http.Request) { + handle := r.URL.Query().Get("up") + if handle == "" { + handleAuthError(w, http.StatusBadRequest, "missing handle") + + return + } + + profile, err := user.NewStore(c.transientStore).Get(handle) + if errors.Is(err, storage.ErrValueNotFound) { + handleAuthError(w, http.StatusBadRequest, "invalid handle") + + return + } + + if err != nil { + handleAuthError(w, http.StatusInternalServerError, + fmt.Sprintf("failed to query transient store for handle: %s", err)) + + return + } + + response, err := json.Marshal(&bootstrapData{ + SDSURL: c.bootstrapConfig.SDSURL, + SDSPrimaryVaultID: profile.SDSPrimaryVaultID, + KeyServerURL: c.bootstrapConfig.KeyServerURL, + KeyStoreIDs: profile.KeyStoreIDs, + }) + if err != nil { + handleAuthError(w, http.StatusInternalServerError, fmt.Sprintf("failed to marshal bootstrap data: %s", err)) + + return + } + + // TODO We should delete the handle from the transient store after writing the response, + // but edge-core store API doesn't have a Delete() operation: https://github.com/trustbloc/edge-core/issues/45 + _, err = w.Write(response) + if err != nil { + logger.Errorf("failed to write bootstrap data to output: %s", err) + } +} + // TODO redirect to the UI: https://github.com/trustbloc/hub-auth/issues/39 func handleAuthResult(w http.ResponseWriter, r *http.Request, _ *user.Profile) { http.Redirect(w, r, "", http.StatusFound) @@ -373,6 +421,7 @@ func (c *Operation) GetRESTHandlers() []Handler { return []Handler{ support.NewHTTPHandler(oauth2GetRequestPath, http.MethodGet, c.createOIDCRequest), support.NewHTTPHandler(oauth2CallbackPath, http.MethodGet, c.handleOIDCCallback), + support.NewHTTPHandler(bootstrapGetRequestPath, http.MethodGet, c.handleBootstrapDataRequest), } } diff --git a/pkg/restapi/operation/operations_test.go b/pkg/restapi/operation/operations_test.go index bbf609c..5a5d63e 100644 --- a/pkg/restapi/operation/operations_test.go +++ b/pkg/restapi/operation/operations_test.go @@ -17,8 +17,6 @@ import ( "strings" "testing" - "github.com/trustbloc/hub-auth/pkg/internal/common/mockoidc" - "github.com/coreos/go-oidc" "github.com/google/uuid" "github.com/stretchr/testify/require" @@ -27,6 +25,8 @@ import ( "github.com/trustbloc/edge-core/pkg/storage/mockstore" "golang.org/x/oauth2" + "github.com/trustbloc/hub-auth/pkg/bootstrap/user" + "github.com/trustbloc/hub-auth/pkg/internal/common/mockoidc" "github.com/trustbloc/hub-auth/pkg/internal/common/mockstorage" ) @@ -36,7 +36,7 @@ func TestNew(t *testing.T) { svc, err := New(config) require.NoError(t, err) require.NotNil(t, svc) - require.Equal(t, 2, len(svc.GetRESTHandlers())) + require.Equal(t, 3, len(svc.GetRESTHandlers())) }) t.Run("success, bootstrap store already exists", func(t *testing.T) { @@ -50,7 +50,7 @@ func TestNew(t *testing.T) { svc, err := New(config) require.NoError(t, err) require.NotNil(t, svc) - require.Equal(t, 2, len(svc.GetRESTHandlers())) + require.Equal(t, 3, len(svc.GetRESTHandlers())) }) t.Run("error if oidc provider is invalid", func(t *testing.T) { @@ -413,6 +413,69 @@ func TestHandleOIDCCallback(t *testing.T) { }) } +func TestHandleBootstrapDataRequest(t *testing.T) { + t.Run("returns bootstrap data", func(t *testing.T) { + config := config(t) + svc, err := New(config) + require.NoError(t, err) + expected := &user.Profile{ + SDSPrimaryVaultID: uuid.New().String(), + KeyStoreIDs: []string{uuid.New().String()}, + } + + handle := uuid.New().String() + + // put in transient store by onboardUser() + err = svc.transientStore.Put(handle, marshal(t, expected)) + require.NoError(t, err) + + w := httptest.NewRecorder() + svc.handleBootstrapDataRequest(w, newBootstrapDataRequest(handle)) + require.Equal(t, http.StatusOK, w.Code) + result := &bootstrapData{} + err = json.NewDecoder(w.Body).Decode(result) + require.NoError(t, err) + require.Equal(t, expected.SDSPrimaryVaultID, result.SDSPrimaryVaultID) + require.Equal(t, expected.KeyStoreIDs, result.KeyStoreIDs) + require.Equal(t, config.BootstrapConfig.KeyServerURL, result.KeyServerURL) + require.Equal(t, config.BootstrapConfig.SDSURL, result.SDSURL) + }) + + t.Run("bad request if handle is missing", func(t *testing.T) { + svc, err := New(config(t)) + require.NoError(t, err) + w := httptest.NewRecorder() + svc.handleBootstrapDataRequest(w, httptest.NewRequest(http.MethodGet, "http://examepl.com/bootstrap", nil)) + require.Equal(t, http.StatusBadRequest, w.Code) + }) + + t.Run("bad request if handle is invalid", func(t *testing.T) { + svc, err := New(config(t)) + require.NoError(t, err) + w := httptest.NewRecorder() + svc.handleBootstrapDataRequest(w, newBootstrapDataRequest("INVALID")) + require.Equal(t, http.StatusBadRequest, w.Code) + }) + + t.Run("internal server error if transient store FETCH fails generically", func(t *testing.T) { + config := config(t) + handle := uuid.New().String() + config.TransientStoreProvider = &mockstorage.Provider{ + Store: &mockstorage.MockStore{ + Store: map[string][]byte{ + handle: marshal(t, &user.Profile{}), + }, + ErrGet: errors.New("generic"), + }, + } + svc, err := New(config) + require.NoError(t, err) + w := httptest.NewRecorder() + svc.handleBootstrapDataRequest(w, newBootstrapDataRequest(handle)) + require.Equal(t, http.StatusInternalServerError, w.Code) + }) +} + func newCreateOIDCHTTPRequest(scope string) *http.Request { return httptest.NewRequest(http.MethodGet, fmt.Sprintf("http://example.com/oauth2/request?scope=%s", scope), nil) } @@ -422,6 +485,11 @@ func newOIDCCallback(state, code string) *http.Request { fmt.Sprintf("http://example.com/oauth2/callback?state=%s&code=%s", state, code), nil) } +func newBootstrapDataRequest(handle string) *http.Request { + return httptest.NewRequest(http.MethodGet, + fmt.Sprintf("http://example.com/bootstrap?up=%s", handle), nil) +} + type mockOIDCProvider struct { baseURL string verifier *mockVerifier @@ -455,9 +523,20 @@ func config(t *testing.T) *Config { OIDCCallbackURL: "http://test.com", TransientStoreProvider: memstore.NewProvider(), StoreProvider: memstore.NewProvider(), + BootstrapConfig: &BootstrapConfig{ + SDSURL: "http://sds.example.com", + KeyServerURL: "http://keyserver.example.com", + }, } } +func marshal(t *testing.T, v interface{}) []byte { + bits, err := json.Marshal(v) + require.NoError(t, err) + + return bits +} + type mockOAuth2Config struct { authCodeFunc func(string, ...oauth2.AuthCodeOption) string exchangeVal oauth2Token diff --git a/test/bdd/fixtures/auth-rest/docker-compose.yml b/test/bdd/fixtures/auth-rest/docker-compose.yml index 4ec5d54..e2c609c 100644 --- a/test/bdd/fixtures/auth-rest/docker-compose.yml +++ b/test/bdd/fixtures/auth-rest/docker-compose.yml @@ -21,6 +21,8 @@ services: - AUTH_REST_GOOGLE_URL=http://mock.oidc.provider - AUTH_REST_GOOGLE_CLIENTID=TODO # https://github.com/trustbloc/hub-auth/issues/9 - AUTH_REST_GOOGLE_CLIENTSECRET=TODO + - AUTH_REST_SDS_URL=TODO # onboard user: https://github.com/trustbloc/hub-auth/issues/38 + - AUTH_REST_KEYSERVER_URL=TODO # onboard user: https://github.com/trustbloc/hub-auth/issues/38 ports: - 8070:8070 entrypoint: ""