feat(files): configurable UMASK defaulting to system umask

This commit is contained in:
Quentin McGaw
2024-09-16 14:29:57 +00:00
parent 6b9ed56b18
commit 3a6262ef2c
11 changed files with 124 additions and 20 deletions

View File

@@ -1,7 +1,10 @@
package config
import (
"fmt"
"io/fs"
"path/filepath"
"strconv"
"github.com/qdm12/gosettings"
"github.com/qdm12/gosettings/reader"
@@ -11,12 +14,14 @@ import (
type Paths struct {
DataDir *string
Config *string
Umask fs.FileMode
}
func (p *Paths) setDefaults() {
p.DataDir = gosettings.DefaultPointer(p.DataDir, "./data")
defaultConfig := filepath.Join(*p.DataDir, "config.json")
p.Config = gosettings.DefaultPointer(p.Config, defaultConfig)
p.Umask = gosettings.DefaultComparable(p.Umask, getCurrentUmask())
}
func (p Paths) Validate() (err error) {
@@ -31,10 +36,30 @@ func (p Paths) toLinesNode() *gotree.Node {
node := gotree.New("Paths")
node.Appendf("Data directory: %s", *p.DataDir)
node.Appendf("Config file: %s", *p.Config)
node.Appendf("Umask: %s", p.Umask.String())
return node
}
func (p *Paths) read(reader *reader.Reader) {
func (p *Paths) read(reader *reader.Reader) (err error) {
p.DataDir = reader.Get("DATADIR")
p.Config = reader.Get("CONFIG_FILEPATH")
umaskString := reader.String("UMASK")
if umaskString != "" {
p.Umask, err = parseUmask(umaskString)
if err != nil {
return fmt.Errorf("parse umask: %w", err)
}
}
return nil
}
func parseUmask(s string) (umask fs.FileMode, err error) {
const base, bitSize = 8, 32
umaskUint64, err := strconv.ParseUint(s, base, bitSize)
if err != nil {
return 0, err
}
return fs.FileMode(umaskUint64), nil
}

View File

@@ -0,0 +1,47 @@
package config
import (
"io/fs"
"testing"
"github.com/stretchr/testify/assert"
)
func Test_parseUmask(t *testing.T) {
t.Parallel()
testCases := map[string]struct {
s string
umask fs.FileMode
errMessage string
}{
"invalid": {
s: "a",
errMessage: `strconv.ParseUint: parsing "a": invalid syntax`,
},
"704": {
s: "704",
umask: 0o704,
},
"0704": {
s: "0704",
umask: 0o0704,
},
}
for name, testCase := range testCases {
testCase := testCase
t.Run(name, func(t *testing.T) {
t.Parallel()
umask, err := parseUmask(testCase.s)
if testCase.errMessage != "" {
assert.EqualError(t, err, testCase.errMessage)
} else {
assert.NoError(t, err)
}
assert.Equal(t, testCase.umask, umask)
})
}
}

View File

@@ -107,7 +107,10 @@ func (c *Config) Read(reader *reader.Reader,
}
c.Health.Read(reader)
c.Paths.read(reader)
err = c.Paths.read(reader)
if err != nil {
return fmt.Errorf("reading paths settings: %w", err)
}
err = c.Backup.read(reader)
if err != nil {

View File

@@ -40,7 +40,8 @@ func Test_Settings_String(t *testing.T) {
| └── Server listening address: 127.0.0.1:9999
├── Paths
| ├── Data directory: ./data
| ── Config file: data/config.json
| ── Config file: data/config.json
| └── Umask: -----w--w-
├── Backup: disabled
└── Logger
├── Level: INFO

View File

@@ -0,0 +1,15 @@
//go:build !windows
package config
import (
"io/fs"
"syscall"
)
func getCurrentUmask() (mask fs.FileMode) {
const tempMask = 0o022
oldMask := syscall.Umask(tempMask)
syscall.Umask(oldMask)
return fs.FileMode(oldMask)
}

View File

@@ -0,0 +1,9 @@
package config
import (
"io/fs"
)
func getCurrentUmask() (mask fs.FileMode) {
return 0
}

View File

@@ -34,19 +34,19 @@ type commonSettings struct {
// JSONProviders obtain the update settings from the JSON content,
// first trying from the environment variable CONFIG and then from
// the file config.json.
func (r *Reader) JSONProviders(filePath string) (
func (r *Reader) JSONProviders(filePath string, umask fs.FileMode) (
providers []provider.Provider, warnings []string, err error) {
providers, warnings, err = r.getProvidersFromEnv(filePath)
providers, warnings, err = r.getProvidersFromEnv(filePath, umask)
if providers != nil || warnings != nil || err != nil {
return providers, warnings, err
}
return r.getProvidersFromFile(filePath)
return r.getProvidersFromFile(filePath, umask)
}
var errWriteConfigToFile = errors.New("cannot write configuration to file")
// getProvidersFromFile obtain the update settings from config.json.
func (r *Reader) getProvidersFromFile(filePath string) (
func (r *Reader) getProvidersFromFile(filePath string, umask fs.FileMode) (
providers []provider.Provider, warnings []string, err error) {
r.logger.Info("reading JSON config from file " + filePath)
bytes, err := r.readFile(filePath)
@@ -57,9 +57,8 @@ func (r *Reader) getProvidersFromFile(filePath string) (
r.logger.Info("file not found, creating an empty settings file")
const mode = fs.FileMode(0600)
err = r.writeFile(filePath, []byte(`{}`), mode)
filePerm := fs.FileMode(0o666) - umask //nolint:gomnd
err = r.writeFile(filePath, []byte(`{}`), filePerm)
if err != nil {
err = fmt.Errorf("%w: %w", errWriteConfigToFile, err)
}
@@ -72,7 +71,7 @@ func (r *Reader) getProvidersFromFile(filePath string) (
// getProvidersFromEnv obtain the update settings from the environment variable CONFIG.
// If the settings are valid, they are written to the filePath.
func (r *Reader) getProvidersFromEnv(filePath string) (
func (r *Reader) getProvidersFromEnv(filePath string, umask fs.FileMode) (
providers []provider.Provider, warnings []string, err error) {
s := os.Getenv("CONFIG")
if s == "" {
@@ -93,8 +92,8 @@ func (r *Reader) getProvidersFromEnv(filePath string) (
if err != nil {
return providers, warnings, fmt.Errorf("%w: %w", errWriteConfigToFile, err)
}
const mode = fs.FileMode(0600)
err = r.writeFile(filePath, buffer.Bytes(), mode)
filePerm := fs.FileMode(0o666) - umask //nolint:gomnd
err = r.writeFile(filePath, buffer.Bytes(), filePerm)
if err != nil {
return providers, warnings, fmt.Errorf("%w: %w", errWriteConfigToFile, err)
}

View File

@@ -17,6 +17,7 @@ type Database struct {
data dataModel
filepath string
mutex sync.RWMutex
umask fs.FileMode
}
func (db *Database) Close() error {
@@ -26,7 +27,7 @@ func (db *Database) Close() error {
}
// NewDatabase opens or creates the JSON file database.
func NewDatabase(dataDir string) (*Database, error) {
func NewDatabase(dataDir string, umask fs.FileMode) (*Database, error) {
filePath := filepath.Join(dataDir, "updates.json")
file, err := os.Open(filePath)
@@ -34,8 +35,8 @@ func NewDatabase(dataDir string) (*Database, error) {
if !errors.Is(err, os.ErrNotExist) {
return nil, fmt.Errorf("reading file: %w", err)
}
const perm fs.FileMode = 0700
err = os.MkdirAll(filepath.Dir(filePath), perm)
dirPerm := os.FileMode(0o777) - umask //nolint:gomnd
err = os.MkdirAll(filepath.Dir(filePath), dirPerm)
if err != nil {
return nil, fmt.Errorf("creating data directory: %w", err)
}
@@ -80,6 +81,7 @@ func NewDatabase(dataDir string) (*Database, error) {
return &Database{
data: data,
filepath: filePath,
umask: umask,
}, nil
}
@@ -134,8 +136,8 @@ func checkHistoryEvents(events []models.HistoryEvent) (err error) {
}
func (db *Database) write() error {
const createPerms fs.FileMode = 0600
file, err := os.OpenFile(db.filepath, os.O_TRUNC|os.O_CREATE|os.O_WRONLY, createPerms)
filePerm := os.FileMode(0o666) - db.umask //nolint:gomnd
file, err := os.OpenFile(db.filepath, os.O_TRUNC|os.O_CREATE|os.O_WRONLY, filePerm)
if err != nil {
return fmt.Errorf("opening file: %w", err)
}