Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Add REST endpoint to configure/store VCT URL #1274

Merged
merged 1 commit into from
May 2, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions cmd/orb-server/startcmd/start.go
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,7 @@ import (
"github.com/trustbloc/orb/pkg/vct/logmonitoring"
"github.com/trustbloc/orb/pkg/vct/logmonitoring/handler"
"github.com/trustbloc/orb/pkg/vct/proofmonitoring"
vcthandler "github.com/trustbloc/orb/pkg/vct/resthandler"
"github.com/trustbloc/orb/pkg/webcas"
wfclient "github.com/trustbloc/orb/pkg/webfinger/client"
)
Expand Down Expand Up @@ -1136,6 +1137,7 @@ func startOrbServices(parameters *orbParameters) error {
),
auth.NewHandlerWrapper(policyhandler.New(configStore), authTokenManager),
auth.NewHandlerWrapper(policyhandler.NewRetriever(configStore), authTokenManager),
auth.NewHandlerWrapper(vcthandler.New(configStore, logMonitorStore), authTokenManager),
auth.NewHandlerWrapper(nodeinfo.NewHandler(nodeinfo.V2_0, nodeInfoService, nodeInfoLogger), authTokenManager),
auth.NewHandlerWrapper(nodeinfo.NewHandler(nodeinfo.V2_1, nodeInfoService, nodeInfoLogger), authTokenManager),
auth.NewHandlerWrapper(vcresthandler.New(vcStore), authTokenManager),
Expand Down
142 changes: 142 additions & 0 deletions pkg/vct/resthandler/configurator.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
/*
Copyright SecureKey Technologies Inc. All Rights Reserved.

SPDX-License-Identifier: Apache-2.0
*/

package resthandler

import (
"encoding/json"
"io/ioutil"
"net/http"
"net/url"

"github.com/hyperledger/aries-framework-go/spi/storage"
"github.com/trustbloc/edge-core/pkg/log"
"github.com/trustbloc/sidetree-core-go/pkg/restapi/common"
)

const (
logURLKey = "log-url"
endpoint = "/log"
)

const (
badRequestResponse = "Bad Request."
internalServerErrorResponse = "Internal Server Error."
)

var logger = log.New("log-rest-handler")

// LogConfigurator updates VCT log URL in config store.
type LogConfigurator struct {
configStore storage.Store
logMonitorStore logMonitorStore
marshal func(interface{}) ([]byte, error)
}

// Path returns the HTTP REST endpoint for the LogConfigurator service.
func (c *LogConfigurator) Path() string {
return endpoint
}

// Method returns the HTTP REST method for the configure VCT URL service.
func (c *LogConfigurator) Method() string {
return http.MethodPost
}

// Handler returns the HTTP REST handle for the VCT URL Configurator service.
func (c *LogConfigurator) Handler() common.HTTPRequestHandler {
return c.handle
}

type logMonitorStore interface {
Activate(logURL string) error
}

// New returns a new LogConfigurator.
func New(cfgStore storage.Store, lmStore logMonitorStore) *LogConfigurator {
h := &LogConfigurator{
configStore: cfgStore,
logMonitorStore: lmStore,
marshal: json.Marshal,
}

return h
}

func (c *LogConfigurator) handle(w http.ResponseWriter, req *http.Request) {
logURLBytes, err := ioutil.ReadAll(req.Body)
if err != nil {
logger.Errorf("[%s] Error reading request body: %s", endpoint, err)

writeResponse(w, http.StatusBadRequest, []byte(badRequestResponse))

return
}

logURLStr := string(logURLBytes)

if logURLStr != "" {
_, err = url.Parse(logURLStr)
if err != nil {
logger.Errorf("[%s] Invalid log URL: %s", endpoint, err)

writeResponse(w, http.StatusBadRequest, []byte(badRequestResponse))

return
}
}

valueBytes, err := c.marshal(logURLStr)
if err != nil {
logger.Errorf("[%s] Marshal log URL error: %s", endpoint, err)

writeResponse(w, http.StatusInternalServerError, []byte(internalServerErrorResponse))

return
}

err = c.configStore.Put(logURLKey, valueBytes)
if err != nil {
logger.Errorf("[%s] Error storing log URL: %s", endpoint, err)

writeResponse(w, http.StatusInternalServerError, []byte(internalServerErrorResponse))

return
}

logger.Debugf("[%s] Stored log URL %s", endpoint, string(logURLBytes))

if logURLStr != "" {
err = c.logMonitorStore.Activate(logURLStr)
if err != nil {
logger.Errorf("[%s] Error activating log monitoring for log URL: %s", endpoint, err)

writeResponse(w, http.StatusInternalServerError, []byte(internalServerErrorResponse))

return
}
}

writeResponse(w, http.StatusOK, nil)
}

