Skip to content

Commit

Permalink
Merge pull request #9 from FMotalleb/command-credential
Browse files Browse the repository at this point in the history
Command-credential
  • Loading branch information
FMotalleb authored Jun 8, 2024
2 parents 710dcd9 + 01c0e55 commit e6d9e26
Show file tree
Hide file tree
Showing 11 changed files with 200 additions and 46 deletions.
7 changes: 4 additions & 3 deletions abstraction/validatable.go
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
package abstraction

import "github.com/sirupsen/logrus"

type (
ValidatableStr string
Validatable interface {
Validate() error
Validatable interface {
Validate(log *logrus.Entry) error
}
)
3 changes: 2 additions & 1 deletion cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"runtime"

"github.com/joho/godotenv"
"github.com/sirupsen/logrus"
"github.com/spf13/cobra"
"github.com/spf13/viper"

Expand Down Expand Up @@ -139,7 +140,7 @@ func initConfig() {
"Cannot unmarshal the config file: %s",
)
panicOnErr(
CFG.Validate(),
CFG.Validate(logrus.WithField("section", "config.validation")),
"Failed to initialize config file: %s",
)
}
11 changes: 6 additions & 5 deletions config.example.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ jobs:
# disabled: true
tasks:
# This line specifies the actual command to be executed.
- command: echo 15
- command: echo $(whoami)
# This setting determines the number of times the task will be retried if it fails
# A failure is defined by either a non-zero exit code for commands or a status code of 400 or higher for HTTP requests.
retries: 3
Expand All @@ -20,12 +20,13 @@ jobs:
# This sets a maximum time limit for the task to complete.
# If the task exceeds 15 seconds, it will be considered failed and stopped (command) or canceled (http requests)
timeout: 15s
# This defines the working directory for the command.
working-dir: C://
user: root
# # This defines the working directory for the command.
# working-dir: C://
# This section allows you to set environment variables that will be available to the command during execution.
# `SHELL` and `SHELL_ARGS` can be used to change this commands shell environment
env:
DB_HOST: 10.0.0.5
# env:
# DB_HOST: 10.0.0.5

