Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Command-credential #9

Merged
merged 6 commits into from
Jun 8, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 @@
"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 @@
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 @@
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 All @@ -110,7 +115,7 @@
schedules := []bool{
s.Interval != 0,
s.Cron != "",
s.OnInit == true,

Check failure on line 118 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 `s.OnInit` (gosimple)
}
activeSchedules := 0
for _, t := range schedules {
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,
}
}
FMotalleb marked this conversation as resolved.
Show resolved Hide resolved
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 @@
"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 @@
log *logrus.Entry
cancel context.CancelFunc

user string
group string

shell string
shellArgs []string

Expand All @@ -32,53 +36,54 @@
}

// 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
proc.Start()

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

View workflow job for this annotation

GitHub Actions / analyze (go)

Error return value of `proc.Start` is not checked (errcheck)
e = proc.Wait()
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)
}

func NewCommand(
Expand Down Expand Up @@ -119,5 +124,7 @@
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
Loading