func writeResponse(w http.ResponseWriter, status int, body []byte) {
if len(body) > 0 {
w.Header().Set("Content-Type", "text/plain")
}

w.WriteHeader(status)

if len(body) > 0 {
if _, err := w.Write(body); err != nil {
logger.Warnf("[%s] Unable to write response: %s", endpoint, err)

return
}

logger.Debugf("[%s] Wrote response: %s", endpoint, body)
}
}
213 changes: 213 additions & 0 deletions pkg/vct/resthandler/configurator_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,213 @@
/*
Copyright SecureKey Technologies Inc. All Rights Reserved.

SPDX-License-Identifier: Apache-2.0
*/

package resthandler

import (
"bytes"
"errors"
"fmt"
"io/ioutil"
"net/http"
"net/http/httptest"
"testing"

"github.com/hyperledger/aries-framework-go/component/storageutil/mem"
"github.com/stretchr/testify/require"

storemocks "github.com/trustbloc/orb/pkg/store/mocks"
)

const (
testLogURL = "https://vct.com/log"
configStoreName = "orb-config"
)

func TestNew(t *testing.T) {
configStore, err := mem.NewProvider().OpenStore(configStoreName)
require.NoError(t, err)

logConfigurator := New(configStore, &mockLogMonitorStore{})
require.NotNil(t, logConfigurator)
require.Equal(t, endpoint, logConfigurator.Path())
require.Equal(t, http.MethodPost, logConfigurator.Method())
require.NotNil(t, logConfigurator.Handler())
}

