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

NOISSUE - Add certificate fields to the Bootstrap service #752

Merged
merged 15 commits into from
May 22, 2019
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