diff --git a/go.mod b/go.mod index 98b3a0e..9fbc3d0 100644 --- a/go.mod +++ b/go.mod @@ -12,7 +12,7 @@ require ( ) require ( - github.com/SENERGY-Platform/models/go v0.0.0-20230824080159-16585960df38 + github.com/SENERGY-Platform/models/go v0.0.0-20240527081255-52b6a0f84955 github.com/SENERGY-Platform/permission-search v0.0.13 github.com/SENERGY-Platform/service-commons v0.0.0-20240423132428-8eccbc027e71 github.com/testcontainers/testcontainers-go v0.31.0 diff --git a/go.sum b/go.sum index 225d518..de1dad4 100644 --- a/go.sum +++ b/go.sum @@ -10,8 +10,8 @@ github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERo github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= github.com/Microsoft/hcsshim v0.12.3 h1:LS9NXqXhMoqNCplK1ApmVSfB4UnVLRDWRapB6EIlxE0= github.com/Microsoft/hcsshim v0.12.3/go.mod h1:Iyl1WVpZzr+UkzjekHZbV8o5Z9ZkxNGx6CtY2Qg/JVQ= -github.com/SENERGY-Platform/models/go v0.0.0-20230824080159-16585960df38 h1:PDvFwIJBnFyKt1KeepkABNIS57+WhVj4vBw7X5JGZJw= -github.com/SENERGY-Platform/models/go v0.0.0-20230824080159-16585960df38/go.mod h1:bCREPNRN4P8oxLgpC3/ZKK4jXSy4MSPXoiomhohE+aw= +github.com/SENERGY-Platform/models/go v0.0.0-20240527081255-52b6a0f84955 h1:F2/npODU4ODkrkZ7mVmRKjHW1KJFgtaFOKq+rI1XwDM= +github.com/SENERGY-Platform/models/go v0.0.0-20240527081255-52b6a0f84955/go.mod h1:bCREPNRN4P8oxLgpC3/ZKK4jXSy4MSPXoiomhohE+aw= github.com/SENERGY-Platform/permission-search v0.0.13 h1:MCmdcMXsMCsvS1uEIzhrYb0WfAvGkrnTPxwS9dr7ZWo= github.com/SENERGY-Platform/permission-search v0.0.13/go.mod h1:TL+tg9zvYGjBWmdjOhfT10oRBnbdDvHSml1p1U/eeNI= github.com/SENERGY-Platform/service-commons v0.0.0-20240423132428-8eccbc027e71 h1:Z3Vj9ImRERXQE5fprE/KGkAsS/dgX0dMesM/cDSN9Tw= diff --git a/lib/controller/com/permission.go b/lib/controller/com/permission.go index c62ebb2..abb9dd3 100644 --- a/lib/controller/com/permission.go +++ b/lib/controller/com/permission.go @@ -58,6 +58,27 @@ func (this *Com) GetResourceOwner(token auth.Token, kind string, id string, righ return temp[0].Creator, true, nil } +func (this *Com) GetResourceRights(token auth.Token, kind string, id string, rights string) (result permmodel.EntryResult, found bool, err error) { + temp, _, err := client.Query[[]permmodel.EntryResult](this.perm, token.Jwt(), client.QueryMessage{ + Resource: kind, + ListIds: &permmodel.QueryListIds{ + QueryListCommons: permmodel.QueryListCommons{ + Limit: 1, + Offset: 0, + Rights: rights, + }, + Ids: []string{id}, + }, + }) + if err != nil { + return result, false, err + } + if len(temp) == 0 { + return result, false, nil + } + return temp[0], true, nil +} + func (this *Com) PermissionCheckForDeviceList(token auth.Token, ids []string, rights string) (result map[string]bool, err error, code int) { ids = append(ids, removeIdModifiers(ids)...) ids = RemoveDuplicates(ids) diff --git a/lib/controller/controller.go b/lib/controller/controller.go index d7a4ace..692c849 100644 --- a/lib/controller/controller.go +++ b/lib/controller/controller.go @@ -145,6 +145,7 @@ type Publisher interface { type Com interface { ResourcesEffectedByUserDelete(token auth.Token, resource string) (deleteResourceIds []string, deleteUserFromResourceIds []string, err error) GetResourceOwner(token auth.Token, kind string, id string, rights string) (owner string, found bool, err error) + GetResourceRights(token auth.Token, kind string, id string, rights string) (result permmodel.EntryResult, found bool, err error) GetPermissions(token auth.Token, kind string, id string) (permmodel.ResourceRights, error) GetTechnicalDeviceGroup(token auth.Token, id string) (dt models.DeviceGroup, err error, code int) diff --git a/lib/controller/device.go b/lib/controller/device.go index 482ace2..d396473 100644 --- a/lib/controller/device.go +++ b/lib/controller/device.go @@ -26,6 +26,7 @@ import ( "log" "net/http" "runtime/debug" + "slices" ) func (this *Controller) DeviceLocalIdToId(token auth.Token, localId string) (id string, err error, errCode int) { @@ -43,6 +44,11 @@ func (this *Controller) ReadDevice(token auth.Token, id string) (device models.D func (this *Controller) PublishDeviceCreate(token auth.Token, device models.Device, options model.DeviceCreateOptions) (models.Device, error, int) { device.GenerateId() + if device.OwnerId != "" && device.OwnerId != token.GetUserId() { + return device, errors.New("new devices must be initialised with the requesting user as owner-id"), http.StatusBadRequest + } + device.OwnerId = token.GetUserId() + err, code := this.com.ValidateDevice(token, device) if err != nil { return device, err, code @@ -80,27 +86,54 @@ func (this *Controller) PublishDeviceUpdate(token auth.Token, id string, device } } + var original models.Device + original, err, code = this.com.GetDevice(token, device.Id) + if err != nil && code != http.StatusNotFound { + return device, err, code + } else { + //device does not exist, but we want to continue to enable admins to create devices with a predetermined id + err, code = nil, 200 + } if len(options.UpdateOnlySameOriginAttributes) > 0 { - var original models.Device - original, err, code = this.com.GetDevice(token, device.Id) - if err != nil { - return device, err, code - } device.Attributes = updateSameOriginAttributes(original.Attributes, device.Attributes, options.UpdateOnlySameOriginAttributes) } + //set device owner-id if none is given + //prefer existing owner, fallback to requesting user + if device.OwnerId == "" { + device.OwnerId = original.OwnerId //may be empty for new devices + } + if device.OwnerId == "" { + device.OwnerId = token.GetUserId() + } + + //only old owner or system-admin may set new owner + if original.OwnerId != device.OwnerId && //change happened + original.OwnerId != "" && //is not a new device + !token.IsAdmin() && + original.OwnerId != token.GetUserId() { + return device, errors.New("only old owner or system-admin may set new owner"), http.StatusBadRequest + } + err, code = this.com.ValidateDevice(token, device) if err != nil { return device, err, code } - //ensure retention of original owner - owner, found, err := this.com.GetResourceOwner(token, this.config.DeviceTopic, device.Id, "w") + rights, found, err := this.com.GetResourceRights(token, this.config.DeviceTopic, device.Id, "w") if err != nil { log.Println("ERROR:", err) debug.PrintStack() return device, err, http.StatusInternalServerError } + + //new device owner-id must be existing admin user (ignore for new devices or devices with unchanged owner) + if found && device.OwnerId != original.OwnerId && !slices.Contains(rights.PermissionHolders.AdminUsers, device.OwnerId) { + return device, errors.New("new owner must have existing user admin rights"), http.StatusBadRequest + } + + //ensure retention of original owner + owner := rights.Creator if !found || owner == "" { owner = token.GetUserId() } diff --git a/lib/tests/device_test.go b/lib/tests/device_test.go index be528f6..91c5992 100644 --- a/lib/tests/device_test.go +++ b/lib/tests/device_test.go @@ -19,6 +19,7 @@ package tests import ( "encoding/json" "errors" + "fmt" "github.com/SENERGY-Platform/device-manager/lib/api" "github.com/SENERGY-Platform/device-manager/lib/tests/helper" "github.com/SENERGY-Platform/models/go/models" @@ -684,6 +685,7 @@ func testDevice(t *testing.T, port string) { func tryDeviceDisplayNameUpdate(port string, deviceId string, displayName string, expectedDevice models.Device) func(t *testing.T) { return func(t *testing.T) { + expectedDevice.OwnerId = userjwtUser resp, err := helper.Jwtput(userjwt, "http://localhost:"+port+"/devices/"+url.PathEscape(deviceId)+"/display_name", displayName) if err != nil { t.Fatal(err) @@ -811,7 +813,8 @@ func initDevice(port string, dt models.DeviceType) (models.Device, error) { return models.Device{}, err } if resp.StatusCode != http.StatusOK { - return models.Device{}, errors.New(resp.Status) + temp, _ := io.ReadAll(resp.Body) + return models.Device{}, fmt.Errorf("%v %v", resp.Status, string(temp)) } result := models.Device{} err = json.NewDecoder(resp.Body).Decode(&result) @@ -819,6 +822,7 @@ func initDevice(port string, dt models.DeviceType) (models.Device, error) { return models.Device{}, err } device.Id = result.Id + device.OwnerId = userjwtUser if !reflect.DeepEqual(result, device) { log.Printf("ERROR: \n%#v\n!=\n%#v\n", result, device) return models.Device{}, errors.New("returned device != expected device") diff --git a/lib/tests/main_test.go b/lib/tests/main_test.go index c96d347..3d43436 100644 --- a/lib/tests/main_test.go +++ b/lib/tests/main_test.go @@ -33,6 +33,7 @@ import ( const adminjwt = "Bearer eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICIzaUtabW9aUHpsMmRtQnBJdS1vSkY4ZVVUZHh4OUFIckVOcG5CcHM5SjYwIn0.eyJqdGkiOiJiOGUyNGZkNy1jNjJlLTRhNWQtOTQ4ZC1mZGI2ZWVkM2JmYzYiLCJleHAiOjE1MzA1MzIwMzIsIm5iZiI6MCwiaWF0IjoxNTMwNTI4NDMyLCJpc3MiOiJodHRwczovL2F1dGguc2VwbC5pbmZhaS5vcmcvYXV0aC9yZWFsbXMvbWFzdGVyIiwiYXVkIjoiZnJvbnRlbmQiLCJzdWIiOiJkZDY5ZWEwZC1mNTUzLTQzMzYtODBmMy03ZjQ1NjdmODVjN2IiLCJ0eXAiOiJCZWFyZXIiLCJhenAiOiJmcm9udGVuZCIsIm5vbmNlIjoiMjJlMGVjZjgtZjhhMS00NDQ1LWFmMjctNGQ1M2JmNWQxOGI5IiwiYXV0aF90aW1lIjoxNTMwNTI4NDIzLCJzZXNzaW9uX3N0YXRlIjoiMWQ3NWE5ODQtNzM1OS00MWJlLTgxYjktNzMyZDgyNzRjMjNlIiwiYWNyIjoiMCIsImFsbG93ZWQtb3JpZ2lucyI6WyIqIl0sInJlYWxtX2FjY2VzcyI6eyJyb2xlcyI6WyJjcmVhdGUtcmVhbG0iLCJhZG1pbiIsImRldmVsb3BlciIsInVtYV9hdXRob3JpemF0aW9uIiwidXNlciJdfSwicmVzb3VyY2VfYWNjZXNzIjp7Im1hc3Rlci1yZWFsbSI6eyJyb2xlcyI6WyJ2aWV3LWlkZW50aXR5LXByb3ZpZGVycyIsInZpZXctcmVhbG0iLCJtYW5hZ2UtaWRlbnRpdHktcHJvdmlkZXJzIiwiaW1wZXJzb25hdGlvbiIsImNyZWF0ZS1jbGllbnQiLCJtYW5hZ2UtdXNlcnMiLCJxdWVyeS1yZWFsbXMiLCJ2aWV3LWF1dGhvcml6YXRpb24iLCJxdWVyeS1jbGllbnRzIiwicXVlcnktdXNlcnMiLCJtYW5hZ2UtZXZlbnRzIiwibWFuYWdlLXJlYWxtIiwidmlldy1ldmVudHMiLCJ2aWV3LXVzZXJzIiwidmlldy1jbGllbnRzIiwibWFuYWdlLWF1dGhvcml6YXRpb24iLCJtYW5hZ2UtY2xpZW50cyIsInF1ZXJ5LWdyb3VwcyJdfSwiYWNjb3VudCI6eyJyb2xlcyI6WyJtYW5hZ2UtYWNjb3VudCIsIm1hbmFnZS1hY2NvdW50LWxpbmtzIiwidmlldy1wcm9maWxlIl19fSwicm9sZXMiOlsidW1hX2F1dGhvcml6YXRpb24iLCJhZG1pbiIsImNyZWF0ZS1yZWFsbSIsImRldmVsb3BlciIsInVzZXIiLCJvZmZsaW5lX2FjY2VzcyJdLCJuYW1lIjoiZGYgZGZmZmYiLCJwcmVmZXJyZWRfdXNlcm5hbWUiOiJzZXBsIiwiZ2l2ZW5fbmFtZSI6ImRmIiwiZmFtaWx5X25hbWUiOiJkZmZmZiIsImVtYWlsIjoic2VwbEBzZXBsLmRlIn0.eOwKV7vwRrWr8GlfCPFSq5WwR_p-_rSJURXCV1K7ClBY5jqKQkCsRL2V4YhkP1uS6ECeSxF7NNOLmElVLeFyAkvgSNOUkiuIWQpMTakNKynyRfH0SrdnPSTwK2V1s1i4VjoYdyZWXKNjeT2tUUX9eCyI5qOf_Dzcai5FhGCSUeKpV0ScUj5lKrn56aamlW9IdmbFJ4VwpQg2Y843Vc0TqpjK9n_uKwuRcQd9jkKHkbwWQ-wyJEbFWXHjQ6LnM84H0CQ2fgBqPPfpQDKjGSUNaCS-jtBcbsBAWQSICwol95BuOAqVFMucx56Wm-OyQOuoQ1jaLt2t-Uxtr-C9wKJWHQ" const userjwt = "Bearer eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICIzaUtabW9aUHpsMmRtQnBJdS1vSkY4ZVVUZHh4OUFIckVOcG5CcHM5SjYwIn0.eyJqdGkiOiIzMmE1OTljZC0zNDgxLTQzYWUtYWY0NC04YTVmNjU4NzYxZTUiLCJleHAiOjE1NjI5MjAwMDUsIm5iZiI6MCwiaWF0IjoxNTYyOTE2NDA1LCJpc3MiOiJodHRwczovL2F1dGguc2VwbC5pbmZhaS5vcmcvYXV0aC9yZWFsbXMvbWFzdGVyIiwiYXVkIjoiZnJvbnRlbmQiLCJzdWIiOiJlYmJhZDkyNy00YzM5LTRkMTItODY5MC04OWIwNjdkZDRjZTciLCJ0eXAiOiJCZWFyZXIiLCJhenAiOiJmcm9udGVuZCIsIm5vbmNlIjoiNTVlMzA4N2UtZjljNi00MmQ2LWE0MmEtMGZiMjcxNWE4OTkyIiwiYXV0aF90aW1lIjoxNTYyOTE2NDA0LCJzZXNzaW9uX3N0YXRlIjoiYmU5MDQ2MmYtOGE3Yy00NWU4LTg1MjAtMGRlYzViZWI1ZWZlIiwiYWNyIjoiMSIsImFsbG93ZWQtb3JpZ2lucyI6WyIqIl0sInJlYWxtX2FjY2VzcyI6eyJyb2xlcyI6WyJ1bWFfYXV0aG9yaXphdGlvbiIsInVzZXIiXX0sInJlc291cmNlX2FjY2VzcyI6eyJhY2NvdW50Ijp7InJvbGVzIjpbIm1hbmFnZS1hY2NvdW50IiwibWFuYWdlLWFjY291bnQtbGlua3MiLCJ2aWV3LXByb2ZpbGUiXX19LCJyb2xlcyI6WyJ1bWFfYXV0aG9yaXphdGlvbiIsInVzZXIiLCJvZmZsaW5lX2FjY2VzcyJdLCJwcmVmZXJyZWRfdXNlcm5hbWUiOiJpbmdvIn0.pggKYb3V0VxFINWBqpFE_t14MKhSM7bhw8YqrYBRvOzh8ft7zu_-bOvLOYbJBwo0GU1D68U2d_eerkYEIt-mc0dNtdFasy5DG_GtvnWA4nsbf0BVsYKSZcRiDK4d4qbHu9NMjBdEwSkP9KDGEtou0yHtOnVzB1eHHNm_uSUO-O_kz2LWsXOPK2sbL1LTiCKS0XToJPdlaNczDMZB0nXR3sHbyi3Lwk-Va2ATS6Kke5M1KmFMowK-Y0jK2urt8GnCBIXvZMT6gUW9-dvlv4w_lAuVXQ9hFg_r0sBnoWzZOUR_xlrz2T-syjrZzmXlAkJrcD8KWPH-lCs0jD9pdiROhQ" +const userjwtUser = "ebbad927-4c39-4d12-8690-89b067dd4ce7" const userid = "dd69ea0d-f553-4336-80f3-7f4567f85c7b"