From 83a747794e864277f37a8a858f0b0c0b22add49d Mon Sep 17 00:00:00 2001 From: Derek Buckley Date: Tue, 15 Oct 2019 20:57:59 -0400 Subject: [PATCH] engine: resources: Adds symbolic mode to file resource Adds a symbolic parsing function to the util package for parsing in the file resource. --- docs/resources.md | 2 +- engine/resources/file.go | 22 ++- engine/util/mode.go | 307 +++++++++++++++++++++++++++++++++++++++ engine/util/mode_test.go | 91 ++++++++++++ 4 files changed, 416 insertions(+), 6 deletions(-) create mode 100644 engine/util/mode.go create mode 100644 engine/util/mode_test.go diff --git a/docs/resources.md b/docs/resources.md index 010c8ca2..279b5ea6 100644 --- a/docs/resources.md +++ b/docs/resources.md @@ -72,7 +72,7 @@ It has the following properties: * `path`: absolute file path (directories have a trailing slash here) * `state`: either `exists`, `absent`, or undefined * `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 * `group`: group name or gid for the file group diff --git a/engine/resources/file.go b/engine/resources/file.go index d42cd10f..e7120989 100644 --- a/engine/resources/file.go +++ b/engine/resources/file.go @@ -123,8 +123,7 @@ type FileRes struct { // name, or a string representation of the group integer gid. Group string `lang:"group" yaml:"group"` // Mode is the mode of the file as a string representation of the octal - // form. - // TODO: add symbolic representations + // form or symbolic form. Mode string `lang:"mode" yaml:"mode"` Recurse bool `lang:"recurse" yaml:"recurse"` 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 // not empty. func (obj *FileRes) mode() (os.FileMode, error) { - m, err := strconv.ParseInt(obj.Mode, 8, 32) - if err != nil { - return os.FileMode(0), errwrap.Wrapf(err, "mode should be an octal number (%s)", obj.Mode) + if n, err := strconv.ParseInt(obj.Mode, 8, 32); err == nil { + return os.FileMode(n), nil } + + // 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 } diff --git a/engine/util/mode.go b/engine/util/mode.go new file mode 100644 index 00000000..76051701 --- /dev/null +++ b/engine/util/mode.go @@ -0,0 +1,307 @@ +// Mgmt +// Copyright (C) 2013-2019+ James Shubin and the project contributors +// Written by James Shubin 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 . + +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 +} diff --git a/engine/util/mode_test.go b/engine/util/mode_test.go new file mode 100644 index 00000000..62c4997d --- /dev/null +++ b/engine/util/mode_test.go @@ -0,0 +1,91 @@ +// Mgmt +// Copyright (C) 2013-2019+ James Shubin and the project contributors +// Written by James Shubin 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 . + +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) + } + }) + } +}