engine: resources: Adds symbolic mode to file resource

Adds a symbolic parsing function to the util package for parsing in the
file resource.
This commit is contained in:
Derek Buckley
2019-10-15 20:57:59 -04:00
committed by James Shubin
parent 3e16d1da46
commit 83a747794e
4 changed files with 416 additions and 6 deletions

View File

@@ -72,7 +72,7 @@ It has the following properties:
* `path`: absolute file path (directories have a trailing slash here) * `path`: absolute file path (directories have a trailing slash here)
* `state`: either `exists`, `absent`, or undefined * `state`: either `exists`, `absent`, or undefined
* `content`: raw file content * `content`: raw file content
* `mode`: octal unix file permissions * `mode`: octal unix file permissions or symbolic string
* `owner`: username or uid for the file owner * `owner`: username or uid for the file owner
* `group`: group name or gid for the file group * `group`: group name or gid for the file group

View File

@@ -123,8 +123,7 @@ type FileRes struct {
// name, or a string representation of the group integer gid. // name, or a string representation of the group integer gid.
Group string `lang:"group" yaml:"group"` Group string `lang:"group" yaml:"group"`
// Mode is the mode of the file as a string representation of the octal // Mode is the mode of the file as a string representation of the octal
// form. // form or symbolic form.
// TODO: add symbolic representations
Mode string `lang:"mode" yaml:"mode"` Mode string `lang:"mode" yaml:"mode"`
Recurse bool `lang:"recurse" yaml:"recurse"` Recurse bool `lang:"recurse" yaml:"recurse"`
Force bool `lang:"force" yaml:"force"` Force bool `lang:"force" yaml:"force"`
@@ -170,10 +169,23 @@ func (obj *FileRes) isDir() bool {
// the case where the mode is not specified. The caller should check obj.Mode is // the case where the mode is not specified. The caller should check obj.Mode is
// not empty. // not empty.
func (obj *FileRes) mode() (os.FileMode, error) { func (obj *FileRes) mode() (os.FileMode, error) {
m, err := strconv.ParseInt(obj.Mode, 8, 32) if n, err := strconv.ParseInt(obj.Mode, 8, 32); err == nil {
if err != nil { return os.FileMode(n), nil
return os.FileMode(0), errwrap.Wrapf(err, "mode should be an octal number (%s)", obj.Mode)
} }
// Try parsing symbolic by first getting the files current mode.
stat, err := os.Stat(obj.getPath())
if err != nil {
return os.FileMode(0), errwrap.Wrapf(err, "failed to get the current file mode")
}
m := stat.Mode()
modes := strings.Split(obj.Mode, ",")
m, err = engineUtil.ParseSymbolicModes(modes, m, false)
if err != nil {
return os.FileMode(0), errwrap.Wrapf(err, "mode should be an octal number or symbolic mode (%s)", obj.Mode)
}
return os.FileMode(m), nil return os.FileMode(m), nil
} }

307
engine/util/mode.go Normal file
View File

@@ -0,0 +1,307 @@
// Mgmt
// Copyright (C) 2013-2019+ James Shubin and the project contributors
// Written by James Shubin <james@shubin.ca> and the project contributors
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
package util
import (
"fmt"
"os"
"strings"
)
// Constant bytes for the who (u, g, or o) and the what (r, w, x, s, or t).
const (
ModeUser uint32 = 64
ModeGroup uint32 = 8
ModeOther uint32 = 1
ModeRead uint32 = 4
ModeWrite uint32 = 2
ModeExec uint32 = 1
ModeSetU uint32 = 4
ModeSetG uint32 = 2
ModeSticky uint32 = 1
)
// modeIsValidWho checks that only the characters 'u', 'g', 'o' are in the who
// string. It expects that 'a' was expanded to "ugo" already and will return
// false if not.
func modeIsValidWho(who string) bool {
for _, w := range []string{"u", "g", "o"} {
who = strings.Replace(who, w, "", -1)
}
return len(who) == 0
}
// modeIsValidWhat checks that only the valid mode characters are in the what
// string ('w', 'r', 'x', 's', 't').
func modeIsValidWhat(what string) bool {
for _, w := range []string{"w", "r", "x", "s", "t"} {
what = strings.Replace(what, w, "", -1)
}
return len(what) == 0
}
// modeAssigned executes an assigment symbolic mode string (u=r). It clears out
// any bits for every subject in who and then assigns the specified modes in
// what.
func modeAssigned(who, what string, from os.FileMode) (os.FileMode, error) {
// Clear out any users defined in 'who'.
for _, w := range who {
switch w {
case 'u':
from = from &^ os.FileMode(448) // 111000000 in bytes
from = from &^ os.ModeSetuid
case 'g':
from = from &^ os.FileMode(56) // 111000 in bytes
from = from &^ os.ModeSetgid
case 'o':
from = from &^ os.FileMode(7) // 111 in bytes
}
}
for _, c := range what {
switch c {
case 'w':
m := modeValueFrom(who, ModeWrite)
if from&m == 0 {
from = from | m
}
case 'r':
m := modeValueFrom(who, ModeRead)
if from&m == 0 {
from = from | m
}
case 'x':
m := modeValueFrom(who, ModeExec)
if from&m == 0 {
from = from | m
}
case 's':
for _, w := range who {
switch w {
case 'u':
from = from | os.ModeSetuid
case 'g':
from = from | os.ModeSetgid
}
}
case 't':
from = from | os.ModeSticky
default:
return os.FileMode(0), fmt.Errorf("invalid character %q", c)
}
}
return from, nil
}
// modeAdded executes an addition symbolic mode string (u+x) and will add the
// bits requested in what if not present.
func modeAdded(who, what string, from os.FileMode) (os.FileMode, error) {
for _, c := range what {
switch c {
case 'w':
m := modeValueFrom(who, ModeWrite)
if from&m == 0 {
from = from | m
}
case 'r':
m := modeValueFrom(who, ModeRead)
if from&m == 0 {
from = from | m
}
case 'x':
m := modeValueFrom(who, ModeExec)
if from&m == 0 {
from = from | m
}
case 's':
for _, w := range who {
switch w {
case 'u':
from = from | os.ModeSetuid
case 'g':
from = from | os.ModeSetgid
}
}
case 't':
from = from | os.ModeSticky
default:
return os.FileMode(0), fmt.Errorf("invalid character %q", c)
}
}
return from, nil
}
// modeSubtracted executes an subtraction symbolic mode string (u+x) and will
// removethe bits requested in what if present.
func modeSubtracted(who, what string, from os.FileMode) (os.FileMode, error) {
for _, c := range what {
switch c {
case 'w':
m := modeValueFrom(who, ModeWrite)
if from&m != 0 {
from = from &^ m
}
case 'r':
m := modeValueFrom(who, ModeRead)
if from&m != 0 {
from = from &^ m
}
case 'x':
m := modeValueFrom(who, ModeExec)
if from&m != 0 {
from = from &^ m
}
case 's':
for _, w := range who {
switch w {
case 'u':
if from&os.ModeSetuid != 0 {
from = from &^ os.ModeSetuid
}
case 'g':
if from&os.ModeSetgid != 0 {
from = from &^ os.ModeSetgid
}
}
}
case 't':
if from&os.ModeSticky != 0 {
from = from | os.ModeSticky
}
default:
return os.FileMode(0), fmt.Errorf("invalid character %q", c)
}
}
return from, nil
}
// modeValueFrom will return the bits requested for the mode in the correct
// possitions for the specified subjects in who.
func modeValueFrom(who string, modeType uint32) os.FileMode {
i := uint32(0)
for _, w := range who {
switch w {
case 'u':
i += ModeUser * uint32(modeType)
case 'g':
i += ModeGroup * uint32(modeType)
case 'o':
i += ModeOther * uint32(modeType)
}
}
return os.FileMode(i)
}
// ParseSymbolicModes parses a slice of symbolic mode strings. By default it
// will accept all symbolic mode strings (=,+,-) but can be set to only accept
// the assignment input with the 'onlyAssign' bool.
//
// Symbolic mode is expected to be a string of who (user, group, other) then
// the operation (=, +, -) then the change (read, write, execute, setuid,
// setgid, sticky).
//
// Ex. ug=rw
//
// If you repeat yourself in the slice (ex. u=rw,u=w) ParseSymbolicModes will
// fail with an error.
func ParseSymbolicModes(modes []string, from os.FileMode, onlyAssign bool) (os.FileMode, error) {
symModes := make([]struct {
mode, who, what string
parse func(who, what string, from os.FileMode) (os.FileMode, error)
}, len(modes))
for i, mode := range modes {
symModes[i].mode = mode
// If string contains '=' and no '+/-' it is safe to guess it is an assign
if strings.Contains(mode, "=") && !strings.ContainsAny(mode, "+-") {
m := strings.Split(mode, "=")
if len(m) != 2 {
return os.FileMode(0), fmt.Errorf("only a single %q is allowed but found %d", "=", len(m))
}
symModes[i].who = m[0]
symModes[i].what = m[1]
symModes[i].parse = modeAssigned
continue
} else if strings.Contains(mode, "+") && !strings.ContainsAny(mode, "=-") && !onlyAssign {
m := strings.Split(mode, "+")
if len(m) != 2 {
return os.FileMode(0), fmt.Errorf("only a single %q is allowed but found %d", "+", len(m))
}
symModes[i].who = m[0]
symModes[i].what = m[1]
symModes[i].parse = modeAdded
continue
} else if strings.Contains(mode, "-") && !strings.ContainsAny(mode, "=+") && !onlyAssign {
m := strings.Split(mode, "-")
if len(m) != 2 {
return os.FileMode(0), fmt.Errorf("only a single %q is allowed but found %d", "-", len(m))
}
symModes[i].who = m[0]
symModes[i].what = m[1]
symModes[i].parse = modeSubtracted
continue
}
return os.FileMode(0), fmt.Errorf("%s is not a valid a symbolic mode", symModes[i].mode)
}
// Validate input and verify the slice of symbolic modes does not contain
// redundancy.
seen := make(map[rune]struct{}) // To validate all subjects are not dupicated
for i := range symModes {
if strings.ContainsRune(symModes[i].who, 'a') || symModes[i].who == "" {
// If 'a' or empty who (implicit 'a') is called and there are more
// symbolic modes in the slice then it must be a repetition.
if len(symModes) > 1 {
return os.FileMode(0), fmt.Errorf("subject was repeated: each subject (u,g,o) is only accepted once")
}
symModes[i].who = "ugo"
}
if !modeIsValidWhat(symModes[i].what) || !modeIsValidWho(symModes[i].who) {
return os.FileMode(0), fmt.Errorf("unexpected character assignment in %s", symModes[i].mode)
}
for _, w := range symModes[i].who {
if _, ok := seen[w]; ok {
return os.FileMode(0), fmt.Errorf("subject was repeated: only define each subject (u,g,o) once")
}
seen[w] = struct{}{}
}
}
// Parse each sybolic mode accumulatively onto the from file mode.
for _, m := range symModes {
var err error
from, err = m.parse(m.who, m.what, from)
if err != nil {
return os.FileMode(0), err
}
}
return from, nil
}

91
engine/util/mode_test.go Normal file
View File

@@ -0,0 +1,91 @@
// Mgmt
// Copyright (C) 2013-2019+ James Shubin and the project contributors
// Written by James Shubin <james@shubin.ca> and the project contributors
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
package util_test
import (
"fmt"
"os"
"strings"
"testing"
engineutil "github.com/purpleidea/mgmt/engine/util"
)
func TestSymbolicMode(t *testing.T) {
def := os.FileMode(0644) | os.ModeSetgid
symModeTests := []struct {
name string
input []string
expect os.FileMode
onlyAssign bool
err error
}{
// Test single mode inputs.
{"assign", []string{"a=rwx"}, 0777, false, nil},
{"assign", []string{"ug=rwx"}, 0774, false, nil},
{"assign", []string{"ug=srwx"}, 0774 | os.ModeSetgid | os.ModeSetuid, false, nil},
{"assign", []string{"ug=trwx"}, 0774 | os.ModeSticky, false, nil},
{"assign", []string{"o=rx"}, 0645 | os.ModeSetgid, false, nil},
{"assign", []string{"ug=srwx"}, 0774 | os.ModeSetgid | os.ModeSetuid, false, nil},
{"addition", []string{"o+rwx"}, 0647 | os.ModeSetgid, false, nil},
{"addition", []string{"u+x"}, 0744 | os.ModeSetgid, false, nil},
{"addition", []string{"u+x"}, 0744 | os.ModeSetgid, false, nil},
{"addition", []string{"u+s"}, 0644 | os.ModeSetgid | os.ModeSetuid, false, nil},
{"addition", []string{"u+t"}, 0644 | os.ModeSetgid | os.ModeSticky, false, nil},
{"subtraction", []string{"o-rwx"}, 0640 | os.ModeSetgid, false, nil},
{"subtraction", []string{"u-w"}, 0444 | os.ModeSetgid, false, nil},
{"subtraction", []string{"g-s"}, 0644, false, nil},
{"subtraction", []string{"u-t"}, 0644 | os.ModeSetgid, false, nil},
// Test multiple mode inputs.
{"mixed", []string{"u=rwx", "g+w"}, 0764 | os.ModeSetgid, false, nil},
{"mixed", []string{"u+rwx", "g=w"}, 0724, false, nil},
// Test that a engineutil.ModeError is returned. Value is not checked so the
// empty string works.
{"invalid separator", []string{"ug_rwx"}, os.FileMode(0), false, fmt.Errorf("ug_rwx is not a valid a symbolic mode")},
{"invalid who", []string{"xg=rwx"}, os.FileMode(0), false, fmt.Errorf("unexpected character assignment in xg=rwx")},
{"invalid what", []string{"g=rwy"}, os.FileMode(0), false, fmt.Errorf("unexpected character assignment in g=rwy")},
{"double assignment", []string{"a=rwx", "u=r"}, os.FileMode(0), false, fmt.Errorf("subject was repeated: each subject (u,g,o) is only accepted once")},
// Test onlyAssign bool
{"only assign", []string{"u+x", "g=rw"}, os.FileMode(0), true, fmt.Errorf("u+x is not a valid a symbolic mode")},
{"not only assign", []string{"u+x", "g=rw"}, os.FileMode(0764), false, nil},
}
for _, ts := range symModeTests {
test := ts
t.Run(test.name+" "+strings.Join(test.input, ","), func(t *testing.T) {
got, err := engineutil.ParseSymbolicModes(test.input, def, test.onlyAssign)
if test.err != nil {
if err == nil {
t.Errorf("input: %s, expected error: %#v, but got nil", def, test.err)
} else if err.Error() != test.err.Error() {
t.Errorf("input: %s, expected error: %q, got: %q", def, test.err, err)
}
} else if test.err == nil && err != nil {
t.Errorf("input: %s, did not expect error but got: %#v", def, err)
}
// Verify we get the expected value (including zero on error).
if test.expect != got {
t.Errorf("input: %s, expected: %v, got: %v", def, test.expect, got)
}
})
}
}