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

fix: cron string validation #19071

Merged
merged 2 commits into from
Aug 9, 2023
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.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
16 changes: 16 additions & 0 deletions src/common/utils/utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -301,6 +301,22 @@ func CronParser() cronlib.Parser {
return cronlib.NewParser(cronlib.Second | cronlib.Minute | cronlib.Hour | cronlib.Dom | cronlib.Month | cronlib.Dow)
}

// ValidateCronString check whether it is a valid cron string and whether the 1st field (indicating Seconds of time) of the cron string is a fixed value of 0 or not
func ValidateCronString(cron string) error {
if len(cron) == 0 {
return fmt.Errorf("empty cron string is invalid")
}
_, err := CronParser().Parse(cron)
if err != nil {
return err
}
cronParts := strings.Split(cron, " ")
Copy link
Contributor

@wy65701436 wy65701436 Aug 4, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

s, err := CronParser().Parse(cron)
if err != nil {
   return false, err
}

t, ok := s.(*cronlib.SpecSchedule)
if ok {
    if t.Second == 0 {
	    return false, err
    }
}

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

tried setting cron as 0 0 0 1 1 *, the t.Second is not 0.

if len(cronParts) == 6 && cronParts[0] != "0" {
return fmt.Errorf("the 1st field (indicating Seconds of time) of the cron setting must be 0")
}
return nil
}

// MostMatchSorter is a sorter for the most match, usually invoked in sort Less function
// usage:
//
Expand Down
50 changes: 50 additions & 0 deletions src/common/utils/utils_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -436,3 +436,53 @@ func Test_sortMostMatch(t *testing.T) {
})
}
}

func TestValidateCronString(t *testing.T) {
testCases := []struct {
description string
input string
hasErr bool
}{
// empty cron string
{
description: "test case 1",
input: "",
hasErr: true,
},

// invalid cron format
{
description: "test case 2",
input: "0 2 3",
hasErr: true,
},

// the 1st field (indicating Seconds of time) of the cron setting must be 0
{
description: "test case 3",
input: "1 0 0 1 1 0",
hasErr: true,
},

// valid cron string
{
description: "test case 4",
input: "0 1 2 1 1 *",
hasErr: false,
},
}

for _, tc := range testCases {
err := ValidateCronString(tc.input)
if tc.hasErr {
if err == nil {
t.Errorf("%s, expect having error, while actual error returned is nil", tc.description)
}
} else {
// tc.hasErr == false
if err != nil {
t.Errorf("%s, expect having no error, while actual error returned is not nil, err=%v", tc.description, err)
}
}
}
}
12 changes: 12 additions & 0 deletions src/controller/p2p/preheat/controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -294,6 +294,12 @@
return 0, err
}

// valid policy schema
err = schema.ValidatePreheatPolicy()
if err != nil {
return 0, err
}

Check warning on line 301 in src/controller/p2p/preheat/controller.go

View check run for this annotation

Codecov / codecov/patch

src/controller/p2p/preheat/controller.go#L300-L301

Added lines #L300 - L301 were not covered by tests

id, err = c.pManager.Create(ctx, schema)
if err != nil {
return
Expand Down Expand Up @@ -360,6 +366,12 @@
return err
}

// valid policy schema
err = schema.ValidatePreheatPolicy()
if err != nil {
return err
}

Check warning on line 373 in src/controller/p2p/preheat/controller.go

View check run for this annotation

Codecov / codecov/patch

src/controller/p2p/preheat/controller.go#L372-L373

Added lines #L372 - L373 were not covered by tests