func TestHandler(t *testing.T) {
t.Run("success", func(t *testing.T) {
configStore, err := mem.NewProvider().OpenStore(configStoreName)
require.NoError(t, err)

logConfigurator := New(configStore, &mockLogMonitorStore{})
require.NotNil(t, logConfigurator)

rw := httptest.NewRecorder()
req := httptest.NewRequest(http.MethodPost, endpoint, bytes.NewBuffer([]byte(testLogURL)))

logConfigurator.handle(rw, req)

result := rw.Result()
require.Equal(t, http.StatusOK, result.StatusCode)
require.NoError(t, result.Body.Close())

respBytes, err := ioutil.ReadAll(result.Body)
require.NoError(t, err)
require.Empty(t, respBytes)
require.NoError(t, result.Body.Close())
})

t.Run("success - empty URL (equivalent to no log)", func(t *testing.T) {
configStore, err := mem.NewProvider().OpenStore(configStoreName)
require.NoError(t, err)

logConfigurator := New(configStore, &mockLogMonitorStore{})
require.NotNil(t, logConfigurator)

rw := httptest.NewRecorder()
req := httptest.NewRequest(http.MethodPost, endpoint, bytes.NewBuffer([]byte("")))

logConfigurator.handle(rw, req)

result := rw.Result()
require.Equal(t, http.StatusOK, result.StatusCode)
require.NoError(t, result.Body.Close())

respBytes, err := ioutil.ReadAll(result.Body)
require.NoError(t, err)
require.Empty(t, respBytes)
require.NoError(t, result.Body.Close())
})

t.Run("error - reader error", func(t *testing.T) {
configStore, err := mem.NewProvider().OpenStore(configStoreName)
require.NoError(t, err)

logConfigurator := New(configStore, &mockLogMonitorStore{})
require.NotNil(t, logConfigurator)

rw := httptest.NewRecorder()
req := httptest.NewRequest(http.MethodPost, endpoint, errReader(0))

logConfigurator.handle(rw, req)

result := rw.Result()
require.Equal(t, http.StatusBadRequest, result.StatusCode)
require.NoError(t, result.Body.Close())

respBytes, err := ioutil.ReadAll(result.Body)
require.NoError(t, err)
require.Equal(t, []byte(badRequestResponse), respBytes)
require.NoError(t, result.Body.Close())
})

t.Run("error - parse URL error", func(t *testing.T) {
configStore, err := mem.NewProvider().OpenStore(configStoreName)
require.NoError(t, err)

logConfigurator := New(configStore, &mockLogMonitorStore{})
require.NotNil(t, logConfigurator)

rw := httptest.NewRecorder()
req := httptest.NewRequest(http.MethodPost, endpoint, bytes.NewBuffer([]byte(":InvalidURL")))

logConfigurator.handle(rw, req)

result := rw.Result()
require.Equal(t, http.StatusBadRequest, result.StatusCode)
require.NoError(t, result.Body.Close())

respBytes, err := ioutil.ReadAll(result.Body)
require.NoError(t, err)
require.Equal(t, []byte(badRequestResponse), respBytes)
require.NoError(t, result.Body.Close())
})

t.Run("error - config store error", func(t *testing.T) {
configStore := &storemocks.Store{}
configStore.PutReturns(fmt.Errorf("put error"))

logConfigurator := New(configStore, &mockLogMonitorStore{})
require.NotNil(t, logConfigurator)

rw := httptest.NewRecorder()
req := httptest.NewRequest(http.MethodPost, endpoint, bytes.NewBuffer([]byte(testLogURL)))

logConfigurator.handle(rw, req)

result := rw.Result()
require.Equal(t, http.StatusInternalServerError, result.StatusCode)
require.NoError(t, result.Body.Close())

respBytes, err := ioutil.ReadAll(result.Body)
require.NoError(t, err)
require.Equal(t, []byte(internalServerErrorResponse), respBytes)
require.NoError(t, result.Body.Close())
})

t.Run("error - marshal error", func(t *testing.T) {
configStore := &storemocks.Store{}

logConfigurator := New(configStore, &mockLogMonitorStore{})
require.NotNil(t, logConfigurator)

errExpected := errors.New("injected marshal error")

logConfigurator.marshal = func(interface{}) ([]byte, error) {
return nil, errExpected
}

rw := httptest.NewRecorder()
req := httptest.NewRequest(http.MethodPost, endpoint, bytes.NewBuffer([]byte(testLogURL)))

logConfigurator.handle(rw, req)

result := rw.Result()
require.Equal(t, http.StatusInternalServerError, result.StatusCode)
require.NoError(t, result.Body.Close())

respBytes, err := ioutil.ReadAll(result.Body)
require.NoError(t, err)
require.Equal(t, []byte(internalServerErrorResponse), respBytes)
require.NoError(t, result.Body.Close())
})

t.Run("error - log monitor store error", func(t *testing.T) {
configStore, err := mem.NewProvider().OpenStore(configStoreName)
require.NoError(t, err)

logConfigurator := New(configStore, &mockLogMonitorStore{Err: fmt.Errorf("log monitor store error")})
require.NotNil(t, logConfigurator)

rw := httptest.NewRecorder()
req := httptest.NewRequest(http.MethodPost, endpoint, bytes.NewBuffer([]byte(testLogURL)))

logConfigurator.handle(rw, req)

result := rw.Result()
require.Equal(t, http.StatusInternalServerError, result.StatusCode)
require.NoError(t, result.Body.Close())

respBytes, err := ioutil.ReadAll(result.Body)
require.NoError(t, err)
require.Equal(t, []byte(internalServerErrorResponse), respBytes)
require.NoError(t, result.Body.Close())
})
}

type errReader int

func (errReader) Read(p []byte) (n int, err error) {
return 0, fmt.Errorf("reader error")
}

type mockLogMonitorStore struct {
Err error
}

func (m *mockLogMonitorStore) Activate(_ string) error {
return m.Err
}
9 changes: 9 additions & 0 deletions test/bdd/features/did-orb.feature
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,15 @@ Feature:

Then we wait 3 seconds

@vct_log_rotation_test
Scenario: various did doc operations
Given the authorization bearer token for "POST" requests to path "/log" is set to "ADMIN_TOKEN"

Then we wait 2 seconds

# domain1 will start following 2022 VCT log
When an HTTP POST is sent to "https://orb.domain1.com/log" with content "http://orb.vct:8077/maple2022" of type "text/plain"

@all
@discover_did_hashlink
Scenario: discover did (hashlink)
Expand Down
Loading