Skip to content

Commit

Permalink
NOISSUE - Add certificate fields to the Bootstrap service (#752)
Browse files Browse the repository at this point in the history
* Add cert fields to the BS

Signed-off-by: Dušan Borovčanin <dusan.borovcanin@mainflux.com>

* Add cert fields when creating a config

Signed-off-by: Dušan Borovčanin <dusan.borovcanin@mainflux.com>

* Add update cert endpoint

Signed-off-by: Dušan Borovčanin <dusan.borovcanin@mainflux.com>

* Fix key column name

Signed-off-by: Dušan Borovčanin <dusan.borovcanin@mainflux.com>

* Add cert fields to db converters

Signed-off-by: Dušan Borovčanin <dusan.borovcanin@mainflux.com>

* Secure cert update endpoint

Signed-off-by: Dušan Borovčanin <dusan.borovcanin@mainflux.com>

* Authroize cert update methods

Signed-off-by: Dušan Borovčanin <dusan.borovcanin@mainflux.com>

* Fix Bootstrap service tests

Signed-off-by: Dušan Borovčanin <dusan.borovcanin@mainflux.com>

* Add cert update service tests

Signed-off-by: Dušan Borovčanin <dusan.borovcanin@mainflux.com>

* Update endpoit tests

Signed-off-by: Dušan Borovčanin <dusan.borovcanin@mainflux.com>

* Update API docs

Signed-off-by: Dušan Borovčanin <dusan.borovcanin@mainflux.com>

* Update request tests

Signed-off-by: Dušan Borovčanin <dusan.borovcanin@mainflux.com>

* Fix request tests

Signed-off-by: Dušan Borovčanin <dusan.borovcanin@mainflux.com>

* Update repository tests

Signed-off-by: Dušan Borovčanin <dusan.borovcanin@mainflux.com>

* Fix typo in repo tests

Signed-off-by: Dušan Borovčanin <dusan.borovcanin@mainflux.com>
  • Loading branch information
dborovcanin authored and manuio committed May 22, 2019
1 parent e38b598 commit eae8072
Show file tree
Hide file tree
Showing 17 changed files with 489 additions and 23 deletions.
20 changes: 20 additions & 0 deletions bootstrap/api/endpoint.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,9 @@ func addEndpoint(svc bootstrap.Service) endpoint.Endpoint {
ExternalKey: req.ExternalKey,
MFChannels: channels,
Name: req.Name,
ClientCert: req.ClientCert,
ClientKey: req.ClientKey,
CACert: req.CACert,
Content: req.Content,
}

Expand All @@ -49,6 +52,23 @@ func addEndpoint(svc bootstrap.Service) endpoint.Endpoint {
}
}

func updateCertEndpoint(svc bootstrap.Service) endpoint.Endpoint {
return func(_ context.Context, request interface{}) (interface{}, error) {
req := request.(updateCertReq)
if err := req.validate(); err != nil {
return nil, err
}

if err := svc.UpdateCert(req.key, req.thingKey, req.ClientCert, req.ClientKey, req.CACert); err != nil {
return nil, err
}

res := configRes{}

return res, nil
}
}