var cron = schema.Trigger.Settings.Cron
var oldCron = s0.Trigger.Settings.Cron
var needUn bool
Expand Down
8 changes: 5 additions & 3 deletions src/controller/p2p/preheat/controllor_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -240,7 +240,7 @@ func (s *preheatSuite) TestCreatePolicy() {
policy := &policy.Schema{
Name: "test",
FiltersStr: `[{"type":"repository","value":"harbor*"},{"type":"tag","value":"2*"}]`,
TriggerStr: fmt.Sprintf(`{"type":"%s", "trigger_setting":{"cron":"* * * * */1"}}`, policy.TriggerTypeScheduled),
TriggerStr: fmt.Sprintf(`{"type":"%s", "trigger_setting":{"cron":"0 * * * * */1"}}`, policy.TriggerTypeScheduled),
}
s.fakeScheduler.On("Schedule", s.ctx, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(int64(1), nil)
s.fakePolicyMgr.On("Create", s.ctx, policy).Return(int64(1), nil)
Expand Down Expand Up @@ -269,7 +269,7 @@ func (s *preheatSuite) TestGetPolicyByName() {

func (s *preheatSuite) TestUpdatePolicy() {
var p0 = &policy.Schema{Name: "test", Trigger: &policy.Trigger{Type: policy.TriggerTypeScheduled}}
p0.Trigger.Settings.Cron = "* * * * */1"
p0.Trigger.Settings.Cron = "0 * * * * */1"
p0.Filters = []*policy.Filter{
{
Type: policy.FilterTypeRepository,
Expand All @@ -281,6 +281,8 @@ func (s *preheatSuite) TestUpdatePolicy() {
},
}
s.fakePolicyMgr.On("Get", s.ctx, int64(1)).Return(p0, nil)
s.fakeScheduler.On("UnScheduleByVendor", s.ctx, mock.Anything, mock.Anything).Return(nil)
s.fakeScheduler.On("Schedule", s.ctx, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(int64(1), nil)

// need change to schedule
p1 := &policy.Schema{
Expand All @@ -299,7 +301,7 @@ func (s *preheatSuite) TestUpdatePolicy() {
ID: 1,
Name: "test",
FiltersStr: `[{"type":"repository","value":"harbor*"},{"type":"tag","value":"2*"}]`,
TriggerStr: fmt.Sprintf(`{"type":"%s", "trigger_setting":{"cron":"* * * * */2"}}`, policy.TriggerTypeScheduled),
TriggerStr: fmt.Sprintf(`{"type":"%s", "trigger_setting":{"cron":"0 * * * * */2"}}`, policy.TriggerTypeScheduled),
}
s.fakePolicyMgr.On("Update", s.ctx, p2, mock.Anything).Return(nil)
err = s.controller.UpdatePolicy(s.ctx, p2, "")
Expand Down
8 changes: 8 additions & 0 deletions src/controller/retention/controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,10 @@

// CreateRetention Create Retention
func (r *defaultController) CreateRetention(ctx context.Context, p *policy.Metadata) (int64, error) {
err := p.ValidateRetentionPolicy()
if err != nil {
return 0, err
}

Check warning on line 106 in src/controller/retention/controller.go

View check run for this annotation

Codecov / codecov/patch

src/controller/retention/controller.go#L105-L106

Added lines #L105 - L106 were not covered by tests
id, err := r.manager.CreatePolicy(ctx, p)
if err != nil {
return 0, err
Expand All @@ -125,6 +129,10 @@

// UpdateRetention Update Retention
func (r *defaultController) UpdateRetention(ctx context.Context, p *policy.Metadata) error {
err := p.ValidateRetentionPolicy()
if err != nil {
return err
}

Check warning on line 135 in src/controller/retention/controller.go

View check run for this annotation

Codecov / codecov/patch

src/controller/retention/controller.go#L134-L135

Added lines #L134 - L135 were not covered by tests
p0, err := r.manager.GetPolicy(ctx, p.ID)
if err != nil {
return err
Expand Down
4 changes: 2 additions & 2 deletions src/controller/retention/controller_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -160,7 +160,7 @@ func (s *ControllerTestSuite) TestPolicy() {
Trigger: &policy.Trigger{
Kind: "Schedule",
Settings: map[string]interface{}{
"cron": "* 22 11 * * *",
"cron": "0 22 11 * * *",
},
},
Scope: &policy.Scope{
Expand Down Expand Up @@ -271,7 +271,7 @@ func (s *ControllerTestSuite) TestExecution() {
Trigger: &policy.Trigger{
Kind: "Schedule",
Settings: map[string]interface{}{
"cron": "* 22 11 * * *",
"cron": "0 22 11 * * *",
},
},
Scope: &policy.Scope{
Expand Down
12 changes: 12 additions & 0 deletions src/pkg/p2p/preheat/models/policy/policy.go
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,18 @@ type Trigger struct {
} `json:"trigger_setting,omitempty"`
}

// ValidatePreheatPolicy validate preheat policy
func (s *Schema) ValidatePreheatPolicy() error {
// currently only validate cron string of preheat policy
if s.Trigger != nil && s.Trigger.Type == TriggerTypeScheduled && len(s.Trigger.Settings.Cron) > 0 {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

duplicate validation in the length of cron string

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is because we need to skip utils.ValidateCronString() validation for cron with empty string "". In preheat and tag retention, it uses empty string cron to unschedule an existing job.

if err := utils.ValidateCronString(s.Trigger.Settings.Cron); err != nil {
return errors.New(nil).WithCode(errors.BadRequestCode).
WithMessage("invalid cron string for scheduled preheat: %s, error: %v", s.Trigger.Settings.Cron, err)
}
}
return nil
}

// Valid the policy
func (s *Schema) Valid(v *validation.Validation) {
if len(s.Name) == 0 {
Expand Down
24 changes: 24 additions & 0 deletions src/pkg/p2p/preheat/models/policy/policy_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,13 +37,37 @@ func TestPolicy(t *testing.T) {
// SetupSuite prepares the env for PolicyTestSuite.
func (p *PolicyTestSuite) SetupSuite() {
p.schema = &Schema{}
p.schema.Trigger = &Trigger{}
}

// TearDownSuite clears the env for PolicyTestSuite.
func (p *PolicyTestSuite) TearDownSuite() {
p.schema = nil
}

// TestValidatePreheatPolicy tests the ValidatePreheatPolicy method
func (p *PolicyTestSuite) TestValidatePreheatPolicy() {
// manual trigger
p.schema.Trigger.Type = TriggerTypeManual
p.NoError(p.schema.ValidatePreheatPolicy())

// event trigger
p.schema.Trigger.Type = TriggerTypeEventBased
p.NoError(p.schema.ValidatePreheatPolicy())

// scheduled trigger
p.schema.Trigger.Type = TriggerTypeScheduled
// cron string is empty
p.schema.Trigger.Settings.Cron = ""
p.NoError(p.schema.ValidatePreheatPolicy())
// the 1st field of cron string is not 0
p.schema.Trigger.Settings.Cron = "1 0 0 1 1 *"
p.Error(p.schema.ValidatePreheatPolicy())
// valid cron string
p.schema.Trigger.Settings.Cron = "0 0 0 1 1 *"
p.NoError(p.schema.ValidatePreheatPolicy())
}

// TestValid tests Valid method.
func (p *PolicyTestSuite) TestValid() {
// policy name is empty, should return error
Expand Down
19 changes: 19 additions & 0 deletions src/pkg/retention/policy/models.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ package policy
import (
"github.com/beego/beego/v2/core/validation"

"github.com/goharbor/harbor/src/common/utils"
"github.com/goharbor/harbor/src/lib/errors"
"github.com/goharbor/harbor/src/lib/selector/selectors/doublestar"
"github.com/goharbor/harbor/src/pkg/retention/policy/rule"
"github.com/goharbor/harbor/src/pkg/retention/policy/rule/index"
Expand Down Expand Up @@ -58,6 +60,23 @@ type Metadata struct {
Scope *Scope `json:"scope" valid:"Required"`
}

// ValidateRetentionPolicy validate the retention policy
func (m *Metadata) ValidateRetentionPolicy() error {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ditto

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same as above. This is because we need to skip utils.ValidateCronString() validation for cron with empty string "". In preheat and tag retention, it uses empty string cron to unschedule an existing job.

// currently only validate the cron string of retention policy
if m.Trigger != nil {
if m.Trigger.Kind == TriggerKindSchedule && m.Trigger.Settings != nil {
cronItem, ok := m.Trigger.Settings[TriggerSettingsCron]
if ok && len(cronItem.(string)) > 0 {
if err := utils.ValidateCronString(cronItem.(string)); err != nil {
return errors.New(nil).WithCode(errors.BadRequestCode).
WithMessage("invalid cron string for scheduled tag retention: %s, error: %v", cronItem.(string), err)
}
}
}
}
return nil
}

// Valid Valid
func (m *Metadata) Valid(v *validation.Validation) {
if m.Trigger == nil {
Expand Down
42 changes: 42 additions & 0 deletions src/pkg/retention/policy/models_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (

"github.com/beego/beego/v2/core/validation"
"github.com/stretchr/testify/require"
"github.com/stretchr/testify/suite"

"github.com/goharbor/harbor/src/pkg/retention/policy/rule"
)
Expand Down Expand Up @@ -35,6 +36,47 @@ func TestAlgorithm(t *testing.T) {
// }
// }

type PolicyTestSuite struct {
suite.Suite

policy *Metadata
}

// TestRetentionPolicy is the entry method of running PolicyTestSuite.
func TestRetentionPolicy(t *testing.T) {
suite.Run(t, &PolicyTestSuite{})
}

// SetupSuite prepares the env for PolicyTestSuite.
func (p *PolicyTestSuite) SetupSuite() {
p.policy = &Metadata{}
p.policy.Trigger = &Trigger{}
}

// TearDownSuite clears the env for PolicyTestSuite.
func (p *PolicyTestSuite) TearDownSuite() {
p.policy = nil
}

func (p *PolicyTestSuite) TestValidateRetentionPolicy() {
p.policy.Trigger.Kind = TriggerKindSchedule

// cron is not in the map of trigger setting
p.NoError(p.policy.ValidateRetentionPolicy())

// cron value is an empty string
p.policy.Trigger.Settings = map[string]interface{}{"cron": ""}
p.NoError(p.policy.ValidateRetentionPolicy())

// the 1st field of cron value is not 0
p.policy.Trigger.Settings = map[string]interface{}{"cron": "1 0 0 1 1 *"}
p.Error(p.policy.ValidateRetentionPolicy())

// valid cron value
p.policy.Trigger.Settings = map[string]interface{}{"cron": "0 0 0 1 1 *"}
p.NoError(p.policy.ValidateRetentionPolicy())
}

func TestRule(t *testing.T) {
p := &Metadata{
Algorithm: "or",
Expand Down
9 changes: 5 additions & 4 deletions src/server/v2.0/handler/gc.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
"github.com/go-openapi/runtime/middleware"

"github.com/goharbor/harbor/src/common/rbac"
"github.com/goharbor/harbor/src/common/utils"
"github.com/goharbor/harbor/src/controller/gc"
"github.com/goharbor/harbor/src/jobservice/job"
"github.com/goharbor/harbor/src/lib/config"
Expand Down Expand Up @@ -139,10 +140,6 @@
}

func (g *gcAPI) createSchedule(ctx context.Context, cronType, cron string, policy gc.Policy) error {
if cron == "" {
return errors.New(nil).WithCode(errors.BadRequestCode).
WithMessage("empty cron string for gc schedule")
}
_, err := g.gcCtr.CreateSchedule(ctx, cronType, cron, policy)
if err != nil {
return err
Expand All @@ -151,6 +148,10 @@
}

func (g *gcAPI) updateSchedule(ctx context.Context, cronType, cron string, policy gc.Policy) error {
if err := utils.ValidateCronString(cron); err != nil {
return errors.New(nil).WithCode(errors.BadRequestCode).
WithMessage("invalid cron string for scheduled gc: %s, error: %v", cron, err)
}

Check warning on line 154 in src/server/v2.0/handler/gc.go

View check run for this annotation

Codecov / codecov/patch

src/server/v2.0/handler/gc.go#L151-L154

Added lines #L151 - L154 were not covered by tests
if err := g.gcCtr.DeleteSchedule(ctx); err != nil {
return err
}
Expand Down
8 changes: 4 additions & 4 deletions src/server/v2.0/handler/purge.go
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,10 @@
}

func (p *purgeAPI) updateSchedule(ctx context.Context, vendorType, cronType, cron string, policy pg.JobPolicy, extraParams map[string]interface{}) error {
if err := utils.ValidateCronString(cron); err != nil {
return errors.New(nil).WithCode(errors.BadRequestCode).
WithMessage("invalid cron string for scheduled log rotation purge: %s, error: %v", cron, err)
}

Check warning on line 147 in src/server/v2.0/handler/purge.go

View check run for this annotation

Codecov / codecov/patch

src/server/v2.0/handler/purge.go#L144-L147

Added lines #L144 - L147 were not covered by tests
if err := p.schedulerCtl.Delete(ctx, vendorType); err != nil {
return err
}
Expand Down Expand Up @@ -315,10 +319,6 @@
}

func (p *purgeAPI) createSchedule(ctx context.Context, vendorType string, cronType string, cron string, policy pg.JobPolicy, extraParam map[string]interface{}) error {
if cron == "" {
return errors.New(nil).WithCode(errors.BadRequestCode).
WithMessage("empty cron string for schedule")
}
_, err := p.schedulerCtl.Create(ctx, vendorType, cronType, cron, pg.SchedulerCallback, policy, extraParam)
if err != nil {
return err
Expand Down
5 changes: 5 additions & 0 deletions src/server/v2.0/handler/scan_all.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@

"github.com/goharbor/harbor/src/common/rbac"
"github.com/goharbor/harbor/src/common/secret"
"github.com/goharbor/harbor/src/common/utils"
"github.com/goharbor/harbor/src/controller/scan"
"github.com/goharbor/harbor/src/controller/scanner"
"github.com/goharbor/harbor/src/jobservice/job"
Expand Down Expand Up @@ -194,6 +195,10 @@
}

func (s *scanAllAPI) createOrUpdateScanAllSchedule(ctx context.Context, cronType, cron string, previous *scheduler.Schedule) (int64, error) {
if err := utils.ValidateCronString(cron); err != nil {
return 0, errors.New(nil).WithCode(errors.BadRequestCode).
WithMessage("invalid cron string for scheduled scan all: %s, error: %v", cron, err)
}

Check warning on line 201 in src/server/v2.0/handler/scan_all.go

View check run for this annotation

Codecov / codecov/patch

src/server/v2.0/handler/scan_all.go#L199-L201

Added lines #L199 - L201 were not covered by tests
if previous != nil {
if cronType == previous.CRONType && cron == previous.CRON {
return previous.ID, nil
Expand Down