# # A simple get request
# - get: https://example.com/get
Expand Down
3 changes: 3 additions & 0 deletions config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,9 @@ type (
Headers map[string]string `mapstructure:"headers" json:"headers,omitempty"`
Data any `mapstructure:"data" json:"data,omitempty"`

UserName string `mapstructure:"user" json:"user,omitempty"`
GroupName string `mapstructure:"group" json:"group,omitempty"`

Retries uint `mapstructure:"retries" json:"retries,omitempty"`
RetryDelay time.Duration `mapstructure:"retry-delay" json:"retry_delay,omitempty"`
Timeout time.Duration `mapstructure:"timeout" json:"timeout,omitempty"`
Expand Down
25 changes: 15 additions & 10 deletions config/validators.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,53 +4,56 @@ import (
"encoding/json"
"fmt"

"github.com/sirupsen/logrus"

credential "github.com/FMotalleb/crontab-go/core/os_credential"
"github.com/FMotalleb/crontab-go/core/schedule"
)

func (cfg *Config) Validate() error {
func (cfg *Config) Validate(log *logrus.Entry) error {
if err := cfg.LogFormat.Validate(); err != nil {
return err
}
if err := cfg.LogLevel.Validate(); err != nil {
return err
}
for _, job := range cfg.Jobs {
if err := job.Validate(); err != nil {
if err := job.Validate(log); err != nil {
return err
}
}
return nil
}

func (c *JobConfig) Validate() error {
func (c *JobConfig) Validate(log *logrus.Entry) error {
if c.Disabled == true {

Check failure on line 29 in config/validators.go

View workflow job for this annotation

GitHub Actions / analyze (go)

S1002: should omit comparison to bool constant, can be simplified to `c.Disabled` (gosimple)
return nil
}

for _, s := range c.Schedulers {
if err := s.Validate(); err != nil {
if err := s.Validate(log); err != nil {
return err
}
}
for _, t := range c.Tasks {
if err := t.Validate(); err != nil {
if err := t.Validate(log); err != nil {
return err
}
}
for _, t := range c.Hooks.Done {
if err := t.Validate(); err != nil {
if err := t.Validate(log); err != nil {
return err
}
}
for _, t := range c.Hooks.Failed {
if err := t.Validate(); err != nil {
if err := t.Validate(log); err != nil {
return err
}
}
return nil
}

func (t *Task) Validate() error {
func (t *Task) Validate(log *logrus.Entry) error {
actions := []bool{
t.Get != "",
t.Command != "",
Expand All @@ -70,7 +73,9 @@ func (t *Task) Validate() error {
t.Post,
)
}

if err := credential.Validate(log, t.UserName, t.GroupName); err != nil {
return err
}
if t.Command != "" && (t.Data != nil || t.Headers != nil) {
return fmt.Errorf("command cannot have data or headers field, violating command: `%s`", t.Command)
}
Expand Down Expand Up @@ -101,7 +106,7 @@ func (t *Task) Validate() error {
return nil
}

func (s *JobScheduler) Validate() error {
func (s *JobScheduler) Validate(log *logrus.Entry) error {
if s.Interval < 0 {
return fmt.Errorf("received a negative time in interval: `%v`", s.Interval)
} else if _, err := schedule.CronParser.Parse(s.Cron); s.Cron != "" && err != nil {
Expand Down
2 changes: 1 addition & 1 deletion core/jobs/runner.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ func InitializeJobs(log *logrus.Entry, cronInstance *cron.Cron) {

logger := initLogger(c, log, job)
logger = logger.WithField("concurrency", job.Concurrency)
if err := job.Validate(); err != nil {
if err := job.Validate(log); err != nil {
log.Panicf("failed to validate job (%s): %v", job.Name, err)
}

Expand Down
95 changes: 95 additions & 0 deletions core/os_credential/unix_credential.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
//go:build !windows
// +build !windows

package credential

import (
"errors"
"fmt"
"os/exec"
osUser "os/user"
"strconv"
"syscall"

"github.com/sirupsen/logrus"
)

func Validate(log *logrus.Entry, usr string, grp string) error {
cu, err := osUser.Current()
if err != nil {
return fmt.Errorf("cannot get current user error: %s", err)
}
if usr != "" && cu.Uid != "0" {
return errors.New("cannot switch user of tasks without root privilege, if you need to use user in tasks run crontab-go as user root")
}
_, _, err = lookupUIDAndGID(usr, log)
if err != nil {
return fmt.Errorf("cannot get uid and gid of user `%s` error: %s", usr, err)
}
_, err = lookupGID(grp, log)
if err != nil {
return fmt.Errorf("cannot get gid of group `%s` error: %s", grp, err)
}

return nil
}

func SetUser(log *logrus.Entry, proc *exec.Cmd, usr string, grp string) {
if usr == "" {
log.Trace("no username given, running as current user")
return
}

uid, gid, err := lookupUIDAndGID(usr, log)
if err != nil {
log.Panicf("cannot get uid and gid of user %s, error: %s", usr, err)
}
if grp != "" {
gid, _ = lookupGID(grp, log)
}

setUID(log, proc, uid, gid)
}

func lookupGID(grp string, log *logrus.Entry) (gid uint32, err error) {
g, err := osUser.LookupGroup(grp)
if err != nil {
log.Panicf("cannot find group with name %s in the os: %s, you've changed os users during application runtime", grp, err)
}
gidU, err := strconv.ParseUint(g.Gid, 10, 32)
if err != nil {
return 0, err
}
return uint32(gidU), nil
}

func lookupUIDAndGID(usr string, log *logrus.Entry) (uid uint32, gid uint32, err error) {
u, err := osUser.Lookup(usr)
if err != nil {
log.Panicf("cannot find user with name %s in the os: %s, you've changed os users during application runtime", usr, err)
}
uidU, err := strconv.ParseUint(u.Uid, 10, 32)
if err != nil {
return 0, 0, err
}
gidU, err := strconv.ParseUint(u.Gid, 10, 32)
if err != nil {
return 0, 0, err
}
return uint32(uidU), uint32(gidU), nil
}

func setUID(
log *logrus.Entry,
proc *exec.Cmd,
uid uint32,
gid uint32,
) {
log.Tracef("Setting: uid(%d) and gid(%d)", uid, gid)
attrib := &syscall.SysProcAttr{}
proc.SysProcAttr = attrib
proc.SysProcAttr.Credential = &syscall.Credential{
Uid: uid,
Gid: gid,
}
}
19 changes: 19 additions & 0 deletions core/os_credential/windows_credential.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
//go:build windows
// +build windows

package credential

import (
"os/exec"

"github.com/sirupsen/logrus"
)

func Validate(log *logrus.Entry, usr string, grp string) error {
log.Warn("windows os does not have capability to set user dus validation will pass but will not work")
return nil
}

func SetUser(log *logrus.Entry, _ *exec.Cmd, _ string, _ string) {
log.Warn("cannot set user in windows platform")
}
41 changes: 24 additions & 17 deletions core/task/command.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import (
"github.com/FMotalleb/crontab-go/abstraction"
"github.com/FMotalleb/crontab-go/cmd"
"github.com/FMotalleb/crontab-go/config"
credential "github.com/FMotalleb/crontab-go/core/os_credential"
)

type Command struct {
Expand All @@ -23,6 +24,9 @@ type Command struct {
log *logrus.Entry
cancel context.CancelFunc

user string
group string

shell string
shellArgs []string

Expand All @@ -32,42 +36,43 @@ type Command struct {
}

// Cancel implements abstraction.Executable.
func (g *Command) Cancel() {
if g.cancel != nil {
g.log.Debugln("canceling executable")
g.cancel()
func (c *Command) Cancel() {
if c.cancel != nil {
c.log.Debugln("canceling executable")
c.cancel()
}
}

// Execute implements abstraction.Executable.
func (cmmnd *Command) Execute(ctx context.Context) (e error) {
func (c *Command) Execute(ctx context.Context) (e error) {
r := getRetry(ctx)
log := cmmnd.log.WithField("retry", r)
if getRetry(ctx) > cmmnd.retries {
log := c.log.WithField("retry", r)
if getRetry(ctx) > c.retries {
log.Warn("maximum retry reached")
return fmt.Errorf("maximum retries reached")
}
if r != 0 {
log.Debugln("waiting", cmmnd.retryDelay, "before executing the next iteration after last fail")
time.Sleep(cmmnd.retryDelay)
log.Debugln("waiting", c.retryDelay, "before executing the next iteration after last fail")
time.Sleep(c.retryDelay)
}
ctx = increaseRetry(ctx)
var procCtx context.Context
var cancel context.CancelFunc
if cmmnd.timeout != 0 {
procCtx, cancel = context.WithTimeout(ctx, cmmnd.timeout)
if c.timeout != 0 {
procCtx, cancel = context.WithTimeout(ctx, c.timeout)
} else {
procCtx, cancel = context.WithCancel(ctx)
}
cmmnd.cancel = cancel
c.cancel = cancel

proc := exec.CommandContext(

Check failure on line 68 in core/task/command.go

View workflow job for this annotation

GitHub Actions / analyze (go)

G204: Subprocess launched with a potential tainted input or cmd arguments (gosec)
procCtx,
cmmnd.shell,
append(cmmnd.shellArgs, *&cmmnd.exe)...,
c.shell,
append(c.shellArgs, *&c.exe)...,
)
proc.Env = *cmmnd.envVars
proc.Dir = cmmnd.workingDirectory
credential.SetUser(log, proc, c.user, c.group)
proc.Env = *c.envVars
proc.Dir = c.workingDirectory
var res bytes.Buffer
proc.Stdout = &res
proc.Stderr = &res
Expand All @@ -76,7 +81,7 @@ func (cmmnd *Command) Execute(ctx context.Context) (e error) {
log.Infof("command finished with answer: `%s`", strings.TrimSpace(string(res.Bytes())))

Check failure on line 81 in core/task/command.go

View workflow job for this annotation

GitHub Actions / analyze (go)

S1030: should use res.String() instead of string(res.Bytes()) (gosimple)
if e != nil {
log.Warn("failed to execute the command ", e)
return cmmnd.Execute(ctx)
return c.Execute(ctx)
}
return

Check failure on line 86 in core/task/command.go

View workflow job for this annotation

GitHub Actions / analyze (go)

naked return in func `Execute` with 40 lines of code (nakedret)
}
Expand Down Expand Up @@ -119,5 +124,7 @@ func NewCommand(
retries: task.Retries,
retryDelay: task.RetryDelay,
timeout: task.Timeout,
user: task.UserName,
group: task.GroupName,
}
}
23 changes: 23 additions & 0 deletions helpers/err_handlers.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package helpers

import (
"github.com/sirupsen/logrus"
)

func PanicOnErr(log *logrus.Entry, err error, message string) {
if err != nil {
log.Panicf(message, err)
}
}

func FatalOnErr(log *logrus.Entry, err error, message string) {
if err != nil {
log.Fatalf(message, err)
}
}

func WarnOnErr(log *logrus.Entry, err error, message string) {
if err != nil {
log.Warnf(message, err)
}
}
Loading

0 comments on commit e6d9e26

Please sign in to comment.