func viewEndpoint(svc bootstrap.Service) endpoint.Endpoint {
return func(_ context.Context, request interface{}) (interface{}, error) {
req := request.(entityReq)
Expand Down
112 changes: 106 additions & 6 deletions bootstrap/api/endpoint_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -62,13 +62,19 @@ var (
}

updateReq = struct {
Channels []string `json:"channels"`
Content string `json:"content"`
State bootstrap.State `json:"state"`
Channels []string `json:"channels,omitempty"`
Content string `json:"content,omitempty"`
State bootstrap.State `json:"state,omitempty"`
ClientCert string `json:"client_cert,omitempty"`
ClientKey string `json:"client_key,omitempty"`
CACert string `json:"ca_cert,omitempty"`
}{
Channels: []string{"2", "3"},
Content: "config update",
State: 1,
Channels: []string{"2", "3"},
Content: "config update",
State: 1,
ClientCert: "newcert",
ClientKey: "newkey",
CACert: "newca",
}
)

Expand Down Expand Up @@ -466,6 +472,100 @@ func TestUpdate(t *testing.T) {
assert.Equal(t, tc.status, res.StatusCode, fmt.Sprintf("%s: expected status code %d got %d", tc.desc, tc.status, res.StatusCode))
}
}
func TestUpdateCert(t *testing.T) {
users := mocks.NewUsersService(map[string]string{validToken: email})

ts := newThingsServer(newThingsService(users))
svc := newService(users, nil, ts.URL)
bs := newBootstrapServer(svc)

c := newConfig([]bootstrap.Channel{bootstrap.Channel{ID: "1"}})

saved, err := svc.Add(validToken, c)
require.Nil(t, err, fmt.Sprintf("Saving config expected to succeed: %s.\n", err))

data := toJSON(updateReq)

cases := []struct {
desc string
req string
key string
auth string
contentType string
status int
}{
{
desc: "update unauthorized",
req: data,
key: saved.MFKey,
auth: invalidToken,
contentType: contentType,
status: http.StatusForbidden,
},
{
desc: "update with an empty token",
req: data,
key: saved.MFKey,
auth: "",
contentType: contentType,
status: http.StatusForbidden,
},
{
desc: "update a valid config",
req: data,
key: saved.MFKey,
auth: validToken,
contentType: contentType,
status: http.StatusOK,
},
{
desc: "update a config with wrong content type",
req: data,
key: saved.MFKey,
auth: validToken,
contentType: "",
status: http.StatusUnsupportedMediaType,
},
{
desc: "update a non-existing config",
req: data,
key: wrongID,
auth: validToken,
contentType: contentType,
status: http.StatusNotFound,
},
{
desc: "update a config with invalid request format",
req: "}",
key: saved.MFKey,
auth: validToken,
contentType: contentType,
status: http.StatusBadRequest,
},
{
desc: "update a config with an empty request",
key: saved.MFKey,
req: "",
auth: validToken,
contentType: contentType,
status: http.StatusBadRequest,
},
}

for _, tc := range cases {
req := testRequest{
client: bs.Client(),
method: http.MethodPut,
url: fmt.Sprintf("%s/things/configs/certs/%s", bs.URL, tc.key),
contentType: tc.contentType,
token: tc.auth,
body: strings.NewReader(tc.req),
}
res, err := req.make()
assert.Nil(t, err, fmt.Sprintf("%s: unexpected error %s", tc.desc, err))
assert.Equal(t, tc.status, res.StatusCode, fmt.Sprintf("%s: expected status code %d got %d", tc.desc, tc.status, res.StatusCode))
}
}

