diff --git a/backend/services/deviceauth/api/http/api_devauth.go b/backend/services/deviceauth/api/http/api_devauth.go index 9df4c72b3..fa2fbf8bb 100644 --- a/backend/services/deviceauth/api/http/api_devauth.go +++ b/backend/services/deviceauth/api/http/api_devauth.go @@ -455,14 +455,7 @@ func (i *DevAuthApiHandlers) UpdateDeviceStatusHandler(c *gin.Context) { return } - switch status.Status { - case model.DevStatusAccepted: - err = i.app.AcceptDeviceAuth(ctx, devid, authid) - case model.DevStatusRejected: - err = i.app.RejectDeviceAuth(ctx, devid, authid) - case model.DevStatusPending: - err = i.app.ResetDeviceAuth(ctx, devid, authid) - } + err = i.app.SetAuthSetStatus(ctx, devid, authid, status.Status) if err != nil { switch err { case store.ErrDevNotFound, store.ErrAuthSetNotFound: diff --git a/backend/services/deviceauth/api/http/api_devauth_test.go b/backend/services/deviceauth/api/http/api_devauth_test.go index f1be36698..dbad9f110 100644 --- a/backend/services/deviceauth/api/http/api_devauth_test.go +++ b/backend/services/deviceauth/api/http/api_devauth_test.go @@ -568,7 +568,7 @@ func TestApiV2DevAuthUpdateStatusDevice(t *testing.T) { }, } - mockaction := func(_ context.Context, dev_id string, auth_id string) error { + mockaction := func(dev_id string, auth_id string) error { d, ok := devs[dev_id+","+auth_id] if ok == false { return store.ErrDevNotFound @@ -578,116 +578,72 @@ func TestApiV2DevAuthUpdateStatusDevice(t *testing.T) { } return nil } - da := &mocks.App{} - da.On("AcceptDeviceAuth", - mtest.ContextMatcher(), - mock.AnythingOfType("string"), - mock.AnythingOfType("string")).Return(mockaction) - da.On("RejectDeviceAuth", - mtest.ContextMatcher(), - mock.AnythingOfType("string"), - mock.AnythingOfType("string")).Return(mockaction) - da.On("ResetDeviceAuth", - mtest.ContextMatcher(), - mock.AnythingOfType("string"), - mock.AnythingOfType("string")).Return(mockaction) - - apih := makeMockApiHandler(t, da, nil) - - accstatus := DevAuthApiStatus{"accepted"} - rejstatus := DevAuthApiStatus{"rejected"} - penstatus := DevAuthApiStatus{"pending"} tcases := []struct { - req *http.Request - code int - body string + deviceID, authID string + status string + code int + body string }{ { - req: rtest.MakeTestRequest(&rtest.TestRequest{ - Method: "PUT", - Path: "http://localhost/api/management/v2/devauth/devices/123/auth/456/status", - Auth: true, - }), - code: http.StatusBadRequest, - body: RestError("failed to decode status data: invalid request"), + deviceID: "123", + authID: "456", + status: "", + code: http.StatusBadRequest, + body: RestError("failed to decode status data: invalid request"), }, { - req: rtest.MakeTestRequest(&rtest.TestRequest{ - Method: "PUT", - Path: "http://localhost/api/management/v2/devauth/devices/123/auth/456/status", - Auth: true, - Body: DevAuthApiStatus{"foo"}, - }), - code: http.StatusBadRequest, - body: RestError("incorrect device status"), + deviceID: "123", + authID: "456", + status: "foo", + code: http.StatusBadRequest, + body: RestError("incorrect device status"), }, { - req: rtest.MakeTestRequest(&rtest.TestRequest{ - Method: "PUT", - Path: "http://localhost/api/management/v2/devauth/devices/123/auth/456/status", - Auth: true, - Body: accstatus, - }), - code: http.StatusNoContent, + deviceID: "123", + authID: "456", + status: model.DevStatusAccepted, + code: http.StatusNoContent, }, { - req: rtest.MakeTestRequest(&rtest.TestRequest{ - Method: "PUT", - Path: "http://localhost/api/management/v2/devauth/devices/345/auth/678/status", - Auth: true, - Body: accstatus, - }), - code: http.StatusInternalServerError, - body: RestError("internal error"), + deviceID: "345", + authID: "678", + status: model.DevStatusAccepted, + code: http.StatusInternalServerError, + body: RestError("internal error"), }, { - req: rtest.MakeTestRequest(&rtest.TestRequest{ - Method: "PUT", - Path: "http://localhost/api/management/v2/devauth/devices/999/auth/123/status", - Auth: true, - Body: accstatus, - }), - code: http.StatusNotFound, - body: RestError(store.ErrDevNotFound.Error()), + deviceID: "999", + authID: "123", + status: model.DevStatusAccepted, + code: http.StatusNotFound, + body: RestError(store.ErrDevNotFound.Error()), }, { - req: rtest.MakeTestRequest(&rtest.TestRequest{ - Method: "PUT", - Path: "http://localhost/api/management/v2/devauth/devices/123/auth/456/status", - Auth: true, - Body: rejstatus, - }), - code: http.StatusNoContent, + deviceID: "123", + authID: "456", + status: model.DevStatusRejected, + code: http.StatusNoContent, }, { - req: rtest.MakeTestRequest(&rtest.TestRequest{ - Method: "PUT", - Path: "http://localhost/api/management/v2/devauth/devices/123/auth/456/status", - Auth: true, - Body: penstatus, - }), - code: http.StatusNoContent, + deviceID: "123", + authID: "456", + status: model.DevStatusPending, + code: http.StatusNoContent, }, { - req: rtest.MakeTestRequest(&rtest.TestRequest{ - Method: "PUT", - Path: "http://localhost/api/management/v2/devauth/devices/234/auth/567/status", - Auth: true, - Body: penstatus, - }), - code: http.StatusBadRequest, - body: RestError("dev auth: dev ID and auth ID mismatch"), + deviceID: "234", + authID: "567", + status: model.DevStatusPending, + code: http.StatusBadRequest, + body: RestError("dev auth: dev ID and auth ID mismatch"), }, { - req: rtest.MakeTestRequest(&rtest.TestRequest{ - Method: "PUT", - Path: "http://localhost/api/management/v2/devauth/devices/567/auth/890/status", - Auth: true, - Body: accstatus, - }), - code: http.StatusUnprocessableEntity, - body: RestError("maximum number of accepted devices reached"), + deviceID: "567", + authID: "890", + status: model.DevStatusAccepted, + code: http.StatusUnprocessableEntity, + body: RestError("maximum number of accepted devices reached"), }, } @@ -695,8 +651,30 @@ func TestApiV2DevAuthUpdateStatusDevice(t *testing.T) { tc := tcases[idx] t.Run(fmt.Sprintf("tc %d", idx), func(t *testing.T) { t.Parallel() + da := mocks.NewApp(t) + da.On("SetAuthSetStatus", + mtest.ContextMatcher(), + mock.AnythingOfType("string"), + mock.AnythingOfType("string"), + tc.status, + ). + Return(mockaction(tc.deviceID, tc.authID)). + Maybe() - runTestRequest(t, apih, tc.req, tc.code, tc.body) + apih := makeMockApiHandler(t, da, nil) + var body any + if tc.status != "" { + body = DevAuthApiStatus{Status: tc.status} + } + req := rtest.MakeTestRequest(&rtest.TestRequest{ + Method: "PUT", + Path: fmt.Sprintf("http://localhost/api/management/v2/devauth/devices/%s/auth/%s/status", + tc.deviceID, tc.authID), + Auth: true, + Body: body, + }) + + runTestRequest(t, apih, req, tc.code, tc.body) }) } diff --git a/backend/services/deviceauth/devauth/devauth.go b/backend/services/deviceauth/devauth/devauth.go index 32d93394c..541a84288 100644 --- a/backend/services/deviceauth/devauth/devauth.go +++ b/backend/services/deviceauth/devauth/devauth.go @@ -17,6 +17,7 @@ import ( "context" "crypto/sha256" "encoding/json" + "fmt" "strings" "time" @@ -92,9 +93,12 @@ type App interface { DecommissionDevice(ctx context.Context, dev_id string) error DeleteDevice(ctx context.Context, dev_id string) error DeleteAuthSet(ctx context.Context, dev_id string, auth_id string) error - AcceptDeviceAuth(ctx context.Context, dev_id string, auth_id string) error - RejectDeviceAuth(ctx context.Context, dev_id string, auth_id string) error - ResetDeviceAuth(ctx context.Context, dev_id string, auth_id string) error + SetAuthSetStatus( + ctx context.Context, + deviceID string, + authID string, + status string, + ) error PreauthorizeDevice(ctx context.Context, req *model.PreAuthReq) (*model.Device, error) RevokeToken(ctx context.Context, tokenID string) error @@ -359,66 +363,21 @@ func (d *DevAuth) handlePreAuthDevice( return nil, ErrDevAuthUnauthorized } - currentStatus := dev.Status - if dev.Status != model.DevStatusAccepted { - // auth set is ok for auto-accepting, check device limit - allow, err := d.canAcceptDevice(ctx) - if err != nil { - return nil, err - } - - if !allow { - return nil, ErrMaxDeviceCountReached - } - } - - // Ensure that the old acceptable auth sets are rejected - if err := d.db.RejectAuthSetsForDevice(ctx, aset.DeviceId, aset.Id); err != nil && - !errors.Is(err, store.ErrAuthSetNotFound) { - return nil, errors.Wrap(err, "failed to reject auth sets") - } - update := model.AuthSetUpdate{ - Status: model.DevStatusAccepted, - } - // persist the 'accepted' status in both auth set, and device - if err := d.db.UpdateAuthSetById(ctx, aset.Id, update); err != nil { - return nil, errors.Wrap(err, "failed to update auth set status") + err = d.updateAuthSetStatus(ctx, aset, model.DevStatusAccepted) + if err != nil { + return nil, err } if err := d.updateDeviceStatus( ctx, - aset.DeviceId, + aset, + dev, model.DevStatusAccepted, - currentStatus, ); err != nil { return nil, err } - aset.Status = model.DevStatusAccepted - dev.Status = model.DevStatusAccepted dev.AuthSets = append(dev.AuthSets, *aset) - - if !dev.Provisioned { - reqId := requestid.FromContext(ctx) - var tenantID string - if idty := identity.FromContext(ctx); idty != nil { - tenantID = idty.Tenant - } - - // submit device accepted job - //nolint:bodyclose - _, _, err := d.cOrch.StartWorkflow(ctx, "provision_device"). - RequestBody(map[string]interface{}{ - "request_id": reqId, - "device_id": aset.DeviceId, - "tenant_id": tenantID, - "device": dev, - "status": dev.Status, - }).Execute() - if err != nil { - return nil, errors.Wrap(err, "submit device provisioning job error") - } - } return aset, nil } @@ -454,31 +413,63 @@ func (d *DevAuth) processPreAuthRequest( return d.handlePreAuthDevice(ctx, aset) } +func (d *DevAuth) aggregateDeviceStatus(ctx context.Context, deviceID string) (string, error) { + status, err := d.db.GetDeviceStatus(ctx, deviceID) + if err != nil { + if errors.Is(err, store.ErrAuthSetNotFound) { + status = model.DevStatusNoAuth + } else { + return "", errors.Wrap(err, "cannot determine device status") + } + } + return status, nil + +} + +func updateDeviceStatusEvent( + authSet *model.AuthSet, + device *model.Device, + status string, +) client.DeviceAuthEvent { + event := client.DeviceAuthEvent{ + Id: device.Id, + Status: &status, + AdditionalProperties: map[string]any{"revision": device.Revision}, + CreatedTs: &device.CreatedTs, + } + if authSet != nil { + event.AuthSets = append(event.AuthSets, client.AuthSet{ + Id: &authSet.Id, + DeviceId: &authSet.DeviceId, + IdentityData: authSet.IdDataStruct, + Pubkey: &authSet.PubKey, + Status: &authSet.Status, + Ts: authSet.Timestamp, + }) + } + return event +} + func (d *DevAuth) updateDeviceStatus( ctx context.Context, - devId, + authSet *model.AuthSet, + device *model.Device, status string, - currentStatus string, ) error { - newStatus, err := d.db.GetDeviceStatus(ctx, devId) - if err == nil && currentStatus == newStatus { - return nil + if device.Status == status { + return nil // No-op } - if status == "" { - switch err { - case nil: - status = newStatus - case store.ErrAuthSetNotFound: - status = model.DevStatusNoAuth - default: - return errors.Wrap(err, "Cannot determine device status") + if err := d.db.UpdateDeviceWithRevision(ctx, + device.Id, + device.Revision, + model.DeviceUpdate{ + Status: status, + UpdatedTs: uto.TimePtr(time.Now().UTC()), + }); err != nil { + if errors.Is(err, store.ErrDevNotFound) { + return nil } - } - - // submit device status change job - dev, err := d.db.GetDeviceById(ctx, devId) - if err != nil { - return errors.Wrap(err, "db get device by id error") + return errors.Wrap(err, "failed to update device status") } tenantId := "" @@ -486,29 +477,39 @@ func (d *DevAuth) updateDeviceStatus( if idData != nil { tenantId = idData.Tenant } - //nolint:bodyclose - _, _, err = d.cOrch.StartWorkflow(ctx, "update_device_status"). - RequestBody(map[string]interface{}{ - "request_id": requestid.FromContext(ctx), - "devices": []model.DeviceInventoryUpdate{{ - Id: dev.Id, - Revision: dev.Revision + 1, - }}, - "tenant_id": tenantId, - "device_status": status, - }).Execute() - if err != nil { - return errors.Wrap(err, "update device status job error") - } + device.Revision += 1 + device.Status = status - if err := d.db.UpdateDevice(ctx, - devId, - model.DeviceUpdate{ - Status: status, - UpdatedTs: uto.TimePtr(time.Now().UTC()), - }); err != nil { - return errors.Wrap(err, "failed to update device status") + if status == model.DevStatusAccepted && !device.Provisioned { + //nolint:bodyclose + _, _, err := d.cOrch.StartWorkflow(ctx, "provision_device"). + RequestBody(map[string]any{ + "request_id": requestid.FromContext(ctx), + "device_id": device.Id, + "tenant_id": tenantId, + "device": updateDeviceStatusEvent(authSet, device, status), + "status": status, + }).Execute() + + if err != nil { + return errors.Wrap(err, "submit device provisioning job error") + } + } else { + //nolint:bodyclose + _, _, err := d.cOrch.StartWorkflow(ctx, "update_device_status"). + RequestBody(map[string]any{ + "request_id": requestid.FromContext(ctx), + "devices": []client.DeviceAuthEvent{ + updateDeviceStatusEvent(authSet, device, status), + }, + "tenant_id": tenantId, + "device_status": status, + }).Execute() + + if err != nil { + return errors.Wrap(err, "update device status job error") + } } return nil @@ -551,8 +552,13 @@ func (d *DevAuth) processAuthRequest( return nil, err } + status, err := d.aggregateDeviceStatus(ctx, dev.Id) + if err != nil { + return nil, err + } + // update the device status - if err := d.updateDeviceStatus(ctx, dev.Id, "", dev.Status); err != nil { + if err := d.updateDeviceStatus(ctx, areq, dev, status); err != nil { return nil, err } @@ -748,8 +754,13 @@ func (d *DevAuth) DeleteAuthSet(ctx context.Context, devID string, authId string return err } + newStatus, err := d.aggregateDeviceStatus(ctx, devID) + if err != nil { + return err + } + // If the auth set status is 'preauthorized' and this is the only auth set - // for this device, the device is deleted from deviceauth. + // for this device (newStatus: noauth), the device is deleted from deviceauth. // We cannot start the decommission_device workflow because // we don't provision devices until they are accepted. Still, we need to // remove the device from the inventory service because we index pre-authorized @@ -757,21 +768,22 @@ func (d *DevAuth) DeleteAuthSet(ctx context.Context, devID string, authId string // from the inventory service, we start the status update workflow with the // special value "decommissioned", which will cause the deletion of the // device from the inventory service's database. - if authSet.Status == model.DevStatusPreauth { - authSets, err := d.db.GetAuthSetsForDevice(ctx, authSet.DeviceId) + if authSet.Status == model.DevStatusPreauth && newStatus == model.DevStatusNoAuth { + err = d.deletePreauthDevice(ctx, authSet.DeviceId) if err != nil { - return errors.Wrap(err, "db get auth sets error") + return errors.Wrap(err, "failed to delete preauthorized device") } - if len(authSets) == 0 { - err = d.deletePreauthDevice(ctx, authSet.DeviceId) - if err != nil { - return errors.Wrap(err, "failed to delete preauthorized device") - } + return nil + } + dev, err := d.db.GetDeviceById(ctx, devID) + if err != nil { + if errors.Is(err, store.ErrDevNotFound) { return nil } + return fmt.Errorf("failed to update device status: %w", err) } - return d.updateDeviceStatus(ctx, devID, "", authSet.Status) + return d.updateDeviceStatus(ctx, authSet, dev, newStatus) } func (d *DevAuth) deletePreauthDevice(ctx context.Context, devId string) error { @@ -829,83 +841,49 @@ func (d *DevAuth) deleteAuthSet(ctx context.Context, authSet *model.AuthSet) err return nil } -func (d *DevAuth) AcceptDeviceAuth(ctx context.Context, device_id string, auth_id string) error { - l := log.FromContext(ctx) - - aset, err := d.db.GetAuthSetById(ctx, auth_id) - if err != nil { - if err == store.ErrAuthSetNotFound { +func (d *DevAuth) updateAuthSetStatus( + ctx context.Context, aset *model.AuthSet, status string, +) error { + if status == model.DevStatusAccepted { + // if accepting an auth set + allow, err := d.canAcceptDevice(ctx) + if err != nil { return err } - return errors.Wrap(err, "db get auth set error") - } - - // device authentication set already accepted, nothing to do here - if aset.Status == model.DevStatusAccepted { - l.Debugf("Device %s already accepted", device_id) - return nil - } else if aset.Status != model.DevStatusRejected && aset.Status != model.DevStatusPending { - // device authentication set can be accepted only from 'pending' or 'rejected' statuses - return ErrDevAuthBadRequest - } - - // check the device status - // if the device status is accepted then do not trigger provisioning workflow - // this needs to be checked before changing authentication set status - dev, err := d.db.GetDeviceById(ctx, device_id) - if err != nil { - return err - } - - // possible race, consider accept-count-unaccept pattern if that's problematic - allow, err := d.canAcceptDevice(ctx) - if err != nil { - return err - } - - if !allow { - return ErrMaxDeviceCountReached - } - - if err := d.setAuthSetStatus(ctx, device_id, auth_id, model.DevStatusAccepted); err != nil { - return err - } - - if dev.Provisioned { - // Device already provisioned - // We're done... - return nil - } - - dev.Status = model.DevStatusAccepted - aset.Status = model.DevStatusAccepted - dev.AuthSets = []model.AuthSet{*aset} - - reqId := requestid.FromContext(ctx) - - var tenantID string - if idty := identity.FromContext(ctx); idty != nil { - tenantID = idty.Tenant + if !allow { + return ErrMaxDeviceCountReached + } + // reject all accepted auth sets for this device first + err = d.db.RejectAuthSetsForDevice(ctx, aset.DeviceId, aset.Id) + if err != nil && err != store.ErrAuthSetNotFound { + return errors.Wrap(err, "failed to reject auth sets") + } + } else if aset.Status == model.DevStatusAccepted { + // Authset transitions from accepted + err := d.cacheDeleteToken(ctx, aset.DeviceId) + if err != nil { + return errors.Wrapf(err, + "failed to delete token for %s from cache", + aset.DeviceId) + } + deviceOID := oid.FromString(aset.DeviceId) + // delete device token + err = d.db.DeleteTokenByDevId(ctx, deviceOID) + if err != nil && err != store.ErrTokenNotFound { + return errors.Wrap(err, "db delete device token error") + } } - // submit device accepted job - //nolint:bodyclose - _, _, err = d.cOrch.StartWorkflow(ctx, "provision_device"). - RequestBody(map[string]interface{}{ - "request_id": reqId, - "device_id": aset.DeviceId, - "tenant_id": tenantID, - "device": dev, - "status": dev.Status, - }).Execute() - if err != nil { - return errors.Wrap(err, "submit device provisioning job error") + if err := d.db.UpdateAuthSetById(ctx, aset.Id, model.AuthSetUpdate{ + Status: status, + }); err != nil { + return errors.Wrap(err, "db update device auth set error") } - + aset.Status = status return nil } -func (d *DevAuth) setAuthSetStatus( +func (d *DevAuth) SetAuthSetStatus( ctx context.Context, deviceID string, authID string, @@ -924,74 +902,35 @@ func (d *DevAuth) setAuthSetStatus( } if aset.Status == status { + // No-op return nil } - - currentStatus := aset.Status - - if aset.Status == model.DevStatusAccepted && - (status == model.DevStatusRejected || status == model.DevStatusPending) { - deviceOID := oid.FromString(aset.DeviceId) - // delete device token - err := d.db.DeleteTokenByDevId(ctx, deviceOID) - if err != nil && err != store.ErrTokenNotFound { - return errors.Wrap(err, "db delete device token error") - } - } - - // if accepting an auth set - if status == model.DevStatusAccepted { - // reject all accepted auth sets for this device first - err := d.db.RejectAuthSetsForDevice(ctx, deviceID, aset.Id) - if err != nil && err != store.ErrAuthSetNotFound { - return errors.Wrap(err, "failed to reject auth sets") - } - } - - if err := d.db.UpdateAuthSetById(ctx, aset.Id, model.AuthSetUpdate{ - Status: status, - }); err != nil { - return errors.Wrap(err, "db update device auth set error") - } - - if status == model.DevStatusAccepted { - return d.updateDeviceStatus(ctx, deviceID, status, currentStatus) - } - return d.updateDeviceStatus(ctx, deviceID, "", currentStatus) -} - -func (d *DevAuth) RejectDeviceAuth(ctx context.Context, device_id string, auth_id string) error { - aset, err := d.db.GetAuthSetById(ctx, auth_id) + // Validate status transition + err = model.ValidateStatusTransition(aset.Status, status) if err != nil { - if err == store.ErrAuthSetNotFound { - return err - } - return errors.Wrap(err, "db get auth set error") - } else if aset.Status != model.DevStatusPending && aset.Status != model.DevStatusAccepted { - // device authentication set can be rejected only from 'accepted' or 'pending' statuses return ErrDevAuthBadRequest } - err = d.cacheDeleteToken(ctx, device_id) + err = d.updateAuthSetStatus(ctx, aset, status) if err != nil { - return errors.Wrapf(err, "failed to delete token for %s from cache", device_id) + return err } - return d.setAuthSetStatus(ctx, device_id, auth_id, model.DevStatusRejected) -} - -func (d *DevAuth) ResetDeviceAuth(ctx context.Context, device_id string, auth_id string) error { - aset, err := d.db.GetAuthSetById(ctx, auth_id) + device, err := d.db.GetDeviceById(ctx, deviceID) if err != nil { - if err == store.ErrAuthSetNotFound { + return err + } + if status != model.DevStatusAccepted { + status, err = d.aggregateDeviceStatus(ctx, deviceID) + if err != nil { return err } - return errors.Wrap(err, "db get auth set error") - } else if aset.Status == model.DevStatusPreauth { - // preauthorized auth set should not go into pending state - return ErrDevAuthBadRequest } - return d.setAuthSetStatus(ctx, device_id, auth_id, model.DevStatusPending) + err = d.updateDeviceStatus(ctx, aset, device, status) + if err != nil { + return err + } + return nil } func parseIdData(idData string) (map[string]interface{}, []byte, error) { @@ -1066,6 +1005,7 @@ func (d *DevAuth) PreauthorizeDevice( if err != nil { return nil, err } + // FIXME: what about device status and iot-manager? return dev, nil } return dev, ErrDeviceExists @@ -1079,21 +1019,6 @@ func (d *DevAuth) PreauthorizeDevice( tenantId = idData.Tenant } - //nolint:bodyclose - _, _, err = d.cOrch.StartWorkflow(ctx, "update_device_status"). - RequestBody(map[string]interface{}{ - "request_id": requestid.FromContext(ctx), - "devices": []model.DeviceInventoryUpdate{{ - Id: dev.Id, - Revision: dev.Revision, - }}, - "tenant_id": tenantId, - "device_status": dev.Status, - }).Execute() - if err != nil { - return nil, errors.Wrap(err, "update device status job error") - } - // record authentication request authset := model.AuthSet{ Id: req.AuthSetId, @@ -1112,7 +1037,21 @@ func (d *DevAuth) PreauthorizeDevice( if err := d.setDeviceIdentity(ctx, dev, tenantId); err != nil { return nil, err } - return nil, nil + //nolint:bodyclose + _, _, err = d.cOrch.StartWorkflow(ctx, "update_device_status"). + RequestBody(map[string]any{ + "request_id": requestid.FromContext(ctx), + "devices": []client.DeviceAuthEvent{ + updateDeviceStatusEvent(&authset, dev, dev.Status), + }, + "tenant_id": tenantId, + "device_status": dev.Status, + }).Execute() + if err != nil { + return nil, errors.Wrap(err, "update device status job error") + } + + return dev, nil case store.ErrObjectExists: dev, err = d.db.GetDeviceByIdentityDataHash(ctx, idDataSha256) if err != nil { diff --git a/backend/services/deviceauth/devauth/devauth_test.go b/backend/services/deviceauth/devauth/devauth_test.go index 05c3cf26a..cfa1c7559 100644 --- a/backend/services/deviceauth/devauth/devauth_test.go +++ b/backend/services/deviceauth/devauth/devauth_test.go @@ -482,6 +482,7 @@ func TestDevAuthSubmitAuthRequest(t *testing.T) { IdDataSha256: idDataHash, IdDataStruct: idDataStruct, Id: devId, + Provisioned: true, } } return nil @@ -541,6 +542,11 @@ func TestDevAuthSubmitAuthRequest(t *testing.T) { db.On("GetDeviceStatus", ctxMatcher, mock.AnythingOfType("string")).Return( "pending", nil) + db.On("UpdateDeviceWithRevision", ctxMatcher, + devId, + uint(0), + mock.AnythingOfType("model.DeviceUpdate")).Return(nil) + // Update check in time db.On("UpdateDevice", ctxMatcher, devId, mock.AnythingOfType("model.DeviceUpdate")).Return(nil) @@ -653,7 +659,7 @@ func TestDevAuthSubmitAuthRequestPreauth(t *testing.T) { dbGetDevCountByStatusRes: 0, dev: &model.Device{ Id: dummyDevId, - Status: model.DevStatusPending, + Status: model.DevStatusPreauth, }, res: dummyToken, expectedWorkflows: map[string]error{ @@ -676,7 +682,7 @@ func TestDevAuthSubmitAuthRequestPreauth(t *testing.T) { dbGetDevCountByStatusRes: 0, dev: &model.Device{ Id: dummyDevId, - Status: model.DevStatusPending, + Status: model.DevStatusPreauth, Decommissioning: true, }, err: ErrDevAuthUnauthorized, @@ -686,7 +692,7 @@ func TestDevAuthSubmitAuthRequestPreauth(t *testing.T) { dbGetAuthSetByDataKeyErr: errors.New("db error"), dev: &model.Device{ Id: dummyDevId, - Status: model.DevStatusPending, + Status: model.DevStatusPreauth, }, err: errors.New("failed to fetch auth set: db error"), }, @@ -705,7 +711,7 @@ func TestDevAuthSubmitAuthRequestPreauth(t *testing.T) { dbGetDevCountByStatusRes: 5, dev: &model.Device{ Id: dummyDevId, - Status: model.DevStatusPending, + Status: model.DevStatusPreauth, }, err: ErrMaxDeviceCountReached, }, @@ -721,7 +727,7 @@ func TestDevAuthSubmitAuthRequestPreauth(t *testing.T) { dbGetLimitErr: errors.New("db error"), dev: &model.Device{ Id: dummyDevId, - Status: model.DevStatusPending, + Status: model.DevStatusPreauth, }, err: errors.New("can't get current device limit: db error"), }, @@ -740,7 +746,7 @@ func TestDevAuthSubmitAuthRequestPreauth(t *testing.T) { dbGetDevCountByStatusRes: 0, dev: &model.Device{ Id: dummyDevId, - Status: model.DevStatusPending, + Status: model.DevStatusPreauth, }, expectedWorkflows: map[string]error{ "provision_device": errors.New("workflows failed"), @@ -767,8 +773,6 @@ func TestDevAuthSubmitAuthRequestPreauth(t *testing.T) { //coSubmitProvisionDeviceJobErr: errors.New("workflows shouldn't be called"), // MEN-6961: we accept preauth at all times res: "dummytoken", expectedWorkflows: map[string]error{ - "update_device_status": nil, - "provision_device": nil, "update_device_inventory": nil, }, }, @@ -847,12 +851,22 @@ func TestDevAuthSubmitAuthRequestPreauth(t *testing.T) { ).Return(nil) // at the end of processing, updates the device status to 'accepted' + db.On("UpdateDeviceWithRevision", + ctxMatcher, + dummyDevId, + uint(0), + mock.MatchedBy( + func(u model.DeviceUpdate) bool { + return u.Status == model.DevStatusAccepted + }), + ).Return(nil) + // Check in time db.On("UpdateDevice", ctxMatcher, dummyDevId, mock.MatchedBy( func(u model.DeviceUpdate) bool { - return u.Status == model.DevStatusAccepted || !u.CheckInTime.IsZero() + return !u.CheckInTime.IsZero() }), ).Return(nil) @@ -883,12 +897,14 @@ func TestDevAuthSubmitAuthRequestPreauth(t *testing.T) { for name, err := range tc.expectedWorkflows { co.EXPECT(). StartWorkflow(mtesting.ContextMatcher(), name). + Run(func(context.Context, string) { + co.EXPECT(). + StartWorkflowExecute(mock.Anything). + Return(nil, mockResponseOK, err). + Once() + }). Return(req). Once() - co.EXPECT(). - StartWorkflowExecute(mock.Anything). - Return(nil, mockResponseOK, err). - Once() } // setup devauth @@ -955,6 +971,10 @@ func TestDevAuthPreauthorizeDevice(t *testing.T) { updateDeviceStatus: true, updateDeviceInventory: true, callDb: true, + outDev: &model.Device{ + Id: deviceID, + Status: model.DevStatusPreauth, + }, }, { desc: "error: add device, exists", @@ -963,8 +983,11 @@ func TestDevAuthPreauthorizeDevice(t *testing.T) { addDeviceErr: store.ErrObjectExists, - outDev: &model.Device{Id: deviceID}, - err: ErrDeviceExists, + outDev: &model.Device{ + Id: deviceID, + Status: model.DevStatusAccepted, + }, + err: ErrDeviceExists, }, { desc: "error: add device, generic", @@ -976,21 +999,22 @@ func TestDevAuthPreauthorizeDevice(t *testing.T) { err: errors.New("failed to add device: generic error"), }, { - desc: "error: add auth set, exists", - req: req, - updateDeviceStatus: true, - callDb: true, + desc: "error: add auth set, exists", + req: req, + callDb: true, addAuthSetErr: store.ErrObjectExists, - outDev: &model.Device{Id: deviceID}, - err: ErrDeviceExists, + outDev: &model.Device{ + Id: deviceID, + Status: model.DevStatusAccepted, + }, + err: ErrDeviceExists, }, { - desc: "error: add auth set, exists", - req: req, - updateDeviceStatus: true, - callDb: true, + desc: "error: add auth set, generic error", + req: req, + callDb: true, addAuthSetErr: errors.New("generic error"), @@ -1070,6 +1094,7 @@ func TestDevAuthPreauthorizeDevice(t *testing.T) { return &model.Device{ IdDataSha256: idDataSha256, Id: deviceID, + Status: model.DevStatusAccepted, } } return nil @@ -1087,6 +1112,7 @@ func TestDevAuthPreauthorizeDevice(t *testing.T) { if tc.outDev != nil { assert.Equal(t, tc.outDev.Id, dev.Id) + assert.Equal(t, tc.outDev.Status, dev.Status) } else { assert.Nil(t, dev) } @@ -1367,8 +1393,9 @@ func TestDevAuthAcceptDevice(t *testing.T) { } devauth := NewDevAuth(&db, nil, nil, nil, Config{}) - err := devauth.AcceptDeviceAuth( - context.Background(), dummyDevID, dummyAuthID) + err := devauth.SetAuthSetStatus( + context.Background(), dummyDevID, dummyAuthID, model.DevStatusAccepted, + ) if tc.outErr != "" { assert.EqualError(t, err, tc.outErr) @@ -1396,6 +1423,8 @@ func TestDevAuthRejectDevice(t *testing.T) { dbErr error dbDelDevTokenErr error + submitJob bool + outErr string }{ { @@ -1404,6 +1433,7 @@ func TestDevAuthRejectDevice(t *testing.T) { DeviceId: dummyDevID, Status: model.DevStatusAccepted, }, + submitJob: true, }, { aset: &model.AuthSet{ @@ -1431,6 +1461,7 @@ func TestDevAuthRejectDevice(t *testing.T) { DeviceId: dummyDevID, Status: model.DevStatusAccepted, }, + submitJob: true, withCache: true, tenant: "acme", }, @@ -1445,6 +1476,7 @@ func TestDevAuthRejectDevice(t *testing.T) { DeviceId: dummyDevID, Status: model.DevStatusAccepted, }, + submitJob: true, dbDelDevTokenErr: store.ErrTokenNotFound, }, { @@ -1483,23 +1515,36 @@ func TestDevAuthRejectDevice(t *testing.T) { db.On("GetAuthSetById", ctx, dummyAuthID). Return(tc.aset, tc.dbErr) + + co := oas_mocks.NewMockWorkflowsOtherAPI(t) if tc.aset != nil { db.On("UpdateAuthSetById", ctx, tc.aset.Id, model.AuthSetUpdate{Status: model.DevStatusRejected}).Return(nil) + db.On("GetDeviceById", ctx, + mock.AnythingOfType("string")). + Return(&model.Device{Id: tc.aset.DeviceId, Status: tc.aset.Status}, nil) + db.On("UpdateDeviceWithRevision", ctx, + tc.aset.DeviceId, + uint(0), + mock.AnythingOfType("model.DeviceUpdate")).Return(nil) + db.On("GetDeviceStatus", ctx, + tc.aset.DeviceId). + Return(model.DevStatusRejected, nil) + db.On("DeleteTokenByDevId", ctx, + oid.FromString(tc.aset.DeviceId)). + Return(tc.dbDelDevTokenErr) + + if tc.submitJob { + req := client.ApiStartWorkflowRequest{ApiService: co} + co.On("StartWorkflow", ctx, "update_device_status"). + Run(func(args mock.Arguments) { + co.On("StartWorkflowExecute", mock.Anything). + Return(nil, mockResponseOK, nil) + }). + Return(req). + Once() + } } - db.On("DeleteTokenByDevId", ctx, - dummyDevUUID). - Return(tc.dbDelDevTokenErr) - db.On("GetDeviceStatus", ctx, - dummyDevID). - Return("accepted", nil) - db.On("UpdateDevice", ctx, - dummyDevID, - mock.AnythingOfType("model.DeviceUpdate")).Return(nil) - db.On("GetDeviceById", ctx, - mock.AnythingOfType("string")).Return(&model.Device{}, nil) - - co := oas_mocks.NewMockWorkflowsOtherAPI(t) devauth := NewDevAuth(&db, co, nil, nil, Config{}) @@ -1524,8 +1569,8 @@ func TestDevAuthRejectDevice(t *testing.T) { c.AssertNotCalled(t, "DeleteToken") } - err := devauth.RejectDeviceAuth( - ctx, dummyDevID, dummyAuthID, + err := devauth.SetAuthSetStatus( + ctx, dummyDevID, dummyAuthID, model.DevStatusRejected, ) if tc.outErr != "" { @@ -1687,8 +1732,8 @@ func TestDevAuthResetDevice(t *testing.T) { DeviceId: dummyDevID, Status: "accepted", }, + submitJob: true, dbDelDevTokenErr: store.ErrTokenNotFound, - outErr: "db delete device token error: token not found", }, { aset: &model.AuthSet{ @@ -1713,23 +1758,29 @@ func TestDevAuthResetDevice(t *testing.T) { if tc.aset != nil { db.On("UpdateAuthSetById", context.Background(), tc.aset.Id, model.AuthSetUpdate{Status: model.DevStatusPending}).Return(nil) + db.On("GetDeviceById", context.Background(), + mock.AnythingOfType("string")). + Return(&model.Device{ + Id: tc.aset.DeviceId, + Status: tc.aset.Status, + Provisioned: true, + }, nil) } db.On("DeleteTokenByDevId", context.Background(), dummyDevUUID).Return( tc.dbDelDevTokenErr) db.On("GetDeviceStatus", context.Background(), dummyDevID).Return( - "accepted", nil) - db.On("UpdateDevice", context.Background(), + model.DevStatusPending, nil) + db.On("UpdateDeviceWithRevision", context.Background(), func() interface{} { if tc.aset != nil { return tc.aset.DeviceId } return mock.AnythingOfType("string") }(), + uint(0), mock.AnythingOfType("model.DeviceUpdate")).Return(nil) - db.On("GetDeviceById", context.Background(), - mock.AnythingOfType("string")).Return(&model.Device{}, nil) co := oas_mocks.NewMockWorkflowsOtherAPI(t) if tc.submitJob { @@ -1747,8 +1798,8 @@ func TestDevAuthResetDevice(t *testing.T) { } devauth := NewDevAuth(&db, co, nil, nil, Config{}) - err := devauth.ResetDeviceAuth( - context.Background(), dummyDevID, dummyAuthID, + err := devauth.SetAuthSetStatus( + context.Background(), dummyDevID, dummyAuthID, model.DevStatusPending, ) if tc.dbErr != nil || @@ -2885,8 +2936,6 @@ func TestDevAuthDeleteAuthSet(t *testing.T) { dbGetDeviceStatus string dbGetDeviceStatusErr error dbUpdateDeviceErr error - dbGetAuthSetsForDevice []model.AuthSet - dbGetAuthSetsForDeviceErr error submitJob bool orchestratorErr error @@ -2917,6 +2966,7 @@ func TestDevAuthDeleteAuthSet(t *testing.T) { }, dbDeleteTokenByDevIdErr: errors.New("DeleteTokenByDevId Error"), outErr: "db delete device tokens error: DeleteTokenByDevId Error", + dbGetDeviceStatus: model.DevStatusAccepted, }, { devId: oid.NewUUIDv5("devId4").String(), @@ -2928,6 +2978,7 @@ func TestDevAuthDeleteAuthSet(t *testing.T) { }, submitJob: true, dbDeleteTokenByDevIdErr: errors.New("DeleteTokenByDevId Error"), + dbGetDeviceStatus: model.DevStatusPending, }, { devId: oid.NewUUIDv5("devId5").String(), @@ -2939,6 +2990,7 @@ func TestDevAuthDeleteAuthSet(t *testing.T) { }, submitJob: true, dbDeleteTokenByDevIdErr: store.ErrTokenNotFound, + dbGetDeviceStatus: model.DevStatusAccepted, }, { devId: oid.NewUUIDv5("devId6").String(), @@ -2959,7 +3011,7 @@ func TestDevAuthDeleteAuthSet(t *testing.T) { Status: model.DevStatusPreauth, }, submitJob: true, - dbGetDeviceStatus: "decommissioned", + dbGetDeviceStatus: model.DevStatusNoAuth, dbDeleteDeviceErr: errors.New("DeleteDevice Error"), outErr: "failed to delete preauthorized device: DeleteDevice Error", }, @@ -2972,7 +3024,7 @@ func TestDevAuthDeleteAuthSet(t *testing.T) { Status: model.DevStatusPreauth, }, submitJob: true, - dbGetDeviceStatus: "decommissioned", + dbGetDeviceStatus: model.DevStatusNoAuth, orchestratorErr: errors.New("orchestrator error"), outErr: "failed to delete preauthorized device: failed to start update device status job: orchestrator error", }, @@ -2985,7 +3037,7 @@ func TestDevAuthDeleteAuthSet(t *testing.T) { Status: model.DevStatusPreauth, }, submitJob: true, - dbGetDeviceStatus: "decommissioned", + dbGetDeviceStatus: model.DevStatusNoAuth, dbDeleteDeviceErr: errors.New("DeleteDevice Error"), outErr: "failed to delete preauthorized device: DeleteDevice Error", }, @@ -2997,7 +3049,7 @@ func TestDevAuthDeleteAuthSet(t *testing.T) { DeviceId: oid.NewUUIDv5("devId10").String(), }, dbGetDeviceStatusErr: errors.New("Get Device Status Error"), - outErr: "Cannot determine device status: Get Device Status Error", + outErr: "cannot determine device status: Get Device Status Error", }, { devId: oid.NewUUIDv5("devId11").String(), @@ -3007,7 +3059,7 @@ func TestDevAuthDeleteAuthSet(t *testing.T) { DeviceId: oid.NewUUIDv5("devId11").String(), Status: model.DevStatusPending, }, - submitJob: true, + dbGetDeviceStatus: model.DevStatusPending, dbUpdateDeviceErr: errors.New("Update Device Error"), outErr: "failed to update device status: Update Device Error", }, @@ -3075,25 +3127,8 @@ func TestDevAuthDeleteAuthSet(t *testing.T) { DeviceId: oid.NewUUIDv5("devId16").String(), Status: model.DevStatusPreauth, }, - dbGetDeviceStatus: "decommissioned", - dbGetAuthSetsForDeviceErr: errors.New("GetAuthSetsForDevice Error"), - outErr: "db get auth sets error: GetAuthSetsForDevice Error", - }, - { - devId: oid.NewUUIDv5("devId16").String(), - authId: oid.NewUUIDv5("authId16").String(), - authSet: &model.AuthSet{ - Id: oid.NewUUIDv5("authId16").String(), - DeviceId: oid.NewUUIDv5("devId16").String(), - Status: model.DevStatusPreauth, - }, - dbGetDeviceStatus: "pending", - dbGetAuthSetsForDevice: []model.AuthSet{ - { - Id: "foo", - }, - }, - submitJob: true, + dbGetDeviceStatus: model.DevStatusNoAuth, + submitJob: true, }, } @@ -3134,38 +3169,45 @@ func TestDevAuthDeleteAuthSet(t *testing.T) { tc.devId).Return( tc.dbGetDeviceStatus, tc.dbGetDeviceStatusErr) - db.On("UpdateDevice", ctx, + db.On("UpdateDeviceWithRevision", ctx, tc.devId, + uint(0), mock.AnythingOfType("model.DeviceUpdate")).Return(tc.dbUpdateDeviceErr) db.On("GetDeviceById", ctx, - mock.AnythingOfType("string")).Return(&model.Device{Id: tc.devId}, nil) - db.On("GetAuthSetsForDevice", - ctx, - tc.devId, - ).Return( - tc.dbGetAuthSetsForDevice, - tc.dbGetAuthSetsForDeviceErr) - + mock.AnythingOfType("string")).Return(&model.Device{ + Id: tc.devId, + Provisioned: true, + }, nil) co := oas_mocks.NewMockWorkflowsOtherAPI(t) status := tc.dbGetDeviceStatus var revision uint = 1 - if tc.dbGetDeviceStatusErr == store.ErrAuthSetNotFound { - status = "noauth" - } - if tc.dbGetDeviceStatus == "decommissioned" { + if tc.dbGetDeviceStatus == model.DevStatusNoAuth { revision = 0 } if tc.submitJob { + if tc.dbGetDeviceStatusErr == store.ErrAuthSetNotFound { + status = "noauth" + } + var events any = []client.DeviceAuthEvent{updateDeviceStatusEvent(tc.authSet, &model.Device{ + Id: tc.devId, + Revision: revision, + }, status)} + + if tc.authSet.Status == model.DevStatusPreauth && + status == model.DevStatusNoAuth { + status = "decommissioned" + events = []model.DeviceInventoryUpdate{{ + Id: tc.devId, + Revision: revision, + }} + } req := client.ApiStartWorkflowRequest{ ApiService: co, }.RequestBody(map[string]interface{}{ "device_status": status, - "devices": []model.DeviceInventoryUpdate{{ - Id: tc.devId, - Revision: revision, - }}, - "request_id": "", - "tenant_id": tc.tenant, + "devices": events, + "request_id": "", + "tenant_id": tc.tenant, }) co.EXPECT(). StartWorkflow(mtesting.ContextMatcher(), "update_device_status"). @@ -3206,10 +3248,10 @@ func TestDevAuthDeleteAuthSet(t *testing.T) { assert.EqualError(t, err, tc.outErr) } else { assert.NoError(t, err) - if authSet.Status == model.DevStatusPreauth && len(tc.dbGetAuthSetsForDevice) == 0 { - db.AssertCalled(t, "DeleteDevice", tc.devId) + if authSet.Status == model.DevStatusPreauth && tc.dbGetDeviceStatus == model.DevStatusNoAuth { + db.AssertCalled(t, "DeleteDevice", ctx, tc.devId) } else { - db.AssertNotCalled(t, "DeleteDevice", tc.devId) + db.AssertNotCalled(t, "DeleteDevice", mock.Anything, mock.Anything) } } c.AssertExpectations(t) diff --git a/backend/services/deviceauth/devauth/mocks/App.go b/backend/services/deviceauth/devauth/mocks/App.go index 15eed72fe..b83c9608d 100644 --- a/backend/services/deviceauth/devauth/mocks/App.go +++ b/backend/services/deviceauth/devauth/mocks/App.go @@ -29,24 +29,6 @@ type App struct { mock.Mock } -// AcceptDeviceAuth provides a mock function with given fields: ctx, dev_id, auth_id -func (_m *App) AcceptDeviceAuth(ctx context.Context, dev_id string, auth_id string) error { - ret := _m.Called(ctx, dev_id, auth_id) - - if len(ret) == 0 { - panic("no return value specified for AcceptDeviceAuth") - } - - var r0 error - if rf, ok := ret.Get(0).(func(context.Context, string, string) error); ok { - r0 = rf(ctx, dev_id, auth_id) - } else { - r0 = ret.Error(0) - } - - return r0 -} - // DecommissionDevice provides a mock function with given fields: ctx, dev_id func (_m *App) DecommissionDevice(ctx context.Context, dev_id string) error { ret := _m.Called(ctx, dev_id) @@ -363,35 +345,17 @@ func (_m *App) PreauthorizeDevice(ctx context.Context, req *model.PreAuthReq) (* return r0, r1 } -// RejectDeviceAuth provides a mock function with given fields: ctx, dev_id, auth_id -func (_m *App) RejectDeviceAuth(ctx context.Context, dev_id string, auth_id string) error { - ret := _m.Called(ctx, dev_id, auth_id) - - if len(ret) == 0 { - panic("no return value specified for RejectDeviceAuth") - } - - var r0 error - if rf, ok := ret.Get(0).(func(context.Context, string, string) error); ok { - r0 = rf(ctx, dev_id, auth_id) - } else { - r0 = ret.Error(0) - } - - return r0 -} - -// ResetDeviceAuth provides a mock function with given fields: ctx, dev_id, auth_id -func (_m *App) ResetDeviceAuth(ctx context.Context, dev_id string, auth_id string) error { - ret := _m.Called(ctx, dev_id, auth_id) +// RevokeToken provides a mock function with given fields: ctx, tokenID +func (_m *App) RevokeToken(ctx context.Context, tokenID string) error { + ret := _m.Called(ctx, tokenID) if len(ret) == 0 { - panic("no return value specified for ResetDeviceAuth") + panic("no return value specified for RevokeToken") } var r0 error - if rf, ok := ret.Get(0).(func(context.Context, string, string) error); ok { - r0 = rf(ctx, dev_id, auth_id) + if rf, ok := ret.Get(0).(func(context.Context, string) error); ok { + r0 = rf(ctx, tokenID) } else { r0 = ret.Error(0) } @@ -399,17 +363,17 @@ func (_m *App) ResetDeviceAuth(ctx context.Context, dev_id string, auth_id strin return r0 } -// RevokeToken provides a mock function with given fields: ctx, tokenID -func (_m *App) RevokeToken(ctx context.Context, tokenID string) error { - ret := _m.Called(ctx, tokenID) +// SetAuthSetStatus provides a mock function with given fields: ctx, deviceID, authID, status +func (_m *App) SetAuthSetStatus(ctx context.Context, deviceID string, authID string, status string) error { + ret := _m.Called(ctx, deviceID, authID, status) if len(ret) == 0 { - panic("no return value specified for RevokeToken") + panic("no return value specified for SetAuthSetStatus") } var r0 error - if rf, ok := ret.Get(0).(func(context.Context, string) error); ok { - r0 = rf(ctx, tokenID) + if rf, ok := ret.Get(0).(func(context.Context, string, string, string) error); ok { + r0 = rf(ctx, deviceID, authID, status) } else { r0 = ret.Error(0) } diff --git a/backend/services/deviceauth/model/device.go b/backend/services/deviceauth/model/device.go index cff2d6a5e..9c4fa4a70 100644 --- a/backend/services/deviceauth/model/device.go +++ b/backend/services/deviceauth/model/device.go @@ -15,6 +15,7 @@ package model import ( "encoding/json" + "fmt" "net/url" "time" @@ -48,6 +49,26 @@ var ( } ) +func ValidateStatusTransition(fromStatus, toStatus string) error { + switch toStatus { + case DevStatusAccepted: + if fromStatus == DevStatusRejected || + fromStatus == DevStatusPending { + return nil + } + case DevStatusPending: + if fromStatus != DevStatusPreauth { + return nil + } + case DevStatusRejected: + if fromStatus == DevStatusPending || + fromStatus == DevStatusAccepted { + return nil + } + } + return fmt.Errorf("invalid status transition: %s -> %s", fromStatus, toStatus) +} + // note: fields with underscores need the 'bson' decorator // otherwise the underscore will be removed upon write to mongo type Device struct { diff --git a/backend/services/deviceauth/store/datastore.go b/backend/services/deviceauth/store/datastore.go index adc1b0e05..8ea959e81 100644 --- a/backend/services/deviceauth/store/datastore.go +++ b/backend/services/deviceauth/store/datastore.go @@ -82,6 +82,12 @@ type DataStore interface { // updates a single device with deviceID, using data from `up` UpdateDevice(ctx context.Context, deviceID string, up model.DeviceUpdate) error + UpdateDeviceWithRevision( + ctx context.Context, + deviceID string, + revision uint, + up model.DeviceUpdate, + ) error // deletes device DeleteDevice(ctx context.Context, id string) error diff --git a/backend/services/deviceauth/store/mocks/DataStore.go b/backend/services/deviceauth/store/mocks/DataStore.go index 517d08dda..fae11bd80 100644 --- a/backend/services/deviceauth/store/mocks/DataStore.go +++ b/backend/services/deviceauth/store/mocks/DataStore.go @@ -723,6 +723,24 @@ func (_m *DataStore) UpdateDevice(ctx context.Context, deviceID string, up model return r0 } +// UpdateDeviceWithRevision provides a mock function with given fields: ctx, deviceID, revision, up +func (_m *DataStore) UpdateDeviceWithRevision(ctx context.Context, deviceID string, revision uint, up model.DeviceUpdate) error { + ret := _m.Called(ctx, deviceID, revision, up) + + if len(ret) == 0 { + panic("no return value specified for UpdateDeviceWithRevision") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, string, uint, model.DeviceUpdate) error); ok { + r0 = rf(ctx, deviceID, revision, up) + } else { + r0 = ret.Error(0) + } + + return r0 +} + // UpsertAuthSetStatus provides a mock function with given fields: ctx, authSet func (_m *DataStore) UpsertAuthSetStatus(ctx context.Context, authSet *model.AuthSet) error { ret := _m.Called(ctx, authSet) diff --git a/backend/services/deviceauth/store/mongo/datastore_mongo.go b/backend/services/deviceauth/store/mongo/datastore_mongo.go index 1ed35ff6e..e48123c5a 100644 --- a/backend/services/deviceauth/store/mongo/datastore_mongo.go +++ b/backend/services/deviceauth/store/mongo/datastore_mongo.go @@ -339,9 +339,20 @@ func (db *DataStoreMongo) AddDevice(ctx context.Context, d model.Device) error { func (db *DataStoreMongo) UpdateDevice(ctx context.Context, deviceID string, updev model.DeviceUpdate) error { + return db.updateDevice(ctx, deviceID, nil, updev) +} +func (db *DataStoreMongo) updateDevice(ctx context.Context, + deviceID string, revision *uint, updev model.DeviceUpdate) error { c := db.client.Database(DbName).Collection(DbDevicesColl) + filter := bson.M{ + dbFieldID: deviceID, + } + if revision != nil { + filter[DbKeyDeviceRevision] = revision + } + updev.UpdatedTs = uto.TimePtr(time.Now().UTC()) update := bson.M{ "$inc": bson.M{ @@ -350,7 +361,7 @@ func (db *DataStoreMongo) UpdateDevice(ctx context.Context, "$set": updev, } - res, err := c.UpdateOne(ctx, mongostore.WithTenantID(ctx, bson.M{"_id": deviceID}), update) + res, err := c.UpdateOne(ctx, mongostore.WithTenantID(ctx, filter), update) if err != nil { return errors.Wrap(err, "failed to update device") } else if res.MatchedCount < 1 { @@ -360,6 +371,15 @@ func (db *DataStoreMongo) UpdateDevice(ctx context.Context, return nil } +func (db *DataStoreMongo) UpdateDeviceWithRevision( + ctx context.Context, + deviceID string, + revision uint, + up model.DeviceUpdate, +) error { + return db.updateDevice(ctx, deviceID, &revision, up) +} + func (db *DataStoreMongo) DeleteDevice(ctx context.Context, id string) error { c := db.client.Database(DbName).Collection(DbDevicesColl) diff --git a/backend/services/iot-manager/api/http/internal.go b/backend/services/iot-manager/api/http/internal.go index d2dacd249..4baece99d 100644 --- a/backend/services/iot-manager/api/http/internal.go +++ b/backend/services/iot-manager/api/http/internal.go @@ -134,9 +134,7 @@ const ( // PUT /tenants/:tenant_id/devices/status/{status} func (h *InternalHandler) BulkSetDeviceStatus(c *gin.Context) { - var schema []struct { - DeviceID string `json:"id"` - } + var schema []internalDevice status := model.Status(c.Param("status")) if err := status.Validate(); err != nil { rest.RenderError(c, http.StatusBadRequest, err) @@ -162,7 +160,7 @@ func (h *InternalHandler) BulkSetDeviceStatus(c *gin.Context) { }, ) for _, item := range schema { - _ = h.app.SetDeviceStatus(ctx, item.DeviceID, status) + _ = h.app.SetDeviceStatus(ctx, item.ID, status) } c.Status(http.StatusAccepted) } diff --git a/backend/services/workflows/tests/mmock/inventory_POST_devices_v2.json b/backend/services/workflows/tests/mmock/inventory_POST_devices_v2.json index 57310fe12..e10aaafdc 100644 --- a/backend/services/workflows/tests/mmock/inventory_POST_devices_v2.json +++ b/backend/services/workflows/tests/mmock/inventory_POST_devices_v2.json @@ -2,7 +2,7 @@ "description": "inventory: POST devices v2", "request": { "method": "POST", - "path": "/api/internal/v1/inventory/tenants/1/devices/status/accepted" + "path": "/api/internal/v1/inventory/tenants/*/devices/status/accepted" }, "response": { "statusCode": 200 diff --git a/backend/services/workflows/tests/tests/test_provision_device.py b/backend/services/workflows/tests/tests/test_provision_device.py index 4c63bc8f0..4c53a38f7 100644 --- a/backend/services/workflows/tests/tests/test_provision_device.py +++ b/backend/services/workflows/tests/tests/test_provision_device.py @@ -80,19 +80,19 @@ def do_provision_device(mmock_url, workflows_url, tenant_id): "host": "mender-inventory", "port": "8080", "method": "POST", - "path": "/api/internal/v1/inventory/tenants/" + tenant_id + "/devices", + "path": f"/api/internal/v1/inventory/tenants/{tenant_id}/devices/status/accepted", "queryStringParameters": {}, "fragment": "", "headers": { "Accept-Encoding": ["gzip"], - "Content-Length": ["81"], + "Content-Length": ["12"], "Content-Type": ["application/json"], "User-Agent": ["Go-http-client/1.1"], "X-Men-Requestid": ["1234567890"], }, "cookies": {}, - "body": '{"attributes":[{"name":"status","scope":"identity","value":"accepted"}],"id":"1"}', - }, + "body": '[{"id":"1"}]', + } }, { "request": { diff --git a/backend/services/workflows/worker/workflows/provision_device.json b/backend/services/workflows/worker/workflows/provision_device.json index ceae3d08b..f00c6e5a2 100644 --- a/backend/services/workflows/worker/workflows/provision_device.json +++ b/backend/services/workflows/worker/workflows/provision_device.json @@ -1,24 +1,17 @@ { "name": "provision_device", "description": "Provision device.", - "version": 11, + "version": 12, "tasks": [{ "name": "create_device_inventory", "type": "http", "retries": 3, "retryDelaySeconds": 5, "http": { - "uri": "http://${env.INVENTORY_ADDR|mender-inventory:8080}/api/internal/v1/inventory/tenants/${encoding=url;workflow.input.tenant_id}/devices", + "uri": "http://${env.INVENTORY_ADDR|mender-inventory:8080}/api/internal/v1/inventory/tenants/${encoding=url;workflow.input.tenant_id}/devices/status/${encoding=url;workflow.input.status}", "method": "POST", "contentType": "application/json", - "json": { - "id": "${workflow.input.device_id}", - "attributes": [{ - "name": "status", - "scope": "identity", - "value": "${workflow.input.status}" - }] - }, + "json": ["${workflow.input.device}"], "headers": { "X-MEN-RequestID": "${workflow.input.request_id}" }, @@ -112,6 +105,30 @@ 404 ] } + }, + { + "name": "update_iot_manager_status", + "type": "http", + "retries": 3, + "retryDelaySeconds": 5, + "http": { + "uri": "http://${env.IOT_MANAGER_ADDR|mender-iot-manager:8080}/api/internal/v1/iot-manager/tenants/${encoding=url;workflow.input.tenant_id}/bulk/devices/status/${encoding=url;workflow.input.status}", + "method": "PUT", + "contentType": "application/json", + "json": ["${workflow.input.device}"], + "headers": { + "X-MEN-RequestID": "${workflow.input.request_id}" + }, + "connectionTimeOut": 8000, + "readTimeOut": 8000, + "statusCodes": [ + 200, + 201, + 202, + 204, + 404 + ] + } } ], "inputParameters": [