Skip to content

Commit

Permalink
Add mode property for restricting file upload access (#28)
Browse files Browse the repository at this point in the history
* Add mode property for restricting file upload access

- implementation and tests

Signed-off-by: Georgi Boyvalenkov <[email protected]>
  • Loading branch information
gboyvalenkov-bosch authored Jul 15, 2022
1 parent f33739d commit 6ba573d
Show file tree
Hide file tree
Showing 7 changed files with 212 additions and 13 deletions.
32 changes: 30 additions & 2 deletions client/fileupload.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ package client

import (
"errors"
"fmt"
"net/http"
"path/filepath"

Expand All @@ -26,15 +27,17 @@ const uploadFilesProperty = "upload.files"
// AutoUploadable ss performing all communication with the backend, FileUpload only specifies the files to be uploaded.
type FileUpload struct {
filesGlob string
mode AccessMode

uploadable *AutoUploadable
}

// NewFileUpload construct FileUpload from the provided configurations
func NewFileUpload(filesGlob string, mqttClient MQTT.Client, edgeCfg *EdgeConfiguration, uploadableCfg *UploadableConfig) (*FileUpload, error) {
func NewFileUpload(filesGlob string, mode AccessMode, mqttClient MQTT.Client, edgeCfg *EdgeConfiguration, uploadableCfg *UploadableConfig) (*FileUpload, error) {
result := &FileUpload{}

result.filesGlob = filesGlob
result.mode = mode

uploadable, err := NewAutoUploadable(mqttClient, edgeCfg, uploadableCfg, result,
"com.bosch.iot.suite.manager.upload:AutoUploadable:1.0.0", "com.bosch.iot.suite.manager.upload:Uploadable:1.0.0")
Expand Down Expand Up @@ -65,6 +68,16 @@ func (fu *FileUpload) DoTrigger(correlationID string, options map[string]string)

if !ok {
glob = fu.filesGlob
} else {
ok, err := fu.isGlobUploadPermitted(glob)

if err != nil {
return err
}

if !ok {
return fmt.Errorf("uploading '%s' with mode '%s' is not permitted", glob, fu.mode)
}
}

if glob == "" {
Expand Down Expand Up @@ -100,9 +113,24 @@ func (fu *FileUpload) HandleOperation(operation string, payload []byte) *ErrorRe

// OnTick triggers periodic file uploads. Invoked from the periodic executor in AutoUploadable
func (fu *FileUpload) OnTick() {
err := fu.DoTrigger(fu.uploadable.nextgUID(), nil)
err := fu.DoTrigger(fu.uploadable.nextUID(), nil)

if err != nil {
logger.Errorf("error on periodic trigger: %v", err)
}
}

func (fu *FileUpload) isGlobUploadPermitted(glob string) (bool, error) {
switch fu.mode {
case ModeLax:
return true, nil
case ModeStrict:
return glob == fu.filesGlob, nil
case ModeScoped:
return filepath.Match(fu.filesGlob, glob)
default:
logger.Errorf("unexpected file upload mode value: %v", fu.mode)

return false, nil
}
}
78 changes: 72 additions & 6 deletions client/fileupload_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -63,20 +63,86 @@ func TestUpload(t *testing.T) {
a, b, _, _ := getTestFiles(t)
glob := filepath.Join(basedir, "*.txt")

f, client := newConnectedFileUpload(t, glob)
f, client := newConnectedFileUpload(t, glob, ModeStrict)
defer f.Disconnect()

checkUploadTrigger(t, f, client, nil, a, b)
}

func TestUploadModeStrict(t *testing.T) {
setUp(t)
defer tearDown(t)

a, b, _, _ := getTestFiles(t)

glob := filepath.Join(basedir, "*.txt")

f, client := newConnectedFileUpload(t, glob, ModeStrict)
defer f.Disconnect()

checkUploadTrigger(t, f, client, nil, a, b)

dynamicGlob := filepath.Join(basedir, "*.dat")
options := map[string]string{uploadFilesProperty: dynamicGlob}
err := f.DoTrigger("testCorrelationID", options)
assertError(t, err)
}

func TestUploadModeScoped(t *testing.T) {
setUp(t)
defer tearDown(t)

a, b, _, _ := getTestFiles(t)
a1 := addTestFile(t, "a1.txt")
b1 := addTestFile(t, "b1.txt")

glob := filepath.Join(basedir, "*.txt")

f, client := newConnectedFileUpload(t, glob, ModeScoped)
defer f.Disconnect()

checkUploadTrigger(t, f, client, nil, a, b, a1, b1)

dynamicGlob := filepath.Join(basedir, "?1.txt")
options := map[string]string{uploadFilesProperty: dynamicGlob}
checkUploadTrigger(t, f, client, options, a1, b1)

options[uploadFilesProperty] = filepath.Join(basedir, "*.dat")
err := f.DoTrigger("testCorrelationID", options)
assertError(t, err)
}

func TestUploadModeLax(t *testing.T) {
setUp(t)
defer tearDown(t)

a, b, c, d := getTestFiles(t)

f, client := newConnectedFileUpload(t, "", ModeLax)
defer f.Disconnect()

options := make(map[string]string)
options[uploadFilesProperty] = filepath.Join(basedir, "*.txt")
checkUploadTrigger(t, f, client, options, a, b)

options[uploadFilesProperty] = filepath.Join(basedir, "*.dat")
checkUploadTrigger(t, f, client, options, c, d)

x := addTestFile(t, "sub/x.one")
y := addTestFile(t, "sub/y.two")

options[uploadFilesProperty] = filepath.Join(basedir, "sub/*.*")
checkUploadTrigger(t, f, client, options, x, y)
}

func TestUploadDynamicGlob(t *testing.T) {
setUp(t)
defer tearDown(t)

a, b, c, d := getTestFiles(t)
glob := filepath.Join(basedir, "*.txt")

f, client := newConnectedFileUpload(t, glob)
f, client := newConnectedFileUpload(t, glob, ModeLax)
defer f.Disconnect()

checkUploadTrigger(t, f, client, nil, a, b)
Expand All @@ -90,7 +156,7 @@ func TestUploadDynamicGlob(t *testing.T) {
}

func TestUploadDynamicGlobError(t *testing.T) {
f, _ := newConnectedFileUpload(t, "")
f, _ := newConnectedFileUpload(t, "", ModeLax)
defer f.Disconnect()

var err error
Expand Down Expand Up @@ -147,7 +213,7 @@ func addTestFile(t *testing.T, path string) string {
return path
}

func newConnectedFileUpload(t *testing.T, filesGlob string) (*FileUpload, *mockedClient) {
func newConnectedFileUpload(t *testing.T, filesGlob string, mode AccessMode) (*FileUpload, *mockedClient) {
testCfg = &UploadableConfig{}
testCfg.Name = featureID
testCfg.Type = "test_type"
Expand All @@ -157,7 +223,7 @@ func newConnectedFileUpload(t *testing.T, filesGlob string) (*FileUpload, *mocke
edgeCfg := &EdgeConfiguration{DeviceID: namespace + ":" + deviceID, TenantID: "testTenantID", PolicyID: "testPolicyID"}

var err error
u, err := NewFileUpload(filesGlob, client, edgeCfg, testCfg)
u, err := NewFileUpload(filesGlob, mode, client, edgeCfg, testCfg)
assertNoError(t, err)

err = u.Connect()
Expand Down Expand Up @@ -246,7 +312,7 @@ func (client *mockedClient) msg(t *testing.T, channel string, action string) map
assertEquals(t, deviceID, env.Topic.EntityID)
assertEquals(t, action, string(env.Topic.Action))

// Valdiate its starting path.
// Validate its starting path.
prefix := "/features/" + featureID
if !strings.HasPrefix(env.Path, prefix) {
t.Fatalf("message path do not starts with [%v]: %v", prefix, env.Path)
Expand Down
89 changes: 89 additions & 0 deletions client/mode.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
// Copyright (c) 2022 Contributors to the Eclipse Foundation
//
// See the NOTICE file(s) distributed with this work for additional
// information regarding copyright ownership.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License 2.0 which is available at
// http://www.eclipse.org/legal/epl-2.0
//
// SPDX-License-Identifier: EPL-2.0

package client

import (
"encoding/json"
"fmt"
)

// AccessMode type, for restricting files allowed to be dynamically requested for upload
type AccessMode int

// Allowed values for AccessMode
const (
ModeNA = iota
ModeStrict
ModeLax
ModeScoped
)

// AccessMode names
const (
ModeNameStrict = "strict"
ModeNameLax = "lax"
ModeNameScoped = "scoped"
)

// String returns string representation of AccessMode
func (m AccessMode) String() string {
switch m {
case ModeStrict:
return ModeNameStrict
case ModeLax:
return ModeNameLax
case ModeScoped:
return ModeNameScoped
default:
return ""
}
}

// Set implements flag.Value Set method
func (m *AccessMode) Set(v string) error {
switch v {
case ModeNameStrict:
*m = ModeStrict
case ModeNameLax:
*m = ModeLax
case ModeNameScoped:
*m = ModeScoped
case "":
*m = ModeNA
default:
return fmt.Errorf("accepted values are '%s', '%s' and '%s'", ModeNameStrict, ModeNameLax, ModeNameScoped)
}

return nil
}

// MarshalJSON marshals AccessMode as JSON
func (m AccessMode) MarshalJSON() ([]byte, error) {
s := m.String()

return json.Marshal(s)
}

// UnmarshalJSON un-marshals AccessMode from JSON
func (m *AccessMode) UnmarshalJSON(b []byte) error {
var s string
if err := json.Unmarshal(b, &s); err != nil {
return err
}

err := m.Set(s)
if err != nil {
return fmt.Errorf("invalid value '%s' for property 'mode' - %w", s, err)
}

return nil
}
4 changes: 2 additions & 2 deletions client/uploadable.go
Original file line number Diff line number Diff line change
Expand Up @@ -395,7 +395,7 @@ func (u *AutoUploadable) trigger(payload []byte) *ErrorResponse {

correlationID := params.CorrelationID
if correlationID == "" {
correlationID = u.nextgUID()
correlationID = u.nextUID()
}

err = u.customizer.DoTrigger(correlationID, params.Options)
Expand Down Expand Up @@ -501,7 +501,7 @@ func (u *AutoUploadable) stopExecutor() {
}
}

func (u *AutoUploadable) nextgUID() string {
func (u *AutoUploadable) nextUID() string {
u.mutex.Lock()
defer u.mutex.Unlock()

Expand Down
17 changes: 16 additions & 1 deletion flagparse/flags.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,18 +41,33 @@ type UploadConfig struct {
client.UploadableConfig
logger.LogConfig

Files string `json:"files,omitempty" descr:"Glob pattern for the files to upload"`
Files string `json:"files,omitempty" descr:"Glob pattern for the files to upload"`
Mode client.AccessMode `json:"mode,omitempty" def:"strict" descr:"{mode}"`
}

//ConfigNames contains template names to be replaced in config properties descriptions and default values
var ConfigNames = map[string]string{
"name": "AutoUploadable", "feature": "Uploadable", "period": "Upload period",
"action": "upload", "actions": "uploads", "running_actions": "uploads",
"mode": "File access mode. Restricts which files can be requested dynamically for upload through 'upload.files' " +
"trigger operation property.\nAllowed values are:" +
"\n 'strict' - dynamically specifying files for upload is forbidden, the 'files' property must be used instead" +
"\n 'scoped' - allows upload of any files that match the 'files' glob filter" +
"\n 'lax' - allows upload of any files the upload process has access to",
}

//ConfigFileMissing error, which represents a warning for missing config file
type ConfigFileMissing error

//Validate file upload config
func (cfg *UploadConfig) Validate() {
if cfg.Files == "" && cfg.Mode != client.ModeLax {
log.Fatalln("Files glob not specified. To permit unrestricted file upload set 'mode' property to 'lax'.")
}

cfg.UploadableConfig.Validate()
}

//ParseFlags parses the CLI flags and generates an upload file configuration
func ParseFlags(version string) (*UploadConfig, ConfigFileMissing) {
dumpFiles := flag.Bool("dumpFiles", false, "On startup dump the file paths matching the '-files' glob pattern to standard output.")
Expand Down
1 change: 1 addition & 0 deletions flagparse/testdata/testConfig.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
{
"files": "test",
"mode": "strict",
"broker": "testBroker",
"username": "testUsername",
"password": "testPassword",
Expand Down
4 changes: 2 additions & 2 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ func main() {
logger.Warning(warn)
}

logger.Infof("files glob: '%s'", config.Files)
logger.Infof("files glob: '%s', mode: '%s'", config.Files, config.Mode)
logger.Infof("uploadable config: %+v", config.UploadableConfig)
logger.Infof("log config: %+v", config.LogConfig)

Expand All @@ -63,7 +63,7 @@ func main() {
edgeCfg = cfg
}

uploadable, err := client.NewFileUpload(config.Files, broker, edgeCfg, &config.UploadableConfig)
uploadable, err := client.NewFileUpload(config.Files, config.Mode, broker, edgeCfg, &config.UploadableConfig)
if err != nil {
panic(err)
}
Expand Down

0 comments on commit 6ba573d

Please sign in to comment.