func TestUpdateConnections(t *testing.T) {
users := mocks.NewUsersService(map[string]string{validToken: email})
Expand Down
13 changes: 13 additions & 0 deletions bootstrap/api/logging.go
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,19 @@ func (lm *loggingMiddleware) Update(key string, cfg bootstrap.Config) (err error
return lm.svc.Update(key, cfg)
}

func (lm *loggingMiddleware) UpdateCert(key, thingKey, clientCert, clientKey, caCert string) (err error) {
defer func(begin time.Time) {
message := fmt.Sprintf("Method update_cert for thing with key %s took %s to complete", thingKey, time.Since(begin))
if err != nil {
lm.logger.Warn(fmt.Sprintf("%s with error: %s.", message, err))
return
}
lm.logger.Info(fmt.Sprintf("%s without errors.", message))
}(time.Now())

return lm.svc.UpdateCert(key, thingKey, clientCert, clientKey, caCert)
}

func (lm *loggingMiddleware) UpdateConnections(key, id string, connections []string) (err error) {
defer func(begin time.Time) {
message := fmt.Sprintf("Method update_connections for key %s and thing %s took %s to complete", key, id, time.Since(begin))
Expand Down
9 changes: 9 additions & 0 deletions bootstrap/api/metrics.go
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,15 @@ func (mm *metricsMiddleware) Update(key string, cfg bootstrap.Config) (err error
return mm.svc.Update(key, cfg)
}

func (mm *metricsMiddleware) UpdateCert(key, thingKey, clientCert, clientKey, caCert string) (err error) {
defer func(begin time.Time) {
mm.counter.With("method", "update_cert").Add(1)
mm.latency.With("method", "update_cert").Observe(time.Since(begin).Seconds())
}(time.Now())

return mm.svc.UpdateCert(key, thingKey, clientCert, clientKey, caCert)
}

func (mm *metricsMiddleware) UpdateConnections(key, id string, connections []string) (err error) {
defer func(begin time.Time) {
mm.counter.With("method", "update_connections").Add(1)
Expand Down
23 changes: 23 additions & 0 deletions bootstrap/api/requests.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,9 @@ type addReq struct {
Channels []string `json:"channels"`
Name string `json:"name"`
Content string `json:"content"`
ClientCert string `json:"client_cert"`
ClientKey string `json:"client_key"`
CACert string `json:"ca_cert"`
}

func (req addReq) validate() error {
Expand Down Expand Up @@ -71,6 +74,26 @@ func (req updateReq) validate() error {
return nil
}

type updateCertReq struct {
key string
thingKey string
ClientCert string `json:"client_cert"`
ClientKey string `json:"client_key"`
CACert string `json:"ca_cert"`
}

func (req updateCertReq) validate() error {
if req.key == "" {
return bootstrap.ErrUnauthorizedAccess
}

if req.thingKey == "" {
return bootstrap.ErrNotFound
}

return nil
}

type updateConnReq struct {
key string
id string
Expand Down
32 changes: 32 additions & 0 deletions bootstrap/api/requests_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,38 @@ func TestUpdateReqValidation(t *testing.T) {
}
}

func TestUpdateCertReqValidation(t *testing.T) {
cases := []struct {
desc string
key string
thingKey string
err error
}{
{
desc: "empty key",
key: "",
thingKey: "thingKey",
err: bootstrap.ErrUnauthorizedAccess,
},
{
desc: "empty thing key",
key: "key",
thingKey: "",
err: bootstrap.ErrNotFound,
},
}

for _, tc := range cases {
req := updateCertReq{
key: tc.key,
thingKey: tc.thingKey,
}

err := req.validate()
assert.Equal(t, tc.err, err, fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
}
}

func TestUpdateConnReqValidation(t *testing.T) {
cases := []struct {
desc string
Expand Down
20 changes: 20 additions & 0 deletions bootstrap/api/transport.go
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,12 @@ func MakeHandler(svc bootstrap.Service, reader bootstrap.ConfigReader) http.Hand
encodeResponse,
opts...))

r.Put("/things/configs/certs/:key", kithttp.NewServer(
updateCertEndpoint(svc),
decodeUpdateCertRequest,
encodeResponse,
opts...))

r.Put("/things/configs/connections/:id", kithttp.NewServer(
updateConnEndpoint(svc),
decodeUpdateConnRequest,
Expand Down Expand Up @@ -131,6 +137,20 @@ func decodeUpdateRequest(_ context.Context, r *http.Request) (interface{}, error
return req, nil
}

func decodeUpdateCertRequest(_ context.Context, r *http.Request) (interface{}, error) {
if !strings.Contains(r.Header.Get("Content-Type"), contentType) {
return nil, errUnsupportedContentType
}

req := updateCertReq{key: r.Header.Get("Authorization")}
req.thingKey = bone.GetValue(r, "key")
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
return nil, err
}

return req, nil
}

func decodeUpdateConnRequest(_ context.Context, r *http.Request) (interface{}, error) {
if !strings.Contains(r.Header.Get("Content-Type"), contentType) {
return nil, errUnsupportedContentType
Expand Down
9 changes: 8 additions & 1 deletion bootstrap/configs.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@ type Config struct {
MFThing string
Owner string
Name string
ClientCert string
ClientKey string
CACert string
MFKey string
MFChannels []Channel
ExternalID string
Expand Down Expand Up @@ -64,10 +67,14 @@ type ConfigRepository interface {
// RetrieveByExternalID returns Config for given external ID.
RetrieveByExternalID(string, string) (Config, error)

// Update performs and update to an existing Config. A non-nil error is returned
// Update updates an existing Config. A non-nil error is returned
// to indicate operation failure.
Update(Config) error

// UpdateCerts updates an existing Config certificate and key.
// A non-nil error is returned to indicate operation failure.
UpdateCert(string, string, string, string, string) error

// UpdateConnections updates a list of Channels the Config is connected to
// adding new Channels if needed.
UpdateConnections(string, string, []Channel, []string) error
Expand Down
21 changes: 21 additions & 0 deletions bootstrap/mocks/configs.go
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,27 @@ func (crm *configRepositoryMock) Update(config bootstrap.Config) error {
return nil
}

func (crm *configRepositoryMock) UpdateCert(owner, thingKey, clientCert, clientKey, caCert string) error {
crm.mu.Lock()
defer crm.mu.Unlock()
var forUpdate bootstrap.Config
for _, v := range crm.configs {
if v.MFKey == thingKey && v.Owner == owner {
forUpdate = v
break
}
}
if _, ok := crm.configs[forUpdate.MFThing]; !ok {
return bootstrap.ErrNotFound
}
forUpdate.ClientCert = clientCert
forUpdate.ClientKey = clientKey
forUpdate.CACert = caCert
crm.configs[forUpdate.MFThing] = forUpdate

return nil
}

func (crm *configRepositoryMock) UpdateConnections(key, id string, channels []bootstrap.Channel, connections []string) error {
crm.mu.Lock()
defer crm.mu.Unlock()
Expand Down
Loading

0 comments on commit eae8072

Please sign in to comment.