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

Add mode property for restricting file upload access #28

Merged
merged 2 commits into from
Jul 15, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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) {
e-grigorov marked this conversation as resolved.
Show resolved Hide resolved
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' " +
gboyvalenkov-bosch marked this conversation as resolved.
Show resolved Hide resolved
"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