This commit is contained in:
jamesread
2022-10-24 23:17:31 +01:00
parent f9ac78d27f
commit e950a00a1e
6 changed files with 264 additions and 264 deletions

View File

@@ -86,7 +86,7 @@ func UserFromContext(ctx context.Context, cfg *config.Config) *AuthenticatedUser
buildUserAcls(cfg, ret) buildUserAcls(cfg, ret)
log.WithFields(log.Fields{ log.WithFields(log.Fields{
"username": ret.Username, "username": ret.Username,
"usergroup": ret.Usergroup, "usergroup": ret.Usergroup,
}).Infof("UserFromContext") }).Infof("UserFromContext")

View File

@@ -15,12 +15,12 @@ type Action struct {
// ActionArgument objects appear on Actions. // ActionArgument objects appear on Actions.
type ActionArgument struct { type ActionArgument struct {
Name string Name string
Title string Title string
Description string Description string
Type string Type string
Default string Default string
Choices []ActionArgumentChoice Choices []ActionArgumentChoice
} }
// ActionArgumentChoice represents a predefined choice for an argument. // ActionArgumentChoice represents a predefined choice for an argument.

View File

@@ -0,0 +1,121 @@
package executor
import (
config "github.com/OliveTin/OliveTin/internal/config"
log "github.com/sirupsen/logrus"
"errors"
"net/url"
"regexp"
"strings"
)
var (
typecheckRegex = map[string]string{
"very_dangerous_raw_string": "",
"int": "^[\\d]+$",
"ascii": "^[a-zA-Z0-9]+$",
"ascii_identifier": "^[a-zA-Z0-9\\-\\.\\_]+$",
"ascii_sentence": "^[a-zA-Z0-9 \\,\\.]+$",
}
)
func parseActionArguments(rawShellCommand string, values map[string]string, action *config.Action) (string, error) {
log.WithFields(log.Fields{
"cmd": rawShellCommand,
}).Infof("Before Parse Args")
r := regexp.MustCompile("{{ *?([a-zA-Z0-9_]+?) *?}}")
matches := r.FindAllStringSubmatch(rawShellCommand, -1)
for _, match := range matches {
argValue, argProvided := values[match[1]]
if !argProvided {
log.Infof("%v", values)
return "", errors.New("Required arg not provided: " + match[1])
}
err := typecheckActionArgument(match[1], argValue, action)
if err != nil {
return "", err
}
log.WithFields(log.Fields{
"name": match[1],
"value": argValue,
}).Debugf("Arg assigned")
rawShellCommand = strings.ReplaceAll(rawShellCommand, match[0], argValue)
}
log.WithFields(log.Fields{
"cmd": rawShellCommand,
}).Infof("After Parse Args")
return rawShellCommand, nil
}
func typecheckActionArgument(name string, value string, action *config.Action) error {
arg := action.FindArg(name)
if arg == nil {
return errors.New("Action arg not defined: " + name)
}
if len(arg.Choices) > 0 {
return typecheckChoice(value, arg)
}
return TypeSafetyCheck(name, value, arg.Type)
}
func typecheckChoice(value string, arg *config.ActionArgument) error {
for _, choice := range arg.Choices {
if value == choice.Value {
return nil
}
}
return errors.New("argument value is not one of the predefined choices")
}
// TypeSafetyCheck checks argument values match a specific type. The types are
// defined in typecheckRegex, and, you guessed it, uses regex to check for allowed
// characters.
func TypeSafetyCheck(name string, value string, argumentType string) error {
if argumentType == "url" {
return typeSafetyCheckUrl(name, value)
}
return typeSafetyCheckRegex(name, value, argumentType)
}
func typeSafetyCheckRegex(name string, value string, argumentType string) error {
pattern, found := typecheckRegex[argumentType]
if !found {
return errors.New("argument type not implemented " + argumentType)
}
matches, _ := regexp.MatchString(pattern, value)
if !matches {
log.WithFields(log.Fields{
"name": name,
"value": value,
"type": argumentType,
}).Warn("Arg type check safety failure")
return errors.New("invalid argument, doesn't match " + argumentType)
}
return nil
}
func typeSafetyCheckUrl(name string, value string) error {
_, err := url.ParseRequestURI(value)
return err
}

View File

@@ -0,0 +1,69 @@
package executor
import (
config "github.com/OliveTin/OliveTin/internal/config"
"github.com/stretchr/testify/assert"
"testing"
)
func TestSanitizeUnsafe(t *testing.T) {
assert.Nil(t, TypeSafetyCheck("", "_zomg_ c:/ haxxor ' bobby tables && rm -rf ", "very_dangerous_raw_string"))
}
func TestSanitizeUnimplemented(t *testing.T) {
err := TypeSafetyCheck("", "I am a happy little argument", "greeting_type")
assert.NotNil(t, err, "Test an argument type that does not exist")
}
func TestArgumentNameNumbers(t *testing.T) {
a1 := config.Action{
Title: "Do some tickles",
Shell: "echo 'Tickling {{ person1name }}'",
Arguments: []config.ActionArgument{
{
Name: "person1name",
Type: "ascii",
},
},
}
values := map[string]string{
"person1name": "Fred",
}
out, err := parseActionArguments(a1.Shell, values, &a1)
assert.Equal(t, "echo 'Tickling Fred'", out)
assert.Nil(t, err)
}
func TestArgumentNotProvided(t *testing.T) {
a1 := config.Action{
Title: "Do some tickles",
Shell: "echo 'Tickling {{ personName }}'",
Arguments: []config.ActionArgument{
{
Name: "person",
Type: "ascii",
},
},
}
values := map[string]string{}
out, err := parseActionArguments(a1.Shell, values, &a1)
assert.Equal(t, "", out)
assert.Equal(t, err.Error(), "Required arg not provided: personName")
}
func TestTypeSafetyCheckUrl(t *testing.T) {
assert.Nil(t, TypeSafetyCheck("test1", "http://google.com", "url"), "Test URL: google.com")
assert.Nil(t, TypeSafetyCheck("test2", "http://technowax.net:80?foo=bar", "url"), "Test URL: technowax.net with query arguments")
assert.Nil(t, TypeSafetyCheck("test3", "http://localhost:80?foo=bar", "url"), "Test URL: localhost with query arguments")
assert.NotNil(t, TypeSafetyCheck("test4", "http://lo host:80", "url"), "Test a badly formed URL")
assert.NotNil(t, TypeSafetyCheck("test5", "12345", "url"), "Test a badly formed URL")
assert.NotNil(t, TypeSafetyCheck("test6", "_!23;", "url"), "Test a badly formed URL")
}

View File

@@ -8,23 +8,21 @@ import (
"bytes" "bytes"
"context" "context"
"errors"
"os/exec" "os/exec"
"net/url"
"regexp"
"strings"
"time" "time"
) )
var ( // ExecutionRequest is a request to execute an action. It's passed to an
typecheckRegex = map[string]string{ // Executor. They're created from the grpcapi.
"very_dangerous_raw_string": "", type ExecutionRequest struct {
"int": "^[\\d]+$", ActionName string
"ascii": "^[a-zA-Z0-9]+$", Arguments map[string]string
"ascii_identifier": "^[a-zA-Z0-9\\-\\.\\_]+$", action *config.Action
"ascii_sentence": "^[a-zA-Z0-9 \\,\\.]+$", Cfg *config.Config
} AuthenticatedUser *acl.AuthenticatedUser
) logEntry *InternalLogEntry
finalParsedCommand string
}
// InternalLogEntry objects are created by an Executor, and represent the final // InternalLogEntry objects are created by an Executor, and represent the final
// state of execution (even if the command is not executed). It's designed to be // state of execution (even if the command is not executed). It's designed to be
@@ -45,71 +43,14 @@ type InternalLogEntry struct {
ActionIcon string ActionIcon string
} }
// ExecutionRequest is a request to execute an action. It's passed to an type executorStepFunc func(*ExecutionRequest) bool
// Executor. They're created from the grpcapi.
type ExecutionRequest struct {
ActionName string
Arguments map[string]string
action *config.Action
Cfg *config.Config
AuthenticatedUser *acl.AuthenticatedUser
logEntry *InternalLogEntry
finalParsedCommand string
}
type executorStep interface {
Exec(*ExecutionRequest) bool
}
// Executor represents a helper class for executing commands. It's main method // Executor represents a helper class for executing commands. It's main method
// is ExecRequest // is ExecRequest
type Executor struct { type Executor struct {
Logs []InternalLogEntry Logs []InternalLogEntry
chainOfCommand []executorStep chainOfCommand []executorStepFunc
}
// DefaultExecutor returns an Executor, with a sensible "chain of command" for
// executing actions.
func DefaultExecutor() *Executor {
e := Executor{}
e.chainOfCommand = []executorStep{
stepFindAction{},
stepACLCheck{},
stepParseArgs{},
stepLogStart{},
stepExec{},
stepLogFinish{},
}
return &e
}
type stepFindAction struct{}
func (s stepFindAction) Exec(req *ExecutionRequest) bool {
actualAction := req.Cfg.FindAction(req.ActionName)
if actualAction == nil {
log.WithFields(log.Fields{
"actionName": req.ActionName,
}).Warnf("Action not found")
req.logEntry.Stderr = "Action not found"
return false
}
req.action = actualAction
req.logEntry.ActionIcon = actualAction.Icon
return true
}
type stepACLCheck struct{}
func (s stepACLCheck) Exec(req *ExecutionRequest) bool {
return acl.IsAllowedExec(req.Cfg, req.AuthenticatedUser, req.action)
} }
// ExecRequest processes an ExecutionRequest // ExecRequest processes an ExecutionRequest
@@ -123,7 +64,7 @@ func (e *Executor) ExecRequest(req *ExecutionRequest) *pb.StartActionResponse {
} }
for _, step := range e.chainOfCommand { for _, step := range e.chainOfCommand {
if !step.Exec(req) { if !step(req) {
break break
} }
} }
@@ -143,34 +84,46 @@ func (e *Executor) ExecRequest(req *ExecutionRequest) *pb.StartActionResponse {
} }
} }
type stepLogStart struct{} // DefaultExecutor returns an Executor, with a sensible "chain of command" for
// executing actions.
func DefaultExecutor() *Executor {
e := Executor{}
e.chainOfCommand = []executorStepFunc{
stepFindAction,
stepACLCheck,
stepParseArgs,
stepLogStart,
stepExec,
stepLogFinish,
}
func (e stepLogStart) Exec(req *ExecutionRequest) bool { return &e
log.WithFields(log.Fields{ }
"title": req.action.Title,
"timeout": req.action.Timeout, func stepFindAction(req *ExecutionRequest) bool {
}).Infof("Action starting") actualAction := req.Cfg.FindAction(req.ActionName)
if actualAction == nil {
log.WithFields(log.Fields{
"actionName": req.ActionName,
}).Warnf("Action not found")
req.logEntry.Stderr = "Action not found"
return false
}
req.action = actualAction
req.logEntry.ActionIcon = actualAction.Icon
return true return true
} }
type stepLogFinish struct{} func stepACLCheck(req *ExecutionRequest) bool {
return acl.IsAllowedExec(req.Cfg, req.AuthenticatedUser, req.action)
func (e stepLogFinish) Exec(req *ExecutionRequest) bool {
log.WithFields(log.Fields{
"title": req.action.Title,
"stdout": req.logEntry.Stdout,
"stderr": req.logEntry.Stderr,
"timedOut": req.logEntry.TimedOut,
"exit": req.logEntry.ExitCode,
}).Infof("Action finished")
return true
} }
type stepParseArgs struct{} func stepParseArgs(req *ExecutionRequest) bool {
func (e stepParseArgs) Exec(req *ExecutionRequest) bool {
var err error var err error
req.finalParsedCommand, err = parseActionArguments(req.action.Shell, req.Arguments, req.action) req.finalParsedCommand, err = parseActionArguments(req.action.Shell, req.Arguments, req.action)
@@ -186,9 +139,28 @@ func (e stepParseArgs) Exec(req *ExecutionRequest) bool {
return true return true
} }
type stepExec struct{} func stepLogStart(req *ExecutionRequest) bool {
log.WithFields(log.Fields{
"title": req.action.Title,
"timeout": req.action.Timeout,
}).Infof("Action starting")
func (e stepExec) Exec(req *ExecutionRequest) bool { return true
}
func stepLogFinish(req *ExecutionRequest) bool {
log.WithFields(log.Fields{
"title": req.action.Title,
"stdout": req.logEntry.Stdout,
"stderr": req.logEntry.Stderr,
"timedOut": req.logEntry.TimedOut,
"exit": req.logEntry.ExitCode,
}).Infof("Action finished")
return true
}
func stepExec(req *ExecutionRequest) bool {
ctx, cancel := context.WithTimeout(context.Background(), time.Duration(req.action.Timeout)*time.Second) ctx, cancel := context.WithTimeout(context.Background(), time.Duration(req.action.Timeout)*time.Second)
defer cancel() defer cancel()
@@ -215,103 +187,3 @@ func (e stepExec) Exec(req *ExecutionRequest) bool {
return true return true
} }
func parseActionArguments(rawShellCommand string, values map[string]string, action *config.Action) (string, error) {
log.WithFields(log.Fields{
"cmd": rawShellCommand,
}).Infof("Before Parse Args")
r := regexp.MustCompile("{{ *?([a-zA-Z0-9_]+?) *?}}")
matches := r.FindAllStringSubmatch(rawShellCommand, -1)
for _, match := range matches {
argValue, argProvided := values[match[1]]
if !argProvided {
log.Infof("%v", values)
return "", errors.New("Required arg not provided: " + match[1])
}
err := typecheckActionArgument(match[1], argValue, action)
if err != nil {
return "", err
}
log.WithFields(log.Fields{
"name": match[1],
"value": argValue,
}).Debugf("Arg assigned")
rawShellCommand = strings.ReplaceAll(rawShellCommand, match[0], argValue)
}
log.WithFields(log.Fields{
"cmd": rawShellCommand,
}).Infof("After Parse Args")
return rawShellCommand, nil
}
func typecheckActionArgument(name string, value string, action *config.Action) error {
arg := action.FindArg(name)
if arg == nil {
return errors.New("Action arg not defined: " + name)
}
if len(arg.Choices) > 0 {
return typecheckChoice(value, arg)
}
return TypeSafetyCheck(name, value, arg.Type)
}
func typecheckChoice(value string, arg *config.ActionArgument) error {
for _, choice := range arg.Choices {
if value == choice.Value {
return nil
}
}
return errors.New("argument value is not one of the predefined choices")
}
// TypeSafetyCheck checks argument values match a specific type. The types are
// defined in typecheckRegex, and, you guessed it, uses regex to check for allowed
// characters.
func TypeSafetyCheck(name string, value string, argumentType string) error {
if argumentType == "url" {
return typeSafetyCheckUrl(name, value)
}
return typeSafetyCheckRegex(name, value, argumentType)
}
func typeSafetyCheckRegex(name string, value string, argumentType string) error {
pattern, found := typecheckRegex[argumentType]
if !found {
return errors.New("argument type not implemented " + argumentType)
}
matches, _ := regexp.MatchString(pattern, value)
if !matches {
log.WithFields(log.Fields{
"name": name,
"value": value,
"type": argumentType,
}).Warn("Arg type check safety failure")
return errors.New("invalid argument, doesn't match " + argumentType)
}
return nil
}
func typeSafetyCheckUrl(name string, value string) error {
_, err := url.ParseRequestURI(value)
return err
}

View File

@@ -8,17 +8,6 @@ import (
config "github.com/OliveTin/OliveTin/internal/config" config "github.com/OliveTin/OliveTin/internal/config"
) )
func TestSanitizeUnsafe(t *testing.T) {
assert.Nil(t, TypeSafetyCheck("", "_zomg_ c:/ haxxor ' bobby tables && rm -rf ", "very_dangerous_raw_string"))
}
func TestSanitizeUnimplemented(t *testing.T) {
err := TypeSafetyCheck("", "I am a happy little argument", "greeting_type")
assert.NotNil(t, err, "Test an argument type that does not exist")
}
func testingExecutor() (*Executor, *config.Config) { func testingExecutor() (*Executor, *config.Config) {
e := DefaultExecutor() e := DefaultExecutor()
@@ -119,54 +108,3 @@ func TestArgumentNameSnakeCase(t *testing.T) {
assert.Equal(t, "echo 'Tickling Fred'", out) assert.Equal(t, "echo 'Tickling Fred'", out)
assert.Nil(t, err) assert.Nil(t, err)
} }
func TestArgumentNameNumbers(t *testing.T) {
a1 := config.Action{
Title: "Do some tickles",
Shell: "echo 'Tickling {{ person1name }}'",
Arguments: []config.ActionArgument{
{
Name: "person1name",
Type: "ascii",
},
},
}
values := map[string]string{
"person1name": "Fred",
}
out, err := parseActionArguments(a1.Shell, values, &a1)
assert.Equal(t, "echo 'Tickling Fred'", out)
assert.Nil(t, err)
}
func TestArgumentNotProvided(t *testing.T) {
a1 := config.Action{
Title: "Do some tickles",
Shell: "echo 'Tickling {{ personName }}'",
Arguments: []config.ActionArgument{
{
Name: "person",
Type: "ascii",
},
},
}
values := map[string]string{}
out, err := parseActionArguments(a1.Shell, values, &a1)
assert.Equal(t, "", out)
assert.Equal(t, err.Error(), "Required arg not provided: personName")
}
func TestTypeSafetyCheckUrl(t *testing.T) {
assert.Nil(t, TypeSafetyCheck("test1", "http://google.com", "url"), "Test URL: google.com")
assert.Nil(t, TypeSafetyCheck("test2", "http://technowax.net:80?foo=bar", "url"), "Test URL: technowax.net with query arguments")
assert.Nil(t, TypeSafetyCheck("test3", "http://localhost:80?foo=bar", "url"), "Test URL: localhost with query arguments")
assert.NotNil(t, TypeSafetyCheck("test4", "http://lo host:80", "url"), "Test a badly formed URL")
assert.NotNil(t, TypeSafetyCheck("test5", "12345", "url"), "Test a badly formed URL")
assert.NotNil(t, TypeSafetyCheck("test6", "_!23;", "url"), "Test a badly formed URL")
}